From e37b56e7ad1a6881144179b45eaf56ca89fdf38a Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Sun, 25 Jun 2023 06:56:39 -0400 Subject: [PATCH 01/79] add polars dep, upgrade pyarrow min version to 8.0.0 and cfb package conversion --- .gitignore | 3 + examples/timing.py | 53 ++++ setup.py | 6 +- sportsdataverse.egg-info/SOURCES.txt | 2 + sportsdataverse.egg-info/requires.txt | 4 +- sportsdataverse/cfb/cfb_loaders.py | 48 ++-- sportsdataverse/cfb/cfb_schedule.py | 375 +++++++++++++++++++------- sportsdataverse/cfb/cfb_teams.py | 18 +- 8 files changed, 372 insertions(+), 137 deletions(-) create mode 100644 examples/timing.py diff --git a/.gitignore b/.gitignore index 100c211..44e91c1 100755 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,6 @@ docs/node_modules build dist *.ipynb +.ipynb_checkpoints +./.ipynb_checkpoints +*/.ipynb_checkpoints \ No newline at end of file diff --git a/examples/timing.py b/examples/timing.py new file mode 100644 index 0000000..f8ae48c --- /dev/null +++ b/examples/timing.py @@ -0,0 +1,53 @@ +import sportsdataverse as sdv +import logging +from sportsdataverse.decorators import * + +logging.basicConfig(level=logging.DEBUG, filename = 'wehoop_wnba_raw_logfile.txt') +logger = logging.getLogger(__name__) + +@timer(number=10) +@record_mem_usage +def test_polars_cfb_schedule(): + sdv.cfb.espn_cfb_schedule() + +# @timer(number=10) +# @record_mem_usage +# def test_pandas_cfb_schedule(): +# sdv.cfb.espn_cfb_schedule_pandas() + +@timer(number=10) +@record_mem_usage +def test_polars_cfb_calendar_ondays(): + sdv.cfb.espn_cfb_calendar(season=2022, ondays=True) + +# @timer(number=10) +# @record_mem_usage +# def test_pandas_cfb_calendar_ondays(): +# sdv.cfb.espn_cfb_calendar_pandas(season=2022, ondays=True) + +@timer(number=10) +@record_mem_usage +def test_polars_cfb_calendar(): + sdv.cfb.espn_cfb_calendar(season=2022) + +# @timer(number=10) +# @record_mem_usage +# def test_pandas_cfb_calendar(): +# sdv.cfb.espn_cfb_calendar_pandas(season=2022) +@timer(number=10) +@record_mem_usage +def test_polars_load_cfb_pbp(): + sdv.cfb.load_cfb_pbp(seasons=2021) + +def main(): + test_polars_cfb_schedule() + # test_pandas_cfb_schedule() + test_polars_cfb_calendar() + # test_pandas_cfb_calendar() + test_polars_cfb_calendar_ondays() + # test_pandas_cfb_calendar_ondays() + test_polars_load_cfb_pbp() + sdv.cfb.espn_cfb_schedule(dates=20241010, logger = logger) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/setup.py b/setup.py index 002dd90..34619aa 100755 --- a/setup.py +++ b/setup.py @@ -89,18 +89,18 @@ # https://packaging.python.org/en/latest/requirements.html install_requires=[ "numpy>=1.13.0", - "pandas >= 1.0.3", + "pandas>= 1.0.3", + "polars>=0.18.2", "tqdm>=4.50.0", "beautifulsoup4>=4.4.0", "inflection>=0.5.1", "requests>=2.18.1", "lxml>=4.2.1", - "pyarrow>=1.0.1", + "pyarrow>=8.0.0", "pyjanitor>=0.23.1", "pyreadr>=0.4.0", "scipy>=1.4.0", "matplotlib>=2.0.0", - "tqdm>=4.50.0", "attrs>=20.3.0", "xgboost>=1.2.0", ], diff --git a/sportsdataverse.egg-info/SOURCES.txt b/sportsdataverse.egg-info/SOURCES.txt index 54554de..9a30348 100755 --- a/sportsdataverse.egg-info/SOURCES.txt +++ b/sportsdataverse.egg-info/SOURCES.txt @@ -190,6 +190,7 @@ docs/src/css/custom.css docs/src/pages/styles.module.css sportsdataverse/__init__.py sportsdataverse/config.py +sportsdataverse/decorators.py sportsdataverse/dl_utils.py sportsdataverse/errors.py sportsdataverse.egg-info/PKG-INFO @@ -200,6 +201,7 @@ sportsdataverse.egg-info/top_level.txt sportsdataverse/cfb/__init__.py sportsdataverse/cfb/cfb_loaders.py sportsdataverse/cfb/cfb_pbp.py +sportsdataverse/cfb/cfb_pbp_pd.py sportsdataverse/cfb/cfb_schedule.py sportsdataverse/cfb/cfb_teams.py sportsdataverse/cfb/model_vars.py diff --git a/sportsdataverse.egg-info/requires.txt b/sportsdataverse.egg-info/requires.txt index 7107bdd..7c4d934 100755 --- a/sportsdataverse.egg-info/requires.txt +++ b/sportsdataverse.egg-info/requires.txt @@ -1,16 +1,16 @@ numpy>=1.13.0 pandas>=1.0.3 +polars>=0.18.2 tqdm>=4.50.0 beautifulsoup4>=4.4.0 inflection>=0.5.1 requests>=2.18.1 lxml>=4.2.1 -pyarrow>=1.0.1 +pyarrow>=8.0.0 pyjanitor>=0.23.1 pyreadr>=0.4.0 scipy>=1.4.0 matplotlib>=2.0.0 -tqdm>=4.50.0 attrs>=20.3.0 xgboost>=1.2.0 diff --git a/sportsdataverse/cfb/cfb_loaders.py b/sportsdataverse/cfb/cfb_loaders.py index 2c7cd5f..6677d81 100755 --- a/sportsdataverse/cfb/cfb_loaders.py +++ b/sportsdataverse/cfb/cfb_loaders.py @@ -1,4 +1,5 @@ import pandas as pd +import polars as pl import json from tqdm import tqdm from typing import List, Callable, Iterator, Union, Optional @@ -21,18 +22,17 @@ def load_cfb_pbp(seasons: List[int]) -> pd.DataFrame: Raises: ValueError: If `season` is less than 2003. """ - data = pd.DataFrame() + data = pl.DataFrame() if type(seasons) is int: seasons = [seasons] for i in tqdm(seasons): if int(i) < 2003: raise SeasonNotFoundError("season cannot be less than 2003") - i_data = pd.read_parquet(CFB_BASE_URL.format(season=i), engine='auto', columns=None) + i_data = pl.read_parquet(CFB_BASE_URL.format(season=i), use_pyarrow=True, columns=None) #data = data.append(i_data) - data = pd.concat([data, i_data], axis = 0, ignore_index = True) - #Give each row a unique index - data.reset_index(drop=True, inplace=True) - return data + data = pl.concat([data, i_data], how = 'vertical') + + return data.to_pandas(use_pyarrow_extension_array = True) def load_cfb_schedule(seasons: List[int]) -> pd.DataFrame: """Load college football schedule data @@ -49,19 +49,17 @@ def load_cfb_schedule(seasons: List[int]) -> pd.DataFrame: Raises: ValueError: If `season` is less than 2002. """ - data = pd.DataFrame() + data = pl.DataFrame() if type(seasons) is int: seasons = [seasons] for i in tqdm(seasons): if int(i) < 2002: raise SeasonNotFoundError("season cannot be less than 2002") - i_data = pd.read_parquet(CFB_TEAM_SCHEDULE_URL.format(season = i), engine='auto', columns=None) + i_data = pl.read_parquet(CFB_TEAM_SCHEDULE_URL.format(season = i), use_pyarrow=True, columns=None) #data = data.append(i_data) - data = pd.concat([data, i_data], axis = 0, ignore_index = True) - #Give each row a unique index - data.reset_index(drop=True, inplace=True) + data = pl.concat([data, i_data], how = 'vertical') - return data + return data.to_pandas(use_pyarrow_extension_array = True) def load_cfb_rosters(seasons: List[int]) -> pd.DataFrame: """Load roster data @@ -78,18 +76,16 @@ def load_cfb_rosters(seasons: List[int]) -> pd.DataFrame: Raises: ValueError: If `season` is less than 2014. """ - data = pd.DataFrame() + data = pl.DataFrame() if type(seasons) is int: seasons = [seasons] for i in tqdm(seasons): if int(i) < 2004: raise SeasonNotFoundError("season cannot be less than 2004") - i_data = pd.read_parquet(CFB_ROSTER_URL.format(season = i), engine='auto', columns=None) - data = pd.concat([data, i_data], axis = 0, ignore_index = True) - #Give each row a unique index - data.reset_index(drop=True, inplace=True) + i_data = pl.read_parquet(CFB_ROSTER_URL.format(season = i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how = 'vertical') - return data + return data.to_pandas(use_pyarrow_extension_array = True) def load_cfb_team_info(seasons: List[int]) -> pd.DataFrame: """Load college football team info @@ -106,21 +102,18 @@ def load_cfb_team_info(seasons: List[int]) -> pd.DataFrame: Raises: ValueError: If `season` is less than 2002. """ - data = pd.DataFrame() + data = pl.DataFrame() if type(seasons) is int: seasons = [seasons] for i in tqdm(seasons): if int(i) < 2002: raise SeasonNotFoundError("season cannot be less than 2002") try: - i_data = pd.read_parquet(CFB_TEAM_INFO_URL.format(season = i), engine='auto', columns=None) - except: + i_data = pl.read_parquet(CFB_TEAM_INFO_URL.format(season = i), use_pyarrow=True, columns=None) + except Exception: print(f'We don\'t seem to have data for the {i} season.') - data = pd.concat([data, i_data], axis = 0, ignore_index = True) - #Give each row a unique index - data.reset_index(drop=True, inplace=True) - - return data + data = pl.concat([data, i_data], how = 'vertical') + return data.to_pandas(use_pyarrow_extension_array = True) def get_cfb_teams() -> pd.DataFrame: """Load college football team ID information and logos @@ -133,6 +126,5 @@ def get_cfb_teams() -> pd.DataFrame: Returns: pd.DataFrame: Pandas dataframe containing teams available for the requested seasons. """ - df = pd.read_parquet(CFB_TEAM_LOGO_URL, engine='auto', columns=None) - return df + return pl.read_parquet(CFB_TEAM_LOGO_URL, use_pyarrow=True, columns=None).to_pandas(use_pyarrow_extension_array = True) diff --git a/sportsdataverse/cfb/cfb_schedule.py b/sportsdataverse/cfb/cfb_schedule.py index a8e662f..644f81c 100755 --- a/sportsdataverse/cfb/cfb_schedule.py +++ b/sportsdataverse/cfb/cfb_schedule.py @@ -1,10 +1,12 @@ import pandas as pd +import polars as pl import json import time import datetime from sportsdataverse.dl_utils import download, underscore -def espn_cfb_schedule(dates=None, week=None, season_type=None, groups=None, limit=500) -> pd.DataFrame: +def espn_cfb_schedule(dates=None, week=None, season_type=None, groups=None, limit=500, + **kwargs) -> pd.DataFrame: """espn_cfb_schedule - look up the college football schedule for a given season Args: @@ -17,82 +19,46 @@ def espn_cfb_schedule(dates=None, week=None, season_type=None, groups=None, limi Returns: pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. """ - if week is None: - week = '' - else: - week = '&week=' + str(week) - if dates is None: - dates = '' - else: - dates = '&dates=' + str(dates) - if season_type is None: - season_type = '' - else: - season_type = '&seasontype=' + str(season_type) - if groups is None: - groups = '&groups=80' - else: - groups = '&groups=' + str(groups) - if limit is None: - limit_url = '' - else: - limit_url = '&limit=' + str(limit) + cache_buster = int(time.time() * 1000) - cache_buster_url = '&'+str(cache_buster) - url = "http://site.api.espn.com/apis/site/v2/sports/football/college-football/scoreboard?{}{}{}{}{}{}".format( - limit_url, - groups, - dates, - week, - season_type, - cache_buster_url - ) - resp = download(url=url) + cache_buster_url = f'&{cache_buster}' + params = { + 'week': week, + 'dates': dates, + 'seasontype': season_type, + 'groups': groups if groups is not None else '80', + 'limit': limit + } - ev = pd.DataFrame() + url = "http://site.api.espn.com/apis/site/v2/sports/football/college-football/scoreboard" + resp = download(url=url, params=params, **kwargs) + + ev = pl.DataFrame() if resp is not None: - events_txt = json.loads(resp) + events_txt = resp.json() events = events_txt.get('events') + if len(events) == 0: + ev = pd.DataFrame() + return ev for event in events: event.get('competitions')[0].get('competitors')[0].get('team').pop('links',None) event.get('competitions')[0].get('competitors')[1].get('team').pop('links',None) - if event.get('competitions')[0].get('competitors')[0].get('homeAway')=='home': - event['competitions'][0]['home'] = event.get('competitions')[0].get('competitors')[0].get('team') - event['competitions'][0]['home']['score'] = event.get('competitions')[0].get('competitors')[0].get('score') - event['competitions'][0]['home']['winner'] = event.get('competitions')[0].get('competitors')[0].get('winner') - event['competitions'][0]['home']['currentRank'] = event.get('competitions')[0].get('competitors')[0].get('curatedRank', {}).get('current', '99') - event['competitions'][0]['home']['linescores'] = event.get('competitions')[0].get('competitors')[0].get('linescores', []) - event['competitions'][0]['home']['records'] = event.get('competitions')[0].get('competitors')[0].get('records', []) - event['competitions'][0]['away'] = event.get('competitions')[0].get('competitors')[1].get('team') - event['competitions'][0]['away']['score'] = event.get('competitions')[0].get('competitors')[1].get('score') - event['competitions'][0]['away']['winner'] = event.get('competitions')[0].get('competitors')[1].get('winner') - event['competitions'][0]['away']['currentRank'] = event.get('competitions')[0].get('competitors')[1].get('curatedRank', {}).get('current', '99') - event['competitions'][0]['away']['linescores'] = event.get('competitions')[0].get('competitors')[1].get('linescores', []) - event['competitions'][0]['away']['records'] = event.get('competitions')[0].get('competitors')[1].get('records', []) + if event.get('competitions')[0].get('competitors')[0].get('homeAway') == 'home': + event = _extract_home_away_from_espn_cfb_schedule(event, 0, 'home') + event = _extract_home_away_from_espn_cfb_schedule(event, 1, 'away') else: - event['competitions'][0]['away'] = event.get('competitions')[0].get('competitors')[0].get('team') - event['competitions'][0]['away']['score'] = event.get('competitions')[0].get('competitors')[0].get('score') - event['competitions'][0]['away']['winner'] = event.get('competitions')[0].get('competitors')[0].get('winner') - event['competitions'][0]['away']['currentRank'] = event.get('competitions')[0].get('competitors')[0].get('curatedRank', {}).get('current', '99') - event['competitions'][0]['away']['linescores'] = event.get('competitions')[0].get('competitors')[0].get('linescores', []) - event['competitions'][0]['away']['records'] = event.get('competitions')[0].get('competitors')[0].get('records', []) - event['competitions'][0]['home'] = event.get('competitions')[0].get('competitors')[1].get('team') - event['competitions'][0]['home']['score'] = event.get('competitions')[0].get('competitors')[1].get('score') - event['competitions'][0]['home']['winner'] = event.get('competitions')[0].get('competitors')[1].get('winner') - event['competitions'][0]['home']['currentRank'] = event.get('competitions')[0].get('competitors')[1].get('curatedRank', {}).get('current', '99') - event['competitions'][0]['home']['linescores'] = event.get('competitions')[0].get('competitors')[1].get('linescores', []) - event['competitions'][0]['home']['records'] = event.get('competitions')[0].get('competitors')[1].get('records', []) - + event = _extract_home_away_from_espn_cfb_schedule(event, 0, 'away') + event = _extract_home_away_from_espn_cfb_schedule(event, 1, 'home') del_keys = ['geoBroadcasts', 'headlines', 'series', 'situation', 'tickets', 'odds'] for k in del_keys: event.get('competitions')[0].pop(k, None) - if len(event.get('competitions')[0]['notes'])>0: + if len(event.get('competitions')[0]['notes']) > 0: event.get('competitions')[0]['notes_type'] = event.get('competitions')[0]['notes'][0].get("type") event.get('competitions')[0]['notes_headline'] = event.get('competitions')[0]['notes'][0].get("headline").replace('"','') else: event.get('competitions')[0]['notes_type'] = '' event.get('competitions')[0]['notes_headline'] = '' - if len(event.get('competitions')[0].get('broadcasts'))>0: + if len(event.get('competitions')[0].get('broadcasts')) > 0: event.get('competitions')[0]['broadcast_market'] = event.get('competitions')[0].get('broadcasts', [])[0].get('market', "") event.get('competitions')[0]['broadcast_name'] = event.get('competitions')[0].get('broadcasts', [])[0].get('names', [])[0] else: @@ -100,19 +66,72 @@ def espn_cfb_schedule(dates=None, week=None, season_type=None, groups=None, limi event.get('competitions')[0]['broadcast_name'] = "" event.get('competitions')[0].pop('broadcasts', None) event.get('competitions')[0].pop('notes', None) - x = pd.json_normalize(event.get('competitions')[0], sep='_') - x['game_id'] = x['id'].astype(int) - x['season'] = event.get('season').get('year') - x['season_type'] = event.get('season').get('type') - x['week'] = event.get('week', {}).get('number') - ev = pd.concat([ev, x], axis = 0, ignore_index = True) - ev = pd.DataFrame(ev) - ev.columns = [underscore(c) for c in ev.columns.tolist()] - return ev + event.get('competitions')[0].pop('competitors', None) + x = pd.json_normalize(event.get('competitions')[0], sep = '_') + x = pl.from_pandas(x) + x = x.with_columns( + game_id = (pl.col('id').cast(pl.Int64)), + season = (event.get('season').get('year')), + season_type = (event.get('season').get('type')), + week = (event.get('week', {}).get('number')), + home_linescores = pl.when(pl.col('status_type_description') == 'Postponed').then(None).otherwise(pl.col('home_linescores')), + away_linescores = pl.when(pl.col('status_type_description') == 'Postponed').then(None).otherwise(pl.col('away_linescores')), + ) + x = x[[s.name for s in x if s.null_count() != x.height]] + # x['game_id'] = x['id'].cast(pl.Int64) + # x['season'] = event.get('season').get('year') + # x['season_type'] = event.get('season').get('type') + # x['week'] = event.get('week', {}).get('number') + ev = pl.concat([ev, x], how = 'diagonal') + ev.columns = [underscore(c) for c in ev.columns] -def espn_cfb_calendar(season = None, groups = None, ondays = None) -> pd.DataFrame: + return ev.to_pandas() + + +# TODO Rename this here and in `espn_cfb_schedule` +def _extract_home_away_from_espn_cfb_schedule(event, arg1, arg2): + event['competitions'][0][arg2] = ( + event.get('competitions')[0].get('competitors')[arg1].get('team') + ) + event['competitions'][0][arg2]['score'] = ( + event.get('competitions')[0].get('competitors')[arg1].get('score') + ) + event['competitions'][0][arg2]['winner'] = ( + event.get('competitions')[0].get('competitors')[arg1].get('winner') + ) + ## add winner back to main competitors if does not exist + event['competitions'][0]['competitors'][arg1]['winner'] = ( + event.get('competitions')[0].get('competitors')[arg1].get('winner', False) + ) + event['competitions'][0][arg2]['currentRank'] = ( + event.get('competitions')[0] + .get('competitors')[arg1] + .get('curatedRank', {}) + .get('current', 99) + ) + event['competitions'][0][arg2]['linescores'] = ( + event.get('competitions')[0] + .get('competitors')[arg1] + .get('linescores', [{'value': 'N/A'}]) + ) + ## add linescores back to main competitors if does not exist + event['competitions'][0]['competitors'][arg1]['linescores'] = ( + event.get('competitions')[0] + .get('competitors')[arg1] + .get('linescores', []) + ) + event['competitions'][0][arg2]['records'] = ( + event.get('competitions')[0] + .get('competitors')[arg1] + .get('records', []) + ) + return event + + + +def espn_cfb_calendar(season = None, groups = None, ondays = None, **kwargs) -> pd.DataFrame: """espn_cfb_calendar - look up the men's college football calendar for a given season Args: @@ -127,27 +146,17 @@ def espn_cfb_calendar(season = None, groups = None, ondays = None) -> pd.DataFra ValueError: If `season` is less than 2002. """ if ondays is not None: - url = "https://sports.core.api.espn.com/v2/sports/football/leagues/college-football/seasons/{}/types/2/calendar/ondays".format(season) - resp = download(url=url) - txt = json.loads(resp).get('eventDate').get('dates') - full_schedule = pd.DataFrame(txt,columns=['dates']) - full_schedule['dateURL'] = list(map(lambda x: x[:10].replace("-",""),full_schedule['dates'])) - full_schedule['url']="http://site.api.espn.com/apis/site/v2/sports/football/college-football/scoreboard?dates=" - full_schedule['url']= full_schedule['url'] + full_schedule['dateURL'] + full_schedule = _ondays_from_espn_cfb_calendar(season) else: - if season is None: - season_url = '' - else: - season_url = '&dates=' + str(season) - if groups is None: - groups_url = '&groups=80' - else: - groups_url = '&groups=' + str(groups) - url = "http://site.api.espn.com/apis/site/v2/sports/football/college-football/scoreboard?{}{}".format(season_url, groups_url) - resp = download(url=url) - txt = json.loads(resp) + params = { + 'dates': season, + 'groups': groups if groups is not None else '80' + } + url = "http://site.api.espn.com/apis/site/v2/sports/football/college-football/scoreboard" + resp = download(url = url, params = params, **kwargs) + txt = resp.json() txt = txt.get('leagues')[0].get('calendar') - full_schedule = pd.DataFrame() + full_schedule = pl.DataFrame() for i in range(len(txt)): if txt[i].get('entries', None) is not None: reg = pd.json_normalize(data = txt[i], @@ -157,11 +166,28 @@ def espn_cfb_calendar(season = None, groups = None, ondays = None) -> pd.DataFra record_prefix='week_', errors="ignore", sep='_') - full_schedule = pd.concat([full_schedule,reg], ignore_index=True) - full_schedule['season']=season - full_schedule.columns = [underscore(c) for c in full_schedule.columns.tolist()] - full_schedule = full_schedule.rename(columns={"week_value": "week", "season_type_value": "season_type"}) - return full_schedule + full_schedule = pl.concat([full_schedule, pl.from_pandas(reg)], how = 'vertical') + full_schedule = full_schedule.with_columns( + season = season + ) + full_schedule.columns = [underscore(c) for c in full_schedule.columns] + full_schedule = full_schedule.rename({"week_value": "week", "season_type_value": "season_type"}) + return full_schedule.to_pandas() + + +def _ondays_from_espn_cfb_calendar(season): + url = f"https://sports.core.api.espn.com/v2/sports/football/leagues/college-football/seasons/{season}/types/2/calendar/ondays" + resp = download(url=url) + if resp is not None: + txt = resp.json().get('eventDate').get('dates') + result = pl.DataFrame(txt, schema=['dates']) + result = result.with_columns(dateURL = pl.col('dates').str.slice(0, 10)) + result = result.with_columns( + url="http://site.api.espn.com/apis/site/v2/sports/football/college-football/scoreboard?dates=" + + pl.col('dateURL') + ) + + return result def most_recent_cfb_season(): date = datetime.datetime.now() @@ -171,3 +197,162 @@ def most_recent_cfb_season(): return date.year else: return date.year - 1 + + +# def espn_cfb_schedule_pandas(dates=None, week=None, season_type=None, groups=None, limit=500) -> pd.DataFrame: +# """espn_cfb_schedule - look up the college football schedule for a given season + +# Args: +# dates (int): Used to define different seasons. 2002 is the earliest available season. +# week (int): Week of the schedule. +# groups (int): Used to define different divisions. 80 is FBS, 81 is FCS. +# season_type (int): 2 for regular season, 3 for post-season, 4 for off-season. +# limit (int): number of records to return, default: 500. + +# Returns: +# pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. +# """ +# if week is None: +# week = '' +# else: +# week = '&week=' + str(week) +# if dates is None: +# dates = '' +# else: +# dates = '&dates=' + str(dates) +# if season_type is None: +# season_type = '' +# else: +# season_type = '&seasontype=' + str(season_type) +# if groups is None: +# groups = '&groups=80' +# else: +# groups = '&groups=' + str(groups) +# if limit is None: +# limit_url = '' +# else: +# limit_url = '&limit=' + str(limit) +# cache_buster = int(time.time() * 1000) +# cache_buster_url = '&'+str(cache_buster) +# url = "http://site.api.espn.com/apis/site/v2/sports/football/college-football/scoreboard?{}{}{}{}{}{}".format( +# limit_url, +# groups, +# dates, +# week, +# season_type, +# cache_buster_url +# ) +# resp = download(url=url) + +# ev = pd.DataFrame() +# if resp is not None: +# events_txt = resp.json() +# events = events_txt.get('events') +# for event in events: +# event.get('competitions')[0].get('competitors')[0].get('team').pop('links',None) +# event.get('competitions')[0].get('competitors')[1].get('team').pop('links',None) +# if event.get('competitions')[0].get('competitors')[0].get('homeAway')=='home': +# event['competitions'][0]['home'] = event.get('competitions')[0].get('competitors')[0].get('team') +# event['competitions'][0]['home']['score'] = event.get('competitions')[0].get('competitors')[0].get('score') +# event['competitions'][0]['home']['winner'] = event.get('competitions')[0].get('competitors')[0].get('winner') +# event['competitions'][0]['home']['currentRank'] = event.get('competitions')[0].get('competitors')[0].get('curatedRank', {}).get('current', '99') +# event['competitions'][0]['home']['linescores'] = event.get('competitions')[0].get('competitors')[0].get('linescores', []) +# event['competitions'][0]['home']['records'] = event.get('competitions')[0].get('competitors')[0].get('records', []) +# event['competitions'][0]['away'] = event.get('competitions')[0].get('competitors')[1].get('team') +# event['competitions'][0]['away']['score'] = event.get('competitions')[0].get('competitors')[1].get('score') +# event['competitions'][0]['away']['winner'] = event.get('competitions')[0].get('competitors')[1].get('winner') +# event['competitions'][0]['away']['currentRank'] = event.get('competitions')[0].get('competitors')[1].get('curatedRank', {}).get('current', '99') +# event['competitions'][0]['away']['linescores'] = event.get('competitions')[0].get('competitors')[1].get('linescores', []) +# event['competitions'][0]['away']['records'] = event.get('competitions')[0].get('competitors')[1].get('records', []) +# else: +# event['competitions'][0]['away'] = event.get('competitions')[0].get('competitors')[0].get('team') +# event['competitions'][0]['away']['score'] = event.get('competitions')[0].get('competitors')[0].get('score') +# event['competitions'][0]['away']['winner'] = event.get('competitions')[0].get('competitors')[0].get('winner') +# event['competitions'][0]['away']['currentRank'] = event.get('competitions')[0].get('competitors')[0].get('curatedRank', {}).get('current', '99') +# event['competitions'][0]['away']['linescores'] = event.get('competitions')[0].get('competitors')[0].get('linescores', []) +# event['competitions'][0]['away']['records'] = event.get('competitions')[0].get('competitors')[0].get('records', []) +# event['competitions'][0]['home'] = event.get('competitions')[0].get('competitors')[1].get('team') +# event['competitions'][0]['home']['score'] = event.get('competitions')[0].get('competitors')[1].get('score') +# event['competitions'][0]['home']['winner'] = event.get('competitions')[0].get('competitors')[1].get('winner') +# event['competitions'][0]['home']['currentRank'] = event.get('competitions')[0].get('competitors')[1].get('curatedRank', {}).get('current', '99') +# event['competitions'][0]['home']['linescores'] = event.get('competitions')[0].get('competitors')[1].get('linescores', []) +# event['competitions'][0]['home']['records'] = event.get('competitions')[0].get('competitors')[1].get('records', []) + +# del_keys = ['geoBroadcasts', 'headlines', 'series', 'situation', 'tickets', 'odds'] +# for k in del_keys: +# event.get('competitions')[0].pop(k, None) +# if len(event.get('competitions')[0]['notes'])>0: +# event.get('competitions')[0]['notes_type'] = event.get('competitions')[0]['notes'][0].get("type") +# event.get('competitions')[0]['notes_headline'] = event.get('competitions')[0]['notes'][0].get("headline").replace('"','') +# else: +# event.get('competitions')[0]['notes_type'] = '' +# event.get('competitions')[0]['notes_headline'] = '' +# if len(event.get('competitions')[0].get('broadcasts'))>0: +# event.get('competitions')[0]['broadcast_market'] = event.get('competitions')[0].get('broadcasts', [])[0].get('market', "") +# event.get('competitions')[0]['broadcast_name'] = event.get('competitions')[0].get('broadcasts', [])[0].get('names', [])[0] +# else: +# event.get('competitions')[0]['broadcast_market'] = "" +# event.get('competitions')[0]['broadcast_name'] = "" +# event.get('competitions')[0].pop('broadcasts', None) +# event.get('competitions')[0].pop('notes', None) +# x = pd.json_normalize(event.get('competitions')[0], sep='_') +# x['game_id'] = x['id'].astype(int) +# x['season'] = event.get('season').get('year') +# x['season_type'] = event.get('season').get('type') +# x['week'] = event.get('week', {}).get('number') +# ev = pd.concat([ev, x], axis = 0, ignore_index = True) +# ev = pd.DataFrame(ev) +# ev.columns = [underscore(c) for c in ev.columns.tolist()] +# return ev + + +# def espn_cfb_calendar_pandas(season = None, groups = None, ondays = None) -> pd.DataFrame: +# """espn_cfb_calendar - look up the men's college football calendar for a given season + +# Args: +# season (int): Used to define different seasons. 2002 is the earliest available season. +# groups (int): Used to define different divisions. 80 is FBS, 81 is FCS. +# ondays (boolean): Used to return dates for calendar ondays + +# Returns: +# pd.DataFrame: Pandas dataframe containing calendar dates for the requested season. + +# Raises: +# ValueError: If `season` is less than 2002. +# """ +# if ondays is not None: +# url = "https://sports.core.api.espn.com/v2/sports/football/leagues/college-football/seasons/{}/types/2/calendar/ondays".format(season) +# resp = download(url=url) +# txt = resp.json().get('eventDate').get('dates') +# full_schedule = pd.DataFrame(txt,columns=['dates']) +# full_schedule['dateURL'] = list(map(lambda x: x[:10].replace("-",""),full_schedule['dates'])) +# full_schedule['url']="http://site.api.espn.com/apis/site/v2/sports/football/college-football/scoreboard?dates=" +# full_schedule['url']= full_schedule['url'] + full_schedule['dateURL'] +# else: +# if season is None: +# season_url = '' +# else: +# season_url = '&dates=' + str(season) +# if groups is None: +# groups_url = '&groups=80' +# else: +# groups_url = '&groups=' + str(groups) +# url = "http://site.api.espn.com/apis/site/v2/sports/football/college-football/scoreboard?{}{}".format(season_url, groups_url) +# resp = download(url=url) +# txt = resp.json() +# txt = txt.get('leagues')[0].get('calendar') +# full_schedule = pd.DataFrame() +# for i in range(len(txt)): +# if txt[i].get('entries', None) is not None: +# reg = pd.json_normalize(data = txt[i], +# record_path = 'entries', +# meta=["label","value","startDate","endDate"], +# meta_prefix='season_type_', +# record_prefix='week_', +# errors="ignore", +# sep='_') +# full_schedule = pd.concat([full_schedule,reg], ignore_index=True) +# full_schedule['season']=season +# full_schedule.columns = [underscore(c) for c in full_schedule.columns.tolist()] +# full_schedule = full_schedule.rename(columns={"week_value": "week", "season_type_value": "season_type"}) +# return full_schedule \ No newline at end of file diff --git a/sportsdataverse/cfb/cfb_teams.py b/sportsdataverse/cfb/cfb_teams.py index c8799f9..99127d6 100755 --- a/sportsdataverse/cfb/cfb_teams.py +++ b/sportsdataverse/cfb/cfb_teams.py @@ -1,8 +1,9 @@ import pandas as pd +import polars as pl import json from sportsdataverse.dl_utils import download, underscore -def espn_cfb_teams(groups=None) -> pd.DataFrame: +def espn_cfb_teams(groups=None, **kwargs) -> pd.DataFrame: """espn_cfb_teams - look up the college football teams Args: @@ -11,15 +12,14 @@ def espn_cfb_teams(groups=None) -> pd.DataFrame: Returns: pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. """ - if groups is None: - groups = '&groups=80' - else: - groups = '&groups=' + str(groups) - ev = pd.DataFrame() - url = "http://site.api.espn.com/apis/site/v2/sports/football/college-football/teams?{}&limit=1000".format(groups) - resp = download(url=url) + params = { + "groups": groups if groups is not None else "80", + "limit": 1000 + } + url = "http://site.api.espn.com/apis/site/v2/sports/football/college-football/teams" + resp = download(url=url, params = params, **kwargs) if resp is not None: - events_txt = json.loads(resp) + events_txt = resp.json() teams = events_txt.get('sports')[0].get('leagues')[0].get('teams') del_keys = ['record', 'links'] From f493ca867d7b4ff70447ebd47b9dfb2b103b7956 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Sun, 25 Jun 2023 06:57:57 -0400 Subject: [PATCH 02/79] now using `requests` module with req options/parameters passed to `download()` via kwargs of top level functions --- sportsdataverse/dl_utils.py | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/sportsdataverse/dl_utils.py b/sportsdataverse/dl_utils.py index af92831..d5d3088 100755 --- a/sportsdataverse/dl_utils.py +++ b/sportsdataverse/dl_utils.py @@ -4,29 +4,30 @@ import time import http.client import urllib.request +import logging +import requests import json from urllib.error import URLError, HTTPError, ContentTooShortError from datetime import datetime from itertools import chain, starmap -def download(url, params = {}, num_retries=15): +def download(url, params = None, headers = None, proxy = None, timeout = 30, num_retries = 15, logger = None): + if params is None: + params = {} + if logger is None: + logger = logging.getLogger(__name__) try: - html = urllib.request.urlopen(url).read() + response = requests.get(url, params = params, proxies = proxy, headers = headers, timeout = timeout) + # print(response.url) except (URLError, HTTPError, ContentTooShortError, http.client.HTTPException, http.client.IncompleteRead) as e: - print('Download error:', url) - html = None - if num_retries > 0: - if hasattr(e, 'code') and 500 <= e.code < 600: - time.sleep(2) - # recursively retry 5xx HTTP errors - return download(url, num_retries=num_retries - 1) - if num_retries > 0: - if e == http.client.IncompleteRead: - time.sleep(2) - return download(url, num_retries=num_retries - 1) + logger.warn("Download error: %i - %s for url (%s)", response.status_code, response.reason, response.url) + response = None + if num_retries > 0 and (hasattr(e, 'code') and 500 <= getattr(e, 'code') < 600): + time.sleep(2) + return download(url, params = params, proxies = proxy, headers = headers, timeout = timeout, num_retries = num_retries - 1, logger = logger) if num_retries == 0: - print("Retry Limit Exceeded") - return html + logger.error("Retry Limit Exceeded") + return response def flatten_json_iterative(dictionary, sep = '.', ind_start = 0): """Flattening a nested json file""" @@ -57,11 +58,7 @@ def unpack_one(parent_key, parent_value): return dictionary def key_check(obj, key, replacement = np.array([])): - if key in obj.keys(): - obj_key = obj[key] - else: - obj_key = replacement - return obj_key + return obj[key] if key in obj.keys() else replacement def underscore(word): """ From e83fa072d70aa54c4e20369514691d0398589ef4 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Sun, 25 Jun 2023 06:58:17 -0400 Subject: [PATCH 03/79] added some timing/memory usage decorators for profiling --- sportsdataverse/decorators.py | 57 +++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 sportsdataverse/decorators.py diff --git a/sportsdataverse/decorators.py b/sportsdataverse/decorators.py new file mode 100644 index 0000000..17f74c2 --- /dev/null +++ b/sportsdataverse/decorators.py @@ -0,0 +1,57 @@ +from functools import wraps +import os +import functools +import time +import psutil + +def timer(number=10): + """Decorator that times the function it wraps over repeated executions + + Parameters + ---------- + number : int + The number of repeated executions of the function being wrapped + Returns + ------- + func + """ + def actual_wrapper(func): + @functools.wraps(func) + def wrapper_timer(*args, **kwargs): + tic = time.perf_counter() + for i in range(number - 1): + func(*args, **kwargs) + else: + value = func(*args, **kwargs) + toc = time.perf_counter() + elapsed_time = toc - tic + print(f"Elapsed time of {func.__name__} for {number} runs:\n" + f" {elapsed_time:0.6f} seconds") + return value + return wrapper_timer + return actual_wrapper + +# this decorator is used to record memory usage of the decorated function +def record_mem_usage(func): + @wraps(func) + def wrapper(*args, **kwargs): + process = psutil.Process(os.getpid()) + mem_start = process.memory_info()[0] + rt = func(*args, **kwargs) + mem_end = process.memory_info()[0] + diff_KB = (mem_end - mem_start) // 1000 + print(f'memory usage of {func.__name__}: {diff_KB} KB') + return rt + + return wrapper + +def record_time_usage(func): + @wraps(func) + def timeit_wrapper(*args, **kwargs): + start_time = time.perf_counter() + result = func(*args, **kwargs) + end_time = time.perf_counter() + total_time = end_time - start_time + print(f'Function {func.__name__}{args} {kwargs} Took {total_time:.4f} seconds') + return result + return timeit_wrapper From b42ced24f66c95be6baa155647d7976088b524f4 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Sun, 9 Jul 2023 14:04:54 -0400 Subject: [PATCH 04/79] updates to add `return_as_pandas` parameters --- CHANGELOG.md | 4 +- setup.py | 2 +- sportsdataverse/cfb/cfb_loaders.py | 28 +- sportsdataverse/cfb/cfb_pbp.py | 8195 +++++++++++---------------- sportsdataverse/cfb/cfb_schedule.py | 265 +- sportsdataverse/cfb/cfb_teams.py | 5 +- 6 files changed, 3455 insertions(+), 5044 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9be5a92..0dc3a8a 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,6 @@ - +## 0.0.36-7 Release: July 9, 2023 +- Switched most under the hood dataframe operations to use the python `polars` library +- Added `**kwargs` which pass arguments to the `dl_utils.download()` function, including `headers`, `proxy`, `timeout` (default 30s), `num_retries` (default = 15), `logger` (default = None) ## 0.0.34-35 Release: May 7-9, 2023 - Reconfigured some imports diff --git a/setup.py b/setup.py index 34619aa..96ea9d3 100755 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # https://packaging.python.org/en/latest/single_source_version.html - version="0.0.36", + version="0.0.37", description="Retrieve Sports data in Python", long_description=long_description, long_description_content_type="text/markdown", diff --git a/sportsdataverse/cfb/cfb_loaders.py b/sportsdataverse/cfb/cfb_loaders.py index 6677d81..5e95a4b 100755 --- a/sportsdataverse/cfb/cfb_loaders.py +++ b/sportsdataverse/cfb/cfb_loaders.py @@ -7,7 +7,7 @@ from sportsdataverse.errors import SeasonNotFoundError from sportsdataverse.dl_utils import download -def load_cfb_pbp(seasons: List[int]) -> pd.DataFrame: +def load_cfb_pbp(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: """Load college football play by play data going back to 2003 Example: @@ -15,6 +15,7 @@ def load_cfb_pbp(seasons: List[int]) -> pd.DataFrame: Args: seasons (list): Used to define different seasons. 2003 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing the play-by-plays available for the requested seasons. @@ -31,10 +32,9 @@ def load_cfb_pbp(seasons: List[int]) -> pd.DataFrame: i_data = pl.read_parquet(CFB_BASE_URL.format(season=i), use_pyarrow=True, columns=None) #data = data.append(i_data) data = pl.concat([data, i_data], how = 'vertical') + return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data - return data.to_pandas(use_pyarrow_extension_array = True) - -def load_cfb_schedule(seasons: List[int]) -> pd.DataFrame: +def load_cfb_schedule(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: """Load college football schedule data Example: @@ -42,6 +42,7 @@ def load_cfb_schedule(seasons: List[int]) -> pd.DataFrame: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing the schedule for the requested seasons. @@ -58,10 +59,10 @@ def load_cfb_schedule(seasons: List[int]) -> pd.DataFrame: i_data = pl.read_parquet(CFB_TEAM_SCHEDULE_URL.format(season = i), use_pyarrow=True, columns=None) #data = data.append(i_data) data = pl.concat([data, i_data], how = 'vertical') + return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data - return data.to_pandas(use_pyarrow_extension_array = True) -def load_cfb_rosters(seasons: List[int]) -> pd.DataFrame: +def load_cfb_rosters(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: """Load roster data Example: @@ -69,6 +70,7 @@ def load_cfb_rosters(seasons: List[int]) -> pd.DataFrame: Args: seasons (list): Used to define different seasons. 2014 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing rosters available for the requested seasons. @@ -84,10 +86,9 @@ def load_cfb_rosters(seasons: List[int]) -> pd.DataFrame: raise SeasonNotFoundError("season cannot be less than 2004") i_data = pl.read_parquet(CFB_ROSTER_URL.format(season = i), use_pyarrow=True, columns=None) data = pl.concat([data, i_data], how = 'vertical') + return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data - return data.to_pandas(use_pyarrow_extension_array = True) - -def load_cfb_team_info(seasons: List[int]) -> pd.DataFrame: +def load_cfb_team_info(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: """Load college football team info Example: @@ -95,6 +96,7 @@ def load_cfb_team_info(seasons: List[int]) -> pd.DataFrame: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing the team info available for the requested seasons. @@ -113,18 +115,20 @@ def load_cfb_team_info(seasons: List[int]) -> pd.DataFrame: except Exception: print(f'We don\'t seem to have data for the {i} season.') data = pl.concat([data, i_data], how = 'vertical') - return data.to_pandas(use_pyarrow_extension_array = True) + return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data -def get_cfb_teams() -> pd.DataFrame: +def get_cfb_teams(return_as_pandas = True) -> pd.DataFrame: """Load college football team ID information and logos Example: `cfb_df = sportsdataverse.cfb.cfb_teams()` Args: + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing teams available for the requested seasons. """ - return pl.read_parquet(CFB_TEAM_LOGO_URL, use_pyarrow=True, columns=None).to_pandas(use_pyarrow_extension_array = True) + return pl.read_parquet(CFB_TEAM_LOGO_URL, use_pyarrow=True, columns=None).to_pandas(use_pyarrow_extension_array = True) \ + if return_as_pandas else pl.read_parquet(CFB_TEAM_LOGO_URL, use_pyarrow=True, columns=None) diff --git a/sportsdataverse/cfb/cfb_pbp.py b/sportsdataverse/cfb/cfb_pbp.py index c32464d..5f77528 100755 --- a/sportsdataverse/cfb/cfb_pbp.py +++ b/sportsdataverse/cfb/cfb_pbp.py @@ -1,4 +1,5 @@ import pandas as pd +import polars as pl import numpy as np import re import os @@ -9,7 +10,7 @@ from numpy.core.fromnumeric import mean from functools import reduce, partial from sportsdataverse.dl_utils import download, key_check -from .model_vars import * +from sportsdataverse.cfb.model_vars import * # "td" : float(p[0]), # "opp_td" : float(p[1]), @@ -45,7 +46,7 @@ class CFBPlayProcess(object): ran_cleaning_pipeline = False raw = False path_to_json = '/' - def __init__(self, gameId=0, raw=False, path_to_json='/'): + def __init__(self, gameId=0, raw=False, path_to_json='/', **kwargs): self.gameId = int(gameId) # self.logger = logger self.ran_pipeline = False @@ -53,7 +54,7 @@ def __init__(self, gameId=0, raw=False, path_to_json='/'): self.raw = raw self.path_to_json = path_to_json - def espn_cfb_pbp(self): + def espn_cfb_pbp(self, **kwargs): """espn_cfb_pbp() - Pull the game by id. Data from API endpoints: `college-football/playbyplay`, `college-football/summary` Args: @@ -69,12 +70,11 @@ def espn_cfb_pbp(self): `cfb_df = sportsdataverse.cfb.CFBPlayProcess(gameId=401256137).espn_cfb_pbp()` """ cache_buster = int(time.time() * 1000) - pbp_txt = {} - pbp_txt["timeouts"] = {} + pbp_txt = {"timeouts": {}} # summary endpoint for pickcenter array summary_url = f"http://site.api.espn.com/apis/site/v2/sports/football/college-football/summary?event={self.gameId}&{cache_buster}" - summary_resp = download(summary_url) - summary = json.loads(summary_resp) + summary_resp = download(url = summary_url, **kwargs) + summary = summary_resp.json() incoming_keys_expected = [ 'boxscore', 'format', 'gameInfo', 'drives', 'leaders', 'broadcasts', 'predictor', 'pickcenter', 'againstTheSpread', 'odds', 'winprobability', @@ -95,21 +95,14 @@ def espn_cfb_pbp(self): if k in summary.keys(): pbp_json[k] = summary[k] else: - if k in dict_keys_expected: - pbp_json[k] = {} - else: - pbp_json[k] = [] + pbp_json[k] = {} if k in dict_keys_expected else [] return pbp_json for k in incoming_keys_expected: if k in summary.keys(): pbp_txt[k] = summary[k] else: - if k in dict_keys_expected: - pbp_txt[k] = {} - else: - pbp_txt[k] = [] - + pbp_txt[k] = {} if k in dict_keys_expected else [] for k in [ "scoringPlays", "standings", @@ -127,13 +120,13 @@ def espn_cfb_pbp(self): ]: pbp_txt[k] = key_check(obj=summary, key=k) for k in ['news','shop']: - pbp_txt.pop('{}'.format(k), None) + pbp_txt.pop(f'{k}', None) self.json = pbp_txt return self.json def cfb_pbp_disk(self): - with open(os.path.join(self.path_to_json, "{}.json".format(self.gameId))) as json_file: + with open(os.path.join(self.path_to_json, f"{self.gameId}.json")) as json_file: pbp_txt = json.load(json_file) self.json = pbp_txt return self.json @@ -145,7 +138,7 @@ def __helper_cfb_pbp_drives(self, pbp_txt): awayTeamId, awayTeamMascot, awayTeamName,\ awayTeamAbbrev, awayTeamNameAlt = self.__helper_cfb_pbp(pbp_txt) - pbp_txt["plays"] = pd.DataFrame() + pbp_txt["plays"] = pl.DataFrame() # negotiating the drive meta keys into columns after unnesting drive plays # concatenating the previous and current drives categories when necessary if "drives" in pbp_txt.keys() and pbp_txt.get('header').get('competitions')[0].get('playByPlaySource') != 'none': @@ -167,7 +160,7 @@ def __helper_cfb_pbp_features(self, pbp_txt, homeTeamName, homeTeamAbbrev, homeTeamNameAlt, awayTeamId, awayTeamMascot, awayTeamName, awayTeamAbbrev, awayTeamNameAlt): - pbp_txt["plays"] = pd.DataFrame() + pbp_txt["plays"] = pl.DataFrame() for key in pbp_txt.get("drives").keys(): prev_drives = pd.json_normalize( data=pbp_txt.get("drives").get("{}".format(key)), @@ -199,558 +192,425 @@ def __helper_cfb_pbp_features(self, pbp_txt, meta_prefix="drive.", errors="ignore", ) - pbp_txt["plays"] = pd.concat( - [pbp_txt["plays"], prev_drives], ignore_index=True - ) - - pbp_txt["plays"] = pbp_txt["plays"].to_dict(orient="records") - pbp_txt["plays"] = pd.DataFrame(pbp_txt["plays"]) - pbp_txt["plays"]["game_id"] = int(self.gameId) - pbp_txt["plays"]["season"] = pbp_txt.get("header").get("season").get("year") - pbp_txt["plays"]["seasonType"] = pbp_txt.get("header").get("season").get("type") - pbp_txt["plays"]["homeTeamId"] = homeTeamId - pbp_txt["plays"]["awayTeamId"] = awayTeamId - pbp_txt["plays"]["homeTeamName"] = str(homeTeamName) - pbp_txt["plays"]["awayTeamName"] = str(awayTeamName) - pbp_txt["plays"]["homeTeamMascot"] = str(homeTeamMascot) - pbp_txt["plays"]["awayTeamMascot"] = str(awayTeamMascot) - pbp_txt["plays"]["homeTeamAbbrev"] = str(homeTeamAbbrev) - pbp_txt["plays"]["awayTeamAbbrev"] = str(awayTeamAbbrev) - pbp_txt["plays"]["homeTeamNameAlt"] = str(homeTeamNameAlt) - pbp_txt["plays"]["awayTeamNameAlt"] = str(awayTeamNameAlt) - pbp_txt["plays"]["period.number"] = pbp_txt["plays"]["period.number"].apply( - lambda x: int(x) - ) - pbp_txt["plays"]["homeTeamSpread"] = np.where( - homeFavorite == True, abs(gameSpread), -1 * abs(gameSpread) - ) - pbp_txt["plays"]["gameSpread"] = gameSpread - pbp_txt["plays"]["gameSpreadAvailable"] = gameSpreadAvailable - pbp_txt["plays"]["overUnder"] = float(overUnder) - pbp_txt["plays"]["homeFavorite"] = homeFavorite - pbp_txt["homeTeamSpread"] = np.where( - homeFavorite == True, abs(gameSpread), -1 * abs(gameSpread) - ) - pbp_txt["gameSpread"] = gameSpread - pbp_txt["gameSpreadAvailable"] = gameSpreadAvailable - pbp_txt["overUnder"] = float(overUnder) - pbp_txt["homeFavorite"] = homeFavorite - - # ----- Figuring out Timeouts --------- - pbp_txt["timeouts"] = {} - pbp_txt["timeouts"][homeTeamId] = {"1": [], "2": []} - pbp_txt["timeouts"][awayTeamId] = {"1": [], "2": []} - - # ----- Time --------------- - pbp_txt["plays"]["clock.mm"] = pbp_txt["plays"][ - "clock.displayValue" - ].str.split(pat=":") - pbp_txt["plays"][["clock.minutes", "clock.seconds"]] = pbp_txt["plays"][ - "clock.mm" - ].to_list() - pbp_txt["plays"]["half"] = np.where( - pbp_txt["plays"]["period.number"] <= 2, "1", "2" - ) - pbp_txt["plays"]["lag_half"] = pbp_txt["plays"]["half"].shift(1) - pbp_txt["plays"]["lead_half"] = pbp_txt["plays"]["half"].shift(-1) - pbp_txt["plays"]["start.TimeSecsRem"] = np.where( - pbp_txt["plays"]["period.number"].isin([1, 3]), - 900 - + 60 * pbp_txt["plays"]["clock.minutes"].astype(int) - + pbp_txt["plays"]["clock.seconds"].astype(int), - 60 * pbp_txt["plays"]["clock.minutes"].astype(int) - + pbp_txt["plays"]["clock.seconds"].astype(int), - ) - pbp_txt["plays"]["start.adj_TimeSecsRem"] = np.select( - [ - pbp_txt["plays"]["period.number"] == 1, - pbp_txt["plays"]["period.number"] == 2, - pbp_txt["plays"]["period.number"] == 3, - pbp_txt["plays"]["period.number"] == 4, - ], - [ - 2700 - + 60 * pbp_txt["plays"]["clock.minutes"].astype(int) - + pbp_txt["plays"]["clock.seconds"].astype(int), - 1800 - + 60 * pbp_txt["plays"]["clock.minutes"].astype(int) - + pbp_txt["plays"]["clock.seconds"].astype(int), - 900 - + 60 * pbp_txt["plays"]["clock.minutes"].astype(int) - + pbp_txt["plays"]["clock.seconds"].astype(int), - 60 * pbp_txt["plays"]["clock.minutes"].astype(int) - + pbp_txt["plays"]["clock.seconds"].astype(int), - ], - default=60 * pbp_txt["plays"]["clock.minutes"].astype(int) - + pbp_txt["plays"]["clock.seconds"].astype(int), - ) - # Pos Team - Start and End Id - pbp_txt["plays"]["id"] = pbp_txt["plays"]["id"].apply(lambda x: int(x)) - pbp_txt["plays"] = pbp_txt["plays"].sort_values( - by=["id", "start.adj_TimeSecsRem"] - ) + pbp_txt["plays"] = pl.concat( + [pbp_txt["plays"], pl.from_pandas(prev_drives)], how='vertical' + ) + + # pbp_txt["plays"] = pbp_txt["plays"].to_dict(orient="records") + # pbp_txt["plays"] = pd.DataFrame(pbp_txt["plays"]) + pbp_txt["plays"] = pbp_txt["plays"].with_columns( + game_id = pl.lit(int(self.gameId)), + season = pbp_txt.get("header").get("season").get("year"), + seasonType = pbp_txt.get("header").get("season").get("type"), + week = pbp_txt.get("header").get("week"), + status_type_completed = pbp_txt.get("header").get("competitions")[0].get("status").get("type").get("completed"), + homeTeamId = pl.lit(homeTeamId), + awayTeamId = pl.lit(awayTeamId), + homeTeamName = pl.lit(str(homeTeamName)), + awayTeamName = pl.lit(str(awayTeamName)), + homeTeamMascot = pl.lit(str(homeTeamMascot)), + awayTeamMascot = pl.lit(str(awayTeamMascot)), + homeTeamAbbrev = pl.lit(str(homeTeamAbbrev)), + awayTeamAbbrev = pl.lit(str(awayTeamAbbrev)), + homeTeamNameAlt = pl.lit(str(homeTeamNameAlt)), + awayTeamNameAlt = pl.lit(str(awayTeamNameAlt)), + homeTeamSpread = pl.when(homeFavorite == True) + .then(abs(gameSpread)) + .otherwise(-1 * abs(gameSpread)), + gameSpread = pl.lit(gameSpread), + gameSpreadAvailable = pl.lit(gameSpreadAvailable), + overUnder = pl.lit(float(overUnder)), + homeFavorite = pl.lit(homeFavorite) + ) + + pbp_txt["plays"] = pbp_txt["plays"].with_columns( + pl.col("period.number").cast(pl.Int32), + pl.col("clock.displayValue").str.split(':').list.to_struct(n_field_strategy="max_width").alias("clock.mm") + ).with_columns( + pl.col("clock.mm").struct.rename_fields(["clock.minutes", "clock.seconds"]) + ).unnest("clock.mm" + ).with_columns( + pl.col("clock.minutes").cast(pl.Int32), + pl.col("clock.seconds").cast(pl.Int32), + half = pl.when(pl.col("period.number") <= 2).then(1).otherwise(2) + ).with_columns( + lag_half = pl.col("half").shift(1), + lead_half = pl.col("half").shift(-1) + ).with_columns( + pl.when(pl.col("period.number").is_in([1, 3])) + .then(900 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) + .otherwise(60 * pl.col("clock.minutes")+ pl.col("clock.seconds")) + .alias("start.TimeSecsRem"), + pl.when(pl.col("period.number") == 1) + .then(2700 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) + .when(pl.col("period.number") == 2) + .then(1800 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) + .when(pl.col("period.number") == 3) + .then( 900 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) + .otherwise(60 * pl.col("clock.minutes") + pl.col("clock.seconds")) + .alias("start.adj_TimeSecsRem"), + pl.col("id").cast(pl.Int64) + ) + pbp_txt["plays"] = pbp_txt["plays"].sort(by=["id", "start.adj_TimeSecsRem"]) # drop play text dupes intelligently, even if they have different play_id values - pbp_txt["plays"]["text"] = pbp_txt["plays"]["text"].astype(str) - pbp_txt["plays"]["lead_text"] = pbp_txt["plays"]["text"].shift(-1) - pbp_txt["plays"]["lead_start_team"] = pbp_txt["plays"]["start.team.id"].shift(-1) - pbp_txt["plays"]["lead_start_yardsToEndzone"] = pbp_txt["plays"]["start.yardsToEndzone"].shift(-1) - pbp_txt["plays"]["lead_start_down"] = pbp_txt["plays"]["start.down"].shift(-1) - pbp_txt["plays"]["lead_start_distance"] = pbp_txt["plays"]["start.distance"].shift(-1) - pbp_txt["plays"]["lead_scoringPlay"] = pbp_txt["plays"]["scoringPlay"].shift(-1) - pbp_txt["plays"]["text_dupe"] = False - - def play_text_dupe_checker(row): - if (row["start.team.id"] == row["lead_start_team"]) and \ - (row["start.down"] == row["lead_start_down"]) and \ - (row["start.yardsToEndzone"] == row["lead_start_yardsToEndzone"]) and \ - (row["start.distance"] == row["lead_start_distance"]): - if (row["text"] == row["lead_text"]): - return True - if (row["text"] in row["lead_text"]) and \ - (row["lead_scoringPlay"] == row["scoringPlay"]): - return True - return False - - pbp_txt["plays"]["text_dupe"] = pbp_txt["plays"].apply(lambda x: play_text_dupe_checker(x), axis=1) - - pbp_txt["plays"] = pbp_txt["plays"][pbp_txt["plays"]["text_dupe"] == False] - - pbp_txt["plays"]["game_play_number"] = np.arange(len(pbp_txt["plays"])) + 1 - pbp_txt["plays"]["start.team.id"] = ( - pbp_txt["plays"]["start.team.id"] - # fill downward first to make sure all playIDs are accurate - .fillna(method="ffill") - # fill upward so that any remaining NAs are covered - .fillna(method="bfill") - .apply(lambda x: int(x)) - ) - pbp_txt["plays"]["end.team.id"] = ( - pbp_txt["plays"]["end.team.id"] - .fillna(value=pbp_txt["plays"]["start.team.id"]) - .apply(lambda x: int(x)) - ) - pbp_txt["plays"]["start.pos_team.id"] = np.select( - [ - (pbp_txt["plays"]["type.text"].isin(kickoff_vec)) - & ( - pbp_txt["plays"]["start.team.id"].astype(int) - == pbp_txt["plays"]["homeTeamId"].astype(int) - ), - (pbp_txt["plays"]["type.text"].isin(kickoff_vec)) - & ( - pbp_txt["plays"]["start.team.id"].astype(int) - == pbp_txt["plays"]["awayTeamId"].astype(int) - ), - ], - [ - pbp_txt["plays"]["awayTeamId"].astype(int), - pbp_txt["plays"]["homeTeamId"].astype(int), - ], - default=pbp_txt["plays"]["start.team.id"].astype(int), - ) - pbp_txt["plays"]["start.def_pos_team.id"] = np.where( - pbp_txt["plays"]["start.pos_team.id"].astype(int) - == pbp_txt["plays"]["homeTeamId"].astype(int), - pbp_txt["plays"]["awayTeamId"].astype(int), - pbp_txt["plays"]["homeTeamId"].astype(int), - ) - pbp_txt["plays"]["end.def_team.id"] = np.where( - pbp_txt["plays"]["end.team.id"].astype(int) - == pbp_txt["plays"]["homeTeamId"].astype(int), - pbp_txt["plays"]["awayTeamId"].astype(int), - pbp_txt["plays"]["homeTeamId"].astype(int), - ) - pbp_txt["plays"]["end.pos_team.id"] = pbp_txt["plays"]["end.team.id"].apply( - lambda x: int(x) - ) - pbp_txt["plays"]["end.def_pos_team.id"] = pbp_txt["plays"][ - "end.def_team.id" - ].apply(lambda x: int(x)) - pbp_txt["plays"]["start.pos_team.name"] = np.where( - pbp_txt["plays"]["start.pos_team.id"].astype(int) - == pbp_txt["plays"]["homeTeamId"].astype(int), - pbp_txt["plays"]["homeTeamName"], - pbp_txt["plays"]["awayTeamName"], - ) - pbp_txt["plays"]["start.def_pos_team.name"] = np.where( - pbp_txt["plays"]["start.pos_team.id"].astype(int) - == pbp_txt["plays"]["homeTeamId"].astype(int), - pbp_txt["plays"]["awayTeamName"], - pbp_txt["plays"]["homeTeamName"], - ) - pbp_txt["plays"]["end.pos_team.name"] = np.where( - pbp_txt["plays"]["end.pos_team.id"].astype(int) - == pbp_txt["plays"]["homeTeamId"].astype(int), - pbp_txt["plays"]["homeTeamName"], - pbp_txt["plays"]["awayTeamName"], - ) - pbp_txt["plays"]["end.def_pos_team.name"] = np.where( - pbp_txt["plays"]["end.pos_team.id"].astype(int) - == pbp_txt["plays"]["homeTeamId"].astype(int), - pbp_txt["plays"]["awayTeamName"], - pbp_txt["plays"]["homeTeamName"], - ) - pbp_txt["plays"]["start.is_home"] = np.where( - pbp_txt["plays"]["start.pos_team.id"].astype(int) - == pbp_txt["plays"]["homeTeamId"].astype(int), - True, - False, - ) - pbp_txt["plays"]["end.is_home"] = np.where( - pbp_txt["plays"]["end.pos_team.id"].astype(int) - == pbp_txt["plays"]["homeTeamId"].astype(int), - True, - False, - ) - pbp_txt["plays"]["homeTimeoutCalled"] = np.where( - (pbp_txt["plays"]["type.text"] == "Timeout") - & ( - ( - pbp_txt["plays"]["text"] - .str.lower() - .str.contains(str(homeTeamAbbrev), case=False) - ) - | ( - pbp_txt["plays"]["text"] - .str.lower() - .str.contains(str(homeTeamName), case=False) - ) - | ( - pbp_txt["plays"]["text"] - .str.lower() - .str.contains(str(homeTeamMascot), case=False) - ) - | ( - pbp_txt["plays"]["text"] - .str.lower() - .str.contains(str(homeTeamNameAlt), case=False) - ) - ), - True, - False, - ) - pbp_txt["plays"]["awayTimeoutCalled"] = np.where( - (pbp_txt["plays"]["type.text"] == "Timeout") - & ( - ( - pbp_txt["plays"]["text"] - .str.lower() - .str.contains(str(awayTeamAbbrev), case=False) - ) - | ( - pbp_txt["plays"]["text"] - .str.lower() - .str.contains(str(awayTeamName), case=False) - ) - | ( - pbp_txt["plays"]["text"] - .str.lower() - .str.contains(str(awayTeamMascot), case=False) - ) - | ( - pbp_txt["plays"]["text"] - .str.lower() - .str.contains(str(awayTeamNameAlt), case=False) - ) - ), - True, - False, - ) + pbp_txt["plays"] = pbp_txt["plays"].with_columns( + pl.col("text").cast(str), + orig_play_type = pl.col("type.text"), + lead_text = pl.col("text").shift(-1), + lead_start_team = pl.col("start.team.id").shift(-1), + lead_start_yardsToEndzone = pl.col("start.yardsToEndzone").shift(-1), + lead_start_down = pl.col("start.down").shift(-1), + lead_start_distance = pl.col("start.distance").shift(-1), + lead_scoringPlay = pl.col("scoringPlay").shift(-1), + text_dupe = pl.lit(False) + ).with_columns( + text_dupe = pl.when((pl.col("start.team.id") == pl.col("lead_start_team")) + .and_(pl.col("start.down") == pl.col("lead_start_down")) + .and_(pl.col("start.yardsToEndzone") == pl.col("lead_start_yardsToEndzone")) + .and_(pl.col("start.distance") == pl.col("lead_start_distance")) + .and_(pl.col("text") == pl.col("lead_text"))) + .then(pl.lit(True)) + .when((pl.col("start.team.id") == pl.col("lead_start_team")) + .and_(pl.col("start.down") == pl.col("lead_start_down")) + .and_(pl.col("start.yardsToEndzone") == pl.col("lead_start_yardsToEndzone")) + .and_(pl.col("start.distance") == pl.col("lead_start_distance")) + .and_(pl.col("text").is_in(pl.col("lead_text")))) + .then(pl.lit(True)) + .otherwise(pl.lit(False)) + ) + pbp_txt["plays"] = pbp_txt["plays"].filter(pl.col("text_dupe") == False) + pbp_txt["plays"] = pbp_txt["plays"].with_row_count("game_play_number", 1) + pbp_txt["plays"] = pbp_txt["plays"].with_columns( + pl.col("start.team.id").fill_null(strategy="forward") + .fill_null(strategy="backward").cast(pl.Int32) + ).with_columns( + pl.col("end.team.id").fill_null(value=pl.col("start.team.id")).cast(pl.Int32) + ).with_columns( + pl.col("start.team.id").cast(pl.Int32), + pl.col("end.team.id").cast(pl.Int32), + pl.col("homeTeamId").cast(pl.Int32), + pl.col("awayTeamId").cast(pl.Int32), + pl.when(pl.col("type.text").is_in(kickoff_vec) + .and_(pl.col("start.team.id") == homeTeamId)) + .then(pl.col("awayTeamId")) + .when(pl.col("type.text").is_in(kickoff_vec) + .and_(pl.col("start.team.id") == awayTeamId)) + .then(pl.col("homeTeamId")) + .otherwise(pl.col("start.team.id")) + .alias("start.pos_team.id") + ).with_columns( + pl.when(pl.col("start.pos_team.id") == homeTeamId) + .then(awayTeamId) + .otherwise(homeTeamId) + .alias("start.def_pos_team.id"), + pl.when(pl.col("end.team.id") == homeTeamId) + .then(awayTeamId) + .otherwise(homeTeamId) + .alias("end.def_pos_team.id"), + pl.col("end.team.id") + .alias("end.pos_team.id") + ).with_columns( + pl.when(pl.col("start.pos_team.id") == homeTeamId) + .then(pl.col("homeTeamName")) + .otherwise(pl.col("awayTeamName")) + .alias("start.pos_team.name"), + pl.when(pl.col("start.pos_team.id") == homeTeamId) + .then(pl.col("awayTeamName")) + .otherwise(pl.col("homeTeamName")) + .alias("start.def_pos_team.name"), + pl.when(pl.col("end.pos_team.id") == homeTeamId) + .then(pl.col("homeTeamName")) + .otherwise(pl.col("awayTeamName")) + .alias("end.pos_team.name"), + pl.when(pl.col("end.pos_team.id") == homeTeamId) + .then(pl.col("awayTeamName")) + .otherwise(pl.col("homeTeamName")) + .alias("end.def_pos_team.name"), + pl.when(pl.col("start.pos_team.id") == homeTeamId) + .then(True) + .otherwise(False) + .alias("start.is_home"), + pl.when(pl.col("end.pos_team.id") == homeTeamId) + .then(True) + .otherwise(False) + .alias("end.is_home"), + pl.when((pl.col("type.text") == "Timeout") + .and_( + pl.col("text").str.to_lowercase().str.contains(str(homeTeamAbbrev).lower()) + .or_( + pl.col("text").str.to_lowercase().str.contains(str(homeTeamAbbrev).lower()), + pl.col("text").str.to_lowercase().str.contains(str(homeTeamName).lower()), + pl.col("text").str.to_lowercase().str.contains(str(homeTeamMascot).lower()), + pl.col("text").str.to_lowercase().str.contains(str(homeTeamNameAlt).lower()) + ) + )) + .then(True) + .otherwise(False) + .alias("homeTimeoutCalled"), + pl.when((pl.col("type.text") == "Timeout") + .and_( + pl.col("text").str.to_lowercase().str.contains(str(awayTeamAbbrev).lower()) + .or_( + pl.col("text").str.to_lowercase().str.contains(str(awayTeamAbbrev).lower()), + pl.col("text").str.to_lowercase().str.contains(str(awayTeamName).lower()), + pl.col("text").str.to_lowercase().str.contains(str(awayTeamMascot).lower()), + pl.col("text").str.to_lowercase().str.contains(str(awayTeamNameAlt).lower()) + ) + )) + .then(True) + .otherwise(False) + .alias("awayTimeoutCalled") + ) + + pbp_txt["timeouts"] = { + homeTeamId: {"1": [], "2": []}, + awayTeamId: {"1": [], "2": []}, + } pbp_txt["timeouts"][homeTeamId]["1"] = ( pbp_txt["plays"] - .loc[ - (pbp_txt["plays"]["homeTimeoutCalled"] == True) - & (pbp_txt["plays"]["period.number"] <= 2) - ] - .reset_index()["id"] + .filter( + (pl.col("homeTimeoutCalled") == True) + .and_(pl.col("period.number") <= 2) + ) + .get_column("id") + .to_list() ) pbp_txt["timeouts"][homeTeamId]["2"] = ( pbp_txt["plays"] - .loc[ - (pbp_txt["plays"]["homeTimeoutCalled"] == True) - & (pbp_txt["plays"]["period.number"] > 2) - ] - .reset_index()["id"] + .filter( + (pl.col("homeTimeoutCalled") == True) + .and_(pl.col("period.number") > 2) + ) + .get_column("id") + .to_list() ) pbp_txt["timeouts"][awayTeamId]["1"] = ( pbp_txt["plays"] - .loc[ - (pbp_txt["plays"]["awayTimeoutCalled"] == True) - & (pbp_txt["plays"]["period.number"] <= 2) - ] - .reset_index()["id"] + .filter( + (pl.col("awayTimeoutCalled") == True) + .and_(pl.col("period.number") <= 2) + ) + .get_column("id") + .to_list() ) pbp_txt["timeouts"][awayTeamId]["2"] = ( pbp_txt["plays"] - .loc[ - (pbp_txt["plays"]["awayTimeoutCalled"] == True) - & (pbp_txt["plays"]["period.number"] > 2) - ] - .reset_index()["id"] - ) - - pbp_txt["timeouts"][homeTeamId]["1"] = pbp_txt["timeouts"][homeTeamId][ - "1" - ].apply(lambda x: int(x)) - pbp_txt["timeouts"][homeTeamId]["2"] = pbp_txt["timeouts"][homeTeamId][ - "2" - ].apply(lambda x: int(x)) - pbp_txt["timeouts"][awayTeamId]["1"] = pbp_txt["timeouts"][awayTeamId][ - "1" - ].apply(lambda x: int(x)) - pbp_txt["timeouts"][awayTeamId]["2"] = pbp_txt["timeouts"][awayTeamId][ - "2" - ].apply(lambda x: int(x)) - pbp_txt["plays"]["end.homeTeamTimeouts"] = ( - 3 - - pbp_txt["plays"] - .apply( - lambda x: ( - (pbp_txt["timeouts"][homeTeamId]["1"] <= x["id"]) - & (x["period.number"] <= 2) - ) - | ( - (pbp_txt["timeouts"][homeTeamId]["2"] <= x["id"]) - & (x["period.number"] > 2) - ), - axis=1, - ) - .apply(lambda x: int(x.sum()), axis=1) - ) - pbp_txt["plays"]["end.awayTeamTimeouts"] = ( - 3 - - pbp_txt["plays"] - .apply( - lambda x: ( - (pbp_txt["timeouts"][awayTeamId]["1"] <= x["id"]) - & (x["period.number"] <= 2) - ) - | ( - (pbp_txt["timeouts"][awayTeamId]["2"] <= x["id"]) - & (x["period.number"] > 2) - ), - axis=1, - ) - .apply(lambda x: int(x.sum()), axis=1) - ) - pbp_txt["plays"]["start.homeTeamTimeouts"] = pbp_txt["plays"][ - "end.homeTeamTimeouts" - ].shift(1) - pbp_txt["plays"]["start.awayTeamTimeouts"] = pbp_txt["plays"][ - "end.awayTeamTimeouts" - ].shift(1) - pbp_txt["plays"]["start.homeTeamTimeouts"] = np.where( - (pbp_txt["plays"]["game_play_number"] == 1) - | ( - (pbp_txt["plays"]["half"] == "2") - & (pbp_txt["plays"]["lag_half"] == "1") - ), - 3, - pbp_txt["plays"]["start.homeTeamTimeouts"], - ) - pbp_txt["plays"]["start.awayTeamTimeouts"] = np.where( - (pbp_txt["plays"]["game_play_number"] == 1) - | ( - (pbp_txt["plays"]["half"] == "2") - & (pbp_txt["plays"]["lag_half"] == "1") - ), - 3, - pbp_txt["plays"]["start.awayTeamTimeouts"], - ) - pbp_txt["plays"]["start.homeTeamTimeouts"] = pbp_txt["plays"][ - "start.homeTeamTimeouts" - ].apply(lambda x: int(x)) - pbp_txt["plays"]["start.awayTeamTimeouts"] = pbp_txt["plays"][ - "start.awayTeamTimeouts" - ].apply(lambda x: int(x)) - pbp_txt["plays"]["end.TimeSecsRem"] = pbp_txt["plays"][ - "start.TimeSecsRem" - ].shift(1) - pbp_txt["plays"]["end.adj_TimeSecsRem"] = pbp_txt["plays"][ - "start.adj_TimeSecsRem" - ].shift(1) - pbp_txt["plays"]["end.TimeSecsRem"] = np.where( - (pbp_txt["plays"]["game_play_number"] == 1) - | ( - (pbp_txt["plays"]["half"] == "2") - & (pbp_txt["plays"]["lag_half"] == "1") - ), - 1800, - pbp_txt["plays"]["end.TimeSecsRem"], - ) - pbp_txt["plays"]["end.adj_TimeSecsRem"] = np.select( - [ - (pbp_txt["plays"]["game_play_number"] == 1), - ( - (pbp_txt["plays"]["half"] == "2") - & (pbp_txt["plays"]["lag_half"] == "1") - ), - ], - [3600, 1800], - default=pbp_txt["plays"]["end.adj_TimeSecsRem"], - ) - pbp_txt["plays"]["start.posTeamTimeouts"] = np.where( - pbp_txt["plays"]["start.pos_team.id"] == pbp_txt["plays"]["homeTeamId"], - pbp_txt["plays"]["start.homeTeamTimeouts"], - pbp_txt["plays"]["start.awayTeamTimeouts"], - ) - pbp_txt["plays"]["start.defPosTeamTimeouts"] = np.where( - pbp_txt["plays"]["start.def_pos_team.id"] - == pbp_txt["plays"]["homeTeamId"], - pbp_txt["plays"]["start.homeTeamTimeouts"], - pbp_txt["plays"]["start.awayTeamTimeouts"], - ) - pbp_txt["plays"]["end.posTeamTimeouts"] = np.where( - pbp_txt["plays"]["end.pos_team.id"] == pbp_txt["plays"]["homeTeamId"], - pbp_txt["plays"]["end.homeTeamTimeouts"], - pbp_txt["plays"]["end.awayTeamTimeouts"], - ) - pbp_txt["plays"]["end.defPosTeamTimeouts"] = np.where( - pbp_txt["plays"]["end.def_pos_team.id"] - == pbp_txt["plays"]["homeTeamId"], - pbp_txt["plays"]["end.homeTeamTimeouts"], - pbp_txt["plays"]["end.awayTeamTimeouts"], - ) + .filter( + (pl.col("awayTimeoutCalled") == True) + .and_(pl.col("period.number") > 2) + ) + .get_column("id") + .to_list() + ) + pbp_txt["plays"] = pbp_txt["plays"].with_columns( + pl.when(( + (pbp_txt["timeouts"][homeTeamId]["1"] <= pl.col("id")) + .and_(pl.col("period.number") <= 2, + len(pbp_txt["timeouts"][homeTeamId]["1"]) == 1) + ).or_( + (pbp_txt["timeouts"][homeTeamId]["2"] <= pl.col("id")) + .and_(pl.col("period.number") > 2, + len(pbp_txt["timeouts"][homeTeamId]["2"]) == 1) + )) + .then(2) + .when(( + (pbp_txt["timeouts"][homeTeamId]["1"] <= pl.col("id")) + .and_(pl.col("period.number") <= 2, + len(pbp_txt["timeouts"][homeTeamId]["1"]) == 2) + ).or_( + (pbp_txt["timeouts"][homeTeamId]["2"] <= pl.col("id")) + .and_(pl.col("period.number") > 2, + len(pbp_txt["timeouts"][homeTeamId]["2"]) == 2) + )) + .then(1) + .when(( + (pbp_txt["timeouts"][homeTeamId]["1"] <= pl.col("id")) + .and_(pl.col("period.number") <= 2, + len(pbp_txt["timeouts"][homeTeamId]["1"]) == 3) + ).or_( + (pbp_txt["timeouts"][homeTeamId]["2"] <= pl.col("id")) + .and_(pl.col("period.number") > 2, + len(pbp_txt["timeouts"][homeTeamId]["2"]) == 3) + )) + .then(0) + .otherwise(3) + .alias("end.homeTeamTimeouts"), + pl.when(( + (pbp_txt["timeouts"][awayTeamId]["1"] <= pl.col("id")) + .and_(pl.col("period.number") <= 2, + len(pbp_txt["timeouts"][awayTeamId]["1"]) == 1) + ).or_( + (pbp_txt["timeouts"][awayTeamId]["2"] <= pl.col("id")) + .and_(pl.col("period.number") > 2, + len(pbp_txt["timeouts"][awayTeamId]["2"]) == 1) + )) + .then(2) + .when(( + (pbp_txt["timeouts"][awayTeamId]["1"] <= pl.col("id")) + .and_(pl.col("period.number") <= 2, + len(pbp_txt["timeouts"][awayTeamId]["1"]) == 2) + ).or_( + (pbp_txt["timeouts"][awayTeamId]["2"] <= pl.col("id")) + .and_(pl.col("period.number") > 2, + len(pbp_txt["timeouts"][awayTeamId]["2"]) == 2) + )) + .then(1) + .when(( + (pbp_txt["timeouts"][awayTeamId]["1"] <= pl.col("id")) + .and_(pl.col("period.number") <= 2, + len(pbp_txt["timeouts"][awayTeamId]["1"]) == 3) + ).or_( + (pbp_txt["timeouts"][awayTeamId]["2"] <= pl.col("id")) + .and_(pl.col("period.number") > 2, + len(pbp_txt["timeouts"][awayTeamId]["2"]) == 3) + )) + .then(0) + .otherwise(3) + .alias("end.awayTeamTimeouts") + ).with_columns( + pl.col("end.homeTeamTimeouts").shift_and_fill(periods = 1, fill_value = 3) + .alias("start.homeTeamTimeouts"), + pl.col("end.awayTeamTimeouts").shift_and_fill(periods = 1, fill_value = 3) + .alias("start.awayTeamTimeouts"), + pl.col("start.TimeSecsRem").shift(periods = 1) + .alias("end.TimeSecsRem"), + pl.col("start.adj_TimeSecsRem").shift(periods = 1) + .alias("end.adj_TimeSecsRem") + ).with_columns( + pl.when(pl.col("game_play_number") == 1) + .then(pl.lit(1800)) + .when((pl.col("half") == 2) & (pl.col("lag_half") == 1)) + .then(pl.lit(1800)) + .otherwise(pl.col("end.TimeSecsRem")) + .alias("end.TimeSecsRem"), + pl.when(pl.col("game_play_number") == 1) + .then(pl.lit(3600)) + .when((pl.col("half") == 2) & (pl.col("lag_half") == 1)) + .then(pl.lit(1800)) + .otherwise(pl.col("end.adj_TimeSecsRem")) + .alias("end.adj_TimeSecsRem"), + pl.when(pl.col("start.pos_team.id") == pl.col("homeTeamId")) + .then(pl.col("start.homeTeamTimeouts")) + .otherwise(pl.col("start.awayTeamTimeouts")) + .alias("start.posTeamTimeouts"), + pl.when(pl.col("start.pos_team.id") == pl.col("homeTeamId")) + .then(pl.col("start.awayTeamTimeouts")) + .otherwise(pl.col("start.homeTeamTimeouts")) + .alias("start.defPosTeamTimeouts"), + pl.when(pl.col("end.pos_team.id") == pl.col("homeTeamId")) + .then(pl.col("end.homeTeamTimeouts")) + .otherwise(pl.col("end.awayTeamTimeouts")) + .alias("end.posTeamTimeouts"), + pl.when(pl.col("end.pos_team.id") == pl.col("homeTeamId")) + .then(pl.col("end.awayTeamTimeouts")) + .otherwise(pl.col("end.homeTeamTimeouts")) + .alias("end.defPosTeamTimeouts"), + pl.when((pl.col("game_play_number") == 1) + .and_(pl.col("type.text").is_in(kickoff_vec), + pl.col("start.pos_team.id") == pl.col("homeTeamId"))) + .then(pl.col("homeTeamId")) + .otherwise(pl.col("awayTeamId")) + .alias("firstHalfKickoffTeamId"), + pl.col("period.number").alias("period"), + pl.when(pl.col("start.team.id") == pl.col("homeTeamId")) + .then(pl.lit(100) - pl.col("start.yardLine")) + .otherwise(pl.col("start.yardLine")) + .alias("start.yard"), + ).with_columns( + pl.when(pl.col("start.yardLine").is_null() == False) + .then(pl.col("start.yardLine")) + .otherwise(pl.col("start.yard")) + .alias("start.yardLine"), + ).with_columns( + pl.when(pl.col("start.yardLine").is_null() == False) + .then(pl.col("start.yardsToEndzone")) + .otherwise(pl.col("start.yardLine")) + .alias("start.yardsToEndzone") + ).with_columns( + pl.when(pl.col("start.yardsToEndzone") == 0) + .then(pl.col("start.yard")) + .otherwise(pl.col("start.yardsToEndzone")) + .alias("start.yardsToEndzone"), + pl.when(pl.col("end.team.id") == pl.col("homeTeamId")) + .then(pl.lit(100) - pl.col("end.yardLine")) + .otherwise(pl.col("end.yardLine")) + .alias("end.yard") + ).with_columns( + pl.when((pl.col("type.text") == "Penalty") + .and_(pl.col("text").str.contains(r"(?i)declined"))) + .then(pl.col("start.yard")) + .otherwise(pl.col("end.yard")) + .alias("end.yard"), + ).with_columns( + pl.when(pl.col("end.yardLine").is_null() == False) + .then(pl.col("end.yardsToEndzone")) + .otherwise(pl.col("end.yard")) + .alias("end.yardsToEndzone"), + pl.when((pl.col("start.distance") == 0) + .and_(pl.col("start.downDistanceText").str.contains(r"(?i)goal"))) + .then(pl.col("start.yardsToEndzone")) + .otherwise(pl.col("start.distance")) + .alias("start.distance"), + ).with_columns( + pl.when((pl.col("type.text") == "Penalty") + .and_(pl.col("text").str.contains(r"(?i)declined"))) + .then(pl.col("start.yardsToEndzone")) + .otherwise(pl.col("end.yardsToEndzone")) + .alias("end.yardsToEndzone"), + ) pbp_txt["firstHalfKickoffTeamId"] = np.where( (pbp_txt["plays"]["game_play_number"] == 1) - & (pbp_txt["plays"]["type.text"].isin(kickoff_vec)) - & (pbp_txt["plays"]["start.team.id"] == pbp_txt["plays"]["homeTeamId"]), - pbp_txt["plays"]["homeTeamId"], - pbp_txt["plays"]["awayTeamId"], - ) - pbp_txt["plays"]["firstHalfKickoffTeamId"] = pbp_txt[ - "firstHalfKickoffTeamId" - ] - pbp_txt["plays"]["period"] = pbp_txt["plays"]["period.number"] - pbp_txt["plays"]["start.yard"] = np.where( - (pbp_txt["plays"]["start.team.id"] == homeTeamId), - 100 - pbp_txt["plays"]["start.yardLine"], - pbp_txt["plays"]["start.yardLine"], - ) - pbp_txt["plays"]["start.yardsToEndzone"] = np.where( - pbp_txt["plays"]["start.yardLine"].isna() == False, - pbp_txt["plays"]["start.yardsToEndzone"], - pbp_txt["plays"]["start.yard"], - ) - pbp_txt["plays"]["start.yardsToEndzone"] = np.where( - pbp_txt["plays"]["start.yardsToEndzone"] == 0, - pbp_txt["plays"]["start.yard"], - pbp_txt["plays"]["start.yardsToEndzone"], - ) - pbp_txt["plays"]["end.yard"] = np.where( - (pbp_txt["plays"]["end.team.id"] == homeTeamId), - 100 - pbp_txt["plays"]["end.yardLine"], - pbp_txt["plays"]["end.yardLine"], - ) - pbp_txt["plays"]["end.yard"] = np.where( - (pbp_txt["plays"]["type.text"] == "Penalty") - & ( - pbp_txt["plays"]["text"].str.contains( - "declined", case=False, flags=0, na=False, regex=True - ) - ), - pbp_txt["plays"]["start.yard"], - pbp_txt["plays"]["end.yard"], - ) - pbp_txt["plays"]["end.yardsToEndzone"] = np.where( - pbp_txt["plays"]["end.yardLine"].isna() == False, - pbp_txt["plays"]["end.yardsToEndzone"], - pbp_txt["plays"]["end.yard"], - ) - pbp_txt["plays"]["end.yardsToEndzone"] = np.where( - (pbp_txt["plays"]["type.text"] == "Penalty") - & ( - pbp_txt["plays"]["text"].str.contains( - "declined", case=False, flags=0, na=False, regex=True - ) - ), - pbp_txt["plays"]["start.yardsToEndzone"], - pbp_txt["plays"]["end.yardsToEndzone"], - ) - - pbp_txt["plays"]["start.distance"] = np.where( - (pbp_txt["plays"]["start.distance"] == 0) - & ( - pbp_txt["plays"]["start.downDistanceText"] - .str.lower() - .str.contains("goal") - ), - pbp_txt["plays"]["start.yardsToEndzone"], - pbp_txt["plays"]["start.distance"], - ) - - pbp_txt["timeouts"][homeTeamId]["1"] = np.array( - pbp_txt["timeouts"][homeTeamId]["1"] - ).tolist() - pbp_txt["timeouts"][homeTeamId]["2"] = np.array( - pbp_txt["timeouts"][homeTeamId]["2"] - ).tolist() - pbp_txt["timeouts"][awayTeamId]["1"] = np.array( - pbp_txt["timeouts"][awayTeamId]["1"] - ).tolist() - pbp_txt["timeouts"][awayTeamId]["2"] = np.array( - pbp_txt["timeouts"][awayTeamId]["2"] - ).tolist() - if "scoringType.displayName" in pbp_txt["plays"].keys(): - pbp_txt["plays"]["type.text"] = np.where( - pbp_txt["plays"]["scoringType.displayName"] == "Field Goal", - "Field Goal Good", - pbp_txt["plays"]["type.text"], - ) - pbp_txt["plays"]["type.text"] = np.where( - pbp_txt["plays"]["scoringType.displayName"] == "Extra Point", - "Extra Point Good", - pbp_txt["plays"]["type.text"], - ) + & (pbp_txt["plays"]["type.text"].is_in(kickoff_vec)) + & (pbp_txt["plays"]["start.team.id"] == homeTeamId), + homeTeamId, + awayTeamId + ) + pbp_txt["firstHalfKickoffTeamId"] = pbp_txt["firstHalfKickoffTeamId"][0] + + if "scoringType.displayName" in pbp_txt["plays"].columns: + pbp_txt["plays"] = pbp_txt["plays"].with_columns( + pl.when(pl.col("scoringType.displayName") == "Field Goal") + .then("Field Goal Good") + .otherwise(pl.col("type.text")) + .alias("type.text") + ).with_columns( + pl.when(pl.col("scoringType.displayName") == "Extra Point") + .then("Extra Point Good") + .otherwise(pl.col("type.text")) + .alias("type.text") + ) + pbp_txt["plays"] = pbp_txt["plays"].with_columns( + pl.when(pl.col("type.text").is_null()) + .then("Unknown") + .otherwise(pl.col("type.text")) + .alias("type.text") + ).with_columns( + pl.when(pl.col("type.text").str.to_lowercase().str.contains("(?i)extra point") + .and_(pl.col("type.text").str.to_lowercase().str.contains("(?i)no good"))) + .then("Extra Point Missed") + .otherwise(pl.col("type.text")) + .alias("type.text") + ).with_columns( + pl.when(pl.col("type.text").str.to_lowercase().str.contains("(?i)extra point") + .and_(pl.col("type.text").str.to_lowercase().str.contains("(?i)blocked"))) + .then("Extra Point Missed") + .otherwise(pl.col("type.text")) + .alias("type.text") + ).with_columns( + pl.when(pl.col("type.text").str.to_lowercase().str.contains("(?i)field goal") + .and_(pl.col("type.text").str.to_lowercase().str.contains("(?i)blocked"))) + .then("Extra Point Missed") + .otherwise(pl.col("type.text")) + .alias("type.text") + ).with_columns( + pl.when(pl.col("type.text").str.to_lowercase().str.contains("(?i)field goal") + .and_(pl.col("type.text").str.to_lowercase().str.contains("(?i)no good"))) + .then("Extra Point Missed") + .otherwise(pl.col("type.text")) + .alias("type.text") + ) - pbp_txt["plays"]["playType"] = np.where( - pbp_txt["plays"]["type.text"].isna() == False, - pbp_txt["plays"]["type.text"], - "Unknown", - ) - pbp_txt["plays"]["type.text"] = np.where( - pbp_txt["plays"]["text"] - .str.lower() - .str.contains("extra point", case=False) - & pbp_txt["plays"]["text"] - .str.lower() - .str.contains("no good", case=False), - "Extra Point Missed", - pbp_txt["plays"]["type.text"], - ) - pbp_txt["plays"]["type.text"] = np.where( - pbp_txt["plays"]["text"] - .str.lower() - .str.contains("extra point", case=False) - & pbp_txt["plays"]["text"] - .str.lower() - .str.contains("blocked", case=False), - "Extra Point Missed", - pbp_txt["plays"]["type.text"], - ) - pbp_txt["plays"]["type.text"] = np.where( - pbp_txt["plays"]["text"] - .str.lower() - .str.contains("field goal", case=False) - & pbp_txt["plays"]["text"] - .str.lower() - .str.contains("blocked", case=False), - "Blocked Field Goal", - pbp_txt["plays"]["type.text"], - ) - pbp_txt["plays"]["type.text"] = np.where( - pbp_txt["plays"]["text"] - .str.lower() - .str.contains("field goal", case=False) - & pbp_txt["plays"]["text"] - .str.lower() - .str.contains("no good", case=False), - "Field Goal Missed", - pbp_txt["plays"]["type.text"], - ) - del pbp_txt["plays"]["clock.mm"] - pbp_txt["plays"] = pbp_txt["plays"].replace({np.nan: None}) return pbp_txt def __helper_cfb_pbp(self, pbp_txt): @@ -817,407 +677,43 @@ def __helper_cfb_pickcenter(self, pbp_txt): gameSpreadAvailable = False return gameSpread, overUnder, homeFavorite, gameSpreadAvailable - def __setup_penalty_data(self, play_df): + def __add_downs_data(self, play_df): """ Creates the following columns in play_df: - * Penalty flag - * Penalty declined - * Penalty no play - * Penalty off-set - * Penalty 1st down conversion - * Penalty in text - * Yds Penalty + * id, drive_id, game_id + * down, ydstogo (distance), game_half, period """ - ##-- 'Penalty' in play text ---- - # -- T/F flag conditions penalty_flag - play_df["penalty_flag"] = False - play_df.loc[(play_df["type.text"] == "Penalty"), "penalty_flag"] = True - play_df.loc[ - play_df["text"].str.contains( - "penalty", case=False, flags=0, na=False, regex=True - ), - "penalty_flag", - ] = True - - # -- T/F flag conditions penalty_declined - play_df["penalty_declined"] = False - play_df.loc[ - (play_df["type.text"] == "Penalty") - & ( - play_df["text"].str.contains( - "declined", case=False, flags=0, na=False, regex=True - ) - ), - "penalty_declined", - ] = True - play_df.loc[ - ( - play_df["text"].str.contains( - "penalty", case=False, flags=0, na=False, regex=True - ) - ) - & ( - play_df["text"].str.contains( - "declined", case=False, flags=0, na=False, regex=True - ) - ), - "penalty_declined", - ] = True - - # -- T/F flag conditions penalty_no_play - play_df["penalty_no_play"] = False - play_df.loc[ - (play_df["type.text"] == "Penalty") - & ( - play_df["text"].str.contains( - "no play", case=False, flags=0, na=False, regex=True - ) - ), - "penalty_no_play", - ] = True - play_df.loc[ - ( - play_df["text"].str.contains( - "penalty", case=False, flags=0, na=False, regex=True - ) - ) - & ( - play_df["text"].str.contains( - "no play", case=False, flags=0, na=False, regex=True - ) - ), - "penalty_no_play", - ] = True - - # -- T/F flag conditions penalty_offset - play_df["penalty_offset"] = False - play_df.loc[ - (play_df["type.text"] == "Penalty") - & ( - play_df["text"].str.contains( - r"off-setting", case=False, flags=0, na=False, regex=True - ) - ), - "penalty_offset", - ] = True - play_df.loc[ - ( - play_df["text"].str.contains( - "penalty", case=False, flags=0, na=False, regex=True - ) - ) - & ( - play_df["text"].str.contains( - r"off-setting", case=False, flags=0, na=False, regex=True - ) - ), - "penalty_offset", - ] = True - - # -- T/F flag conditions penalty_1st_conv - play_df["penalty_1st_conv"] = False - play_df.loc[ - (play_df["type.text"] == "Penalty") - & ( - play_df["text"].str.contains( - "1st down", case=False, flags=0, na=False, regex=True - ) - ), - "penalty_1st_conv", - ] = True - play_df.loc[ - ( - play_df["text"].str.contains( - "penalty", case=False, flags=0, na=False, regex=True - ) - ) - & ( - play_df["text"].str.contains( - "1st down", case=False, flags=0, na=False, regex=True - ) - ), - "penalty_1st_conv", - ] = True - - # -- T/F flag for penalty text but not penalty play type -- - play_df["penalty_in_text"] = False - play_df.loc[ - ( - play_df["text"].str.contains( - "penalty", case=False, flags=0, na=False, regex=True - ) - ) - & (~(play_df["type.text"] == "Penalty")) - & ( - ~play_df["text"].str.contains( - "declined", case=False, flags=0, na=False, regex=True - ) - ) - & ( - ~play_df["text"].str.contains( - r"off-setting", case=False, flags=0, na=False, regex=True - ) - ) - & ( - ~play_df["text"].str.contains( - "no play", case=False, flags=0, na=False, regex=True - ) - ), - "penalty_in_text", - ] = True - - play_df["penalty_detail"] = np.select( - [ - (play_df.penalty_offset == 1), - (play_df.penalty_declined == 1), - play_df.text.str.contains(" roughing passer ", case=False, regex=True), - play_df.text.str.contains( - " offensive holding ", case=False, regex=True - ), - play_df.text.str.contains(" pass interference", case=False, regex=True), - play_df.text.str.contains(" encroachment", case=False, regex=True), - play_df.text.str.contains( - " defensive pass interference ", case=False, regex=True - ), - play_df.text.str.contains( - " offensive pass interference ", case=False, regex=True - ), - play_df.text.str.contains( - " illegal procedure ", case=False, regex=True - ), - play_df.text.str.contains( - " defensive holding ", case=False, regex=True - ), - play_df.text.str.contains(" holding ", case=False, regex=True), - play_df.text.str.contains( - " offensive offside | offside offense", case=False, regex=True - ), - play_df.text.str.contains( - " defensive offside | offside defense", case=False, regex=True - ), - play_df.text.str.contains(" offside ", case=False, regex=True), - play_df.text.str.contains( - " illegal fair catch signal ", case=False, regex=True - ), - play_df.text.str.contains(" illegal batting ", case=False, regex=True), - play_df.text.str.contains( - " neutral zone infraction ", case=False, regex=True - ), - play_df.text.str.contains( - " ineligible downfield ", case=False, regex=True - ), - play_df.text.str.contains( - " illegal use of hands ", case=False, regex=True - ), - play_df.text.str.contains( - " kickoff out of bounds | kickoff out-of-bounds ", - case=False, - regex=True, - ), - play_df.text.str.contains( - " 12 men on the field ", case=False, regex=True - ), - play_df.text.str.contains(" illegal block ", case=False, regex=True), - play_df.text.str.contains(" personal foul ", case=False, regex=True), - play_df.text.str.contains(" false start ", case=False, regex=True), - play_df.text.str.contains( - " substitution infraction ", case=False, regex=True - ), - play_df.text.str.contains( - " illegal formation ", case=False, regex=True - ), - play_df.text.str.contains(" illegal touching ", case=False, regex=True), - play_df.text.str.contains( - " sideline interference ", case=False, regex=True - ), - play_df.text.str.contains(" clipping ", case=False, regex=True), - play_df.text.str.contains( - " sideline infraction ", case=False, regex=True - ), - play_df.text.str.contains(" crackback ", case=False, regex=True), - play_df.text.str.contains(" illegal snap ", case=False, regex=True), - play_df.text.str.contains( - " illegal helmet contact ", case=False, regex=True - ), - play_df.text.str.contains(" roughing holder ", case=False, regex=True), - play_df.text.str.contains( - " horse collar tackle ", case=False, regex=True - ), - play_df.text.str.contains( - " illegal participation ", case=False, regex=True - ), - play_df.text.str.contains(" tripping ", case=False, regex=True), - play_df.text.str.contains(" illegal shift ", case=False, regex=True), - play_df.text.str.contains(" illegal motion ", case=False, regex=True), - play_df.text.str.contains( - " roughing the kicker ", case=False, regex=True - ), - play_df.text.str.contains(" delay of game ", case=False, regex=True), - play_df.text.str.contains(" targeting ", case=False, regex=True), - play_df.text.str.contains(" face mask ", case=False, regex=True), - play_df.text.str.contains( - " illegal forward pass ", case=False, regex=True - ), - play_df.text.str.contains( - " intentional grounding ", case=False, regex=True - ), - play_df.text.str.contains(" illegal kicking ", case=False, regex=True), - play_df.text.str.contains(" illegal conduct ", case=False, regex=True), - play_df.text.str.contains( - " kick catching interference ", case=False, regex=True - ), - play_df.text.str.contains( - " unnecessary roughness ", case=False, regex=True - ), - play_df.text.str.contains("Penalty, UR"), - play_df.text.str.contains( - " unsportsmanlike conduct ", case=False, regex=True - ), - play_df.text.str.contains( - " running into kicker ", case=False, regex=True - ), - play_df.text.str.contains( - " failure to wear required equipment ", case=False, regex=True - ), - play_df.text.str.contains( - " player disqualification ", case=False, regex=True - ), - (play_df.penalty_flag == True), - ], - [ - "Off-Setting", - "Penalty Declined", - "Roughing the Passer", - "Offensive Holding", - "Pass Interference", - "Encroachment", - "Defensive Pass Interference", - "Offensive Pass Interference", - "Illegal Procedure", - "Defensive Holding", - "Holding", - "Offensive Offside", - "Defensive Offside", - "Offside", - "Illegal Fair Catch Signal", - "Illegal Batting", - "Neutral Zone Infraction", - "Ineligible Man Down-Field", - "Illegal Use of Hands", - "Kickoff Out-of-Bounds", - "12 Men on the Field", - "Illegal Block", - "Personal Foul", - "False Start", - "Substitution Infraction", - "Illegal Formation", - "Illegal Touching", - "Sideline Interference", - "Clipping", - "Sideline Infraction", - "Crackback", - "Illegal Snap", - "Illegal Helmet contact", - "Roughing the Holder", - "Horse-Collar Tackle", - "Illegal Participation", - "Tripping", - "Illegal Shift", - "Illegal Motion", - "Roughing the Kicker", - "Delay of Game", - "Targeting", - "Face Mask", - "Illegal Forward Pass", - "Intentional Grounding", - "Illegal Kicking", - "Illegal Conduct", - "Kick Catch Interference", - "Unnecessary Roughness", - "Unnecessary Roughness", - "Unsportsmanlike Conduct", - "Running Into Kicker", - "Failure to Wear Required Equipment", - "Player Disqualification", - "Missing", - ], - default=None, - ) + play_df = play_df.sort(by=["id", "start.adj_TimeSecsRem"]) - play_df["penalty_text"] = np.select( - [(play_df.penalty_flag == True)], - [play_df.text.str.extract(r"Penalty(.+)", flags=re.IGNORECASE)[0]], - default=None, - ) - play_df["yds_penalty"] = np.select( - [(play_df.penalty_flag == True)], - [ - play_df.penalty_text.str.extract( - "(.{0,3})yards|yds|yd to the ", flags=re.IGNORECASE - )[0] - ], - default=None, + play_df = play_df.unique( + subset=["text", "id", "type.text", "start.down", "sequenceNumber"], keep="last", maintain_order=True ) - play_df["yds_penalty"] = play_df["yds_penalty"].str.replace( - " yards to the | yds to the | yd to the ", "" + play_df = play_df.filter( + pl.col("type.text").str.contains( + "(?i)end of|(?i)coin toss|(?i)end period|(?i)wins toss" + ) == False ) - play_df["yds_penalty"] = np.select( - [ - (play_df.penalty_flag == True) - & (play_df.text.str.contains(r"ards\)", case=False, regex=True)) - & (play_df.yds_penalty.isna()), - ], - [ - play_df.text.str.extract( - r"(.{0,4})yards\)|Yards\)|yds\)|Yds\)", flags=re.IGNORECASE - )[0] - ], - default=play_df.yds_penalty, - ) - play_df["yds_penalty"] = play_df.yds_penalty.str.replace( - "yards\\)|Yards\\)|yds\\)|Yds\\)", "" - ).str.replace("\\(", "") - return play_df + play_df = play_df.with_columns( + period = pl.col("period.number"), + half = pl.when(pl.col("period.number") <= 2).then(1).otherwise(2), + ).with_columns( + lead_half = pl.col("half").shift(-1), + lag_scoringPlay = pl.col("scoringPlay").shift(1), - def __add_downs_data(self, play_df): - """ - Creates the following columns in play_df: - * id, drive_id, game_id - * down, ydstogo (distance), game_half, period - """ - play_df = play_df.copy(deep=True) - play_df.loc[:, "id"] = play_df["id"].astype(float) - play_df.sort_values(by=["id", "start.adj_TimeSecsRem"], inplace=True) - play_df.drop_duplicates( - subset=["text", "id", "type.text", "start.down", "sequenceNumber"], keep="last", inplace=True + ).with_columns( + pl.when(pl.col("lead_half").is_null()).then(2).otherwise(pl.col("lead_half")).alias("lead_half"), + end_of_half = pl.col("half") != pl.col("lead_half"), + down_1 = pl.col("start.down") == 1, + down_2 = pl.col("start.down") == 2, + down_3 = pl.col("start.down") == 3, + down_4 = pl.col("start.down") == 4, + down_1_end = pl.col("end.down") == 1, + down_2_end = pl.col("end.down") == 2, + down_3_end = pl.col("end.down") == 3, + down_4_end = pl.col("end.down") == 4 ) - play_df = play_df[ - ( - play_df["type.text"].str.contains( - "end of|coin toss|end period|wins toss", case=False, regex=True - ) - == False - ) - ] - play_df.loc[:, "period"] = play_df["period.number"].astype(int) - play_df.loc[(play_df.period <= 2), "half"] = 1 - play_df.loc[(play_df.period > 2), "half"] = 2 - play_df["lead_half"] = play_df.half.shift(-1) - play_df["lag_scoringPlay"] = play_df.scoringPlay.shift(1) - play_df.loc[play_df.lead_half.isna() == True, "lead_half"] = 2 - play_df["end_of_half"] = play_df.half != play_df.lead_half - - play_df["down_1"] = play_df["start.down"] == 1 - play_df["down_2"] = play_df["start.down"] == 2 - play_df["down_3"] = play_df["start.down"] == 3 - play_df["down_4"] = play_df["start.down"] == 4 - - play_df["down_1_end"] = play_df["end.down"] == 1 - play_df["down_2_end"] = play_df["end.down"] == 2 - play_df["down_3_end"] = play_df["end.down"] == 3 - play_df["down_4_end"] = play_df["end.down"] == 4 return play_df def __add_play_type_flags(self, play_df): @@ -1226,170 +722,94 @@ def __add_play_type_flags(self, play_df): * Flags for fumbles, scores, kickoffs, punts, field goals """ # --- Touchdown, Fumble, Special Teams flags ----------------- - play_df.loc[:, "scoring_play"] = False - play_df.loc[play_df["type.text"].isin(scores_vec), "scoring_play"] = True - play_df["td_play"] = play_df.text.str.contains( - r"touchdown|for a TD", case=False, flags=0, na=False, regex=True - ) - play_df["touchdown"] = play_df["type.text"].str.contains( - "touchdown", case=False, flags=0, na=False, regex=True - ) - ## Portion of touchdown check for plays where touchdown is not listed in the play_type-- - play_df["td_check"] = play_df["text"].str.contains( - "Touchdown", case=False, flags=0, na=False, regex=True - ) - play_df["safety"] = play_df["text"].str.contains( - "safety", case=False, flags=0, na=False, regex=True - ) - # --- Fumbles---- - play_df["fumble_vec"] = np.select( - [ - play_df["text"].str.contains("fumble", case=False, flags=0, na=False, regex=True), - (~play_df["text"].str.contains("fumble", case=False, flags=0, na=False, regex=True)) & (play_df["type.text"] == "Rush") & (play_df["start.pos_team.id"] != play_df["end.pos_team.id"]), - (~play_df["text"].str.contains("fumble", case=False, flags=0, na=False, regex=True)) & (play_df["type.text"] == "Sack") & (play_df["start.pos_team.id"] != play_df["end.pos_team.id"]), - ], - [ - True, - True, - True - ], - default=False, - ) - play_df["forced_fumble"] = play_df["text"].str.contains( - "forced by", case=False, flags=0, na=False, regex=True - ) - # --- Kicks---- - play_df["kickoff_play"] = play_df["type.text"].isin(kickoff_vec) - play_df["kickoff_tb"] = np.select( - [ - ( - play_df["text"].str.contains( - "touchback", case=False, flags=0, na=False, regex=True - ) - ) - & (play_df.kickoff_play == True), - ( - play_df["text"].str.contains( - "kickoff$", case=False, flags=0, na=False, regex=True - ) - ) - & (play_df.kickoff_play == True), - ], - [True, True], - default=False, - ) - play_df["kickoff_onside"] = ( - play_df["text"].str.contains( - r"on-side|onside|on side", case=False, flags=0, na=False, regex=True - ) - ) & (play_df.kickoff_play == True) - play_df["kickoff_oob"] = ( - play_df["text"].str.contains( - r"out-of-bounds|out of bounds", - case=False, - flags=0, - na=False, - regex=True, - ) - ) & (play_df.kickoff_play == True) + play_df = play_df.with_columns( + scoring_play = pl.when(pl.col("type.text").is_in(scores_vec)).then(True).otherwise(False), + td_play = pl.col("text").str.contains("(?i)touchdown|(?i)for a TD"), + touchdown = pl.col("type.text").str.contains("(?i)touchdown"), + ## Portion of touchdown check for plays where touchdown is not listed in the play_type-- + td_check = pl.col("text").str.contains("(?i)touchdown"), + safety = pl.col("text").str.contains("(?i)safety"), + fumble_vec = pl.when(pl.col("text").str.contains("(?i)fumble")) + .then(True) + .when((pl.col("text").str.contains("(?i)fumble")) + .and_(pl.col("type.text") == "Rush", + pl.col("start.pos_team.id") != pl.col("end.pos_team.id"))) + .then(True) + .when((pl.col("text").str.contains("(?i)fumble")) + .and_(pl.col("type.text") == "Sack", + pl.col("start.pos_team.id") != pl.col("end.pos_team.id"))) + .then(True) + .otherwise(False), + forced_fumble = pl.when(pl.col("text").str.contains("(?i)forced by")) + .then(True) + .otherwise(False), + # --- Kicks---- + kickoff_play = pl.col("type.text").is_in(kickoff_vec), + ).with_columns( + kickoff_tb = pl.when((pl.col("text").str.contains("(?i)touchback")) + .and_(pl.col("kickoff_play") == True)) + .then(True) + .when((pl.col("text").str.contains("(?i)kickoff$")) + .and_(pl.col("kickoff_play") == True)) + .then(True) + .otherwise(False), + kickoff_onside = pl.when((pl.col("text").str.contains("(?i)on-side|(?i)onside|(?i)on side")) + .and_(pl.col("kickoff_play") == True)) + .then(True) + .otherwise(False), + kickoff_oob = pl.when((pl.col("text").str.contains("(?i)out-of-bounds|(?i)out of bounds")) + .and_(pl.col("kickoff_play") == True)) + .then(True) + .otherwise(False), + kickoff_fair_catch = pl.when((pl.col("text").str.contains("(?i)fair catch|(?i)fair caught")) + .and_(pl.col("kickoff_play") == True)) + .then(True) + .otherwise(False), + kickoff_downed = pl.when((pl.col("text").str.contains("(?i)downed")) + .and_(pl.col("kickoff_play") == True)) + .then(True) + .otherwise(False), + kick_play = pl.col("text").str.contains("(?i)kick|(?i)kickoff"), + kickoff_safety = pl.when((pl.col("text").str.contains("(?i)kickoff")) + .and_(pl.col("safety") == True, + pl.col("type.text").is_in(["Blocked Punt", "Penalty"]) == False)) + .then(True) + .otherwise(False), + # --- Punts---- + punt = pl.col("type.text").is_in(punt_vec), + punt_play = pl.col("text").str.contains("(?i)punt") + ).with_columns( + punt_tb = pl.when((pl.col("text").str.contains("(?i)touchback")) + .and_(pl.col("punt") == True)) + .then(True) + .otherwise(False), + punt_oob = pl.when((pl.col("text").str.contains("(?i)out-of-bounds|(?i)out of bounds")) + .and_(pl.col("punt") == True)) + .then(True) + .otherwise(False), + punt_fair_catch = pl.when((pl.col("text").str.contains("(?i)fair catch|(?i)fair caught")) + .and_(pl.col("punt") == True)) + .then(True) + .otherwise(False), + punt_downed = pl.when((pl.col("text").str.contains("(?i)downed")) + .and_(pl.col("punt") == True)) + .then(True) + .otherwise(False), + punt_safety = pl.when((pl.col("text").str.contains("(?i)punt")) + .and_(pl.col("safety") == True)) + .then(True) + .otherwise(False), + punt_blocked = pl.when((pl.col("text").str.contains("(?i)blocked")) + .and_(pl.col("punt") == True)) + .then(True) + .otherwise(False), + penalty_safety = pl.when((pl.col("type.text").is_in(["Penalty"])) + .and_(pl.col("safety") == True)) + .then(True) + .otherwise(False), - play_df["kickoff_fair_catch"] = ( - play_df["text"].str.contains( - r"fair catch|fair caught", case=False, flags=0, na=False, regex=True - ) - ) & (play_df.kickoff_play == True) - play_df["kickoff_downed"] = ( - play_df["text"].str.contains( - "downed", case=False, flags=0, na=False, regex=True - ) - ) & (play_df.kickoff_play == True) - play_df["kick_play"] = play_df["text"].str.contains( - r"kick|kickoff", case=False, flags=0, na=False, regex=True - ) - play_df["kickoff_safety"] = ( - (~play_df["type.text"].isin(["Blocked Punt", "Penalty"])) - & ( - play_df["text"].str.contains( - "kickoff", case=False, flags=0, na=False, regex=True - ) - ) - & (play_df.safety == True) - ) - # --- Punts---- - play_df["punt"] = np.where(play_df["type.text"].isin(punt_vec), True, False) - play_df["punt_play"] = play_df["text"].str.contains( - "punt", case=False, flags=0, na=False, regex=True - ) - play_df["punt_tb"] = np.where( - ( - play_df["text"].str.contains( - "touchback", case=False, flags=0, na=False, regex=True - ) - ) - & (play_df.punt == True), - True, - False, - ) - play_df["punt_oob"] = np.where( - ( - play_df["text"].str.contains( - r"out-of-bounds|out of bounds", - case=False, - flags=0, - na=False, - regex=True, - ) - ) - & (play_df.punt == True), - True, - False, - ) - play_df["punt_fair_catch"] = np.where( - ( - play_df["text"].str.contains( - r"fair catch|fair caught", case=False, flags=0, na=False, regex=True - ) - ) - & (play_df.punt == True), - True, - False, - ) - play_df["punt_downed"] = np.where( - ( - play_df["text"].str.contains( - "downed", case=False, flags=0, na=False, regex=True - ) - ) - & (play_df.punt == True), - True, - False, - ) - play_df["punt_safety"] = np.where( - (play_df["type.text"].isin(["Blocked Punt", "Punt"])) - & ( - play_df["text"].str.contains( - "punt", case=False, flags=0, na=False, regex=True - ) - ) - & (play_df.safety == True), - True, - False, - ) - play_df["penalty_safety"] = np.where( - (play_df["type.text"].isin(["Penalty"])) & (play_df.safety == True), - True, - False, - ) - play_df["punt_blocked"] = np.where( - ( - play_df["text"].str.contains( - "blocked", case=False, flags=0, na=False, regex=True - ) - ) - & (play_df.punt == True), - True, - False, ) + return play_df def __add_rush_pass_flags(self, play_df): @@ -1397,2293 +817,1397 @@ def __add_rush_pass_flags(self, play_df): Creates the following columns in play_df: * Rush, Pass, Sacks """ - # --- Pass/Rush---- - play_df["rush"] = np.where( - ( - (play_df["type.text"] == "Rush") - | (play_df["type.text"] == "Rushing Touchdown") - | ( - play_df["type.text"].isin( - [ - "Safety", - "Fumble Recovery (Opponent)", - "Fumble Recovery (Opponent) Touchdown", - "Fumble Recovery (Own)", - "Fumble Recovery (Own) Touchdown", - "Fumble Return Touchdown", - ] - ) - & play_df["text"].str.contains("run for") - ) - ), - True, - False, - ) - play_df["pass"] = np.where( - ( - ( - play_df["type.text"].isin( - [ - "Pass Reception", - "Pass Completion", - "Passing Touchdown", - "Sack", - "Pass", - "Interception", - "Pass Interception Return", - "Interception Return Touchdown", - "Pass Incompletion", - "Sack Touchdown", - "Interception Return", - ] - ) - ) - | ( - (play_df["type.text"] == "Safety") - & ( - play_df["text"].str.contains( - "sacked", case=False, flags=0, na=False, regex=True - ) - ) - ) - | ( - (play_df["type.text"] == "Safety") - & ( - play_df["text"].str.contains( - "pass complete", case=False, flags=0, na=False, regex=True - ) - ) - ) - | ( - (play_df["type.text"] == "Fumble Recovery (Own)") - & ( - play_df["text"].str.contains( - r"pass complete|pass incomplete|pass intercepted", - case=False, - flags=0, - na=False, - regex=True, - ) - ) - ) - | ( - (play_df["type.text"] == "Fumble Recovery (Own)") - & ( - play_df["text"].str.contains( - "sacked", case=False, flags=0, na=False, regex=True - ) - ) - ) - | ( - (play_df["type.text"] == "Fumble Recovery (Own) Touchdown") - & ( - play_df["text"].str.contains( - r"pass complete|pass incomplete|pass intercepted", - case=False, - flags=0, - na=False, - regex=True, - ) - ) - ) - | ( - (play_df["type.text"] == "Fumble Recovery (Own) Touchdown") - & ( - play_df["text"].str.contains( - "sacked", case=False, flags=0, na=False, regex=True - ) - ) - ) - | ( - (play_df["type.text"] == "Fumble Recovery (Opponent)") - & ( - play_df["text"].str.contains( - r"pass complete|pass incomplete|pass intercepted", - case=False, - flags=0, - na=False, - regex=True, - ) - ) - ) - | ( - (play_df["type.text"] == "Fumble Recovery (Opponent)") - & ( - play_df["text"].str.contains( - "sacked", case=False, flags=0, na=False, regex=True - ) - ) - ) - | ( - (play_df["type.text"] == "Fumble Recovery (Opponent) Touchdown") - & ( - play_df["text"].str.contains( - r"pass complete|pass incomplete", - case=False, - flags=0, - na=False, - regex=True, - ) - ) - ) - | ( - (play_df["type.text"] == "Fumble Return Touchdown") - & ( - play_df["text"].str.contains( - r"pass complete|pass incomplete", - case=False, - flags=0, - na=False, - regex=True, - ) - ) - ) - | ( - (play_df["type.text"] == "Fumble Return Touchdown") - & ( - play_df["text"].str.contains( - "sacked", case=False, flags=0, na=False, regex=True - ) - ) - ) - ), - True, - False, - ) - # --- Sacks---- - play_df["sack_vec"] = np.where( - ( - (play_df["type.text"].isin(["Sack", "Sack Touchdown"])) - | ( - ( - play_df["type.text"].isin( - [ + + play_df = play_df.with_columns( + # --- Pass/Rush---- + pl.when((pl.col("type.text") == "Rush") + .or_(pl.col("type.text") == "Rushing Touchdown") + .or_((pl.col("type.text").is_in(["Safety", "Fumble Recovery (Opponent)", + "Fumble Recovery (Opponent) Touchdown", + "Fumble Recovery (Own)", + "Fumble Recovery (Own) Touchdown", + "Fumble Return Touchdown"])) + .and_(pl.col("text").str.contains("run for")))) + .then(True) + .otherwise(False) + .alias("rush"), + pl.when((pl.col("type.text").is_in(["Pass Reception", "Pass Completion", + "Passing Touchdown", "Sack", "Pass", + "Interception", "Pass Interception Return", + "Interception Return Touchdown", + "Pass Incompletion", "Sack Touchdown", + "Interception Return"])) + .or_((pl.col("type.text") == "Safety") + .and_(pl.col("text").str.contains("sacked"))) + .or_((pl.col("type.text") == "Safety") + .and_(pl.col("text").str.contains("pass complete"))) + .or_((pl.col("type.text") == "Fumble Recovery (Own)") + .and_(pl.col("text").str.contains(r"pass complete|pass incomplete|pass intercepted"))) + .or_((pl.col("type.text") == "Fumble Recovery (Own)") + .and_(pl.col("text").str.contains("sacked"))) + .or_((pl.col("type.text") == "Fumble Recovery (Own) Touchdown") + .and_(pl.col("text").str.contains(r"pass complete|pass incomplete|pass intercepted"))) + .or_((pl.col("type.text") == "Fumble Recovery (Own) Touchdown") + .and_(pl.col("text").str.contains("sacked"))) + .or_((pl.col("type.text") == "Fumble Recovery (Opponent)") + .and_(pl.col("text").str.contains(r"pass complete|pass incomplete|pass intercepted"))) + .or_((pl.col("type.text") == "Fumble Recovery (Opponent)") + .and_(pl.col("text").str.contains("sacked"))) + .or_((pl.col("type.text") == "Fumble Recovery (Opponent) Touchdown") + .and_(pl.col("text").str.contains(r"pass complete|pass incomplete"))) + .or_((pl.col("type.text") == "Fumble Return Touchdown") + .and_(pl.col("text").str.contains(r"pass complete|pass incomplete"))) + .or_((pl.col("type.text") == "Fumble Return Touchdown") + .and_(pl.col("text").str.contains("sacked")))) + .then(True) + .otherwise(False) + .alias("pass") + ).with_columns( + # --- Sacks---- + sack_vec = pl.when((pl.col("type.text").is_in(["Sack", "Sack Touchdown"])) + .or_((pl.col("type.text").is_in([ "Fumble Recovery (Own)", "Fumble Recovery (Own) Touchdown", "Fumble Recovery (Opponent)", "Fumble Recovery (Opponent) Touchdown", "Fumble Return Touchdown", - ] - ) - & (play_df["pass"] == True) - & ( - play_df["text"].str.contains( - "sacked", case=False, flags=0, na=False, regex=True - ) - ) - ) - ) - ), - True, - False, + ])) + .and_(pl.col("text").str.contains("(?i)sacked"), + pl.col("pass") == True))) + .then(True) + .otherwise(False), + ).with_columns( + pl.when(pl.col("sack_vec") == True).then(True).otherwise(pl.col("pass")).alias("pass"), ) - play_df["pass"] = np.where(play_df["sack_vec"] == True, True, play_df["pass"]) - return play_df - def __add_team_score_variables(self, play_df): - """ - Creates the following columns in play_df: - * Team Score variables - * Fix change of poss variables - """ - # ------------------------- - play_df["pos_team"] = play_df["start.pos_team.id"] - play_df["def_pos_team"] = play_df["start.def_pos_team.id"] - play_df["is_home"] = play_df.pos_team == play_df["homeTeamId"] - # --- Team Score variables ------ - play_df["lag_homeScore"] = play_df["homeScore"].shift(1) - play_df["lag_awayScore"] = play_df["awayScore"].shift(1) - play_df["lag_HA_score_diff"] = ( - play_df["lag_homeScore"] - play_df["lag_awayScore"] - ) - play_df["HA_score_diff"] = play_df["homeScore"] - play_df["awayScore"] - play_df["net_HA_score_pts"] = ( - play_df["HA_score_diff"] - play_df["lag_HA_score_diff"] - ) - play_df["H_score_diff"] = play_df["homeScore"] - play_df["lag_homeScore"] - play_df["A_score_diff"] = play_df["awayScore"] - play_df["lag_awayScore"] - play_df["homeScore"] = np.select( - [ - (play_df.scoringPlay == False) - & (play_df["game_play_number"] != 1) - & (play_df["H_score_diff"] >= 9), - (play_df.scoringPlay == False) - & (play_df["game_play_number"] != 1) - & (play_df["H_score_diff"] < 9) - & (play_df["H_score_diff"] > 1), - (play_df.scoringPlay == False) - & (play_df["game_play_number"] != 1) - & (play_df["H_score_diff"] >= -9) - & (play_df["H_score_diff"] < -1), - ], - [play_df["lag_homeScore"], play_df["lag_homeScore"], play_df["homeScore"]], - default=play_df["homeScore"], - ) - play_df["awayScore"] = np.select( - [ - (play_df.scoringPlay == False) - & (play_df["game_play_number"] != 1) - & (play_df["A_score_diff"] >= 9), - (play_df.scoringPlay == False) - & (play_df["game_play_number"] != 1) - & (play_df["A_score_diff"] < 9) - & (play_df["A_score_diff"] > 1), - (play_df.scoringPlay == False) - & (play_df["game_play_number"] != 1) - & (play_df["A_score_diff"] >= -9) - & (play_df["A_score_diff"] < -1), - ], - [play_df["lag_awayScore"], play_df["lag_awayScore"], play_df["awayScore"]], - default=play_df["awayScore"], - ) - play_df.drop(["lag_homeScore", "lag_awayScore"], axis=1, inplace=True) - play_df["lag_homeScore"] = play_df["homeScore"].shift(1) - play_df["lag_homeScore"] = np.where( - (play_df.lag_homeScore.isna()), 0, play_df["lag_homeScore"] - ) - play_df["lag_awayScore"] = play_df["awayScore"].shift(1) - play_df["lag_awayScore"] = np.where( - (play_df.lag_awayScore.isna()), 0, play_df["lag_awayScore"] - ) - play_df["start.homeScore"] = np.where( - (play_df["game_play_number"] == 1), 0, play_df["lag_homeScore"] - ) - play_df["start.awayScore"] = np.where( - (play_df["game_play_number"] == 1), 0, play_df["lag_awayScore"] - ) - play_df["end.homeScore"] = play_df["homeScore"] - play_df["end.awayScore"] = play_df["awayScore"] - play_df["pos_team_score"] = np.where( - play_df.pos_team == play_df["homeTeamId"], - play_df.homeScore, - play_df.awayScore, - ) - play_df["def_pos_team_score"] = np.where( - play_df.pos_team == play_df["homeTeamId"], - play_df.awayScore, - play_df.homeScore, - ) - play_df["start.pos_team_score"] = np.where( - play_df["start.pos_team.id"] == play_df["homeTeamId"], - play_df["start.homeScore"], - play_df["start.awayScore"], - ) - play_df["start.def_pos_team_score"] = np.where( - play_df["start.pos_team.id"] == play_df["homeTeamId"], - play_df["start.awayScore"], - play_df["start.homeScore"], - ) - play_df["start.pos_score_diff"] = ( - play_df["start.pos_team_score"] - play_df["start.def_pos_team_score"] - ) - play_df["end.pos_team_score"] = np.where( - play_df["end.pos_team.id"] == play_df["homeTeamId"], - play_df["end.homeScore"], - play_df["end.awayScore"], - ) - play_df["end.def_pos_team_score"] = np.where( - play_df["end.pos_team.id"] == play_df["homeTeamId"], - play_df["end.awayScore"], - play_df["end.homeScore"], - ) - play_df["end.pos_score_diff"] = ( - play_df["end.pos_team_score"] - play_df["end.def_pos_team_score"] - ) - play_df["lag_pos_team"] = play_df["pos_team"].shift(1) - play_df.loc[ - play_df.lag_pos_team.isna() == True, "lag_pos_team" - ] = play_df.pos_team - play_df["lead_pos_team"] = play_df["pos_team"].shift(-1) - play_df["lead_pos_team2"] = play_df["pos_team"].shift(-2) - play_df["pos_score_diff"] = play_df.pos_team_score - play_df.def_pos_team_score - play_df["lag_pos_score_diff"] = play_df["pos_score_diff"].shift(1) - play_df.loc[play_df.lag_pos_score_diff.isna(), "lag_pos_score_diff"] = 0 - play_df["pos_score_pts"] = np.where( - play_df.lag_pos_team == play_df.pos_team, - play_df.pos_score_diff - play_df.lag_pos_score_diff, - play_df.pos_score_diff + play_df.lag_pos_score_diff, - ) - play_df["pos_score_diff_start"] = np.select( - [ - (play_df.kickoff_play == True) - & (play_df.lag_pos_team == play_df.pos_team), - (play_df.kickoff_play == True) - | (play_df.lag_pos_team != play_df.pos_team), - ], - [play_df.lag_pos_score_diff, -1 * play_df.lag_pos_score_diff], - default=play_df.lag_pos_score_diff, - ) - # --- Timeouts ------ - play_df.loc[ - play_df.pos_score_diff_start.isna() == True, "pos_score_diff_start" - ] = play_df.pos_score_diff - play_df["start.pos_team_receives_2H_kickoff"] = ( - play_df["start.pos_team.id"] == play_df.firstHalfKickoffTeamId - ) - play_df["end.pos_team_receives_2H_kickoff"] = ( - play_df["end.pos_team.id"] == play_df.firstHalfKickoffTeamId - ) - play_df["change_of_poss"] = np.where( - play_df["start.pos_team.id"] == play_df["end.pos_team.id"], False, True - ) - play_df["change_of_poss"] = np.where( - play_df["change_of_poss"].isna(), 0, play_df["change_of_poss"] - ) return play_df - def __add_new_play_types(self, play_df): - """ - Creates the following columns in play_df: - * Fix play types - """ - # -------------------------------------------------- - ## Fix Strip-Sacks to Fumbles---- - play_df["type.text"] = np.where( - (play_df.fumble_vec == True) - & (play_df["pass"] == True) - & (play_df.change_of_poss == 1) - & (play_df.td_play == False) - & (play_df["start.down"] != 4) - & ~(play_df["type.text"].isin(defense_score_vec)), - "Fumble Recovery (Opponent)", - play_df["type.text"], - ) - play_df["type.text"] = np.where( - (play_df.fumble_vec == True) - & (play_df["pass"] == True) - & (play_df.change_of_poss == 1) - & (play_df.td_play == True), - "Fumble Recovery (Opponent) Touchdown", - play_df["type.text"], - ) - ## Fix rushes with fumbles and a change of possession to fumbles---- - play_df["type.text"] = np.where( - (play_df.fumble_vec == True) - & (play_df["rush"] == True) - & (play_df.change_of_poss == 1) - & (play_df.td_play == False) - & (play_df["start.down"] != 4) - & ~(play_df["type.text"].isin(defense_score_vec)), - "Fumble Recovery (Opponent)", - play_df["type.text"], - ) - play_df["type.text"] = np.where( - (play_df.fumble_vec == True) - & (play_df["rush"] == True) - & (play_df.change_of_poss == 1) - & (play_df.td_play == True), - "Fumble Recovery (Opponent) Touchdown", - play_df["type.text"], - ) - - # -- Fix kickoff fumble return TDs ---- - play_df["type.text"] = np.where( - (play_df.kickoff_play == True) - & (play_df.change_of_poss == 1) - & (play_df.td_play == True) - & (play_df.td_check == True), - "Kickoff Return Touchdown", - play_df["type.text"], - ) - # -- Fix punt return TDs ---- - play_df["type.text"] = np.where( - (play_df.punt_play == True) - & (play_df.td_play == True) - & (play_df.td_check == True), - "Punt Return Touchdown", - play_df["type.text"], - ) - # -- Fix kick return TDs---- - play_df["type.text"] = np.where( - (play_df.kickoff_play == True) - & (play_df.fumble_vec == False) - & (play_df.td_play == True) - & (play_df.td_check == True), - "Kickoff Return Touchdown", - play_df["type.text"], - ) - # -- Fix rush/pass tds that aren't explicit---- - play_df["type.text"] = np.where( - (play_df.td_play == True) - & (play_df.rush == True) - & (play_df.fumble_vec == False) - & (play_df.td_check == True), - "Rushing Touchdown", - play_df["type.text"], - ) - play_df["type.text"] = np.where( - (play_df.td_play == True) - & (play_df["pass"] == True) - & (play_df.fumble_vec == False) - & (play_df.td_check == True) - & ~(play_df["type.text"].isin(int_vec)), - "Passing Touchdown", - play_df["type.text"], - ) - play_df["type.text"] = np.where( - (play_df["pass"] == True) - & (play_df["type.text"].isin(["Pass Reception", "Pass Completion", "Pass"])) - & (play_df.statYardage == play_df["start.yardsToEndzone"]) - & (play_df.fumble_vec == False) - & ~(play_df["type.text"].isin(int_vec)), - "Passing Touchdown", - play_df["type.text"], - ) - play_df["type.text"] = np.where( - (play_df["type.text"].isin(["Blocked Field Goal"])) - & ( - play_df["text"].str.contains( - "for a TD", case=False, flags=0, na=False, regex=True - ) - ), - "Blocked Field Goal Touchdown", - play_df["type.text"], - ) - - play_df["type.text"] = np.where( - (play_df["type.text"].isin(["Blocked Punt"])) - & ( - play_df["text"].str.contains( - "for a TD", case=False, flags=0, na=False, regex=True - ) - ), - "Blocked Punt Touchdown", - play_df["type.text"], - ) - # -- Fix duplicated TD play_type labels---- - play_df["type.text"] = np.where( - play_df["type.text"] == "Punt Touchdown Touchdown", - "Punt Touchdown", - play_df["type.text"], - ) - play_df["type.text"] = np.where( - play_df["type.text"] == "Fumble Return Touchdown Touchdown", - "Fumble Return Touchdown", - play_df["type.text"], - ) - play_df["type.text"] = np.where( - play_df["type.text"] == "Rushing Touchdown Touchdown", - "Rushing Touchdown", - play_df["type.text"], - ) - play_df["type.text"] = np.where( - play_df["type.text"] == "Uncategorized Touchdown Touchdown", - "Uncategorized Touchdown", - play_df["type.text"], - ) - # -- Fix Pass Interception Return TD play_type labels---- - play_df["type.text"] = np.where( - play_df["text"].str.contains( - "pass intercepted for a TD", case=False, flags=0, na=False, regex=True - ), - "Interception Return Touchdown", - play_df["type.text"], - ) - # -- Fix Sack/Fumbles Touchdown play_type labels---- - play_df["type.text"] = np.where( - ( - play_df["text"].str.contains( - "sacked", case=False, flags=0, na=False, regex=True - ) - ) - & ( - play_df["text"].str.contains( - "fumbled", case=False, flags=0, na=False, regex=True - ) - ) - & ( - play_df["text"].str.contains( - "TD", case=False, flags=0, na=False, regex=True - ) - ), - "Fumble Recovery (Opponent) Touchdown", - play_df["type.text"], - ) - # -- Fix generic pass plays ---- - ##-- first one looks for complete pass - play_df["type.text"] = np.where( - (play_df["type.text"] == "Pass") - & ( - play_df.text.str.contains( - "pass complete", case=False, flags=0, na=False, regex=True - ) - ), - "Pass Completion", - play_df["type.text"], - ) - ##-- second one looks for incomplete pass - play_df["type.text"] = np.where( - (play_df["type.text"] == "Pass") - & ( - play_df.text.str.contains( - "pass incomplete", case=False, flags=0, na=False, regex=True - ) - ), - "Pass Incompletion", - play_df["type.text"], - ) - ##-- third one looks for interceptions - play_df["type.text"] = np.where( - (play_df["type.text"] == "Pass") - & ( - play_df.text.str.contains( - "pass intercepted", case=False, flags=0, na=False, regex=True - ) - ), - "Pass Interception", - play_df["type.text"], - ) - ##-- fourth one looks for sacked - play_df["type.text"] = np.where( - (play_df["type.text"] == "Pass") - & ( - play_df.text.str.contains( - "sacked", case=False, flags=0, na=False, regex=True - ) - ), - "Sack", - play_df["type.text"], - ) - ##-- fifth one play type is Passing Touchdown, but its intercepted - play_df["type.text"] = np.where( - (play_df["type.text"] == "Passing Touchdown") - & ( - play_df.text.str.contains( - "pass intercepted for a TD", - case=False, - flags=0, - na=False, - regex=True, - ) - ), - "Interception Return Touchdown", - play_df["type.text"], - ) - play_df["type.text"] = np.where( - (play_df["type.text"] == "Passing Touchdown") - & ( - play_df.text.str.contains( - "pass intercepted for a TD", - case=False, - flags=0, - na=False, - regex=True, - ) - ), - "Interception Return Touchdown", - play_df["type.text"], - ) - # --- Moving non-Touchdown pass interceptions to one play_type: "Interception Return" ----- - play_df["type.text"] = np.where( - play_df["type.text"].isin(["Interception"]), - "Interception Return", - play_df["type.text"], - ) - play_df["type.text"] = np.where( - play_df["type.text"].isin(["Pass Interception"]), - "Interception Return", - play_df["type.text"], - ) - play_df["type.text"] = np.where( - play_df["type.text"].isin(["Pass Interception Return"]), - "Interception Return", - play_df["type.text"], - ) - - # --- Moving Kickoff/Punt Touchdowns without fumbles to Kickoff/Punt Return Touchdown - play_df["type.text"] = np.where( - (play_df["type.text"] == "Kickoff Touchdown") - & (play_df.fumble_vec == False), - "Kickoff Return Touchdown", - play_df["type.text"], - ) - - play_df["type.text"] = np.select( - [ - (play_df["type.text"] == "Kickoff Touchdown") - & (play_df.fumble_vec == False), - (play_df["type.text"] == "Kickoff") - & (play_df["td_play"] == True) - & (play_df.fumble_vec == False), - (play_df["type.text"] == "Kickoff") - & ( - play_df.text.str.contains( - "for a TD", case=False, flags=0, na=False, regex=True - ) - ) - & (play_df.fumble_vec == False), - ], - [ - "Kickoff Return Touchdown", - "Kickoff Return Touchdown", - "Kickoff Return Touchdown", - ], - default=play_df["type.text"], - ) - - play_df["type.text"] = np.where( - (play_df["type.text"].isin(["Kickoff", "Kickoff Return (Offense)"])) - & (play_df.fumble_vec == True) - & (play_df.change_of_poss == 1), - "Kickoff Team Fumble Recovery", - play_df["type.text"], - ) - - play_df["type.text"] = np.select( - [ - (play_df["type.text"] == "Punt Touchdown") - & (play_df.fumble_vec == False) - & (play_df.change_of_poss == 1), - (play_df["type.text"] == "Punt") - & ( - play_df.text.str.contains( - "for a TD", case=False, flags=0, na=False, regex=True - ) - ) - & (play_df.change_of_poss == 1), - ], - ["Punt Return Touchdown", "Punt Return Touchdown"], - default=play_df["type.text"], - ) - - play_df["type.text"] = np.where( - (play_df["type.text"] == "Punt") - & (play_df.fumble_vec == True) - & (play_df.change_of_poss == 0), - "Punt Team Fumble Recovery", - play_df["type.text"], - ) - play_df["type.text"] = np.where( - (play_df["type.text"].isin(["Punt Touchdown"])) - | ( - (play_df["scoringPlay"] == True) - & (play_df["punt_play"] == True) - & (play_df.change_of_poss == 0) - ), - "Punt Team Fumble Recovery Touchdown", - play_df["type.text"], - ) - play_df["type.text"] = np.where( - play_df["type.text"].isin(["Kickoff Touchdown"]), - "Kickoff Team Fumble Recovery Touchdown", - play_df["type.text"], - ) - play_df["type.text"] = np.where( - (play_df["type.text"].isin(["Fumble Return Touchdown"])) - & ((play_df["pass"] == True) | (play_df["rush"] == True)), - "Fumble Recovery (Opponent) Touchdown", - play_df["type.text"], - ) - - # --- Safeties (kickoff, punt, penalty) ---- - play_df["type.text"] = np.where( - ( - play_df["type.text"].isin( - ["Pass Reception", "Rush", "Rushing Touchdown"] - ) - & ((play_df["pass"] == True) | (play_df["rush"] == True)) - & (play_df["safety"] == True) - ), - "Safety", - play_df["type.text"], - ) - play_df["type.text"] = np.where( - (play_df.kickoff_safety == True), "Kickoff (Safety)", play_df["type.text"] - ) - play_df["type.text"] = np.where( - (play_df.punt_safety == True), "Punt (Safety)", play_df["type.text"] - ) - play_df["type.text"] = np.where( - (play_df.penalty_safety == True), "Penalty (Safety)", play_df["type.text"] - ) - play_df["type.text"] = np.where( - (play_df["type.text"] == "Extra Point Good") - & ( - play_df["text"].str.contains( - "Two-Point", case=False, flags=0, na=False, regex=True - ) - ), - "Two-Point Conversion Good", - play_df["type.text"], - ) - play_df["type.text"] = np.where( - (play_df["type.text"] == "Extra Point Missed") - & ( - play_df["text"].str.contains( - "Two-Point", case=False, flags=0, na=False, regex=True - ) - ), - "Two-Point Conversion Missed", - play_df["type.text"], - ) - return play_df - - def __add_play_category_flags(self, play_df): - # -------------------------------------------------- - # --- Sacks ---- - play_df["sack"] = np.select( - [ - play_df["type.text"].isin(["Sack"]), - ( - play_df["type.text"].isin( - [ - "Fumble Recovery (Own)", - "Fumble Recovery (Own) Touchdown", - "Fumble Recovery (Opponent)", - "Fumble Recovery (Opponent) Touchdown", - ] - ) - ) - & (play_df["pass"] == True) - & ( - play_df["text"].str.contains( - "sacked", case=False, flags=0, na=False, regex=True - ) - ), - ( - (play_df["type.text"].isin(["Safety"])) - & ( - play_df["text"].str.contains( - "sacked", case=False, flags=0, na=False, regex=True - ) - ) - ), - ], - [True, True, True], - default=False, - ) - # --- Interceptions ------ - play_df["int"] = play_df["type.text"].isin( - ["Interception Return", "Interception Return Touchdown"] - ) - play_df["int_td"] = play_df["type.text"].isin(["Interception Return Touchdown"]) - - # --- Pass Completions, Attempts and Targets ------- - play_df["completion"] = np.select( - [ - play_df["type.text"].isin( - ["Pass Reception", "Pass Completion", "Passing Touchdown"] - ), - ( - play_df["type.text"].isin( - [ - "Fumble Recovery (Own)", - "Fumble Recovery (Own) Touchdown", - "Fumble Recovery (Opponent)", - "Fumble Recovery (Opponent) Touchdown", - ] - ) - & (play_df["pass"] == True) - & ~( - play_df["text"].str.contains( - "sacked", case=False, flags=0, na=False, regex=True - ) - ) - ), - ], - [True, True], - default=False, - ) - - play_df["pass_attempt"] = np.select( - [ - ( - play_df["type.text"].isin( - [ - "Pass Reception", - "Pass Completion", - "Passing Touchdown", - "Pass Incompletion", - ] - ) - ), - ( - play_df["type.text"].isin( - [ - "Fumble Recovery (Own)", - "Fumble Recovery (Own) Touchdown", - "Fumble Recovery (Opponent)", - "Fumble Recovery (Opponent) Touchdown", - ] - ) - & (play_df["pass"] == True) - & ~( - play_df["text"].str.contains( - "sacked", case=False, flags=0, na=False, regex=True - ) - ) - ), - ( - (play_df["pass"] == True) - & ~( - play_df["text"].str.contains( - "sacked", case=False, flags=0, na=False, regex=True - ) - ) - ), - ], - [True, True, True], - default=False, - ) - - play_df["target"] = np.select( - [ - ( - play_df["type.text"].isin( - [ - "Pass Reception", - "Pass Completion", - "Passing Touchdown", - "Pass Incompletion", - ] - ) - ), - ( - play_df["type.text"].isin( - [ - "Fumble Recovery (Own)", - "Fumble Recovery (Own) Touchdown", - "Fumble Recovery (Opponent)", - "Fumble Recovery (Opponent) Touchdown", - ] - ) - & (play_df["pass"] == True) - & ~( - play_df["text"].str.contains( - "sacked", case=False, flags=0, na=False, regex=True - ) - ) - ), - ( - (play_df["pass"] == True) - & ~( - play_df["text"].str.contains( - "sacked", case=False, flags=0, na=False, regex=True - ) - ) - ), - ], - [True, True, True], - default=False, - ) - - play_df["pass_breakup"] = play_df["text"].str.contains( - "broken up by", case=False, flags=0, na=False, regex=True - ) - # --- Pass/Rush TDs ------ - play_df["pass_td"] = (play_df["type.text"] == "Passing Touchdown") | ( - (play_df["pass"] == True) & (play_df["td_play"] == True) - ) - play_df["rush_td"] = (play_df["type.text"] == "Rushing Touchdown") | ( - (play_df["rush"] == True) & (play_df["td_play"] == True) - ) - # --- Change of possession via turnover - play_df["turnover_vec"] = play_df["type.text"].isin(turnover_vec) - play_df["offense_score_play"] = play_df["type.text"].isin(offense_score_vec) - play_df["defense_score_play"] = play_df["type.text"].isin(defense_score_vec) - play_df["downs_turnover"] = np.where( - (play_df["type.text"].isin(normalplay)) - & (play_df["statYardage"] < play_df["start.distance"]) - & (play_df["start.down"] == 4) - & (play_df["penalty_1st_conv"] == False), - True, - False, - ) - # --- Touchdowns---- - play_df["scoring_play"] = play_df["type.text"].isin(scores_vec) - play_df["yds_punted"] = ( - play_df["text"] - .str.extract(r"(?<= punt for)[^,]+(\d+)", flags=re.IGNORECASE) - .astype(float) - ) - play_df["yds_punt_gained"] = np.where( - play_df.punt == True, play_df["statYardage"], None - ) - play_df["fg_attempt"] = np.where( - ( - play_df["type.text"].str.contains( - "Field Goal", case=False, flags=0, na=False, regex=True - ) - ) - | ( - play_df["text"].str.contains( - "Field Goal", case=False, flags=0, na=False, regex=True - ) - ), - True, - False, - ) - play_df["fg_made"] = play_df["type.text"] == "Field Goal Good" - play_df["yds_fg"] = ( - play_df["text"] - .str.extract( - r"(\d+)\s?Yd Field|(\d+)\s?YD FG|(\d+)\s?Yard FG|(\d+)\s?Field|(\d+)\s?Yard Field", - flags=re.IGNORECASE, - ) - .bfill(axis=1)[0] - .astype(float) - ) - # -------------------------------------------------- - play_df["start.yardsToEndzone"] = np.where( - play_df["fg_attempt"] == True, - play_df["yds_fg"] - 17, - play_df["start.yardsToEndzone"], - ) - play_df["start.yardsToEndzone"] = np.select( - [ - (play_df["start.yardsToEndzone"].isna()) - & (~play_df["type.text"].isin(kickoff_vec)) - & (play_df["start.pos_team.id"] == play_df["homeTeamId"]), - (play_df["start.yardsToEndzone"].isna()) - & (~play_df["type.text"].isin(kickoff_vec)) - & (play_df["start.pos_team.id"] == play_df["awayTeamId"]), - ], - [ - 100 - play_df["start.yardLine"].astype(float), - play_df["start.yardLine"].astype(float), - ], - default=play_df["start.yardsToEndzone"], - ) - play_df["pos_unit"] = np.select( - [ - play_df.punt == True, - play_df.kickoff_play == True, - play_df.fg_attempt == True, - play_df["type.text"] == "Defensive 2pt Conversion", - ], - ["Punt Offense", "Kickoff Return", "Field Goal Offense", "Offense"], - default="Offense", - ) - play_df["def_pos_unit"] = np.select( - [ - play_df.punt == True, - play_df.kickoff_play == True, - play_df.fg_attempt == True, - play_df["type.text"] == "Defensive 2pt Conversion", - ], - ["Punt Return", "Kickoff Defense", "Field Goal Defense", "Defense"], - default="Defense", - ) - # --- Lags/Leads play type ---- - play_df["lead_play_type"] = play_df["type.text"].shift(-1) - - play_df["sp"] = np.where( - (play_df.fg_attempt == True) - | (play_df.punt == True) - | (play_df.kickoff_play == True), - True, - False, - ) - play_df["play"] = np.where( - ( - ~play_df["type.text"].isin( - ["Timeout", "End Period", "End of Half", "Penalty"] - ) - ), - True, - False, - ) - play_df["scrimmage_play"] = np.where( - (play_df.sp == False) - & ( - ~play_df["type.text"].isin( - [ - "Timeout", - "Extra Point Good", - "Extra Point Missed", - "Two-Point Pass", - "Two-Point Rush", - "Penalty", - ] - ) - ), - True, - False, - ) - # -------------------------------------------------- - # --- Change of pos_team by lead('pos_team', 1)---- - play_df["change_of_pos_team"] = np.where( - (play_df.pos_team == play_df.lead_pos_team) - & ( - ~(play_df.lead_play_type.isin(["End Period", "End of Half"])) - | play_df.lead_play_type.isna() - == True - ), - False, - np.where( - (play_df.pos_team == play_df.lead_pos_team2) - & ( - (play_df.lead_play_type.isin(["End Period", "End of Half"])) - | play_df.lead_play_type.isna() - == True - ), - False, - True, - ), - ) - play_df["change_of_pos_team"] = np.where( - play_df["change_of_poss"].isna(), False, play_df["change_of_pos_team"] - ) - play_df["pos_score_diff_end"] = np.where( - ( - (play_df["type.text"].isin(end_change_vec)) - & (play_df["start.pos_team.id"] != play_df["end.pos_team.id"]) - ) - | (play_df.downs_turnover == True), - -1 * play_df.pos_score_diff, - play_df.pos_score_diff, - ) - play_df["pos_score_diff_end"] = np.select( - [ - (abs(play_df.pos_score_pts) >= 8) - & (play_df.scoring_play == False) - & (play_df.change_of_pos_team == False), - (abs(play_df.pos_score_pts) >= 8) - & (play_df.scoring_play == False) - & (play_df.change_of_pos_team == True), - ], - [play_df["pos_score_diff_start"], -1 * play_df["pos_score_diff_start"]], - default=play_df["pos_score_diff_end"], - ) - - play_df["fumble_lost"] = np.where( - (play_df.fumble_vec == True) & (play_df.change_of_poss == True), True, False - ) - play_df["fumble_recovered"] = np.where( - (play_df.fumble_vec == True) & (play_df.change_of_poss == False), - True, - False, - ) - return play_df - - def __add_yardage_cols(self, play_df): - play_df["yds_rushed"] = None - play_df["yds_rushed"] = np.select( - [ - (play_df.rush == True) - & ( - play_df.text.str.contains( - "run for no gain", case=False, flags=0, na=False, regex=True - ) - ), - (play_df.rush == True) - & ( - play_df.text.str.contains( - "for no gain", case=False, flags=0, na=False, regex=True - ) - ), - (play_df.rush == True) - & ( - play_df.text.str.contains( - "run for a loss of", case=False, flags=0, na=False, regex=True - ) - ), - (play_df.rush == True) - & ( - play_df.text.str.contains( - "rush for a loss of", case=False, flags=0, na=False, regex=True - ) - ), - (play_df.rush == True) - & ( - play_df.text.str.contains( - "run for", case=False, flags=0, na=False, regex=True - ) - ), - (play_df.rush == True) - & ( - play_df.text.str.contains( - "rush for", case=False, flags=0, na=False, regex=True - ) - ), - (play_df.rush == True) - & ( - play_df.text.str.contains( - "Yd Run", case=False, flags=0, na=False, regex=True - ) - ), - (play_df.rush == True) - & ( - play_df.text.str.contains( - "Yd Rush", case=False, flags=0, na=False, regex=True - ) - ), - (play_df.rush == True) - & ( - play_df.text.str.contains( - "Yard Rush", case=False, flags=0, na=False, regex=True - ) - ), - (play_df.rush == True) - & ( - play_df.text.str.contains( - "rushed", case=False, flags=0, na=False, regex=True - ) - ) - & ( - ~play_df.text.str.contains( - "touchdown", case=False, flags=0, na=False, regex=True - ) - ), - (play_df.rush == True) - & ( - play_df.text.str.contains( - "rushed", case=False, flags=0, na=False, regex=True - ) - ) - & ( - play_df.text.str.contains( - "touchdown", case=False, flags=0, na=False, regex=True - ) - ), - ], - [ - 0.0, - 0.0, - -1 - * play_df.text.str.extract( - r"((?<=run for a loss of)[^,]+)", flags=re.IGNORECASE - )[0] - .str.extract(r"(\d+)")[0] - .astype(float), - -1 - * play_df.text.str.extract( - r"((?<=rush for a loss of)[^,]+)", flags=re.IGNORECASE - )[0] - .str.extract(r"(\d+)")[0] - .astype(float), - play_df.text.str.extract(r"((?<=run for)[^,]+)", flags=re.IGNORECASE)[0] - .str.extract(r"(\d+)")[0] - .astype(float), - play_df.text.str.extract(r"((?<=rush for)[^,]+)", flags=re.IGNORECASE)[ - 0 - ] - .str.extract(r"(\d+)")[0] - .astype(float), - play_df.text.str.extract(r"(\d+) Yd Run", flags=re.IGNORECASE)[ - 0 - ].astype(float), - play_df.text.str.extract(r"(\d+) Yd Rush", flags=re.IGNORECASE)[ - 0 - ].astype(float), - play_df.text.str.extract(r"(\d+) Yard Rush", flags=re.IGNORECASE)[ - 0 - ].astype(float), - play_df.text.str.extract(r"for (\d+) yards", flags=re.IGNORECASE)[ - 0 - ].astype(float), - play_df.text.str.extract(r"for a (\d+) yard", flags=re.IGNORECASE)[ - 0 - ].astype(float), - ], - default=None, - ) - - play_df["yds_receiving"] = None - play_df["yds_receiving"] = np.select( - [ - (play_df["pass"] == True) - & (play_df.text.str.contains("complete to", case=False)) - & (play_df.text.str.contains(r"for no gain", case=False)), - (play_df["pass"] == True) - & (play_df.text.str.contains("complete to", case=False)) - & (play_df.text.str.contains("for a loss", case=False)), - (play_df["pass"] == True) - & (play_df.text.str.contains("complete to", case=False)), - (play_df["pass"] == True) - & (play_df.text.str.contains("complete to", case=False)), - (play_df["pass"] == True) - & (play_df.text.str.contains("incomplete", case=False)), - (play_df["pass"] == True) - & (play_df["type.text"].str.contains("incompletion", case=False)), - (play_df["pass"] == True) - & (play_df.text.str.contains("Yd pass", case=False)), - ], - [ - 0.0, - -1 - * play_df.text.str.extract( - r"((?<=for a loss of)[^,]+)", flags=re.IGNORECASE - )[0] - .str.extract(r"(\d+)")[0] - .astype(float), - play_df.text.str.extract(r"((?<=for)[^,]+)", flags=re.IGNORECASE)[0] - .str.extract(r"(\d+)")[0] - .astype(float), - play_df.text.str.extract(r"((?<=for)[^,]+)", flags=re.IGNORECASE)[0] - .str.extract(r"(\d+)")[0] - .astype(float), - 0.0, - 0.0, - play_df.text.str.extract(r"(\d+)\s+Yd\s+pass", flags=re.IGNORECASE)[0] - .str.extract(r"(\d+)")[0] - .astype(float), - ], - default=None, - ) - - play_df["yds_int_return"] = None - play_df["yds_int_return"] = np.select( - [ - (play_df["pass"] == True) - & (play_df["int_td"] == True) - & (play_df.text.str.contains("Yd Interception Return", case=False)), - (play_df["pass"] == True) - & (play_df["int"] == True) - & (play_df.text.str.contains(r"for no gain", case=False)), - (play_df["pass"] == True) - & (play_df["int"] == True) - & (play_df.text.str.contains(r"for a loss of", case=False)), - (play_df["pass"] == True) - & (play_df["int"] == True) - & (play_df.text.str.contains(r"for a TD", case=False)), - (play_df["pass"] == True) - & (play_df["int"] == True) - & (play_df.text.str.contains(r"return for", case=False)), - (play_df["pass"] == True) & (play_df["int"] == True), - ], - [ - play_df.text.str.extract( - r"(.+) Interception Return", flags=re.IGNORECASE - )[0] - .str.extract(r"(\d+)")[0] - .astype(float), - 0.0, - -1 - * play_df.text.str.extract( - r"((?<= for a loss of)[^,]+)", flags=re.IGNORECASE - )[0] - .str.extract(r"(\d+)")[0] - .astype(float), - play_df.text.str.extract( - r"((?<= return for)[^,]+)", flags=re.IGNORECASE - )[0] - .str.extract(r"(\d+)")[0] - .astype(float), - play_df.text.str.extract( - r"((?<= return for)[^,]+)", flags=re.IGNORECASE - )[0] - .str.extract(r"(\d+)")[0] - .astype(float), - play_df.text.str.replace("for a 1st", "") - .str.extract(r"((?<=for)[^,]+)", flags=re.IGNORECASE)[0] - .str.extract(r"(\d+)")[0] - .astype(float), - ], - default=None, - ) - - # play_df['yds_fumble_return'] = None - # play_df['yds_penalty'] = None - - play_df["yds_kickoff"] = None - play_df["yds_kickoff"] = np.where( - (play_df["kickoff_play"] == True), - play_df.text.str.extract(r"((?<= kickoff for)[^,]+)", flags=re.IGNORECASE)[ - 0 - ] - .str.extract(r"(\d+)")[0] - .astype(float), - play_df["yds_kickoff"], - ) - - play_df["yds_kickoff_return"] = None - play_df["yds_kickoff_return"] = np.select( - [ - (play_df.kickoff_play == True) - & (play_df.kickoff_tb == True) - & (play_df.season > 2013), - (play_df.kickoff_play == True) - & (play_df.kickoff_tb == True) - & (play_df.season <= 2013), - (play_df.kickoff_play == True) - & (play_df.fumble_vec == False) - & ( - play_df.text.str.contains( - r"for no gain|fair catch|fair caught", regex=True, case=False - ) - ), - (play_df.kickoff_play == True) - & (play_df.fumble_vec == False) - & ( - play_df.text.str.contains( - r"out-of-bounds|out of bounds", regex=True, case=False - ) - ), - ( - (play_df.kickoff_downed == True) - | (play_df.kickoff_fair_catch == True) - ), - (play_df.kickoff_play == True) - & (play_df.text.str.contains(r"returned by", regex=True, case=False)), - (play_df.kickoff_play == True) - & (play_df.text.str.contains(r"return for", regex=True, case=False)), - (play_df.kickoff_play == True), - ], - [ - 25, - 20, - 0, - 40, - 0, - play_df.text.str.extract(r"((?<= for)[^,]+)", flags=re.IGNORECASE)[0] - .str.extract(r"(\d+)")[0] - .astype(float), - play_df.text.str.extract( - r"((?<= return for)[^,]+)", flags=re.IGNORECASE - )[0] - .str.extract(r"(\d+)")[0] - .astype(float), - play_df.text.str.extract( - r"((?<= returned for)[^,]+)", flags=re.IGNORECASE - )[0] - .str.extract(r"(\d+)")[0] - .astype(float), - ], - default=play_df["yds_kickoff_return"], - ) - - play_df["yds_punted"] = None - play_df["yds_punted"] = np.select( - [ - (play_df.punt == True) & (play_df.punt_blocked == True), - (play_df.punt == True), - ], - [ - 0, - play_df.text.str.extract(r"((?<= punt for)[^,]+)", flags=re.IGNORECASE)[ - 0 - ] - .str.extract(r"(\d+)")[0] - .astype(float), - ], - default=play_df.yds_punted, - ) - - play_df["yds_punt_return"] = np.select( - [ - (play_df.punt == True) & (play_df.punt_tb == 1), - (play_df.punt == True) - & ( - play_df["text"].str.contains( - r"fair catch|fair caught", - case=False, - flags=0, - na=False, - regex=True, - ) - ), - (play_df.punt == True) - & ( - (play_df.punt_downed == True) - | (play_df.punt_oob == True) - | (play_df.punt_fair_catch == True) - ), - (play_df.punt == True) - & ( - play_df["text"].str.contains( - r"no return|no gain", case=False, flags=0, na=False, regex=True - ) - ), - (play_df.punt == True) - & ( - play_df["text"].str.contains( - r"returned \d+ yards", case=False, flags=0, na=False, regex=True - ) - ), - (play_df.punt == True) & (play_df.punt_blocked == False), - (play_df.punt == True) & (play_df.punt_blocked == True), - ], - [ - 20, - 0, - 0, - 0, - play_df.text.str.extract(r"((?<= returned)[^,]+)", flags=re.IGNORECASE)[ - 0 - ] - .str.extract(r"(\d+)")[0] - .astype(float), - play_df.text.str.extract( - r"((?<= returns for)[^,]+)", flags=re.IGNORECASE - )[0] - .str.extract(r"(\d+)")[0] - .astype(float), - play_df.text.str.extract( - r"((?<= return for)[^,]+)", flags=re.IGNORECASE - )[0] - .str.extract(r"(\d+)")[0] - .astype(float), - ], - default=None, - ) - - play_df["yds_fumble_return"] = np.select( - [(play_df.fumble_vec == True) & (play_df.kickoff_play == False)], - [ - play_df.text.str.extract( - r"((?<= return for)[^,]+)", flags=re.IGNORECASE - )[0] - .str.extract(r"(\d+)")[0] - .astype(float) - ], - default=None, - ) - - play_df["yds_sacked"] = np.select( - [(play_df.sack == True)], - [ - -1 - * play_df.text.str.extract(r"((?<= sacked)[^,]+)", flags=re.IGNORECASE)[ - 0 - ] - .str.extract(r"(\d+)")[0] - .astype(float) - ], - default=None, - ) - - play_df["yds_penalty"] = np.select( - [(play_df.penalty_detail == 1)], - [ - -1 - * play_df.text.str.extract(r"((?<= sacked)[^,]+)", flags=re.IGNORECASE)[ - 0 - ] - .str.extract(r"(\d+)")[0] - .astype(float) - ], - default=None, - ) - - play_df["yds_penalty"] = np.select( - [ - play_df.penalty_detail.isin(["Penalty Declined", "Penalty Offset"]), - play_df.yds_penalty.notna(), - (play_df.penalty_detail.notna()) - & (play_df.yds_penalty.isna()) - & (play_df.rush == True), - (play_df.penalty_detail.notna()) - & (play_df.yds_penalty.isna()) - & (play_df.int == True), - (play_df.penalty_detail.notna()) - & (play_df.yds_penalty.isna()) - & (play_df["pass"] == 1) - & (play_df["sack"] == False) - & (play_df["type.text"] != "Pass Incompletion"), - (play_df.penalty_detail.notna()) - & (play_df.yds_penalty.isna()) - & (play_df["pass"] == 1) - & (play_df["sack"] == False) - & (play_df["type.text"] == "Pass Incompletion"), - (play_df.penalty_detail.notna()) - & (play_df.yds_penalty.isna()) - & (play_df["pass"] == 1) - & (play_df["sack"] == True), - (play_df["type.text"] == "Penalty"), - ], - [ - 0, - play_df.yds_penalty.astype(float), - play_df.statYardage.astype(float) - play_df.yds_rushed.astype(float), - play_df.statYardage.astype(float) - - play_df.yds_int_return.astype(float), - play_df.statYardage.astype(float) - play_df.yds_receiving.astype(float), - play_df.statYardage.astype(float), - play_df.statYardage.astype(float) - play_df.yds_sacked.astype(float), - play_df.statYardage.astype(float), - ], - default=None, - ) - return play_df - - def __add_player_cols(self, play_df): - play_df["rush_player"] = None - play_df["receiver_player"] = None - play_df["pass_player"] = None - play_df["sack_players"] = None - play_df["sack_player1"] = None - play_df["sack_player2"] = None - play_df["interception_player"] = None - play_df["pass_breakup_player"] = None - play_df["fg_kicker_player"] = None - play_df["fg_return_player"] = None - play_df["fg_block_player"] = None - play_df["punter_player"] = None - play_df["punt_return_player"] = None - play_df["punt_block_player"] = None - play_df["punt_block_return_player"] = None - play_df["kickoff_player"] = None - play_df["kickoff_return_player"] = None - play_df["fumble_player"] = None - play_df["fumble_forced_player"] = None - play_df["fumble_recovered_player"] = None - play_df["rush_player_name"] = None - play_df["receiver_player_name"] = None - play_df["passer_player_name"] = None - play_df["sack_player_name"] = None - play_df["sack_player_name2"] = None - play_df["interception_player_name"] = None - play_df["pass_breakup_player_name"] = None - play_df["fg_kicker_player_name"] = None - play_df["fg_return_player_name"] = None - play_df["fg_block_player_name"] = None - play_df["punter_player_name"] = None - play_df["punt_return_player_name"] = None - play_df["punt_block_player_name"] = None - play_df["punt_block_return_player_name"] = None - play_df["kickoff_player_name"] = None - play_df["kickoff_return_player_name"] = None - play_df["fumble_player_name"] = None - play_df["fumble_forced_player_name"] = None - play_df["fumble_recovered_player_name"] = None - - ## Extract player names - # RB names - play_df["rush_player"] = np.where( - (play_df.rush == 1), - play_df.text.str.extract( - r"(.{0,25} )run |(.{0,25} )\d{0,2} Yd Run|(.{0,25} )rush |(.{0,25} )rushed " - ).bfill(axis=1)[0], - None, - ) - play_df["rush_player"] = play_df.rush_player.str.replace( - r" run | \d+ Yd Run| rush ", "", regex=True - ) - play_df["rush_player"] = play_df.rush_player.str.replace( - r" \((.+)\)", "", regex=True - ) - - # QB names - play_df["pass_player"] = np.where( - (play_df["pass"] == 1) & (play_df["type.text"] != "Passing Touchdown"), - play_df.text.str.extract( - r"pass from (.*?) \(|(.{0,30} )pass |(.+) sacked by|(.+) sacked for|(.{0,30} )incomplete " - ).bfill(axis=1)[0], - play_df["pass_player"], - ) - play_df["pass_player"] = play_df.pass_player.str.replace( - r"pass | sacked by| sacked for| incomplete", "", regex=True - ) - - play_df["pass_player"] = np.where( - (play_df["pass"] == 1) & (play_df["type.text"] == "Passing Touchdown"), - play_df.text.str.extract("pass from(.+)")[0], - play_df["pass_player"], - ) - play_df["pass_player"] = play_df.pass_player.str.replace( - "pass from", "", regex=True - ) - play_df["pass_player"] = play_df.pass_player.str.replace( - r"\(.+\)", "", regex=True - ) - play_df["pass_player"] = play_df.pass_player.str.replace(r" \,", "", regex=True) - - play_df["pass_player"] = np.where( - (play_df["type.text"] == "Passing Touchdown") & play_df.pass_player.isna(), - play_df.text.str.extract("(.+)pass(.+)? complete to")[0], - play_df["pass_player"], - ) - play_df["pass_player"] = play_df.pass_player.str.replace( - r" pass complete to(.+)", "", regex=True - ) - play_df["pass_player"] = play_df.pass_player.str.replace( - " pass complete to", "", regex=True - ) - - play_df["pass_player"] = np.where( - (play_df["type.text"] == "Passing Touchdown") & play_df.pass_player.isna(), - play_df.text.str.extract("(.+)pass,to")[0], - play_df["pass_player"], - ) - - play_df["pass_player"] = play_df.pass_player.str.replace( - r" pass,to(.+)", "", regex=True - ) - play_df["pass_player"] = play_df.pass_player.str.replace( - r" pass,to", "", regex=True - ) - play_df["pass_player"] = play_df.pass_player.str.replace( - r" \((.+)\)", "", regex=True - ) - play_df["pass_player"] = np.where( - (play_df["pass"] == 1) - & ( - (play_df.pass_player.str.strip().str.len == 0) - | play_df.pass_player.isna() - ), - "TEAM", - play_df.pass_player, - ) - - play_df["receiver_player"] = np.where( - (play_df["pass"] == 1) - & ~play_df.text.str.contains( - "sacked", case=False, flags=0, na=False, regex=True - ), - play_df.text.str.extract("to (.+)")[0], - None, - ) - - play_df["receiver_player"] = np.where( - play_df.text.str.contains( - "Yd pass", case=False, flags=0, na=False, regex=True - ), - play_df.text.str.extract("(.{0,25} )\\d{0,2} Yd pass", flags=re.IGNORECASE)[ - 0 - ], - play_df["receiver_player"], - ) - - play_df["receiver_player"] = np.where( - play_df.text.str.contains("Yd TD pass", case=False), - play_df.text.str.extract( - "(.{0,25} )\\d{0,2} Yd TD pass", flags=re.IGNORECASE - )[0], - play_df["receiver_player"], - ) - - play_df["receiver_player"] = np.where( - (play_df["type.text"] == "Sack") - | (play_df["type.text"] == "Interception Return") - | (play_df["type.text"] == "Interception Return Touchdown") - | ( - play_df["type.text"].isin( - [ - "Fumble Recovery (Opponent) Touchdown", - "Fumble Recovery (Opponent)", - ] - ) - & play_df.text.str.contains("sacked", case=False) - ), - None, - play_df["receiver_player"], - ) - - play_df.receiver_player = play_df.receiver_player.str.replace( - "to ", "", case=False, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - "\\,.+", "", case=False, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - "for (.+)", "", case=False, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - r" (\d{1,2})", "", case=False, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - " Yd pass", "", case=False, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - " Yd TD pass", "", case=False, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - "pass complete to", "", case=False, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - "penalty", "", case=False, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - ' "', "", case=False, regex=True - ) - play_df.receiver_player = np.where( - ~(play_df.receiver_player.str.contains("III", na=False)), - play_df.receiver_player.str.replace("[A-Z]{3,}", "", case=True, regex=True), - play_df.receiver_player, - ) - - play_df.receiver_player = play_df.receiver_player.str.replace( - " &", "", case=True, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - "A&M", "", case=True, regex=False - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - " ST", "", case=True, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - " GA", "", case=True, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - " UL", "", case=True, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - " FL", "", case=True, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - " OH", "", case=True, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - " NC", "", case=True, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - ' "', "", case=True, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - " \\u00c9", "", case=True, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - " fumbled,", "", case=False, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - "the (.+)", "", case=False, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - "pass incomplete to", "", case=False, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - "(.+)pass incomplete to", "", case=False, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - "(.+)pass incomplete", "", case=False, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - "pass incomplete", "", case=False, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - r" \((.+)\)", "", regex=True - ) - - play_df["sack_players"] = np.where( - (play_df["sack"] == True) - | (play_df["fumble_vec"] == True) & (play_df["pass"] == True), - play_df.text.str.extract("sacked by(.+)", flags=re.IGNORECASE)[0], - play_df.sack_players, - ) - - play_df["sack_players"] = play_df["sack_players"].str.replace( - "for (.+)", "", case=True, regex=True - ) - play_df["sack_players"] = play_df["sack_players"].str.replace( - "(.+) by ", "", case=True, regex=True - ) - play_df["sack_players"] = play_df["sack_players"].str.replace( - " at the (.+)", "", case=True, regex=True - ) - play_df["sack_player1"] = play_df["sack_players"].str.replace( - "and (.+)", "", case=True, regex=True - ) - play_df["sack_player2"] = np.where( - play_df["sack_players"].str.contains("and (.+)"), - play_df["sack_players"].str.replace("(.+) and", "", case=True, regex=True), - None, - ) - - play_df["interception_player"] = np.where( - (play_df["type.text"] == "Interception Return") - | (play_df["type.text"] == "Interception Return Touchdown") - & play_df["pass"] - == True, - play_df.text.str.extract("intercepted (.+)", flags=re.IGNORECASE)[0], - play_df.interception_player, - ) - - play_df["interception_player"] = np.where( - play_df.text.str.contains("Yd Interception Return", case=True, regex=True), - play_df.text.str.extract( - "(.{0,25} )\\d{0,2} Yd Interception Return|(.{0,25} )\\d{0,2} yd interception return", - flags=re.IGNORECASE, - ).bfill(axis=1)[0], - play_df.interception_player, - ) - play_df["interception_player"] = play_df["interception_player"].str.replace( - "return (.+)", "", case=True, regex=True - ) - play_df["interception_player"] = play_df["interception_player"].str.replace( - "(.+) intercepted", "", case=True, regex=True - ) - play_df["interception_player"] = play_df["interception_player"].str.replace( - "intercepted", "", case=True, regex=True - ) - play_df["interception_player"] = play_df["interception_player"].str.replace( - "Yd Interception Return", "", regex=True - ) - play_df["interception_player"] = play_df["interception_player"].str.replace( - "for a 1st down", "", case=True, regex=True - ) - play_df["interception_player"] = play_df["interception_player"].str.replace( - "(\\d{1,2})", "", case=True, regex=True - ) - play_df["interception_player"] = play_df["interception_player"].str.replace( - "for a TD", "", case=True, regex=True - ) - play_df["interception_player"] = play_df["interception_player"].str.replace( - "at the (.+)", "", case=True, regex=True - ) - play_df["interception_player"] = play_df["interception_player"].str.replace( - " by ", "", case=True, regex=True - ) - - play_df["pass_breakup_player"] = np.where( - play_df["pass"] == True, - play_df.text.str.extract("broken up by (.+)").bfill(axis=1)[0], - play_df.pass_breakup_player, - ) - play_df["pass_breakup_player"] = play_df["pass_breakup_player"].str.replace( - "(.+) broken up by", "", case=True, regex=True - ) - play_df["pass_breakup_player"] = play_df["pass_breakup_player"].str.replace( - "broken up by", "", case=True, regex=True - ) - play_df["pass_breakup_player"] = play_df["pass_breakup_player"].str.replace( - "Penalty(.+)", "", case=True, regex=True - ) - play_df["pass_breakup_player"] = play_df["pass_breakup_player"].str.replace( - "SOUTH FLORIDA", "", case=True, regex=True - ) - play_df["pass_breakup_player"] = play_df["pass_breakup_player"].str.replace( - "WEST VIRGINIA", "", case=True, regex=True - ) - play_df["pass_breakup_player"] = play_df["pass_breakup_player"].str.replace( - "MISSISSIPPI ST", "", case=True, regex=True - ) - play_df["pass_breakup_player"] = play_df["pass_breakup_player"].str.replace( - "CAMPBELL", "", case=True, regex=True - ) - play_df["pass_breakup_player"] = play_df["pass_breakup_player"].str.replace( - "COASTL CAROLINA", "", case=True, regex=True - ) - - play_df["punter_player"] = np.where( - play_df["type.text"].str.contains("Punt", regex=True), - play_df.text.str.extract( - r"(.{0,30}) punt|Punt by (.{0,30})", flags=re.IGNORECASE - ).bfill(axis=1)[0], - play_df.punter_player, - ) - play_df["punter_player"] = play_df["punter_player"].str.replace( - " punt", "", case=False, regex=True - ) - play_df["punter_player"] = play_df["punter_player"].str.replace( - r" for(.+)", "", case=False, regex=True - ) - play_df["punter_player"] = play_df["punter_player"].str.replace( - "Punt by ", "", case=False, regex=True - ) - play_df["punter_player"] = play_df["punter_player"].str.replace( - r"\((.+)\)", "", case=False, regex=True - ) - play_df["punter_player"] = play_df["punter_player"].str.replace( - r" returned \d+", "", case=False, regex=True - ) - play_df["punter_player"] = play_df["punter_player"].str.replace( - " returned", "", case=False, regex=True - ) - play_df["punter_player"] = play_df["punter_player"].str.replace( - " no return", "", case=False, regex=True - ) - - play_df["punt_return_player"] = np.where( - play_df["type.text"].str.contains("Punt", regex=True), - play_df.text.str.extract( - r", (.{0,25}) returns|fair catch by (.{0,25})|, returned by (.{0,25})|yards by (.{0,30})| return by (.{0,25})", - flags=re.IGNORECASE, - ).bfill(axis=1)[0], - play_df.punt_return_player, - ) - play_df["punt_return_player"] = play_df["punt_return_player"].str.replace( - ", ", "", case=False, regex=True - ) - play_df["punt_return_player"] = play_df["punt_return_player"].str.replace( - " returns", "", case=False, regex=True - ) - play_df["punt_return_player"] = play_df["punt_return_player"].str.replace( - " returned", "", case=False, regex=True - ) - play_df["punt_return_player"] = play_df["punt_return_player"].str.replace( - " return", "", case=False, regex=True - ) - play_df["punt_return_player"] = play_df["punt_return_player"].str.replace( - "fair catch by", "", case=False, regex=True - ) - play_df["punt_return_player"] = play_df["punt_return_player"].str.replace( - r" at (.+)", "", case=False, regex=True - ) - play_df["punt_return_player"] = play_df["punt_return_player"].str.replace( - r" for (.+)", "", case=False, regex=True - ) - play_df["punt_return_player"] = play_df["punt_return_player"].str.replace( - r"(.+) by ", "", case=False, regex=True - ) - play_df["punt_return_player"] = play_df["punt_return_player"].str.replace( - r" to (.+)", "", case=False, regex=True - ) - play_df["punt_return_player"] = play_df["punt_return_player"].str.replace( - r"\((.+)\)", "", case=False, regex=True - ) - - play_df["punt_block_player"] = np.where( - play_df["type.text"].str.contains("Punt", case=True, regex=True), - play_df.text.str.extract( - "punt blocked by (.{0,25})| blocked by(.+)", flags=re.IGNORECASE - ).bfill(axis=1)[0], - play_df.punt_block_player, - ) - play_df["punt_block_player"] = play_df["punt_block_player"].str.replace( - r"punt blocked by |for a(.+)", "", case=True, regex=True - ) - play_df["punt_block_player"] = play_df["punt_block_player"].str.replace( - r"blocked by(.+)", "", case=True, regex=True - ) - play_df["punt_block_player"] = play_df["punt_block_player"].str.replace( - r"blocked(.+)", "", case=True, regex=True - ) - play_df["punt_block_player"] = play_df["punt_block_player"].str.replace( - r" for(.+)", "", case=True, regex=True - ) - play_df["punt_block_player"] = play_df["punt_block_player"].str.replace( - r",(.+)", "", case=True, regex=True - ) - play_df["punt_block_player"] = play_df["punt_block_player"].str.replace( - r"punt blocked by |for a(.+)", "", case=True, regex=True - ) - - play_df["punt_block_player"] = np.where( - play_df["type.text"].str.contains("yd return of blocked punt"), - play_df.text.str.extract("(.+) yd return of blocked").bfill(axis=1)[0], - play_df.punt_block_player, - ) - play_df["punt_block_player"] = play_df["punt_block_player"].str.replace( - "blocked|Blocked", "", regex=True - ) - play_df["punt_block_player"] = play_df["punt_block_player"].str.replace( - r"\\d+", "", regex=True - ) - play_df["punt_block_player"] = play_df["punt_block_player"].str.replace( - "yd return of", "", regex=True - ) - - play_df["punt_block_return_player"] = np.where( - ( - play_df["type.text"].str.contains( - "Punt", case=False, flags=0, na=False, regex=True - ) - ) - & ( - play_df.text.str.contains( - "blocked", case=False, flags=0, na=False, regex=True - ) - & play_df.text.str.contains( - "return", case=False, flags=0, na=False, regex=True - ) - ), - play_df.text.str.extract("(.+) return").bfill(axis=1)[0], - play_df.punt_block_return_player, - ) - play_df["punt_block_return_player"] = play_df[ - "punt_block_return_player" - ].str.replace("(.+)blocked by {punt_block_player}", "") - play_df["punt_block_return_player"] = play_df[ - "punt_block_return_player" - ].str.replace("blocked by {punt_block_player}", "") - play_df["punt_block_return_player"] = play_df[ - "punt_block_return_player" - ].str.replace("return(.+)", "", regex=True) - play_df["punt_block_return_player"] = play_df[ - "punt_block_return_player" - ].str.replace("return", "", regex=True) - play_df["punt_block_return_player"] = play_df[ - "punt_block_return_player" - ].str.replace("(.+)blocked by", "", regex=True) - play_df["punt_block_return_player"] = play_df[ - "punt_block_return_player" - ].str.replace("for a TD(.+)|for a SAFETY(.+)", "", regex=True) - play_df["punt_block_return_player"] = play_df[ - "punt_block_return_player" - ].str.replace("blocked by", "", regex=True) - play_df["punt_block_return_player"] = play_df[ - "punt_block_return_player" - ].str.replace(", ", "", regex=True) - - play_df["kickoff_player"] = np.where( - play_df["type.text"].str.contains("Kickoff"), - play_df.text.str.extract("(.{0,25}) kickoff|(.{0,25}) on-side").bfill( - axis=1 - )[0], - play_df.kickoff_player, - ) - play_df["kickoff_player"] = play_df["kickoff_player"].str.replace( - " on-side| kickoff", "", regex=True - ) - - play_df["kickoff_return_player"] = np.where( - play_df["type.text"].str.contains("ickoff"), - play_df.text.str.extract( - ", (.{0,25}) return|, (.{0,25}) fumble|returned by (.{0,25})|touchback by (.{0,25})", - flags=re.IGNORECASE, - ).bfill(axis=1)[0], - play_df.kickoff_return_player, - ) - play_df["kickoff_return_player"] = play_df["kickoff_return_player"].str.replace( - ", ", "", case=False, regex=True - ) - play_df["kickoff_return_player"] = play_df["kickoff_return_player"].str.replace( - " return| fumble| returned by| for |touchback by ", - "", - case=False, - regex=True, - ) - play_df["kickoff_return_player"] = play_df["kickoff_return_player"].str.replace( - r"\((.+)\)(.+)", "", case=False, regex=True - ) - - play_df["fg_kicker_player"] = np.where( - play_df["type.text"].str.contains("Field Goal"), - play_df.text.str.extract( - "(.{0,25} )\\d{0,2} yd field goal|(.{0,25} )\\d{0,2} yd fg|(.{0,25} )\\d{0,2} yard field goal", - flags=re.IGNORECASE, - ).bfill(axis=1)[0], - play_df.fg_kicker_player, - ) - play_df["fg_kicker_player"] = play_df["fg_kicker_player"].str.replace( - " Yd Field Goal|Yd FG |yd FG| yd FG", "", case=False, regex=True - ) - play_df["fg_kicker_player"] = play_df["fg_kicker_player"].str.replace( - "(\\d{1,2})", "", case=False, regex=True + def __add_team_score_variables(self, play_df): + """ + Creates the following columns in play_df: + * Team Score variables + * Fix change of poss variables + """ + play_df = play_df.with_columns( + pos_team = pl.col("start.pos_team.id"), + def_pos_team = pl.col("start.def_pos_team.id"), + ).with_columns( + is_home = pl.col("pos_team") == pl.col("homeTeamId"), + + # --- Team Score variables ------ + lag_homeScore = pl.col("homeScore").shift(1), + lag_awayScore = pl.col("awayScore").shift(1), + ).with_columns( + lag_HA_score_diff = pl.col("lag_homeScore") - pl.col("lag_awayScore"), + HA_score_diff = pl.col("homeScore") - pl.col("awayScore"), + ).with_columns( + net_HA_score_pts = pl.col("HA_score_diff") - pl.col("lag_HA_score_diff"), + H_score_diff = pl.col("homeScore") - pl.col("lag_homeScore"), + A_score_diff = pl.col("awayScore") - pl.col("lag_awayScore"), + ).with_columns( + homeScore = pl.when( + (pl.col("scoringPlay") == False) + & (pl.col("game_play_number") != 1) + & (pl.col("H_score_diff") >= 9)) + .then(pl.col("lag_homeScore")) + .when( + (pl.col("scoringPlay") == False) + & (pl.col("game_play_number") != 1) + & (pl.col("H_score_diff") < 9) + & (pl.col("H_score_diff") > 1)) + .then(pl.col("lag_homeScore")) + .when( + (pl.col("scoringPlay") == False) + & (pl.col("game_play_number") != 1) + & (pl.col("H_score_diff") >= -9) + & (pl.col("H_score_diff") < -1)) + .then(pl.col("homeScore")) + .otherwise(pl.col("homeScore")), + awayScore = pl.when( + (pl.col("scoringPlay") == False) + & (pl.col("game_play_number") != 1) + & (pl.col("A_score_diff") >= 9)) + .then(pl.col("lag_awayScore")) + .when( + (pl.col("scoringPlay") == False) + & (pl.col("game_play_number") != 1) + & (pl.col("A_score_diff") < 9) + & (pl.col("A_score_diff") > 1)) + .then(pl.col("lag_awayScore")) + .when( + (pl.col("scoringPlay") == False) + & (pl.col("game_play_number") != 1) + & (pl.col("A_score_diff") >= -9) + & (pl.col("A_score_diff") < -1)) + .then(pl.col("awayScore")) + .otherwise(pl.col("awayScore")), + + + ).drop( + ["lag_homeScore", "lag_awayScore"] + ).with_columns( + lag_homeScore = pl.col("homeScore").shift(1), + lag_awayScore = pl.col("awayScore").shift(1), + ).with_columns( + lag_homeScore = pl.when(pl.col("lag_homeScore").is_null()).then(0).otherwise(pl.col("lag_homeScore")), + lag_awayScore = pl.when(pl.col("lag_awayScore").is_null()).then(0).otherwise(pl.col("lag_awayScore")), + ).with_columns( + pl.when(pl.col("game_play_number") == 1).then(0).otherwise(pl.col("lag_homeScore")).alias("start.homeScore"), + pl.when(pl.col("game_play_number") == 1).then(0).otherwise(pl.col("lag_awayScore")).alias("start.awayScore"), + pl.col("homeScore").alias("end.homeScore"), + pl.col("awayScore").alias("end.awayScore"), + pl.when(pl.col("pos_team") == pl.col("homeTeamId")) + .then(pl.col("homeScore")) + .otherwise(pl.col("awayScore")) + .alias("pos_team_score"), + pl.when(pl.col("pos_team") == pl.col("homeTeamId")) + .then(pl.col("awayScore")) + .otherwise(pl.col("homeScore")) + .alias("def_pos_team_score"), + + ).with_columns( + pl.when(pl.col("start.pos_team.id") == pl.col("homeTeamId")) + .then(pl.col("start.homeScore")) + .otherwise(pl.col("start.awayScore")) + .alias("start.pos_team_score"), + pl.when(pl.col("start.pos_team.id") == pl.col("homeTeamId")) + .then(pl.col("start.awayScore")) + .otherwise(pl.col("start.homeScore")) + .alias("start.def_pos_team_score"), + ).with_columns( + (pl.col("start.pos_team_score") - pl.col("start.def_pos_team_score")).alias("start.pos_score_diff"), + pl.when(pl.col("pos_team") == pl.col("homeTeamId")) + .then(pl.col("end.homeScore")) + .otherwise(pl.col("end.awayScore")) + .alias("end.pos_team_score"), + pl.when(pl.col("pos_team") == pl.col("homeTeamId")) + .then(pl.col("end.awayScore")) + .otherwise(pl.col("end.homeScore")) + .alias("end.def_pos_team_score"), + ).with_columns( + (pl.col("end.pos_team_score") - pl.col("end.def_pos_team_score")).alias("end.pos_score_diff"), + pl.col("pos_team").shift(1).alias("lag_pos_team") + ).with_columns( + pl.when(pl.col("lag_pos_team").is_null()).then(pl.col("pos_team")).otherwise(pl.col("lag_pos_team")).alias("lag_pos_team"), + pl.col("pos_team").shift(-1).alias("lead_pos_team"), + pl.col("pos_team").shift(-2).alias("lead_pos_team2"), + (pl.col("pos_team_score") - pl.col("def_pos_team_score")).alias("pos_score_diff"), + ).with_columns( + pl.col("pos_score_diff").shift(1).alias("lag_pos_score_diff"), + ).with_columns( + pl.when(pl.col("lag_pos_score_diff").is_null()).then(0).otherwise(pl.col("lag_pos_score_diff")).alias("lag_pos_score_diff"), + ).with_columns( + pl.when(pl.col("lag_pos_team") == pl.col("pos_team")) + .then(pl.col("pos_score_diff") - pl.col("lag_pos_score_diff")) + .otherwise(pl.col("pos_score_diff") + pl.col("lag_pos_score_diff")) + .alias("pos_score_pts"), + pl.when((pl.col("kickoff_play") == True) + .and_(pl.col("lag_pos_team") == pl.col("pos_team"))) + .then(pl.col("lag_pos_score_diff")) + .when((pl.col("kickoff_play") == True) + .or_(pl.col("lag_pos_team") != pl.col("pos_team"))) + .then(-1 * pl.col("lag_pos_score_diff")) + .otherwise(pl.col("lag_pos_score_diff")) + .alias("pos_score_diff_start"), + ).with_columns( + pl.when(pl.col("pos_score_diff_start").is_null() == True) + .then(pl.col("pos_score_diff")) + .otherwise(pl.col("pos_score_diff_start")) + .alias("pos_score_diff_start"), + pl.when(pl.col("start.pos_team.id") == pl.col("firstHalfKickoffTeamId")) + .then(True) + .otherwise(False) + .alias("start.pos_team_receives_2H_kickoff"), + pl.when(pl.col("end.pos_team.id") == pl.col("firstHalfKickoffTeamId")) + .then(True) + .otherwise(False) + .alias("end.pos_team_receives_2H_kickoff"), + pl.when(pl.col("start.pos_team.id") == pl.col("end.pos_team.id")) + .then(False) + .otherwise(True) + .alias("change_of_poss"), + ).with_columns( + pl.when(pl.col("change_of_poss").is_null() == True) + .then(False) + .otherwise(pl.col("change_of_poss")) + .alias("change_of_poss"), ) - play_df["fg_block_player"] = np.where( - play_df["type.text"].str.contains("Field Goal"), - play_df.text.str.extract("blocked by (.{0,25})", flags=re.IGNORECASE).bfill(axis=1)[0], - play_df.fg_block_player, - ) - # play_df["fg_block_player"] = play_df["fg_block_player"].str.replace( - # ",(.+)", "", case=False, regex=True - # ) - # play_df["fg_block_player"] = play_df["fg_block_player"].str.replace( - # "blocked by ", "", case=False, regex=True - # ) - # play_df["fg_block_player"] = play_df["fg_block_player"].str.replace( - # " (.)+", "", case=False, regex=True - # ) - - play_df["fg_return_player"] = np.where( - (play_df["type.text"].str.contains("Field Goal")) - & (play_df["type.text"].str.contains("blocked by|missed")) - & (play_df["type.text"].str.contains("return")), - play_df.text.str.extract(" (.+)").bfill(axis=1)[0], - play_df.fg_return_player, - ) + return play_df - play_df["fg_return_player"] = play_df["fg_return_player"].str.replace( - ",(.+)", "", case=False, regex=True - ) - play_df["fg_return_player"] = play_df["fg_return_player"].str.replace( - "return ", "", case=False, regex=True - ) - play_df["fg_return_player"] = play_df["fg_return_player"].str.replace( - "returned ", "", case=False, regex=True - ) - play_df["fg_return_player"] = play_df["fg_return_player"].str.replace( - " for (.+)", "", case=False, regex=True - ) - play_df["fg_return_player"] = play_df["fg_return_player"].str.replace( - " for (.+)", "", case=False, regex=True + def __add_new_play_types(self, play_df): + """ + Creates the following columns in play_df: + * Fix play types + """ + # -------------------------------------------------- + play_df = play_df.with_columns( + # --- Fix Strip Sacks to Fumbles ---- + pl.when((pl.col("fumble_vec") == True) + .and_(pl.col("pass") == True) + .and_(pl.col("change_of_poss") == 1) + .and_(pl.col("td_play") == False) + .and_(pl.col("start.down") != 4) + .and_(pl.col("type.text").is_in(defense_score_vec) == False)) + .then("Fumble Recovery (Opponent)") + .otherwise(pl.col("type.text")) + .alias("type.text"), + ).with_columns( + pl.when((pl.col("fumble_vec") == True) + .and_(pl.col("pass") == True) + .and_(pl.col("change_of_poss") == 1) + .and_(pl.col("td_play") == True)) + .then("Fumble Recovery (Opponent) Touchdown") + .otherwise(pl.col("type.text")) + .alias("type.text"), + ).with_columns( + # --- Fix rushes with fumbles and a change of possession to fumbles---- + pl.when((pl.col("fumble_vec") == True) + .and_(pl.col("rush") == True) + .and_(pl.col("change_of_poss") == 1) + .and_(pl.col("td_play") == False) + .and_(pl.col("start.down") != 4) + .and_(pl.col("type.text").is_in(defense_score_vec) == False)) + .then("Fumble Recovery (Opponent)") + .otherwise(pl.col("type.text")) + .alias("type.text"), + ).with_columns( + pl.when((pl.col("fumble_vec") == True) + .and_(pl.col("rush") == True) + .and_(pl.col("change_of_poss") == 1) + .and_(pl.col("td_play") == True)) + .then("Fumble Recovery (Opponent) Touchdown") + .otherwise(pl.col("type.text")) + .alias("type.text"), + ).with_columns( + # -- Fix kickoff fumble return TDs ---- + pl.when((pl.col("kickoff_play") == True) + .and_(pl.col("change_of_poss") == 1) + .and_(pl.col("td_play") == True) + .and_(pl.col("td_check") == True)) + .then("Kickoff Return Touchdown") + .otherwise(pl.col("type.text")) + .alias("type.text"), + ).with_columns( + # -- Fix punt return TDs ---- + pl.when((pl.col("punt_play") == True) + .and_(pl.col("td_play") == True) + .and_(pl.col("td_check") == True)) + .then("Punt Return Touchdown") + .otherwise(pl.col("type.text")) + .alias("type.text"), + ).with_columns( + # -- Fix kick return TDs ---- + pl.when((pl.col("kickoff_play") == True) + .and_(pl.col("fumble_vec") == False) + .and_(pl.col("td_play") == True) + .and_(pl.col("td_check") == True)) + .then("Kickoff Return Touchdown") + .otherwise(pl.col("type.text")) + .alias("type.text"), + ).with_columns( + # -- Fix rush/pass tds that aren't explicit---- + pl.when((pl.col("td_play") == True) + .and_(pl.col("rush") == True) + .and_(pl.col("fumble_vec") == False) + .and_(pl.col("td_check") == True)) + .then("Rushing Touchdown") + .otherwise(pl.col("type.text")) + .alias("type.text"), + ).with_columns( + pl.when((pl.col("td_play") == True) + .and_(pl.col("pass") == True) + .and_(pl.col("fumble_vec") == False) + .and_(pl.col("td_check") == True) + .and_(pl.col("type.text").is_in(int_vec) == False)) + .then("Passing Touchdown") + .otherwise(pl.col("type.text")) + .alias("type.text"), + ).with_columns( + pl.when((pl.col("pass") == True) + .and_(pl.col("type.text").is_in(["Pass Reception", "Pass Completion", "Pass"])) + .and_(pl.col("statYardage") == pl.col("start.yardsToEndzone")) + .and_(pl.col("fumble_vec") == False) + .and_(pl.col("type.text").is_in(int_vec) == False)) + .then("Passing Touchdown") + .otherwise(pl.col("type.text")) + .alias("type.text"), + ).with_columns( + pl.when((pl.col("type.text").is_in(["Blocked Field Goal"])) + .and_(pl.col("text").str.contains("(?i)for a TD"))) + .then("Blocked Field Goal Touchdown") + .otherwise(pl.col("type.text")) + .alias("type.text"), + ).with_columns( + pl.when((pl.col("type.text").is_in(["Blocked Punt"])) + .and_(pl.col("text").str.contains("(?i)for a TD"))) + .then("Blocked Punt Touchdown") + .otherwise(pl.col("type.text")) + .alias("type.text"), + ).with_columns( + # -- Fix duplicated TD play_type labels---- + pl.col("type.text").str.replace(r"(?i)Touchdown Touchdown", "Touchdown") + .alias("type.text") + ).with_columns( + #-- Fix Pass Interception Return TD play_type labels---- + pl.when(pl.col("text").str.contains("(?i)pass intercepted for a TD")) + .then("Interception Return Touchdown") + .otherwise(pl.col("type.text")) + .alias("type.text"), + ).with_columns( + #-- Fix Sack/Fumbles Touchdown play_type labels---- + pl.when((pl.col("text").str.contains("(?i)sacked")) + .and_(pl.col("text").str.contains("(?i)fumbled")) + .and_(pl.col("text").str.contains("(?i)TD"))) + .then("Fumble Recovery (Opponent) Touchdown") + .otherwise(pl.col("type.text")) + .alias("type.text"), + ).with_columns( + #-- Fix generic pass plays ---- + ##-- first one looks for complete pass + pl.when((pl.col("type.text") == "Pass") + .and_(pl.col("text").str.contains("(?i)pass complete"))) + .then("Pass Completion") + .otherwise(pl.col("type.text")) + .alias("type.text"), + ).with_columns( + ##-- second one looks for incomplete pass + pl.when((pl.col("type.text") == "Pass") + .and_(pl.col("text").str.contains("(?i)pass incomplete"))) + .then("Pass Incompletion") + .otherwise(pl.col("type.text")) + .alias("type.text"), + ).with_columns( + ##-- third one looks for interceptions + pl.when((pl.col("type.text") == "Pass") + .and_(pl.col("text").str.contains("(?i)pass intercepted"))) + .then("Pass Interception") + .otherwise(pl.col("type.text")) + .alias("type.text"), + ).with_columns( + ##-- fourth one looks for sacked + pl.when((pl.col("type.text") == "Pass") + .and_(pl.col("text").str.contains("(?i)sacked"))) + .then("Sack") + .otherwise(pl.col("type.text")) + .alias("type.text"), + ).with_columns( + ##-- fifth one play type is Passing Touchdown, but its intercepted + pl.when((pl.col("type.text") == "Passing Touchdown") + .and_(pl.col("text").str.contains("(?i)pass intercepted for a TD"))) + .then("Interception Return Touchdown") + .otherwise(pl.col("type.text")) + .alias("type.text"), + ).with_columns( + # --- Moving non-Touchdown pass interceptions to one play_type: "Interception Return" ----- + pl.when(pl.col("type.text").is_in(["Interception", "Pass Interception", "Pass Interception Return"])) + .then("Interception Return") + .otherwise(pl.col("type.text")) + .alias("type.text"), + ).with_columns( + # --- Moving Kickoff/Punt Touchdowns without fumbles to Kickoff/Punt Return Touchdown + pl.when((pl.col("type.text") == "Kickoff Touchdown") + .and_(pl.col("fumble_vec") == False)) + .then("Kickoff Return Touchdown") + .otherwise(pl.col("type.text")) + .alias("type.text"), + ).with_columns( + pl.when((pl.col("type.text") == "Kickoff") + .and_(pl.col("td_play") == True) + .and_(pl.col("fumble_vec") == False)) + .then("Kickoff Return Touchdown") + .when((pl.col("type.text") == "Kickoff") + .and_(pl.col("text").str.contains("(?i)for a TD")) + .and_(pl.col("fumble_vec") == False)) + .then("Kickoff Return Touchdown") + .otherwise(pl.col("type.text")) + .alias("type.text"), + ).with_columns( + pl.when((pl.col("type.text").is_in(["Kickoff", "Kickoff Return (Offense)"])) + .and_(pl.col("fumble_vec") == True) + .and_(pl.col("change_of_poss") == 1)) + .then("Kickoff Team Fumble Recovery") + .otherwise(pl.col("type.text")) + .alias("type.text"), + ).with_columns( + pl.when((pl.col("type.text") == "Punt Touchdown") + .and_(pl.col("fumble_vec") == False) + .and_(pl.col("change_of_poss") == 1)) + .then("Punt Return Touchdown") + .when((pl.col("type.text") == "Punt") + .and_(pl.col("text").str.contains("(?i)for a TD")) + .and_(pl.col("change_of_poss") == 1)) + .then("Punt Return Touchdown") + .otherwise(pl.col("type.text")) + .alias("type.text"), + ).with_columns( + pl.when((pl.col("type.text") == "Punt") + .and_(pl.col("fumble_vec") == True) + .and_(pl.col("change_of_poss") == 0)) + .then("Punt Team Fumble Recovery") + .otherwise(pl.col("type.text")) + .alias("type.text"), + ).with_columns( + pl.when(pl.col("type.text").is_in(["Punt Touchdown"])) + .then("Punt Team Fumble Recovery Touchdown") + .when((pl.col("scoringPlay") == True) + .and_(pl.col("punt_play") == True) + .and_(pl.col("change_of_poss") == 0)) + .then("Punt Team Fumble Recovery Touchdown") + .otherwise(pl.col("type.text")) + .alias("type.text"), + ).with_columns( + pl.when(pl.col("type.text").is_in(["Kickoff Touchdown"])) + .then("Kickoff Team Fumble Recovery Touchdown") + .otherwise(pl.col("type.text")) + .alias("type.text"), + ).with_columns( + pl.when((pl.col("type.text").is_in(["Fumble Return Touchdown"])) + .and_((pl.col("pass") == True) + .or_(pl.col("rush") == True))) + .then("Fumble Recovery (Opponent) Touchdown") + .otherwise(pl.col("type.text")) + .alias("type.text"), + ).with_columns( + # --- Safeties (kickoff, punt, penalty) ---- + pl.when((pl.col("type.text").is_in(["Pass Reception", "Rush", "Rushing Touchdown"])) + .and_((pl.col("pass") == True) + .or_(pl.col("rush") == True)) + .and_(pl.col("safety") == True)) + .then("Safety") + .otherwise(pl.col("type.text")) + .alias("type.text"), + ).with_columns( + pl.when(pl.col("kickoff_safety") == True) + .then("Kickoff (Safety)") + .otherwise(pl.col("type.text")) + .alias("type.text"), + ).with_columns( + pl.when(pl.col("punt_safety") == True) + .then("Punt (Safety)") + .otherwise(pl.col("type.text")) + .alias("type.text"), + ).with_columns( + pl.when(pl.col("penalty_safety") == True) + .then("Penalty (Safety)") + .otherwise(pl.col("type.text")) + .alias("type.text"), + ).with_columns( + pl.when((pl.col("type.text") == "Extra Point Good") + .and_(pl.col("text").str.contains("(?i)Two-Point"))) + .then("Two-Point Conversion Good") + .otherwise(pl.col("type.text")) + .alias("type.text"), + ).with_columns( + pl.when((pl.col("type.text") == "Extra Point Missed") + .and_(pl.col("text").str.contains("(?i)Two-Point"))) + .then("Two-Point Conversion Missed") + .otherwise(pl.col("type.text")) + .alias("type.text"), ) - play_df["fg_return_player"] = np.where( - play_df["type.text"].isin( - ["Missed Field Goal Return", "Missed Field Goal Return Touchdown"] - ), - play_df.text.str.extract("(.+)return").bfill(axis=1)[0], - play_df.fg_return_player, - ) - play_df["fg_return_player"] = play_df["fg_return_player"].str.replace( - " return", "", case=False, regex=True - ) - play_df["fg_return_player"] = play_df["fg_return_player"].str.replace( - "(.+),", "", case=False, regex=True - ) + return play_df - play_df["fumble_player"] = np.where( - play_df["text"].str.contains( - "fumble", case=False, flags=0, na=False, regex=True - ), - play_df["text"].str.extract("(.{0,25} )fumble").bfill(axis=1)[0], - play_df.fumble_player, - ) - play_df["fumble_player"] = play_df["fumble_player"].str.replace( - " fumble(.+)", "", case=False, regex=True - ) - play_df["fumble_player"] = play_df["fumble_player"].str.replace( - "fumble", "", case=False, regex=True - ) - play_df["fumble_player"] = play_df["fumble_player"].str.replace( - " yds", "", case=False, regex=True - ) - play_df["fumble_player"] = play_df["fumble_player"].str.replace( - " yd", "", case=False, regex=True - ) - play_df["fumble_player"] = play_df["fumble_player"].str.replace( - "yardline", "", case=False, regex=True - ) - play_df["fumble_player"] = play_df["fumble_player"].str.replace( - " yards| yard|for a TD|or a safety", "", case=False, regex=True - ) - play_df["fumble_player"] = play_df["fumble_player"].str.replace( - " for ", "", case=False, regex=True - ) - play_df["fumble_player"] = play_df["fumble_player"].str.replace( - " a safety", "", case=False, regex=True - ) - play_df["fumble_player"] = play_df["fumble_player"].str.replace( - "r no gain", "", case=False, regex=True - ) - play_df["fumble_player"] = play_df["fumble_player"].str.replace( - "(.+)(\\d{1,2})", "", case=False, regex=True - ) - play_df["fumble_player"] = play_df["fumble_player"].str.replace( - "(\\d{1,2})", "", case=False, regex=True - ) - play_df["fumble_player"] = play_df["fumble_player"].str.replace( - ", ", "", case=False, regex=True - ) - play_df["fumble_player"] = np.where( - play_df["type.text"] == "Penalty", None, play_df.fumble_player + def __setup_penalty_data(self, play_df): + """ + Creates the following columns in play_df: + * Penalty flag + * Penalty declined + * Penalty no play + * Penalty off-set + * Penalty 1st down conversion + * Penalty in text + * Yds Penalty + """ + ##-- 'Penalty' in play text ---- + play_df = play_df.with_columns( + # -- T/F flag conditions penalty_flag + penalty_flag = pl.when((pl.col("type.text") == "Penalty") + .or_(pl.col("text").str.contains("(?i)penalty"))) + .then(True) + .otherwise(False), + # -- T/F flag conditions penalty_declined + penalty_declined = pl.when((pl.col("type.text") == "Penalty") + .and_(pl.col("text").str.contains("(?i)declined"))) + .then(True) + .otherwise(False), + # -- T/F flag conditions penalty_no_play + penalty_no_play = pl.when((pl.col("type.text") == "Penalty") + .and_(pl.col("text").str.contains("(?i)no play"))) + .then(True) + .otherwise(False), + + # -- T/F flag conditions penalty_offset + penalty_offset = pl.when((pl.col("type.text") == "Penalty") + .and_(pl.col("text").str.contains("(?i)off-setting"))) + .then(True) + .when((pl.col("text").str.contains("(?i)penalty")) + .and_(pl.col("text").str.contains("(?i)off-setting"))) + .then(True) + .otherwise(False), + # -- T/F flag conditions penalty_1st_conv + penalty_1st_conv = pl.when((pl.col("type.text") == "Penalty") + .and_(pl.col("text").str.contains("(?i)1st down"))) + .then(True) + .when((pl.col("text").str.contains("(?i)penalty")) + .and_(pl.col("text").str.contains("(?i)1st down"))) + .then(True) + .otherwise(False), + # -- T/F flag for penalty text but not penalty play type -- + penalty_in_text = pl.when((pl.col("text").str.contains("(?i)penalty")) + .and_(pl.col("type.text") != "Penalty", + pl.col("text").str.contains("(?i)declined") == False, + pl.col("text").str.contains("(?i)off-setting") == False, + pl.col("text").str.contains("(?i)no play") == False)) + .then(True) + .otherwise(False), + + ).with_columns( + penalty_detail = pl.when(pl.col("penalty_offset") == 1) + .then("Offsetting") + .when(pl.col("penalty_declined") == 1) + .then("Declined") + .when(pl.col("text").str.contains("(?i)roughing passer")) + .then("Roughing the Passer") + .when(pl.col("text").str.contains("(?i)offensive holding")) + .then("Offensive Holding") + .when(pl.col("text").str.contains("(?i)pass interference")) + .then("Pass Interference") + .when(pl.col("text").str.contains("(?i)encroachment")) + .then("Encroachment") + .when(pl.col("text").str.contains("(?i)defensive pass interference")) + .then("Defensive Pass Interference") + .when(pl.col("text").str.contains("(?i)offensive pass interference")) + .then("Offensive Pass Interference") + .when(pl.col("text").str.contains("(?i)illegal procedure")) + .then("Illegal Procedure") + .when(pl.col("text").str.contains("(?i)defensive holding")) + .then("Defensive Holding") + .when(pl.col("text").str.contains("(?i)holding")) + .then("Holding") + .when(pl.col("text").str.contains("(?i)offensive offside|(?i)offside offense")) + .then("Offensive Offside") + .when(pl.col("text").str.contains("(?i)defensive offside|(?i)offside defense")) + .then("Defensive Offside") + .when(pl.col("text").str.contains("(?i)offside")) + .then("Offside") + .when(pl.col("text").str.contains("(?i)illegal fair catch signal")) + .then("Illegal Fair Catch Signal") + .when(pl.col("text").str.contains("(?i)illegal batting")) + .then("Illegal Batting") + .when(pl.col("text").str.contains("(?i)neutral zone infraction")) + .then("Neutral Zone Infraction") + .when(pl.col("text").str.contains("(?i)ineligible downfield")) + .then("Ineligible Downfield") + .when(pl.col("text").str.contains("(?i)illegal use of hands")) + .then("Illegal Use of Hands") + .when(pl.col("text").str.contains("(?i)kickoff out of bounds|(?i)kickoff out-of-bounds")) + .then("Kickoff Out of Bounds") + .when(pl.col("text").str.contains("(?i)12 men on the field")) + .then("12 Men on the Field") + .when(pl.col("text").str.contains("(?i)illegal block")) + .then("Illegal Block") + .when(pl.col("text").str.contains("(?i)personal foul")) + .then("Personal Foul") + .when(pl.col("text").str.contains("(?i)false start")) + .then("False Start") + .when(pl.col("text").str.contains("(?i)substitution infraction")) + .then("Substitution Infraction") + .when(pl.col("text").str.contains("(?i)illegal formation")) + .then("Illegal Formation") + .when(pl.col("text").str.contains("(?i)illegal touching")) + .then("Illegal Touching") + .when(pl.col("text").str.contains("(?i)sideline interference")) + .then("Sideline Interference") + .when(pl.col("text").str.contains("(?i)clipping")) + .then("Clipping") + .when(pl.col("text").str.contains("(?i)sideline infraction")) + .then("Sideline Infraction") + .when(pl.col("text").str.contains("(?i)crackback")) + .then("Crackback") + .when(pl.col("text").str.contains("(?i)illegal snap")) + .then("Illegal Snap") + .when(pl.col("text").str.contains("(?i)illegal helmet contact")) + .then("Illegal Helmet Contact") + .when(pl.col("text").str.contains("(?i)roughing holder")) + .then("Roughing the Holder") + .when(pl.col("text").str.contains("(?i)horse collar tackle")) + .then("Horse Collar Tackle") + .when(pl.col("text").str.contains("(?i)illegal participation")) + .then("Illegal Participation") + .when(pl.col("text").str.contains("(?i)tripping")) + .then("Tripping") + .when(pl.col("text").str.contains("(?i)illegal shift")) + .then("Illegal Shift") + .when(pl.col("text").str.contains("(?i)illegal motion")) + .then("Illegal Motion") + .when(pl.col("text").str.contains("(?i)roughing the kicker")) + .then("Roughing the Kicker") + .when(pl.col("text").str.contains("(?i)delay of game")) + .then("Delay of Game") + .when(pl.col("text").str.contains("(?i)targeting")) + .then("Targeting") + .when(pl.col("text").str.contains("(?i)face mask")) + .then("Face Mask") + .when(pl.col("text").str.contains("(?i)illegal forward pass")) + .then("Illegal Forward Pass") + .when(pl.col("text").str.contains("(?i)intentional grounding")) + .then("Intentional Grounding") + .when(pl.col("text").str.contains("(?i)illegal kicking")) + .then("Illegal Kicking") + .when(pl.col("text").str.contains("(?i)illegal conduct")) + .then("Illegal Conduct") + .when(pl.col("text").str.contains("(?i)kick catching interference")) + .then("Kick Catch Interference") + .when(pl.col("text").str.contains("(?i)kick catch interference")) + .then("Kick Catch Interference") + .when(pl.col("text").str.contains("(?i)unnecessary roughness")) + .then("Unnecessary Roughness") + .when(pl.col("text").str.contains("(?i)Penalty, UR")) + .then("Unnecessary Roughness") + .when(pl.col("text").str.contains("(?i)roughing the snapper")) + .then("Roughing the Snapper") + .when(pl.col("text").str.contains("(?i)illegal blindside block")) + .then("Illegal Blindside Block") + .when(pl.col("text").str.contains("(?i)unsportsmanlike conduct")) + .then("Unsportsmanlike Conduct") + .when(pl.col("text").str.contains("(?i)running into kicker")) + .then("Running Into Kicker") + .when(pl.col("text").str.contains("(?i)failure to wear required equipment")) + .then("Failure to Wear Required Equipment") + .when(pl.col("text").str.contains("(?i)player disqualification")) + .then("Player Disqualification") + .when(pl.col("penalty_flag") == True) + .then("Missing") + ).with_columns( + penalty_text = pl.when(pl.col("penalty_flag") == True) + .then(pl.col("text").str.extract(r"(?i)Penalty(.+)", 1)) + .otherwise(None), + ).with_columns( + yds_penalty = pl.when(pl.col("penalty_flag") == True) + .then(pl.col("penalty_text").str.extract(r"(?i)(.{0,3}) yards|(?i)yds|(?i)yd to the", 1) + .str.replace(" yards to the | yds to the | yd to the ", "")) + .otherwise(None), + + ).with_columns( + yds_penalty = pl.when((pl.col("penalty_flag") == True) + .and_(pl.col("yds_penalty").is_null(), + pl.col("text").str.contains(r"(?i)ards\)"))) + .then(pl.col("text").str.extract(r"(.{0,4})yards\)|Yards\)|yds\)|Yds\)", 1) + .str.replace("yards\\)|Yards\\)|yds\\)|Yds\\)", "").str.replace("\\(", "")) + .otherwise(pl.col("yds_penalty")), ) - play_df["fumble_forced_player"] = np.where( - ( - play_df.text.str.contains( - "fumble", case=False, flags=0, na=False, regex=True - ) - ) - & ( - play_df.text.str.contains( - "forced by", case=False, flags=0, na=False, regex=True - ) - ), - play_df.text.str.extract("forced by(.{0,25})").bfill(axis=1)[0], - play_df.fumble_forced_player, - ) + return play_df - play_df["fumble_forced_player"] = play_df["fumble_forced_player"].str.replace( - "(.+)forced by", "", case=False, regex=True - ) - play_df["fumble_forced_player"] = play_df["fumble_forced_player"].str.replace( - "forced by", "", case=False, regex=True - ) - play_df["fumble_forced_player"] = play_df["fumble_forced_player"].str.replace( - ", recove(.+)", "", case=False, regex=True - ) - play_df["fumble_forced_player"] = play_df["fumble_forced_player"].str.replace( - ", re(.+)", "", case=False, regex=True - ) - play_df["fumble_forced_player"] = play_df["fumble_forced_player"].str.replace( - ", fo(.+)", "", case=False, regex=True - ) - play_df["fumble_forced_player"] = play_df["fumble_forced_player"].str.replace( - ", r", "", case=False, regex=True - ) - play_df["fumble_forced_player"] = play_df["fumble_forced_player"].str.replace( - ", ", "", case=False, regex=True - ) - play_df["fumble_forced_player"] = np.where( - play_df["type.text"] == "Penalty", None, play_df.fumble_forced_player + def __add_play_category_flags(self, play_df): + play_df = play_df.with_columns( + # --- Sacks ----- + sack = pl.when(pl.col("type.text").is_in(["Sack"])) + .then(True) + .when((pl.col("type.text").is_in(["Fumble Recovery (Opponent)", + "Fumble Recovery (Opponent) Touchdown", + "Fumble Recovery (Own)", + "Fumble Recovery (Own) Touchdown"])) + .and_(pl.col("pass") == True) + .and_(pl.col("text").str.contains("(?i)sacked"))) + .then(True) + .when((pl.col("type.text").is_in(["Safety"])) + .and_(pl.col("text").str.contains("(?i)sacked"))) + .then(True) + .otherwise(False), + # --- Interceptions ------ + int = pl.col("type.text").is_in(["Interception Return", "Interception Return Touchdown"]), + int_td = pl.col("type.text").is_in(["Interception Return Touchdown"]), + # --- Pass Completions, Attempts and Targets ------- + completion = pl.when(pl.col("type.text").is_in(["Pass Reception", "Pass Completion", "Passing Touchdown"])) + .then(True) + .when((pl.col("type.text").is_in(["Fumble Recovery (Opponent)", + "Fumble Recovery (Opponent) Touchdown", + "Fumble Recovery (Own)", + "Fumble Recovery (Own) Touchdown"])) + .and_(pl.col("pass") == True) + .and_(pl.col("text").str.contains("(?i)sacked") == False)) + .then(True) + .otherwise(False), + pass_attempt = pl.when(pl.col("type.text").is_in(["Pass Reception", "Pass Completion", "Passing Touchdown", "Pass Incompletion"])) + .then(True) + .when((pl.col("type.text").is_in(["Fumble Recovery (Opponent)", + "Fumble Recovery (Opponent) Touchdown", + "Fumble Recovery (Own)", + "Fumble Recovery (Own) Touchdown"])) + .and_(pl.col("pass") == True) + .and_(pl.col("text").str.contains("(?i)sacked") == False)) + .then(True) + .when((pl.col("pass") == True) + .and_(pl.col("text").str.contains("(?i)sacked") == False)) + .then(True) + .otherwise(False), + target = pl.when(pl.col("type.text").is_in(["Pass Reception", "Pass Completion", "Passing Touchdown", "Pass Incompletion"])) + .then(True) + .when((pl.col("type.text").is_in(["Fumble Recovery (Opponent)", + "Fumble Recovery (Opponent) Touchdown", + "Fumble Recovery (Own)", + "Fumble Recovery (Own) Touchdown"])) + .and_(pl.col("pass") == True) + .and_(pl.col("text").str.contains("(?i)sacked") == False)) + .then(True) + .when((pl.col("pass") == True) + .and_(pl.col("text").str.contains("(?i)sacked") == False)) + .then(True) + .otherwise(False), + pass_breakup = pl.when(pl.col("text").str.contains("(?i)broken up by")) + .then(True) + .otherwise(False), + # --- Pass/Rush TDs ------ + pass_td = pl.when(pl.col("type.text").is_in(["Passing Touchdown"])) + .then(True) + .when((pl.col("pass") == True) + .and_(pl.col("td_play") == True)) + .then(True) + .otherwise(False), + rush_td = pl.when(pl.col("type.text").is_in(["Rushing Touchdown"])) + .then(True) + .when((pl.col("rush") == True) + .and_(pl.col("td_play") == True)) + .then(True) + .otherwise(False), + # --- Change of possession via turnover + turnover_vec = pl.col("type.text").is_in(turnover_vec), + offense_score_play = pl.col("type.text").is_in(offense_score_vec), + defense_score_play = pl.col("type.text").is_in(defense_score_vec), + downs_turnover = pl.when((pl.col("type.text").is_in(normalplay)) + .and_(pl.col("statYardage") < pl.col("start.distance")) + .and_(pl.col("start.down") == 4) + .and_(pl.col("penalty_1st_conv") == False)) + .then(True) + .otherwise(False), + # --- Touchdowns ---- + scoring_play = pl.col("type.text").is_in(scores_vec), + yds_punted = pl.col("text").str.extract(r"(?i)(punt for \d+)").str.extract(r"(\d+)").cast(pl.Int32), + yds_punt_gained = pl.when(pl.col("punt") == True) + .then(pl.col("statYardage")) + .otherwise(None), + fg_attempt = pl.when((pl.col("type.text").str.contains(r"(?i)Field Goal")) + .or_(pl.col("text").str.contains(r"(?i)Field Goal"))) + .then(True) + .otherwise(False), + fg_made = pl.col("type.text") == "Field Goal Good", + yds_fg = pl.col("text").str.extract(r"(?i)(\d+)\s?Yd Field|(?i)(\d+)\s?YD FG|(?i)(\d+)\s?Yard FG|(?i)(\d+)\s?Field|(?i)(\d+)\s?Yard Field", 0).str.extract(r"(\d+)").cast(pl.Int32), + + ).with_columns( + pl.when(pl.col("fg_attempt") == True) + .then(pl.col("yds_fg") - 17) + .otherwise(pl.col("start.yardsToEndzone")) + .alias("start.yardsToEndzone"), + ).with_columns( + pl.when((pl.col("start.yardsToEndzone").is_null()) + .and_(pl.col("type.text").is_in(kickoff_vec) == False) + .and_(pl.col("start.pos_team.id") == pl.col("homeTeamId"))) + .then(100 - pl.col("start.yardLine").cast(pl.Int32)) + .when((pl.col("start.yardsToEndzone").is_null()) + .and_(pl.col("type.text").is_in(kickoff_vec) == False) + .and_(pl.col("start.pos_team.id") == pl.col("awayTeamId"))) + .then(pl.col("start.yardLine").cast(pl.Int32)) + .otherwise(pl.col("start.yardsToEndzone")) + .alias("start.yardsToEndzone"), + ).with_columns( + pos_unit = pl.when(pl.col("punt") == True) + .then("Punt Offense") + .when(pl.col("kickoff_play") == True) + .then("Kickoff Return") + .when(pl.col("fg_attempt") == True) + .then("Field Goal Offense") + .when(pl.col("type.text") == "Defensive 2pt Conversion") + .then("Offense") + .otherwise("Offense"), + def_pos_unit = pl.when(pl.col("punt") == True) + .then("Punt Return") + .when(pl.col("kickoff_play") == True) + .then("Kickoff Defense") + .when(pl.col("fg_attempt") == True) + .then("Field Goal Defense") + .when(pl.col("type.text") == "Defensive 2pt Conversion") + .then("Defense") + .otherwise("Defense"), + # --- Lags/Leads play type ---- + lead_play_type = pl.col("type.text").shift(-1), + sp = pl.when((pl.col("fg_attempt") == True) + .or_(pl.col("punt") == True) + .or_(pl.col("kickoff_play") == True)) + .then(True) + .otherwise(False), + play = pl.when(pl.col("type.text").is_in([ + "Timeout", "End Period", "End of Half", "Penalty"]) == False) + .then(True) + .otherwise(False), + ).with_columns( + scrimmage_play = pl.when((pl.col("sp") == False) + .and_(pl.col("type.text").is_in([ + "Timeout", "Extra Point Good", "Extra Point Missed", + "Two-Point Pass", "Two-Point Rush", "Penalty"]) == False)) + .then(True) + .otherwise(False), + # --- Change of pos_team by lead('pos_team', 1)---- + change_of_pos_team = pl.when((pl.col("pos_team") == pl.col("lead_pos_team")) + .and_(((pl.col("lead_play_type").is_in(["End Period", "End of Half"])) == False) + .or_(pl.col("lead_play_type").is_null()))) + .then(False) + .when((pl.col("pos_team") == pl.col("lead_pos_team2")) + .and_((pl.col("lead_play_type").is_in(["End Period", "End of Half"])) + .or_(pl.col("lead_play_type").is_null()))) + .then(False) + .otherwise(True), + + ).with_columns( + change_of_pos_team = pl.when(pl.col("change_of_poss").is_null()) + .then(False) + .otherwise(pl.col("change_of_pos_team")), + pos_score_diff_end = pl.when(((pl.col("type.text").is_in(end_change_vec)) + .and_(pl.col("start.pos_team.id") != pl.col("end.pos_team.id"))) + .or_(pl.col("downs_turnover") == True)) + .then(-1 * pl.col("pos_score_diff")) + .otherwise(pl.col("pos_score_diff")), + ).with_columns( + pos_score_diff_end = pl.when((pl.col("pos_score_pts").abs() >= 8) + .and_(pl.col("scoring_play") == False) + .and_(pl.col("change_of_pos_team") == False)) + .then(pl.col("pos_score_diff_start")) + .when((pl.col("pos_score_pts").abs() >= 8) + .and_(pl.col("scoring_play") == False) + .and_(pl.col("change_of_pos_team") == True)) + .then(-1 * pl.col("pos_score_diff_start")) + .otherwise(pl.col("pos_score_diff_end")), + fumble_lost = pl.when((pl.col("fumble_vec") == True) + .and_(pl.col("change_of_pos_team") == True)) + .then(True) + .otherwise(False), + fumble_recovered = pl.when((pl.col("fumble_vec") == True) + .and_(pl.col("change_of_pos_team") == False)) + .then(True) + .otherwise(False) ) - play_df["fumble_recovered_player"] = np.where( - ( - play_df.text.str.contains( - "fumble", case=False, flags=0, na=False, regex=True - ) - ) - & ( - play_df.text.str.contains( - "recovered by", case=False, flags=0, na=False, regex=True - ) - ), - play_df.text.str.extract("recovered by(.{0,30})").bfill(axis=1)[0], - play_df.fumble_recovered_player, - ) + return play_df - play_df["fumble_recovered_player"] = play_df[ - "fumble_recovered_player" - ].str.replace("for a 1ST down", "", case=False, regex=True) - play_df["fumble_recovered_player"] = play_df[ - "fumble_recovered_player" - ].str.replace("for a 1st down", "", case=False, regex=True) - play_df["fumble_recovered_player"] = play_df[ - "fumble_recovered_player" - ].str.replace("(.+)recovered", "", case=False, regex=True) - play_df["fumble_recovered_player"] = play_df[ - "fumble_recovered_player" - ].str.replace("(.+) by", "", case=False, regex=True) - play_df["fumble_recovered_player"] = play_df[ - "fumble_recovered_player" - ].str.replace(", recove(.+)", "", case=False, regex=True) - play_df["fumble_recovered_player"] = play_df[ - "fumble_recovered_player" - ].str.replace(", re(.+)", "", case=False, regex=True) - play_df["fumble_recovered_player"] = play_df[ - "fumble_recovered_player" - ].str.replace("a 1st down", "", case=False, regex=True) - play_df["fumble_recovered_player"] = play_df[ - "fumble_recovered_player" - ].str.replace(" a 1st down", "", case=False, regex=True) - play_df["fumble_recovered_player"] = play_df[ - "fumble_recovered_player" - ].str.replace(", for(.+)", "", case=False, regex=True) - play_df["fumble_recovered_player"] = play_df[ - "fumble_recovered_player" - ].str.replace(" for a", "", case=False, regex=True) - play_df["fumble_recovered_player"] = play_df[ - "fumble_recovered_player" - ].str.replace(" fo", "", case=False, regex=True) - play_df["fumble_recovered_player"] = play_df[ - "fumble_recovered_player" - ].str.replace(" , r", "", case=False, regex=True) - play_df["fumble_recovered_player"] = play_df[ - "fumble_recovered_player" - ].str.replace(", r", "", case=False, regex=True) - play_df["fumble_recovered_player"] = play_df[ - "fumble_recovered_player" - ].str.replace(" (.+)", "", case=False, regex=True) - play_df["fumble_recovered_player"] = play_df[ - "fumble_recovered_player" - ].str.replace(" ,", "", case=False, regex=True) - play_df["fumble_recovered_player"] = play_df[ - "fumble_recovered_player" - ].str.replace("penalty(.+)", "", case=False, regex=True) - play_df["fumble_recovered_player"] = play_df[ - "fumble_recovered_player" - ].str.replace("for a 1ST down", "", case=False, regex=True) - play_df["fumble_recovered_player"] = np.where( - play_df["type.text"] == "Penalty", None, play_df.fumble_recovered_player + def __add_yardage_cols(self, play_df): + play_df = play_df.with_columns( + yds_rushed = pl.when((pl.col("rush") == True) + .and_(pl.col("text").str.contains("(?i)run for no gain"))) + .then(0) + .when((pl.col("rush") == True) + .and_(pl.col("text").str.contains("(?i)for no gain"))) + .then(0) + .when((pl.col("rush") == True) + .and_(pl.col("text").str.contains("(?i)run for a loss of"))) + .then(-1 * pl.col("text").str.extract(r"(?i)run for a loss of (\d+)").cast(pl.Int32)) + .when((pl.col("rush") == True) + .and_(pl.col("text").str.contains("(?i)rush for a loss of"))) + .then(-1 * pl.col("text").str.extract(r"(?i)rush for a loss of (\d+)").cast(pl.Int32)) + .when((pl.col("rush") == True) + .and_(pl.col("text").str.contains("(?i)run for"))) + .then(pl.col("text").str.extract(r"(?i)run for (\d+)").cast(pl.Int32)) + .when((pl.col("rush") == True) + .and_(pl.col("text").str.contains("(?i)rush for"))) + .then(pl.col("text").str.extract(r"(?i)rush for (\d+)").cast(pl.Int32)) + .when((pl.col("rush") == True) + .and_(pl.col("text").str.contains("(?i)Yd Run"))) + .then(pl.col("text").str.extract(r"(?i)(\d+) Yd Run").cast(pl.Int32)) + .when((pl.col("rush") == True) + .and_(pl.col("text").str.contains("(?i)Yd Rush"))) + .then(pl.col("text").str.extract(r"(?i)(\d+) Yd Rush").cast(pl.Int32)) + .when((pl.col("rush") == True) + .and_(pl.col("text").str.contains("(?i)Yard Rush"))) + .then(pl.col("text").str.extract(r"(?i)(\d+) Yard Rush").cast(pl.Int32)) + .when((pl.col("rush") == True) + .and_(pl.col("text").str.contains("(?i)rushed")) + .and_(pl.col("text").str.contains("(?i)touchdown") == False)) + .then(pl.col("text").str.extract(r"(?i)for (\d+) yards").cast(pl.Int32)) + .when((pl.col("rush") == True) + .and_(pl.col("text").str.contains("(?i)rushed")) + .and_(pl.col("text").str.contains("(?i)touchdown") == True)) + .then(pl.col("text").str.extract(r"(?i)for a (\d+) yard").cast(pl.Int32)) + .otherwise(None), + yds_receiving = pl.when((pl.col("pass") == True) + .and_(pl.col("text").str.contains(r"(?i)complete to")) + .and_(pl.col("text").str.contains(r"(?i)for no gain"))) + .then(0) + .when((pl.col("pass") == True) + .and_(pl.col("text").str.contains(r"(?i)complete to")) + .and_(pl.col("text").str.contains(r"(?i)for a loss of"))) + .then(-1 * pl.col("text").str.extract(r"(?i)for a loss of (\d+)").cast(pl.Int32)) + .when((pl.col("pass") == True) + .and_(pl.col("text").str.contains(r"(?i)complete to"))) + .then(pl.col("text").str.extract(r"(?i)for (\d+)").cast(pl.Int32)) + .when((pl.col("pass") == True) + .and_(pl.col("text").str.contains(r"(?i)incomplete|(?i) sacked|(?i)intercepted|(?i)pass defensed"))) + .then(0) + .when((pl.col("pass") == True) + .and_(pl.col("text").str.contains(r"(?i)incompletion"))) + .then(0) + .when((pl.col("pass") == True) + .and_(pl.col("text").str.contains(r"(?i)Yd pass"))) + .then(pl.col("text").str.extract(r"(?i)(\d+) Yd pass").cast(pl.Int32)) + .otherwise(None), + yds_int_return = pl.when((pl.col("pass") == True) + .and_(pl.col("int_td") == True) + .and_(pl.col("text").str.contains(r"(?i)Yd Interception Return"))) + .then(pl.col("text").str.extract(r"(?i)(.+)Yd Interception Return") + .str.extract(r"(\d+)").cast(pl.Int32)) + .when((pl.col("pass") == True) + .and_(pl.col("int") == True) + .and_(pl.col("text").str.contains(r"(?i)for no gain"))) + .then(0) + .when((pl.col("pass") == True) + .and_(pl.col("int") == True) + .and_(pl.col("text").str.contains(r"(?i)for a loss of"))) + .then(-1 * pl.col("text").str.extract(r"(?i)for a loss of (\d+)").cast(pl.Int32)) + .when((pl.col("pass") == True) + .and_(pl.col("int") == True) + .and_(pl.col("text").str.contains(r"(?i)for a TD"))) + .then(pl.col("text").str.extract(r"(?i)return for (.+)") + .str.extract(r"(\d+)").cast(pl.Int32)) + .when((pl.col("pass") == True) + .and_(pl.col("int") == True)) + .then(pl.col("text").str.replace("for a 1st", "") + .str.extract(r"(?i)for (.+)") + .str.extract(r"(\d+)").cast(pl.Int32)) + .otherwise(None), + yds_kickoff = pl.when(pl.col("kickoff_play") == True) + .then(pl.col("text").str.extract(r"(?i)kickoff for (.+)") + .str.extract(r"(\d+)").cast(pl.Int32)) + .otherwise(None), + yds_kickoff_return = pl.when((pl.col("kickoff_play") == True) + .and_(pl.col("kickoff_tb") == True) + .and_(pl.col("season") > 2013)) + .then(25) + .when((pl.col("kickoff_play") == True) + .and_(pl.col("kickoff_tb") == True) + .and_(pl.col("season") <= 2013)) + .then(20) + .when((pl.col("kickoff_play") == True) + .and_(pl.col("fumble_vec") == False) + .and_(pl.col("text").str.contains(r"(?i)for no gain|fair catch|fair caught"))) + .then(0) + .when((pl.col("kickoff_play") == True) + .and_(pl.col("fumble_vec") == False) + .and_(pl.col("text").str.contains(r"(?i)out-of-bounds|out of bounds"))) + .then(40) + .when((pl.col("kickoff_downed") == True) + .or_(pl.col("kickoff_fair_catch") == True)) + .then(0) + .when((pl.col("kickoff_play") == True) + .and_(pl.col("text").str.contains(r"(?i)returned by"))) + .then(pl.col("text").str.extract(r"(?i)returned by (.+)") + .str.extract(r"(\d+)").cast(pl.Int32)) + .when((pl.col("kickoff_play") == True) + .and_(pl.col("text").str.contains(r"(?i)return for"))) + .then(pl.col("text").str.extract(r"(?i)return for (.+)") + .str.extract(r"(\d+)").cast(pl.Int32)) + .otherwise(None), + yds_punted = pl.when((pl.col("punt") == True) + .and_(pl.col("punt_blocked") == True)) + .then(0) + .when(pl.col("punt") == True) + .then(pl.col("text").str.extract(r"(?i)punt for (.+)") + .str.extract(r"(\d+)").cast(pl.Int32)) + .otherwise(None), + yds_punt_return = pl.when((pl.col("punt") == True) + .and_(pl.col("punt_tb") == True)) + .then(20) + .when((pl.col("punt") == True) + .and_(pl.col("text").str.contains(r"(?i)fair catch|fair caught"))) + .then(0) + .when((pl.col("punt") == True) + .and_((pl.col("punt_downed") == True) + .or_(pl.col("punt_oob") == True) + .or_(pl.col("punt_fair_catch") == True))) + .then(0) + .when((pl.col("punt") == True) + .and_(pl.col("text").str.contains(r"(?i)no return|no gain"))) + .then(0) + .when((pl.col("punt") == True) + .and_(pl.col("text").str.contains(r"(?i)returned \d+ yards"))) + .then(pl.col("text").str.extract(r"(?i)returned (.+)") + .str.extract(r"(\d+)").cast(pl.Int32)) + .when((pl.col("punt") == True) + .and_(pl.col("punt_blocked") == False)) + .then(pl.col("text").str.extract(r"(?i)returns for (.+)") + .str.extract(r"(\d+)").cast(pl.Int32)) + .when((pl.col("punt") == True) + .and_(pl.col("punt_blocked") == True)) + .then(pl.col("text").str.extract(r"(?i)return for (.+)") + .str.extract(r"(\d+)").cast(pl.Int32)) + .otherwise(None), + yds_fumble_return = pl.when((pl.col("fumble_vec") == True) + .and_(pl.col("kickoff_play") == False)) + .then(pl.col("text").str.extract(r"(?i)return for (.+)") + .str.extract(r"(\d+)").cast(pl.Int32)) + .otherwise(None), + yds_sacked = pl.when(pl.col("sack") == True) + .then(-1 * pl.col("text").str.extract(r"(?i)sacked (.+)") + .str.extract(r"(\d+)").cast(pl.Int32)) + .otherwise(None), + ).with_columns( + yds_penalty = pl.when(pl.col("penalty_detail").is_in(["Penalty Declined", "Penalty Offset"])) + .then(0) + .when(pl.col("yds_penalty").is_not_null()) + .then(pl.col("yds_penalty")) + .when((pl.col("penalty_detail").is_not_null()) + .and_(pl.col("yds_penalty").is_null()) + .and_(pl.col("rush") == True)) + .then(pl.col("statYardage") - pl.col("yds_rushed")) + .when((pl.col("penalty_detail").is_not_null()) + .and_(pl.col("yds_penalty").is_null()) + .and_(pl.col("int") == True)) + .then(pl.col("statYardage") - pl.col("yds_int_return")) + .when((pl.col("penalty_detail").is_not_null()) + .and_(pl.col("yds_penalty").is_null()) + .and_(pl.col("pass") == True) + .and_(pl.col("sack") == False) + .and_(pl.col("type.text") != "Pass Incompletion")) + .then(pl.col("statYardage") - pl.col("yds_receiving")) + .when((pl.col("penalty_detail").is_not_null()) + .and_(pl.col("yds_penalty").is_null()) + .and_(pl.col("pass") == True) + .and_(pl.col("sack") == False) + .and_(pl.col("type.text") == "Pass Incompletion")) + .then(pl.col("statYardage")) + .when((pl.col("penalty_detail").is_not_null()) + .and_(pl.col("yds_penalty").is_null()) + .and_(pl.col("pass") == True) + .and_(pl.col("sack") == True)) + .then(pl.col("statYardage") - pl.col("yds_sacked")) + .when(pl.col("type.text") == "Penalty") + .then(pl.col("statYardage")) + .otherwise(None), ) + return play_df - ## Extract player names - play_df["passer_player_name"] = play_df["pass_player"].str.strip() - play_df["rusher_player_name"] = play_df["rush_player"].str.strip() - play_df["receiver_player_name"] = play_df["receiver_player"].str.strip() - play_df["sack_player_name"] = play_df["sack_player1"].str.strip() - play_df["sack_player_name2"] = play_df["sack_player2"].str.strip() - play_df["pass_breakup_player_name"] = play_df["pass_breakup_player"].str.strip() - play_df["interception_player_name"] = play_df["interception_player"].str.strip() - play_df["fg_kicker_player_name"] = play_df["fg_kicker_player"].str.strip() - play_df["fg_block_player_name"] = play_df["fg_block_player"].str.strip() - play_df["fg_return_player_name"] = play_df["fg_return_player"].str.strip() - play_df["kickoff_player_name"] = play_df["kickoff_player"].str.strip() - play_df["kickoff_return_player_name"] = play_df[ - "kickoff_return_player" - ].str.strip() - play_df["punter_player_name"] = play_df["punter_player"].str.strip() - play_df["punt_block_player_name"] = play_df["punt_block_player"].str.strip() - play_df["punt_return_player_name"] = play_df["punt_return_player"].str.strip() - play_df["punt_block_return_player_name"] = play_df[ - "punt_block_return_player" - ].str.strip() - play_df["fumble_player_name"] = play_df["fumble_player"].str.strip() - play_df["fumble_forced_player_name"] = play_df[ - "fumble_forced_player" - ].str.strip() - play_df["fumble_recovered_player_name"] = play_df[ - "fumble_recovered_player" - ].str.strip() - - play_df.drop( - [ + def __add_player_cols(self, play_df): + play_df = play_df.with_columns( + # --- RB Names ----- + rush_player = pl.when(pl.col("rush") == True) + .then(pl.col("text").str.extract(r"(?i)(.{0,25} )run |(?i)(.{0,25} )\d{0,2} Yd Run|(?i)(.{0,25} )rush |(?i)(.{0,25} )rushed ") + .str.replace(r"(?i) run |(?i) \d+ Yd Run|(?i) rush ", "") + .str.replace(r" \((.+)\)", "")) + .otherwise(None), + # --- QB Names ----- + pass_player = pl.when((pl.col("pass") == True) + .and_(pl.col("sack_vec") == False) + .and_(pl.col("type.text") != "Passing Touchdown")) + .then(pl.col("text").str.extract(r"(?i)(.{0,30} )pass |(?i)(.{0,30} )sacked by|(?i)(.{0,30} )sacked for|(?i)(.{0,30} )incomplete|(?i)pass from (.{0,30} ) \( ") + .str.replace(r"(?i)pass |(?i) sacked by|(?i) sacked for|(?i) incomplete", "")) + .when((pl.col("pass") == True) + .and_(pl.col("sack_vec") == True) + .and_(pl.col("type.text") != "Passing Touchdown")) + .then(pl.col("text").str.extract(r"(?i)(.{0,30} )sacked by|(?i)(.{0,30} )sacked for") + .str.replace(r"(?i)pass |(?i) sacked by|(?i) sacked for|(?i) incomplete", "")) + .when((pl.col("pass") == True) + .and_(pl.col("type.text") == "Passing Touchdown")) + .then(pl.col("text").str.extract(r"(?i)pass from(.+)") + .str.replace(r"pass from", "") + # .str.replace(r"\((.+)\)", "") + .str.replace(r" \,", "")) + .otherwise(None) + ).with_columns( + pass_player = pl.when((pl.col("type.text") == "Passing Touchdown") + .and_(pl.col("pass_player").is_null())) + .then(pl.col("text").str.extract(r"(.+)pass(.+)? complete to") + .str.replace(r" pass complete to(.+)", "") + .str.replace(r" pass complete to", "")) + .otherwise(pl.col("pass_player")) + ).with_columns( + pass_player = pl.when((pl.col("type.text") == "Passing Touchdown") + .and_(pl.col("pass_player").is_null())) + .then(pl.col("text").str.extract(r"(.+)pass,to") + .str.replace(r" pass,to(.+)", "") + .str.replace(r" pass,to", "") + .str.replace(r" \((.+)\)", "")) + .otherwise(pl.col("pass_player")) + ).with_columns( + pass_player = pl.when((pl.col("pass") == True) + .and_(((pl.col("pass_player").str.strip().str.n_chars() == 0) + .or_(pl.col("pass_player").is_null())))) + .then("TEAM") + .otherwise(pl.col("pass_player")), + + # --- WR Names ----- + receiver_player = pl.when((pl.col("pass") == True) + .and_(pl.col("text").str.contains(r"(?i)sacked") == False)) + .then(pl.col("text").str.extract(r"(?i)to (.+)")) + .when(pl.col("text").str.contains(r"(?i)Yd pass")) + .then(pl.col("text").str.extract(r"(?i)(.{0,25} )\d{0,2} Yd pass")) + .when(pl.col("text").str.contains(r"(?i)Yd TD pass")) + .then(pl.col("text").str.extract(r"(?i)(.{0,25} )\d{0,2} Yd TD pass")) + .otherwise(None) + ).with_columns( + receiver_player = pl.when((pl.col("type.text") == "Sack") + .or_(pl.col("type.text") == "Interception Return") + .or_(pl.col("type.text") == "Interception Return Touchdown") + .or_((pl.col("type.text").is_in(["Fumble Recovery (Opponent) Touchdown", "Fumble Recovery (Opponent)"])) + .and_(pl.col("text").str.contains(r"(?i)sacked")))) + .then(None) + .otherwise(pl.col("receiver_player").str.replace(r"to ", "") + .str.replace(r"(?i)\\,.+", "") + .str.replace(r"(?i)for (.+)", "") + .str.replace(r"(?i) (\d{1,2})", "") + .str.replace(r"(?i) Yd pass", "") + .str.replace(r"(?i) Yd TD pass", "") + .str.replace(r"(?i)pass complete to", "") + .str.replace(r"(?i)penalty", "") + .str.replace(r'(?i) "', "")) + ).with_columns( + receiver_player = pl.when(pl.col("receiver_player").str.contains(r"(?i)III") == True) + .then(pl.col("receiver_player").str.replace(r"(?i)[A-Z]{3,}", "")) + .otherwise(pl.col("receiver_player")) + ).with_columns( + receiver_player = pl.col("receiver_player").str.replace(r"(?i) &", "") + .str.replace(r"(?i)A&M", "") + .str.replace(r"(?i) ST", "") + .str.replace(r"(?i) GA", "") + .str.replace(r"(?i) UL", "") + .str.replace(r"(?i) FL", "") + .str.replace(r"(?i) OH", "") + .str.replace(r"(?i) NC", "") + .str.replace(r'(?i) "', "") + .str.replace(r"(?i) \\u00c9", "") + .str.replace(r"(?i) fumbled,", "") + .str.replace(r"(?i)the (.+)", "") + .str.replace(r"(?i)pass incomplete to", "") + .str.replace(r"(?i)(.+)pass incomplete", "") + .str.replace(r"(?i)pass incomplete", "") + .str.replace(r"(?i) \((.+)\)", ""), + # --- Sack Names ----- + sack_players = pl.when((pl.col("sack") == True) + .or_((pl.col("fumble_vec") == True) + .and_(pl.col("pass") == True))) + .then(pl.col("text").str.extract(r"(?i)sacked by(.+)") + .str.replace(r"for (.+)", "") + .str.replace(r"(.+) by ", "") + .str.replace(r" at the (.+)", "")) + .otherwise(None) + ).with_columns( + sack_player1 = pl.col("sack_players").str.replace(r"and (.+)", ""), + sack_player2 = pl.when(pl.col("sack_players").str.contains(r"and (.+)")) + .then(pl.col("sack_players").str.replace(r"(.+) and", "")) + .otherwise(None), + # --- Interception Names ----- + interception_player = pl.when(((pl.col("type.text") == "Interception Return") + .or_(pl.col("type.text") == "Interception Return Touchdown")) + .and_(pl.col("pass") == True)) + .then(pl.col("text").str.extract(r"(?i)intercepted (.+)")) + .when(pl.col("text").str.contains(r"Yd Interception Return")) + .then(pl.col("text").str.extract(r"(?i)(.{0,25} )\\d{0,2} Yd Interception Return|(?i)(.{0,25} )\\d{0,2} yd interception return") + .str.replace(r"return (.+)", "") + .str.replace(r"(.+) intercepted", "") + .str.replace(r"intercepted", "") + .str.replace(r"Yd Interception Return", "") + .str.replace(r"for a 1st down", "") + .str.replace(r"(\\d{1,2})", "") + .str.replace(r"for a TD", "") + .str.replace(r"at the (.+)", "") + .str.replace(r" by ", "")) + .otherwise(None), + # --- Pass Breakup Players ---- + pass_breakup_player = pl.when(pl.col("pass") == True) + .then(pl.col("text").str.extract(r"(?i)broken up by (.+)") + .str.replace(r"(.+) broken up by", "") + .str.replace(r"broken up by", "") + .str.replace(r"Penalty(.+)", "") + .str.replace(r"SOUTH FLORIDA", "") + .str.replace(r"WEST VIRGINIA", "") + .str.replace(r"MISSISSIPPI ST", "") + .str.replace(r"CAMPBELL", "") + .str.replace(r"COASTL CAROLINA", "")) + .otherwise(None), + # --- Punter Names ---- + punter_player = pl.when(pl.col("type.text").str.contains("Punt")) + .then(pl.col("text").str.extract(r"(?i)(.{0,30}) punt|(?i)Punt by (.{0,30})") + .str.replace(r"(?i) punt", "") + .str.replace(r"(?i) for(.+)", "") + .str.replace(r"(?i)Punt by ", "") + .str.replace(r"(?i)\((.+)\)", "") + .str.replace(r"(?i) returned \d+", "") + .str.replace(r"(?i) returned", "") + .str.replace(r"(?i) no return", "")) + .otherwise(None), + # --- Punt Returner Names ---- + punt_return_player = pl.when(pl.col("type.text").str.contains("Punt")) + .then(pl.col("text").str.extract(r"(?i), (.{0,25}) returns|(?i)fair catch by (.{0,25})|(?i), returned by (.{0,25})|(?i)yards by (.{0,30})|(?i) return by (.{0,25})") + .str.replace(r"(?i), ", "") + .str.replace(r"(?i) returns", "") + .str.replace(r"(?i) returned", "") + .str.replace(r"(?i) return", "") + .str.replace(r"(?i)fair catch by", "") + .str.replace(r"(?i) at (.+)", "") + .str.replace(r"(?i) for (.+)", "") + .str.replace(r"(?i)(.+) by ", "") + .str.replace(r"(?i) to (.+)", "") + .str.replace(r"(?i)\((.+)\)", "")) + .otherwise(None), + # --- Punt Blocker Names ---- + punt_block_player = pl.when(pl.col("type.text").str.contains("Punt")) + .then(pl.col("text").str.extract(r"(?i)punt blocked by (.{0,25})|(?i)blocked by(.+)") + .str.replace(r"punt blocked by |for a(.+)", "") + .str.replace(r"blocked by(.+)", "") + .str.replace(r"blocked(.+)", "") + .str.replace(r" for(.+)", "") + .str.replace(r",(.+)", "") + .str.replace(r"punt blocked by |for a(.+)", "")) + .otherwise(None), + ).with_columns( + punt_block_player = pl.when((pl.col("type.text").str.contains(r"(?i)yd return of blocked punt"))) + .then(pl.col("text").str.extract(r"(?i)(.+) yd return of blocked") + .str.replace(r"(?i)blocked|(?i)Blocked", "") + .str.replace(r"(?i)\\d+", "") + .str.replace(r"(?i)yd return of", "")) + .otherwise(pl.col("punt_block_player")), + + # --- Punt Block Returner Names ---- + punt_block_return_player = pl.when((pl.col("type.text").str.contains(r"Punt")) + .and_(pl.col("text").str.contains(r"(?i)blocked")) + .and_(pl.col("text").str.contains(r"(?i)return"))) + .then(pl.col("text").str.extract(r"(?i)(.+) return")) + .otherwise(None) + ).with_columns( + punt_block_return_player = pl.struct(['punt_block_player', 'punt_block_return_player']) + .apply(lambda cols: cols['punt_block_return_player'].str.replace(r"(?i)(.+)blocked by", "") + .str.replace(pl.format(r"(?i)blocked by {}", cols['punt_block_player']), ""), + return_dtype = pl.Utf8) + ).with_columns( + punt_block_return_player = pl.col("punt_block_return_player").str.replace(r"(?i)return(.+)", "") + .str.replace(r"(?i)return", "") + .str.replace(r"for a TD(.+)|for a SAFETY(.+)", "") + .str.replace(r"(?i)blocked by ", "") + .str.replace(r", ", ""), + # --- Kickoff Names ---- + kickoff_player = pl.when(pl.col("type.text").str.contains(r"(?i)kickoff")) + .then(pl.col("text").str.extract(r"(?i)(.{0,25}) kickoff|(.{0,25}) on-side") + .str.replace(r"(?i) on-side| kickoff", "")) + .otherwise(None), + # --- Kickoff Returner Names ---- + kickoff_return_player = pl.when(pl.col("type.text").str.contains(r"(?i)ickoff")) + .then(pl.col("text").str.extract(r"(?i), (.{0,25}) return|(?i), (.{0,25}) fumble|(?i)returned by (.{0,25})|(?i)touchback by (.{0,25})") + .str.replace(r", ", "") + .str.replace(r"(?i) return|(?i) fumble|(?i) returned by|(?i) for |(?i)touchback by ", "") + .str.replace(r"\((.+)\)(.+)", "")) + .otherwise(None), + # --- Field Goal Kicker Names ---- + fg_kicker_player = pl.when(pl.col("type.text").str.contains(r"(?i)Field Goal")) + .then(pl.col("text").str.extract(r"(?i)(.{0,25} )\\d{0,2} yd field goal|(?i)(.{0,25} )\\d{0,2} yd fg|(?i)(.{0,25} )\\d{0,2} yard field goal") + .str.replace(r"(?i) Yd Field Goal|(?i)Yd FG |(?i)yd FG|(?i) yd FG", "") + .str.replace(r"(\\d{1,2})", "")) + .otherwise(None), + # --- Field Goal Blocker Names ---- + fg_block_player = pl.when(pl.col("type.text").str.contains(r"(?i)Field Goal")) + .then(pl.col("text").str.extract(r"(?i)blocked by (.{0,25})") + .str.replace(r",(.+)", "") + .str.replace(r"blocked by ", "") + .str.replace(r" (.)+", "")) + .otherwise(None), + # --- Field Goal Returner Names ---- + fg_return_player = pl.when((pl.col("type.text").str.contains(r"(?i)Field Goal")) + .and_(pl.col("text").str.contains(r"(?i)blocked by|missed")) + .and_(pl.col("text").str.contains(r"(?i)return"))) + .then(pl.col("text").str.extract(r"(?i) (.+)") + .str.replace(r"(?i),(.+)", "") + .str.replace(r"(?i)return ", "") + .str.replace(r"(?i)returned ", "") + .str.replace(r"(?i) for (.+)", "") + .str.replace(r"(?i) for (.+)", "")) + .otherwise(None), + ).with_columns( + fg_return_player = pl.when((pl.col("type.text").is_in(["Missed Field Goal Return", "Missed Field Goal Return Touchdown"]))) + .then(pl.col("text").str.extract(r"(?i)(.+)return") + .str.replace(r"(?i) return", "") + .str.replace(r"(?i)(.+),", "")) + .otherwise(pl.col("fg_return_player")), + # --- Fumble Recovery Names ---- + fumble_player = pl.when(pl.col("text").str.contains(r"(?i)fumble")) + .then(pl.col("text").str.extract(r"(?i)(.{0,25} )fumble|(?i)(.{0,25} )fumble") + .str.replace(r"(?i) fumble(.+)", "") + .str.replace(r"(?i)fumble", "") + .str.replace(r"(?i) yds", "") + .str.replace(r"(?i) yd", "") + .str.replace(r"(?i)yardline", "") + .str.replace(r"(?i) yards|(?i) yard|(?i)for a TD|(?i)or a safety", "") + .str.replace(r"(?i) for ", "") + .str.replace(r"(?i) a safety", "") + .str.replace(r"(?i)r no gain", "") + .str.replace(r"(?i)(.+)(\\d{1,2})", "") + .str.replace(r"(?i)(\\d{1,2})", "") + .str.replace(r", ", "")) + .otherwise(None), + ).with_columns( + fumble_player = pl.when(pl.col("type.text") == "Penalty") + .then(None) + .otherwise(pl.col("fumble_player")), + # --- Forced Fumble Names ---- + fumble_forced_player = pl.when((pl.col("text").str.contains(r"(?i)fumble")) + .and_(pl.col("text").str.contains(r"(?i)forced by"))) + .then(pl.col("text").str.extract(r"(?i)forced by(.{0,25})") + .str.replace(r"(?i)(.+)forced by", "") + .str.replace(r"(?i)forced by", "") + .str.replace(r"(?i), recove(.+)", "") + .str.replace(r"(?i), re(.+)", "") + .str.replace(r"(?i), fo(.+)", "") + .str.replace(r"(?i), r", "") + .str.replace(r"(?i), ", "")) + .otherwise(None), + ).with_columns( + fumble_forced_player = pl.when(pl.col("type.text") == "Penalty") + .then(None) + .otherwise(pl.col("fumble_forced_player")), + # --- Fumble Recovered Names ---- + fumble_recovered_player = pl.when((pl.col("text").str.contains(r"(?i)fumble")) + .and_(pl.col("text").str.contains(r"(?i)recovered by"))) + .then(pl.col("text").str.extract(r"(?i)recovered by(.{0,30})") + .str.replace(r"(?i)for a 1ST down", "") + .str.replace(r"(?i)for a 1st down", "") + .str.replace(r"(?i)(.+)recovered", "") + .str.replace(r"(?i)(.+) by", "") + .str.replace(r"(?i), recove(.+)", "") + .str.replace(r"(?i), re(.+)", "") + .str.replace(r"(?i)a 1st down", "") + .str.replace(r"(?i) a 1st down", "") + .str.replace(r"(?i), for(.+)", "") + .str.replace(r"(?i) for a", "") + .str.replace(r"(?i) fo", "") + .str.replace(r"(?i) , r", "") + .str.replace(r"(?i), r", "") + .str.replace(r"(?i) (.+)", "") + .str.replace(r"(?i) ,", "") + .str.replace(r"(?i)penalty(.+)", "") + .str.replace(r"(?i)for a 1ST down", "")) + .otherwise(None), + ).with_columns( + fumble_recovered_player = pl.when(pl.col("type.text") == "Penalty") + .then(None) + .otherwise(pl.col("fumble_recovered_player")), + + ).with_columns( + ## Extract player names + passer_player_name = pl.col("pass_player").str.strip(), + rusher_player_name = pl.col("rush_player").str.strip(), + receiver_player_name = pl.col("receiver_player").str.strip(), + sack_player_name = pl.col("sack_player1").str.strip(), + sack_player_name2 = pl.col("sack_player2").str.strip(), + pass_breakup_player_name = pl.col("pass_breakup_player").str.strip(), + interception_player_name = pl.col("interception_player").str.strip(), + fg_kicker_player_name = pl.col("fg_kicker_player").str.strip(), + fg_block_player_name = pl.col("fg_block_player").str.strip(), + fg_return_player_name = pl.col("fg_return_player").str.strip(), + kickoff_player_name = pl.col("kickoff_player").str.strip(), + kickoff_return_player_name = pl.col("kickoff_return_player").str.strip(), + punter_player_name = pl.col("punter_player").str.strip(), + punt_block_player_name = pl.col("punt_block_player").str.strip(), + punt_return_player_name = pl.col("punt_return_player").str.strip(), + punt_block_return_player_name = pl.col("punt_block_return_player").str.strip(), + fumble_player_name = pl.col("fumble_player").str.strip(), + fumble_forced_player_name = pl.col("fumble_forced_player").str.strip(), + fumble_recovered_player_name = pl.col("fumble_recovered_player").str.strip(), + ).drop([ "rush_player", "receiver_player", "pass_player", @@ -3702,353 +2226,327 @@ def __add_player_cols(self, play_df): "punt_block_return_player", "fumble_player", "fumble_forced_player", - "fumble_recovered_player", - ], - axis=1, - inplace=True, - ) + "fumble_recovered_player" + ]) return play_df def __after_cols(self, play_df): - play_df["new_down"] = np.select( - [ - (play_df["type.text"] == "Timeout"), - # 8 cases with three T/F penalty flags - # 4 cases in 1 - (play_df["type.text"].isin(penalty)) - & (play_df["penalty_1st_conv"] == True), - # offsetting penalties, no penalties declined, no 1st down by penalty (1 case) - (play_df["type.text"].isin(penalty)) - & (play_df["penalty_1st_conv"] == False) - & (play_df["penalty_offset"] == True) - & (play_df["penalty_declined"] == False), - # offsetting penalties, penalty declined true, no 1st down by penalty - # seems like it would be a regular play at that point (1 case, split in three) - (play_df["type.text"].isin(penalty)) - & (play_df["penalty_1st_conv"] == False) - & (play_df["penalty_offset"] == True) - & (play_df["penalty_declined"] == True) - & (play_df["statYardage"] < play_df["start.distance"]) - & (play_df["start.down"] <= 3), - (play_df["type.text"].isin(penalty)) - & (play_df["penalty_1st_conv"] == False) - & (play_df["penalty_offset"] == True) - & (play_df["penalty_declined"] == True) - & (play_df["statYardage"] < play_df["start.distance"]) - & (play_df["start.down"] == 4), - (play_df["type.text"].isin(penalty)) - & (play_df["penalty_1st_conv"] == False) - & (play_df["penalty_offset"] == True) - & (play_df["penalty_declined"] == True) - & (play_df["statYardage"] >= play_df["start.distance"]), - # only penalty declined true, same logic as prior (1 case, split in three) - (play_df["type.text"].isin(penalty)) - & (play_df["penalty_1st_conv"] == False) - & (play_df["penalty_offset"] == False) - & (play_df["penalty_declined"] == True) - & (play_df["statYardage"] < play_df["start.distance"]) - & (play_df["start.down"] <= 3), - (play_df["type.text"].isin(penalty)) - & (play_df["penalty_1st_conv"] == False) - & (play_df["penalty_offset"] == False) - & (play_df["penalty_declined"] == True) - & (play_df["statYardage"] < play_df["start.distance"]) - & (play_df["start.down"] == 4), - (play_df["type.text"].isin(penalty)) - & (play_df["penalty_1st_conv"] == False) - & (play_df["penalty_offset"] == False) - & (play_df["penalty_declined"] == True) - & (play_df["statYardage"] >= play_df["start.distance"]), - ], - [ - play_df["start.down"], - 1, - play_df["start.down"], - play_df["start.down"] + 1, - 1, - 1, - play_df["start.down"] + 1, - 1, - 1, - ], - default=play_df["start.down"], - ) - play_df["new_distance"] = np.select( - [ - (play_df["type.text"] == "Timeout"), - # 8 cases with three T/F penalty flags - # 4 cases in 1 - (play_df["type.text"].isin(penalty)) - & (play_df["penalty_1st_conv"] == True), - # offsetting penalties, no penalties declined, no 1st down by penalty (1 case) - (play_df["type.text"].isin(penalty)) - & (play_df["penalty_1st_conv"] == False) - & (play_df["penalty_offset"] == True) - & (play_df["penalty_declined"] == False), - # offsetting penalties, penalty declined true, no 1st down by penalty - # seems like it would be a regular play at that point (1 case, split in three) - (play_df["type.text"].isin(penalty)) - & (play_df["penalty_1st_conv"] == False) - & (play_df["penalty_offset"] == True) - & (play_df["penalty_declined"] == True) - & (play_df["statYardage"] < play_df["start.distance"]) - & (play_df["start.down"] <= 3), - (play_df["type.text"].isin(penalty)) - & (play_df["penalty_1st_conv"] == False) - & (play_df["penalty_offset"] == True) - & (play_df["penalty_declined"] == True) - & (play_df["statYardage"] < play_df["start.distance"]) - & (play_df["start.down"] == 4), - (play_df["type.text"].isin(penalty)) - & (play_df["penalty_1st_conv"] == False) - & (play_df["penalty_offset"] == True) - & (play_df["penalty_declined"] == True) - & (play_df["statYardage"] >= play_df["start.distance"]), - # only penalty declined true, same logic as prior (1 case, split in three) - (play_df["type.text"].isin(penalty)) - & (play_df["penalty_1st_conv"] == False) - & (play_df["penalty_offset"] == False) - & (play_df["penalty_declined"] == True) - & (play_df["statYardage"] < play_df["start.distance"]) - & (play_df["start.down"] <= 3), - (play_df["type.text"].isin(penalty)) - & (play_df["penalty_1st_conv"] == False) - & (play_df["penalty_offset"] == False) - & (play_df["penalty_declined"] == True) - & (play_df["statYardage"] < play_df["start.distance"]) - & (play_df["start.down"] == 4), - (play_df["type.text"].isin(penalty)) - & (play_df["penalty_1st_conv"] == False) - & (play_df["penalty_offset"] == False) - & (play_df["penalty_declined"] == True) - & (play_df["statYardage"] >= play_df["start.distance"]), - ], - [ - play_df["start.distance"], - 10, - play_df["start.distance"], - play_df["start.distance"] - play_df["statYardage"], - 10, - 10, - play_df["start.distance"] - play_df["statYardage"], - 10, - 10, - ], - default=play_df["start.distance"], - ) - - play_df["middle_8"] = np.where( - (play_df["start.adj_TimeSecsRem"] >= 1560) - & (play_df["start.adj_TimeSecsRem"] <= 2040), - True, - False, - ) - play_df["rz_play"] = np.where( - play_df["start.yardsToEndzone"] <= 20, True, False - ) - play_df["scoring_opp"] = np.where( - play_df["start.yardsToEndzone"] <= 40, True, False - ) - play_df["stuffed_run"] = np.where( - (play_df.rush == True) & (play_df.yds_rushed <= 0), True, False - ) - play_df["stopped_run"] = np.where( - (play_df.rush == True) & (play_df.yds_rushed <= 2), True, False - ) - play_df["opportunity_run"] = np.where( - (play_df.rush == True) & (play_df.yds_rushed >= 4), True, False - ) - play_df["highlight_run"] = np.where( - (play_df.rush == True) & (play_df.yds_rushed >= 8), True, False - ) - - play_df["adj_rush_yardage"] = np.select( - [ - (play_df.rush == True) & (play_df.yds_rushed > 10), - (play_df.rush == True) & (play_df.yds_rushed <= 10), - ], - [10, play_df.yds_rushed], - default=None, - ) - play_df["line_yards"] = np.select( - [ - (play_df.rush == 1) & (play_df.yds_rushed < 0), - (play_df.rush == 1) - & (play_df.yds_rushed >= 0) - & (play_df.yds_rushed <= 4), - (play_df.rush == 1) - & (play_df.yds_rushed >= 5) - & (play_df.yds_rushed <= 10), - (play_df.rush == 1) & (play_df.yds_rushed >= 11), - ], - [ - 1.2 * play_df.adj_rush_yardage, - play_df.adj_rush_yardage, - 0.5 * play_df.adj_rush_yardage, - 0.0, - ], - default=None, - ) - - play_df["second_level_yards"] = np.select( - [(play_df.rush == 1) & (play_df.yds_rushed >= 5), (play_df.rush == 1)], - [(0.5 * (play_df.adj_rush_yardage - 5)), 0], - default=None, - ) - - play_df["open_field_yards"] = np.select( - [(play_df.rush == 1) & (play_df.yds_rushed > 10), (play_df.rush == 1)], - [(play_df.yds_rushed - play_df.adj_rush_yardage), 0], - default=None, - ) - - play_df["highlight_yards"] = ( - play_df["second_level_yards"] + play_df["open_field_yards"] - ) - - play_df["opp_highlight_yards"] = np.select( - [ - (play_df.opportunity_run == True), - (play_df.opportunity_run == False) & (play_df.rush == 1), - ], - [play_df["highlight_yards"], 0.0], - default=None, - ) - - play_df["short_rush_success"] = np.where( - (play_df["start.distance"] < 2) - & (play_df.rush == True) - & (play_df.statYardage >= play_df["start.distance"]), - True, - False, - ) - play_df["short_rush_attempt"] = np.where( - (play_df["start.distance"] < 2) & (play_df.rush == True), True, False - ) - play_df["power_rush_success"] = np.where( - (play_df["start.distance"] < 2) - & (play_df["start.down"].isin([3, 4])) - & (play_df.rush == True) - & (play_df.statYardage >= play_df["start.distance"]), - True, - False, - ) - play_df["power_rush_attempt"] = np.where( - (play_df["start.distance"] < 2) - & (play_df["start.down"].isin([3, 4])) - & (play_df.rush == True), - True, - False, - ) - play_df["early_down"] = np.where( - ((play_df.down_1 == True) | (play_df.down_2 == True)) - & (play_df.scrimmage_play == True), - True, - False, - ) - play_df["late_down"] = np.where( - (play_df.early_down == False) & (play_df.scrimmage_play == True), - True, - False, - ) - play_df["early_down_pass"] = np.where( - (play_df["pass"] == 1) & (play_df.early_down == True), True, False - ) - play_df["early_down_rush"] = np.where( - (play_df["rush"] == 1) & (play_df.early_down == True), True, False - ) - play_df["late_down_pass"] = np.where( - (play_df["pass"] == 1) & (play_df.late_down == True), True, False - ) - play_df["late_down_rush"] = np.where( - (play_df["rush"] == 1) & (play_df.late_down == True), True, False - ) - play_df["standard_down"] = np.select( - [ - (play_df.scrimmage_play == True) & (play_df.down_1 == True), - (play_df.scrimmage_play == True) - & (play_df.down_2 == True) - & (play_df["start.distance"] < 8), - (play_df.scrimmage_play == True) - & (play_df.down_3 == True) - & (play_df["start.distance"] < 5), - (play_df.scrimmage_play == True) - & (play_df.down_4 == True) - & (play_df["start.distance"] < 5), - ], - [True, True, True, True], - default=False, - ) - play_df["passing_down"] = np.select( - [ - (play_df.scrimmage_play == True) - & (play_df.down_2 == True) - & (play_df["start.distance"] >= 8), - (play_df.scrimmage_play == True) - & (play_df.down_3 == True) - & (play_df["start.distance"] >= 5), - (play_df.scrimmage_play == True) - & (play_df.down_4 == True) - & (play_df["start.distance"] >= 5), - ], - [True, True, True], - default=False, - ) - play_df["TFL"] = np.select( - [ - (play_df["type.text"] != "Penalty") - & (play_df.sp == False) - & (play_df.statYardage < 0), - (play_df["sack_vec"] == True), - ], - [True, True], - default=False, - ) - play_df["TFL_pass"] = np.where( - (play_df["TFL"] == True) & (play_df["pass"] == True), True, False - ) - play_df["TFL_rush"] = np.where( - (play_df["TFL"] == True) & (play_df["rush"] == True), True, False - ) - play_df["havoc"] = np.select( - [ - (play_df["pass_breakup"] == True), - (play_df["TFL"] == True), - (play_df["int"] == True), - (play_df["forced_fumble"] == True), - ], - [True, True, True, True], - default=False, + play_df = play_df.with_columns( + new_down = pl.when(pl.col("type.text") == "Timeout") + .then(pl.col("start.down")) + .when((pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == True)) + .then(1) + .when((pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == True) + .and_(pl.col("penalty_declined") == False)) + .then(pl.col("start.down")) + .when((pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == True) + .and_(pl.col("penalty_declined") == True) + .and_(pl.col("statYardage") < pl.col("start.distance")) + .and_(pl.col("start.down") <= 3)) + .then(pl.col("start.down") + 1) + .when((pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == True) + .and_(pl.col("penalty_declined") == True) + .and_(pl.col("statYardage") < pl.col("start.distance")) + .and_(pl.col("start.down") == 4)) + .then(1) + .when((pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == True) + .and_(pl.col("penalty_declined") == True) + .and_(pl.col("statYardage") >= pl.col("start.distance"))) + .then(1) + .when((pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == False) + .and_(pl.col("penalty_declined") == True) + .and_(pl.col("statYardage") < pl.col("start.distance")) + .and_(pl.col("start.down") <= 3)) + .then(pl.col("start.down") + 1) + .when((pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == False) + .and_(pl.col("penalty_declined") == True) + .and_(pl.col("statYardage") < pl.col("start.distance")) + .and_(pl.col("start.down") == 4)) + .then(1) + .when((pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == False) + .and_(pl.col("penalty_declined") == True) + .and_(pl.col("statYardage") >= pl.col("start.distance"))) + .then(1) + .otherwise(pl.col("start.down")), + new_distance = pl.when(pl.col("type.text") == "Timeout") + .then(pl.col("start.distance")) + .when((pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == True)) + .then(10) + .when((pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == True) + .and_(pl.col("penalty_declined") == False)) + .then(pl.col("start.distance")) + .when((pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == True) + .and_(pl.col("penalty_declined") == True) + .and_(pl.col("statYardage") < pl.col("start.distance")) + .and_(pl.col("start.down") <= 3)) + .then(pl.col("start.distance")) + .when((pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == True) + .and_(pl.col("penalty_declined") == True) + .and_(pl.col("statYardage") < pl.col("start.distance")) + .and_(pl.col("start.down") == 4)) + .then(pl.col("start.distance")) + .when((pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == True) + .and_(pl.col("penalty_declined") == True) + .and_(pl.col("statYardage") >= pl.col("start.distance"))) + .then(pl.col("start.distance")) + .when((pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == False) + .and_(pl.col("penalty_declined") == True) + .and_(pl.col("statYardage") < pl.col("start.distance")) + .and_(pl.col("start.down") <= 3)) + .then(pl.col("start.distance")) + .when((pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == False) + .and_(pl.col("penalty_declined") == True) + .and_(pl.col("statYardage") < pl.col("start.distance")) + .and_(pl.col("start.down") == 4)) + .then(pl.col("start.distance")) + .when((pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == False) + .and_(pl.col("penalty_declined") == True) + .and_(pl.col("statYardage") >= pl.col("start.distance"))) + .then(pl.col("start.distance")) + .otherwise(pl.col("start.distance")), + middle_8 = pl.when((pl.col("start.adj_TimeSecsRem") >= 1560) + .and_(pl.col("start.adj_TimeSecsRem") <= 2040)) + .then(True) + .otherwise(False), + rz_play = pl.when(pl.col("start.yardLine") <= 20) + .then(True) + .otherwise(False), + under_2 = pl.when(pl.col("start.TimeSecsRem") <= 120) + .then(True) + .otherwise(False), + goal_to_go = pl.when(pl.col("start.yardLine") <= 10) + .then(True) + .otherwise(False), + scoring_opp = pl.when(pl.col("start.yardLine") <= 40) + .then(True) + .otherwise(False), + stuffed_run = pl.when((pl.col("type.text") == "Rush") + .and_(pl.col("yds_rushed") <= 0)) + .then(True) + .otherwise(False), + stopped_run = pl.when((pl.col("type.text") == "Rush") + .and_(pl.col("yds_rushed") <= 2)) + .then(True) + .otherwise(False), + opportunity_run = pl.when((pl.col("type.text") == "Rush") + .and_(pl.col("yds_rushed") <= 4)) + .then(True) + .otherwise(False), + highlight_run = pl.when((pl.col("type.text") == "Rush") + .and_(pl.col("yds_rushed") >= 8)) + .then(True) + .otherwise(False), + adj_rush_yardage = pl.when((pl.col("type.text") == "Rush") + .and_(pl.col("yds_rushed") > 8)) + .then(8) + .when((pl.col("type.text") == "Rush") + .and_(pl.col("yds_rushed") <= 8)) + .then(pl.col("yds_rushed")) + .otherwise(None), + ).with_columns( + line_yards = pl.when((pl.col("rush") == True) + .and_(pl.col("yds_rushed") < 0)) + .then(1.2 * pl.col("adj_rush_yardage")) + .when((pl.col("rush") == True) + .and_(pl.col("yds_rushed") >= 0) + .and_(pl.col("yds_rushed") <= 3)) + .then(pl.col("adj_rush_yardage")) + .when((pl.col("rush") == True) + .and_(pl.col("yds_rushed") >= 4) + .and_(pl.col("yds_rushed") <= 8)) + .then(3 + 0.5 * (pl.col("adj_rush_yardage") - 3)) + .when((pl.col("rush") == True) + .and_(pl.col("yds_rushed") >= 8)) + .then(5.5) + .otherwise(None), + second_level_yards = pl.when((pl.col("rush") == True) + .and_(pl.col("yds_rushed") >= 4)) + .then(0.5 * (pl.col("adj_rush_yardage") - 4)) + .when(pl.col("rush") == True) + .then(0) + .otherwise(None), + open_field_yards = pl.when((pl.col("rush") == True) + .and_(pl.col("yds_rushed") > 8)) + .then(pl.col("yds_rushed") - pl.col("adj_rush_yardage")) + .when(pl.col("rush") == True) + .then(0) + .otherwise(None), + ).with_columns( + highlight_yards = pl.col("second_level_yards") + pl.col("open_field_yards"), + ).with_columns( + opp_highlight_yards = pl.when(pl.col("opportunity_run") == True) + .then(pl.col("highlight_yards")) + .when((pl.col("opportunity_run") == False) + .and_(pl.col("rush") == True)) + .then(0) + .otherwise(None), + short_rush_success = pl.when((pl.col("start.distance") < 2) + .and_(pl.col("rush") == True) + .and_(pl.col("statYardage") >= pl.col("start.distance"))) + .then(True) + .when((pl.col("start.distance") < 2) + .and_(pl.col("rush") == True) + .and_(pl.col("statYardage") < pl.col("start.distance"))) + .then(False) + .otherwise(None), + short_rush_attempt = pl.when((pl.col("start.distance") < 2) + .and_(pl.col("rush") == True)) + .then(True) + .when((pl.col("start.distance") >= 2) + .and_(pl.col("rush") == True)) + .then(False) + .otherwise(None), + power_rush_success = pl.when((pl.col("start.distance") < 2) + .and_(pl.col("rush") == True) + .and_(pl.col("start.down").is_in([3, 4])) + .and_(pl.col("statYardage") >= pl.col("start.distance"))) + .then(True) + .when((pl.col("start.distance") < 2) + .and_(pl.col("rush") == True) + .and_(pl.col("start.down").is_in([3, 4])) + .and_(pl.col("statYardage") < pl.col("start.distance"))) + .then(False) + .otherwise(None), + power_rush_attempt = pl.when((pl.col("start.distance") < 2) + .and_(pl.col("rush") == True) + .and_(pl.col("start.down").is_in([3, 4]))) + .then(True) + .when((pl.col("start.distance") < 2) + .and_(pl.col("rush") == True) + .and_(pl.col("start.down").is_in([3, 4]))) + .then(False) + .otherwise(None), + early_down = pl.when(((pl.col("down_1") == True) + .or_(pl.col("down_2") == True)) + .and_(pl.col("scrimmage_play") == True)) + .then(True) + .otherwise(False), + late_down = pl.when(((pl.col("down_3") == True) + .or_(pl.col("down_4"))) + .and_(pl.col("scrimmage_play") == True)) + .then(True) + .otherwise(False), + ).with_columns( + early_down_pass = pl.when((pl.col("pass") == True) + .and_(pl.col("early_down") == True)) + .then(True) + .otherwise(False), + early_down_rush = pl.when((pl.col("rush") == True) + .and_(pl.col("early_down") == True)) + .then(True) + .otherwise(False), + late_down_pass = pl.when((pl.col("pass") == True) + .and_(pl.col("late_down") == True)) + .then(True) + .otherwise(False), + late_down_rush = pl.when((pl.col("rush") == True) + .and_(pl.col("late_down") == True)) + .then(True) + .otherwise(False), + standard_down = pl.when((pl.col("scrimmage_play") == True) + .and_(pl.col("down_1") == True)) + .then(True) + .when((pl.col("scrimmage_play") == True) + .and_(pl.col("down_2") == True) + .and_(pl.col("start.distance") < 8)) + .then(True) + .when((pl.col("scrimmage_play") == True) + .and_(pl.col("down_3") == True) + .and_(pl.col("start.distance") < 5)) + .then(True) + .when((pl.col("scrimmage_play") == True) + .and_(pl.col("down_4") == True) + .and_(pl.col("start.distance") < 5)) + .then(True) + .otherwise(False), + passing_down = pl.when((pl.col("scrimmage_play") == True) + .and_(pl.col("down_2") == True) + .and_(pl.col("start.distance") >= 8)) + .then(True) + .when((pl.col("scrimmage_play") == True) + .and_(pl.col("down_3") == True) + .and_(pl.col("start.distance") >= 5)) + .then(True) + .when((pl.col("scrimmage_play") == True) + .and_(pl.col("down_4") == True) + .and_(pl.col("start.distance") >= 5)) + .then(True) + .otherwise(False), + TFL = pl.when((pl.col("type.text") != "Penalty") + .and_(pl.col("sp") == False) + .and_(pl.col("statYardage") < 0)) + .then(True) + .when(pl.col("sack_vec") == True) + .then(True) + .otherwise(False), + ).with_columns( + TFL_pass = pl.when((pl.col("TFL") == True) + .and_(pl.col("pass") == True)) + .then(True) + .otherwise(False), + TFL_rush = pl.when((pl.col("TFL") == True) + .and_(pl.col("rush") == True)) + .then(True) + .otherwise(False), + havoc = pl.when(pl.col("pass_breakup") == True) + .then(True) + .when(pl.col("TFL") == True) + .then(True) + .when(pl.col("int") == True) + .then(True) + .when(pl.col("forced_fumble") == True) + .then(True) + .otherwise(False) ) return play_df def __add_spread_time(self, play_df): - play_df["start.pos_team_spread"] = np.where( - (play_df["start.pos_team.id"] == play_df["homeTeamId"]), - play_df["homeTeamSpread"], - -1 * play_df["homeTeamSpread"], - ) - play_df["start.elapsed_share"] = ( - (3600 - play_df["start.adj_TimeSecsRem"]) / 3600 - ).clip(0, 3600) - play_df["start.spread_time"] = play_df["start.pos_team_spread"] * np.exp( - -4 * play_df["start.elapsed_share"] - ) - play_df["end.pos_team_spread"] = np.where( - (play_df["end.pos_team.id"] == play_df["homeTeamId"]), - play_df["homeTeamSpread"], - -1 * play_df["homeTeamSpread"], - ) - play_df["end.pos_team_spread"] = np.where( - (play_df["end.pos_team.id"] == play_df["homeTeamId"]), - play_df["homeTeamSpread"], - -1 * play_df["homeTeamSpread"], - ) - play_df["end.elapsed_share"] = ( - (3600 - play_df["end.adj_TimeSecsRem"]) / 3600 - ).clip(0, 3600) - play_df["end.spread_time"] = play_df["end.pos_team_spread"] * np.exp( - -4 * play_df["end.elapsed_share"] + play_df = play_df.with_columns( + pl.when(pl.col("start.pos_team.id") == pl.col("homeTeamId")) + .then(pl.col("homeTeamSpread")) + .otherwise(-1 * pl.col("homeTeamSpread")) + .alias("start.pos_team_spread"), + ((3600 - pl.col("start.adj_TimeSecsRem")) / 3600).clip(0, 3600).alias("start.elapsed_share") + ).with_columns( + (pl.col("start.pos_team_spread") * np.exp(-4 * pl.col("start.elapsed_share"))).alias("start.spread_time"), + pl.when(pl.col("end.pos_team.id") == pl.col("homeTeamId")) + .then(pl.col("homeTeamSpread")) + .otherwise(-1 * pl.col("homeTeamSpread")) + .alias("end.pos_team_spread"), + ((3600 - pl.col("end.adj_TimeSecsRem")) / 3600).clip(0, 3600).alias("end.elapsed_share"), + ).with_columns( + (pl.col("end.pos_team_spread") * np.exp(-4 * pl.col("end.elapsed_share"))).alias("end.spread_time"), + ) return play_df @@ -4064,25 +2562,48 @@ def __calculate_ep_exp_val(self, matrix): ) def __process_epa(self, play_df): - play_df.loc[play_df["type.text"].isin(kickoff_vec), "down"] = 1 - play_df.loc[play_df["type.text"].isin(kickoff_vec), "start.down"] = 1 - play_df.loc[play_df["type.text"].isin(kickoff_vec), "down_1"] = True - play_df.loc[play_df["type.text"].isin(kickoff_vec), "down_2"] = False - play_df.loc[play_df["type.text"].isin(kickoff_vec), "down_3"] = False - play_df.loc[play_df["type.text"].isin(kickoff_vec), "down_4"] = False - play_df.loc[play_df["type.text"].isin(kickoff_vec), "distance"] = 10 - play_df.loc[play_df["type.text"].isin(kickoff_vec), "start.distance"] = 10 - play_df["start.yardsToEndzone.touchback"] = 99 - play_df.loc[ - (play_df["type.text"].isin(kickoff_vec)) & (play_df["season"] > 2013), - "start.yardsToEndzone.touchback", - ] = 75 - play_df.loc[ - (play_df["type.text"].isin(kickoff_vec)) & (play_df["season"] <= 2013), - "start.yardsToEndzone.touchback", - ] = 80 + play_df = play_df.with_columns( + down = pl.when(pl.col("type.text").is_in(kickoff_vec)) + .then(1) + .otherwise(pl.col("start.down")), + down_1 = pl.when(pl.col("type.text").is_in(kickoff_vec)) + .then(True) + .otherwise(pl.col("down_1")), + down_2 = pl.when(pl.col("type.text").is_in(kickoff_vec)) + .then(False) + .otherwise(pl.col("down_2")), + down_3 = pl.when(pl.col("type.text").is_in(kickoff_vec)) + .then(False) + .otherwise(pl.col("down_3")), + down_4 = pl.when(pl.col("type.text").is_in(kickoff_vec)) + .then(False) + .otherwise(pl.col("down_4")), + distance = pl.when(pl.col("type.text").is_in(kickoff_vec)) + .then(10) + .otherwise(pl.col("start.distance")), + ).with_columns( + pl.when(pl.col("type.text").is_in(kickoff_vec)) + .then(1) + .otherwise(pl.col("start.down")) + .alias("start.down"), + pl.when(pl.col("type.text").is_in(kickoff_vec)) + .then(10) + .otherwise(pl.col("start.distance")) + .alias("start.distance"), + pl.lit(99).alias("start.yardsToEndzone.touchback") + ).with_columns( + pl.when((pl.col("type.text").is_in(kickoff_vec)) + .and_(pl.col("season") > 2013)) + .then(75) + .when((pl.col("type.text").is_in(kickoff_vec)) + .and_(pl.col("season") <= 2013)) + .then(80) + .otherwise(pl.col("start.yardsToEndzone")) + .alias("start.yardsToEndzone.touchback"), + ) start_touchback_data = play_df[ep_start_touchback_columns] + start_touchback_data.columns = ep_final_names # self.logger.info(start_data.iloc[[36]].to_json(orient="records")) @@ -4098,34 +2619,75 @@ def __process_epa(self, play_df): EP_start_parts = ep_model.predict(dtest_start) EP_start = self.__calculate_ep_exp_val(EP_start_parts) - play_df.loc[play_df["end.TimeSecsRem"] <= 0, "end.TimeSecsRem"] = 0 - play_df.loc[ - (play_df["end.TimeSecsRem"] <= 0) & (play_df.period < 5), - "end.yardsToEndzone", - ] = 99 - play_df.loc[ - (play_df["end.TimeSecsRem"] <= 0) & (play_df.period < 5), "down_1_end" - ] = True - play_df.loc[ - (play_df["end.TimeSecsRem"] <= 0) & (play_df.period < 5), "down_2_end" - ] = False - play_df.loc[ - (play_df["end.TimeSecsRem"] <= 0) & (play_df.period < 5), "down_3_end" - ] = False - play_df.loc[ - (play_df["end.TimeSecsRem"] <= 0) & (play_df.period < 5), "down_4_end" - ] = False - - play_df.loc[play_df["end.yardsToEndzone"] >= 100, "end.yardsToEndzone"] = 99 - play_df.loc[play_df["end.yardsToEndzone"] <= 0, "end.yardsToEndzone"] = 99 - - play_df.loc[play_df.kickoff_tb == True, "end.yardsToEndzone"] = 75 - play_df.loc[play_df.kickoff_tb == True, "end.down"] = 1 - play_df.loc[play_df.kickoff_tb == True, "end.distance"] = 10 - - play_df.loc[play_df.punt_tb == True, "end.down"] = 1 - play_df.loc[play_df.punt_tb == True, "end.distance"] = 10 - play_df.loc[play_df.punt_tb == True, "end.yardsToEndzone"] = 80 + + play_df = play_df.with_columns( + pl.when(pl.col("end.TimeSecsRem") <= 0) + .then(0) + .otherwise(pl.col("end.TimeSecsRem")) + .alias("end.TimeSecsRem"), + ).with_columns( + pl.when((pl.col("end.TimeSecsRem") <= 0) + .and_(pl.col("period") < 5)) + .then(99) + .otherwise(pl.col("end.yardsToEndzone")) + .alias("end.yardsToEndzone"), + pl.when((pl.col("end.TimeSecsRem") <= 0) + .and_(pl.col("period") < 5)) + .then(True) + .otherwise(pl.col("down_1_end")) + .alias("down_1_end"), + pl.when((pl.col("end.TimeSecsRem") <= 0) + .and_(pl.col("period") < 5)) + .then(False) + .otherwise(pl.col("down_2_end")) + .alias("down_2_end"), + pl.when((pl.col("end.TimeSecsRem") <= 0) + .and_(pl.col("period") < 5)) + .then(False) + .otherwise(pl.col("down_3_end")) + .alias("down_3_end"), + pl.when((pl.col("end.TimeSecsRem") <= 0) + .and_(pl.col("period") < 5)) + .then(False) + .otherwise(pl.col("down_4_end")) + .alias("down_4_end"), + ).with_columns( + pl.when(pl.col("end.yardsToEndzone") >= 100) + .then(99) + .otherwise(pl.col("end.yardsToEndzone")) + .alias("end.yardsToEndzone"), + ).with_columns( + pl.when(pl.col("end.yardsToEndzone") <= 0) + .then(99) + .otherwise(pl.col("end.yardsToEndzone")) + .alias("end.yardsToEndzone"), + ).with_columns( + pl.when(pl.col("kickoff_tb") == True) + .then(75) + .otherwise(pl.col("end.yardsToEndzone")) + .alias("end.yardsToEndzone"), + pl.when(pl.col("kickoff_tb") == True) + .then(1) + .otherwise(pl.col("end.down")) + .alias("end.down"), + pl.when(pl.col("kickoff_tb") == True) + .then(10) + .otherwise(pl.col("end.distance")) + .alias("end.distance"), + ).with_columns( + pl.when(pl.col("punt_tb") == True) + .then(1) + .otherwise(pl.col("end.down")) + .alias("end.down"), + pl.when(pl.col("punt_tb") == True) + .then(10) + .otherwise(pl.col("end.distance")) + .alias("end.distance"), + pl.when(pl.col("punt_tb") == True) + .then(80) + .otherwise(pl.col("end.yardsToEndzone")) + .alias("end.yardsToEndzone"), + ) end_data = play_df[ep_end_columns] end_data.columns = ep_final_names @@ -4135,577 +2697,422 @@ def __process_epa(self, play_df): EP_end = self.__calculate_ep_exp_val(EP_end_parts) - play_df["EP_start_touchback"] = EP_start_touchback - play_df["EP_start"] = EP_start - play_df["EP_end"] = EP_end + play_df = play_df.with_columns( + EP_start_touchback = pl.lit(EP_start_touchback), + EP_start = pl.lit(EP_start), + EP_end = pl.lit(EP_end), + ) + kick = "kick)" - play_df["EP_start"] = np.where( - play_df["type.text"].isin( - [ + play_df = play_df.with_columns( + EP_start = pl.when(pl.col("type.text").is_in([ "Extra Point Good", "Extra Point Missed", "Two-Point Conversion Good", "Two-Point Conversion Missed", "Two Point Pass", "Two Point Rush", - "Blocked PAT", - ] - ), - 0.92, - play_df["EP_start"], - ) - play_df.EP_end = np.select( - [ - # End of Half - ( - play_df["type.text"] - .str.lower() - .str.contains( - "end of game", case=False, flags=0, na=False, regex=True - ) - ) - | ( - play_df["type.text"] - .str.lower() - .str.contains( - "end of game", case=False, flags=0, na=False, regex=True - ) - ) - | ( - play_df["type.text"] - .str.lower() - .str.contains( - "end of half", case=False, flags=0, na=False, regex=True - ) - ) - | ( - play_df["type.text"] - .str.lower() - .str.contains( - "end of half", case=False, flags=0, na=False, regex=True - ) - ), - # Def 2pt conversion is its own play - (play_df["type.text"].isin(["Defensive 2pt Conversion"])), - # Safeties - ( - (play_df["type.text"].isin(defense_score_vec)) - & ( - play_df["text"] - .str.lower() - .str.contains("safety", case=False, regex=True) - ) - ), - # Defense TD + Successful Two-Point Conversion - ( - (play_df["type.text"].isin(defense_score_vec)) - & ( - play_df["text"] - .str.lower() - .str.contains("conversion", case=False, regex=False) - ) - & ( - ~play_df["text"] - .str.lower() - .str.contains(r"failed\s?\)", case=False, regex=True) - ) - ), - # Defense TD + Failed Two-Point Conversion - ( - (play_df["type.text"].isin(defense_score_vec)) - & ( - play_df["text"] - .str.lower() - .str.contains("conversion", case=False, regex=False) - ) - & ( - play_df["text"] - .str.lower() - .str.contains(r"failed\s?\)", case=False, regex=True) - ) - ), - # Defense TD + Kick/PAT Missed - ( - (play_df["type.text"].isin(defense_score_vec)) - & (play_df["text"].str.contains("PAT", case=True, regex=False)) - & ( - play_df["text"] - .str.lower() - .str.contains(r"missed\s?\)", case=False, regex=True) - ) - ), - # Defense TD + Kick/PAT Good - ( - (play_df["type.text"].isin(defense_score_vec)) - & ( - play_df["text"] - .str.lower() - .str.contains(kick, case=False, regex=False) - ) - ), - # Defense TD - (play_df["type.text"].isin(defense_score_vec)), - # Offense TD + Failed Two-Point Conversion - ( - (play_df["type.text"].isin(offense_score_vec)) - & ( - play_df["text"] - .str.lower() - .str.contains("conversion", case=False, regex=False) - ) - & ( - play_df["text"] - .str.lower() - .str.contains(r"failed\s?\)", case=False, regex=True) - ) - ), - # Offense TD + Successful Two-Point Conversion - ( - (play_df["type.text"].isin(offense_score_vec)) - & ( - play_df["text"] - .str.lower() - .str.contains("conversion", case=False, regex=False) - ) - & ( - ~play_df["text"] - .str.lower() - .str.contains(r"failed\s?\)", case=False, regex=True) - ) - ), - # Offense Made FG - ( - (play_df["type.text"].isin(offense_score_vec)) - & ( - play_df["type.text"] - .str.lower() - .str.contains( - "field goal", case=False, flags=0, na=False, regex=True - ) - ) - & ( - play_df["type.text"] - .str.lower() - .str.contains("good", case=False, flags=0, na=False, regex=True) - ) - ), - # Missed FG -- Not Needed - # (play_df["type.text"].isin(offense_score_vec)) & - # (play_df["type.text"].str.lower().str.contains('field goal', case=False, flags=0, na=False, regex=True)) & - # (~play_df["type.text"].str.lower().str.contains('good', case=False, flags=0, na=False, regex=True)), - # Offense TD + Kick/PAT Missed - ( - (play_df["type.text"].isin(offense_score_vec)) - & ( - ~play_df["text"] - .str.lower() - .str.contains("conversion", case=False, regex=False) - ) - & ((play_df["text"].str.contains("PAT", case=True, regex=False))) - & ( - ( - play_df["text"] - .str.lower() - .str.contains(r"missed\s?\)", case=False, regex=True) - ) - ) - ), - # Offense TD + Kick PAT Good - ( - (play_df["type.text"].isin(offense_score_vec)) - & ( - play_df["text"] - .str.lower() - .str.contains(kick, case=False, regex=False) - ) - ), - # Offense TD - (play_df["type.text"].isin(offense_score_vec)), - # Extra Point Good (pre-2014 data) - (play_df["type.text"] == "Extra Point Good"), - # Extra Point Missed (pre-2014 data) - (play_df["type.text"] == "Extra Point Missed"), - # Extra Point Blocked (pre-2014 data) - (play_df["type.text"] == "Blocked PAT"), - # Two-Point Good (pre-2014 data) - (play_df["type.text"] == "Two-Point Conversion Good"), - # Two-Point Missed (pre-2014 data) - (play_df["type.text"] == "Two-Point Conversion Missed"), - # Two-Point No Good (pre-2014 data) - ( - ( - (play_df["type.text"] == "Two Point Pass") - | (play_df["type.text"] == "Two Point Rush") - ) - & ( - play_df["text"] - .str.lower() - .str.contains("no good", case=False, regex=False) - ) - ), - # Two-Point Good (pre-2014 data) - ( - ( - (play_df["type.text"] == "Two Point Pass") - | (play_df["type.text"] == "Two Point Rush") - ) - & ( - ~play_df["text"] - .str.lower() - .str.contains("no good", case=False, regex=False) - ) - ), - # Flips for Turnovers that aren't kickoffs - ( - ( - (play_df["type.text"].isin(end_change_vec)) - | (play_df.downs_turnover == True) - ) - & (play_df.kickoff_play == False) - ), - # Flips for Turnovers that are on kickoffs - (play_df["type.text"].isin(kickoff_turnovers)), - # onside recoveries - (play_df["kickoff_onside"] == True) & (play_df["change_of_pos_team"] == True), - ], - [ - 0, - -2, - -2, - -6, - -8, - -6, - -7, - -6.92, - 6, - 8, - 3, - 6, - 7, - 6.92, - 1, - 0, - 0, - 2, - 0, - 0, - 2, - (play_df.EP_end * -1), - (play_df.EP_end * -1), - (play_df.EP_end * -1), - ], - default=play_df.EP_end, - ) - play_df["lag_EP_end"] = play_df["EP_end"].shift(1) - play_df["lag_change_of_pos_team"] = play_df.change_of_pos_team.shift(1) - play_df["lag_change_of_pos_team"] = np.where( - play_df["lag_change_of_pos_team"].isna(), - False, - play_df["lag_change_of_pos_team"], - ) - play_df["EP_between"] = np.where( - play_df.lag_change_of_pos_team == True, - play_df["EP_start"] + play_df["lag_EP_end"], - play_df["EP_start"] - play_df["lag_EP_end"], - ) - play_df["EP_start"] = np.where( - (play_df["type.text"].isin(["Timeout", "End Period"])) - & (play_df["lag_change_of_pos_team"] == False), - play_df["lag_EP_end"], - play_df["EP_start"], - ) - play_df["EP_start"] = np.where( - (play_df["type.text"].isin(kickoff_vec)), - play_df["EP_start_touchback"], - play_df["EP_start"], - ) - play_df["EP_end"] = np.where( - (play_df["type.text"] == "Timeout"), play_df["EP_start"], play_df["EP_end"] - ) - play_df["EPA"] = np.select( - [ - (play_df["type.text"] == "Timeout"), - (play_df["scoring_play"] == False) & (play_df["end_of_half"] == True), - (play_df["type.text"].isin(kickoff_vec)) - & (play_df["penalty_in_text"] == True), - (play_df["penalty_in_text"] == True) - & (play_df["type.text"] != "Penalty") - & (~play_df["type.text"].isin(kickoff_vec)), - ], - [ - 0, - -1 * play_df["EP_start"], - play_df["EP_end"] - play_df["EP_start"], - (play_df["EP_end"] - play_df["EP_start"] + play_df["EP_between"]), - ], - default=(play_df["EP_end"] - play_df["EP_start"]), - ) - play_df["def_EPA"] = -1 * play_df["EPA"] - # ----- EPA Summary flags ------ - play_df["EPA_scrimmage"] = np.select( - [(play_df.scrimmage_play == True)], [play_df.EPA], default=None - ) - play_df["EPA_rush"] = np.select( - [ - (play_df.rush == True) & (play_df["penalty_in_text"] == True), - (play_df.rush == True) & (play_df["penalty_in_text"] == False), - ], - [play_df.EPA, play_df.EPA], - default=None, - ) - play_df["EPA_pass"] = np.where((play_df["pass"] == True), play_df.EPA, None) - - play_df["EPA_explosive"] = np.where( - ((play_df["pass"] == True) & (play_df["EPA"] >= 2.4)) - | (((play_df["rush"] == True) & (play_df["EPA"] >= 1.8))), - True, - False, - ) - play_df["EPA_non_explosive"] = np.where((play_df["EPA_explosive"] == False), play_df.EPA, None) - - play_df["EPA_explosive_pass"] = np.where( - ((play_df["pass"] == True) & (play_df["EPA"] >= 2.4)), True, False - ) - play_df["EPA_explosive_rush"] = np.where( - (((play_df["rush"] == True) & (play_df["EPA"] >= 1.8))), True, False - ) - - play_df["first_down_created"] = np.where( - (play_df.scrimmage_play == True) - & (play_df["end.down"] == 1) - & (play_df["start.pos_team.id"] == play_df["end.pos_team.id"]), - True, - False, - ) - - play_df["EPA_success"] = np.where(play_df.EPA > 0, True, False) - play_df["EPA_success_early_down"] = np.where( - (play_df.EPA > 0) & (play_df.early_down == True), True, False - ) - play_df["EPA_success_early_down_pass"] = np.where( - (play_df["pass"] == True) - & (play_df.EPA > 0) - & (play_df.early_down == True), - True, - False, - ) - play_df["EPA_success_early_down_rush"] = np.where( - (play_df["rush"] == True) - & (play_df.EPA > 0) - & (play_df.early_down == True), - True, - False, - ) - play_df["EPA_success_late_down"] = np.where( - (play_df.EPA > 0) & (play_df.late_down == True), True, False - ) - play_df["EPA_success_late_down_pass"] = np.where( - (play_df["pass"] == True) & (play_df.EPA > 0) & (play_df.late_down == True), - True, - False, - ) - play_df["EPA_success_late_down_rush"] = np.where( - (play_df["rush"] == True) & (play_df.EPA > 0) & (play_df.late_down == True), - True, - False, - ) - play_df["EPA_success_standard_down"] = np.where( - (play_df.EPA > 0) & (play_df.standard_down == True), True, False - ) - play_df["EPA_success_passing_down"] = np.where( - (play_df.EPA > 0) & (play_df.passing_down == True), True, False - ) - play_df["EPA_success_pass"] = np.where( - (play_df.EPA > 0) & (play_df["pass"] == True), True, False - ) - play_df["EPA_success_rush"] = np.where( - (play_df.EPA > 0) & (play_df.rush == True), True, False - ) - play_df["EPA_success_EPA"] = np.where(play_df.EPA > 0, play_df.EPA, None) - play_df["EPA_success_standard_down_EPA"] = np.where( - (play_df.EPA > 0) & (play_df.standard_down == True), play_df.EPA, None - ) - play_df["EPA_success_passing_down_EPA"] = np.where( - (play_df.EPA > 0) & (play_df.passing_down == True), play_df.EPA, None - ) - play_df["EPA_success_pass_EPA"] = np.where( - (play_df.EPA > 0) & (play_df["pass"] == True), play_df.EPA, None - ) - play_df["EPA_success_rush_EPA"] = np.where( - (play_df.EPA > 0) & (play_df.rush == True), True, False - ) - play_df["EPA_middle_8_success"] = np.where( - (play_df.EPA > 0) & (play_df["middle_8"] == True), True, False - ) - play_df["EPA_middle_8_success_pass"] = np.where( - (play_df["pass"] == True) - & (play_df.EPA > 0) - & (play_df["middle_8"] == True), - True, - False, - ) - play_df["EPA_middle_8_success_rush"] = np.where( - (play_df["rush"] == True) - & (play_df.EPA > 0) - & (play_df["middle_8"] == True), - True, - False, - ) - play_df["EPA_penalty"] = np.select( - [ - (play_df["type.text"].isin(["Penalty", "Penalty (Kickoff)"])), - (play_df["penalty_in_text"] == True), - ], - [play_df["EPA"], play_df["EP_end"] - play_df["EP_start"]], - default=None, - ) - play_df["EPA_sp"] = np.where( - (play_df.fg_attempt == True) - | (play_df.punt == True) - | (play_df.kickoff_play == True), - play_df["EPA"], - False, - ) - play_df["EPA_fg"] = np.where((play_df.fg_attempt == True), play_df["EPA"], None) - play_df["EPA_punt"] = np.where((play_df.punt == True), play_df["EPA"], None) - play_df["EPA_kickoff"] = np.where( - (play_df.kickoff_play == True), play_df["EPA"], None + "Blocked PAT" + ])) + .then(0.92) + .otherwise(pl.col("EP_start")), + ).with_columns( + # End of Half + EP_end = pl.when((pl.col("type.text").str.to_lowercase().str.contains(r"end of game")) + .or_(pl.col("type.text").str.to_lowercase().str.contains(r"end of half"))) + .then(0) + # Defensive 2pt Conversion + .when(pl.col("type.text").is_in(["Defensive 2pt Conversion"])) + .then(-2) + # Safeties + .when((pl.col("type.text").is_in(defense_score_vec)) + .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)safety"))) + .then(-2) + # Defense TD + Successful Two-Point Conversion + .when((pl.col("type.text").is_in(defense_score_vec)) + .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)conversion")) + .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)failed") == False)) + .then(-8) + # Defense TD + Failed Two-Point Conversion + .when((pl.col("type.text").is_in(defense_score_vec)) + .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)conversion")) + .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)failed"))) + .then(-6) + # Defense TD + Kick/PAT Missed + .when((pl.col("type.text").is_in(defense_score_vec)) + .and_(pl.col("text").str.to_lowercase().str.contains(r"PAT")) + .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)missed"))) + .then(-6) + # Defense TD + Kick/PAT Good + .when((pl.col("type.text").is_in(defense_score_vec)) + .and_(pl.col("text").str.to_lowercase().str.contains(r"kick\)"))) + .then(-7) + # Defense TD + .when(pl.col("type.text").is_in(defense_score_vec)) + .then(-6.92) + # Offense TD + Failed Two-Point Conversion + .when((pl.col("type.text").is_in(offense_score_vec)) + .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)conversion")) + .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)failed"))) + .then(6) + # Offense TD + Successful Two-Point Conversion + .when((pl.col("type.text").is_in(offense_score_vec)) + .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)conversion")) + .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)failed") == False)) + .then(8) + # Offense Made FG + .when((pl.col("type.text").is_in(offense_score_vec)) + .and_(pl.col("type.text").str.to_lowercase().str.contains(r"(?i)field goal")) + .and_(pl.col("type.text").str.to_lowercase().str.contains(r"(?i)good"))) + .then(3) + # Offense TD + Kick/PAT Missed + .when((pl.col("type.text").is_in(offense_score_vec)) + .and_(pl.col("text").str.to_lowercase().str.contains(r"PAT")) + .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)missed"))) + .then(6) + # Offense TD + Kick/PAT Good + .when((pl.col("type.text").is_in(offense_score_vec)) + .and_(pl.col("text").str.to_lowercase().str.contains(r"kick\)"))) + .then(7) + # Offense TD + .when(pl.col("type.text").is_in(offense_score_vec)) + .then(6.92) + # Extra Point Good + .when(pl.col("type.text").is_in(["Extra Point Good"])) + .then(1) + # Extra Point Missed + .when(pl.col("type.text").is_in(["Extra Point Missed"])) + .then(0) + # Two-Point Conversion Good + .when(pl.col("type.text").is_in(["Two-Point Conversion Good"])) + .then(2) + # Two-Point Conversion Missed + .when(pl.col("type.text").is_in(["Two-Point Conversion Missed"])) + .then(0) + # Two Point Pass/Rush Missed (Pre-2014 Data) + .when((pl.col("type.text").is_in(["Two Point Pass", "Two Point Rush"])) + .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)no good"))) + .then(0) + # Two Point Pass/Rush Good (Pre-2014 Data) + .when((pl.col("type.text").is_in(["Two Point Pass", "Two Point Rush"])) + .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)no good") == False)) + .then(2) + # Blocked PAT + .when(pl.col("type.text").is_in(["Blocked PAT"])) + .then(0) + # Flips for Turnovers that aren't kickoffs + .when(((pl.col("type.text").is_in(end_change_vec)) + .or_(pl.col("downs_turnover") == True)) + .and_(pl.col("type.text").is_in(kickoff_vec) == False)) + .then(pl.col("EP_end") * -1) + # Flips for Turnovers that are kickoffs + .when(pl.col("type.text").is_in(kickoff_turnovers)) + .then(pl.col("EP_end") * -1) + # Onside kicks + .when((pl.col("kickoff_onside") == True) + .and_(pl.col("change_of_pos_team") == True)) + .then(pl.col("EP_end") * -1) + .otherwise(pl.col("EP_end")) + ).with_columns( + lag_EP_end = pl.col("EP_end").shift(1), + lag_change_of_pos_team = pl.col("change_of_pos_team").shift(1), + + ).with_columns( + lag_change_of_pos_team = pl.when(pl.col("lag_change_of_pos_team").is_null()) + .then(False) + .otherwise(pl.col("lag_change_of_pos_team")), + + ).with_columns( + EP_between = pl.when(pl.col("lag_change_of_pos_team") == True) + .then(pl.col("EP_start") + pl.col("lag_EP_end")) + .otherwise(pl.col("EP_start") - pl.col("lag_EP_end")), + EP_start = pl.when((pl.col("type.text").is_in(["Timeout", "End Period"])) + .and_(pl.col("lag_change_of_pos_team") == False)) + .then(pl.col("lag_EP_end")) + .otherwise(pl.col("EP_start")), + + ).with_columns( + EP_start = pl.when(pl.col("type.text").is_in(kickoff_vec)) + .then(pl.col("EP_start_touchback")) + .otherwise(pl.col("EP_start")), + + ).with_columns( + EP_end = pl.when(pl.col("type.text").is_in(["Timeout"])) + .then(pl.col("EP_start")) + .otherwise(pl.col("EP_end")), + + ).with_columns( + EPA = pl.when(pl.col("type.text").is_in(["Timeout"])) + .then(0) + .when((pl.col("scoring_play") == False) + .and_(pl.col("end_of_half") == True)) + .then(-1 * pl.col("EP_start")) + .when((pl.col("type.text").is_in(kickoff_vec)) + .and_(pl.col("penalty_in_text") == True)) + .then(pl.col("EP_end") - pl.col("EP_start")) + .when((pl.col("penalty_in_text") == True) + .and_(pl.col("type.text").is_in(["Penalty"]) == False) + .and_(pl.col("type.text").is_in(kickoff_vec) == False)) + .then(pl.col("EP_end") - pl.col("EP_start") + pl.col("EP_between")) + .otherwise(pl.col("EP_end") - pl.col("EP_start")), + ).with_columns( + def_EPA = pl.col("EPA") * -1, + # --- EPA Summary flags ---- + EPA_scrimmage = pl.when(pl.col("scrimmage_play") == True) + .then(pl.col("EPA")) + .otherwise(None), + EPA_rush = pl.when((pl.col("rush") == True) + .and_(pl.col("penalty_in_text") == True)) + .then(pl.col("EPA")) + .when((pl.col("rush") == True) + .and_(pl.col("penalty_in_text") == False)) + .then(pl.col("EPA")) + .otherwise(None), + EPA_pass = pl.when(pl.col("pass") == True) + .then(pl.col("EPA")) + .otherwise(None), + EPA_explosive = pl.when((pl.col("pass") == True) + .and_(pl.col("EPA") >= 2.4)) + .then(True) + .when(((pl.col("rush") == True) + .and_(pl.col("EPA") >= 1.8))) + .then(True) + .otherwise(False), + ).with_columns( + EPA_non_explosive = pl.when(pl.col("EPA_explosive") == False) + .then(pl.col("EPA")) + .otherwise(None), + EPA_explosive_pass = pl.when((pl.col("pass") == True) + .and_(pl.col("EPA") >= 2.4)) + .then(True) + .otherwise(False), + EPA_explosive_rush = pl.when((pl.col("rush") == True) + .and_(pl.col("EPA") >= 1.8)) + .then(True) + .otherwise(False), + first_down_created = pl.when((pl.col("scrimmage_play") == True) + .and_(pl.col("end.down") == 1) + .and_(pl.col("start.pos_team.id") == pl.col("end.pos_team.id"))) + .then(True) + .otherwise(False), + EPA_success = pl.when(pl.col("EPA") > 0) + .then(True) + .otherwise(False), + EPA_success_early_down = pl.when((pl.col("EPA") > 0) + .and_(pl.col("early_down") == True)) + .then(True) + .otherwise(False), + EPA_success_early_down_pass = pl.when((pl.col("pass") == True) + .and_(pl.col("EPA") > 0) + .and_(pl.col("early_down") == True)) + .then(True) + .otherwise(False), + EPA_success_early_down_rush = pl.when((pl.col("rush") == True) + .and_(pl.col("EPA") > 0) + .and_(pl.col("early_down") == True)) + .then(True) + .otherwise(False), + EPA_success_late_down = pl.when((pl.col("EPA") > 0) + .and_(pl.col("late_down") == True)) + .then(True) + .otherwise(False), + EPA_success_late_down_pass = pl.when((pl.col("pass") == True) + .and_(pl.col("EPA") > 0) + .and_(pl.col("late_down") == True)) + .then(True) + .otherwise(False), + EPA_success_late_down_rush = pl.when((pl.col("rush") == True) + .and_(pl.col("EPA") > 0) + .and_(pl.col("late_down") == True)) + .then(True) + .otherwise(False), + EPA_success_standard_down = pl.when((pl.col("EPA") > 0) + .and_(pl.col("standard_down") == True)) + .then(True) + .otherwise(False), + EPA_success_passing_down = pl.when((pl.col("EPA") > 0) + .and_(pl.col("passing_down") == True)) + .then(True) + .otherwise(False), + EPA_success_pass = pl.when((pl.col("EPA") > 0) + .and_(pl.col("pass") == True)) + .then(True) + .otherwise(False), + EPA_success_rush = pl.when((pl.col("EPA") > 0) + .and_(pl.col("rush") == True)) + .then(True) + .otherwise(False), + EPA_success_EPA = pl.when(pl.col("EPA") > 0) + .then(pl.col("EPA")) + .otherwise(None), + EPA_success_standard_down_EPA = pl.when((pl.col("EPA") > 0) + .and_(pl.col("standard_down") == True)) + .then(pl.col("EPA")) + .otherwise(None), + EPA_success_passing_down_EPA = pl.when((pl.col("EPA") > 0) + .and_(pl.col("passing_down") == True)) + .then(pl.col("EPA")) + .otherwise(None), + EPA_success_pass_EPA = pl.when((pl.col("EPA") > 0) + .and_(pl.col("pass") == True)) + .then(pl.col("EPA")) + .otherwise(None), + EPA_success_rush_EPA = pl.when((pl.col("EPA") > 0) + .and_(pl.col("rush") == True)) + .then(pl.col("EPA")) + .otherwise(None), + EPA_middle_8_success = pl.when((pl.col("EPA") > 0) + .and_(pl.col("middle_8") == True)) + .then(True) + .otherwise(False), + EPA_middle_8_success_pass = pl.when((pl.col("pass") == True) + .and_(pl.col("EPA") > 0) + .and_(pl.col("middle_8") == True)) + .then(True) + .otherwise(False), + EPA_middle_8_success_rush = pl.when((pl.col("rush") == True) + .and_(pl.col("EPA") > 0) + .and_(pl.col("middle_8") == True)) + .then(True) + .otherwise(False), + EPA_penalty = pl.when(pl.col("type.text").is_in(["Penalty", "Penalty (Kickoff)"])) + .then(pl.col("EPA")) + .when(pl.col("penalty_in_text") == True) + .then(pl.col("EP_end") - pl.col("EP_start")) + .otherwise(None), + EPA_sp = pl.when((pl.col("fg_attempt") == True) + .or_(pl.col("punt") == True) + .or_(pl.col("kickoff_play") == True)) + .then(pl.col("EPA")) + .otherwise(False), + EPA_fg = pl.when(pl.col("fg_attempt") == True) + .then(pl.col("EPA")) + .otherwise(None), + EPA_punt = pl.when(pl.col("punt") == True) + .then(pl.col("EPA")) + .otherwise(None), + EPA_kickoff = pl.when(pl.col("kickoff_play") == True) + .then(pl.col("EPA")) + .otherwise(None), ) return play_df def __process_qbr(self, play_df): - play_df["qbr_epa"] = np.select( - [ - (play_df.EPA < -5.0), - (play_df.fumble_vec == True), - ], - [-5.0, -3.5], - default=play_df.EPA, - ) - - play_df["weight"] = np.select( - [ - (play_df.home_wp_before < 0.1), - (play_df.home_wp_before >= 0.1) & (play_df.home_wp_before < 0.2), - (play_df.home_wp_before >= 0.8) & (play_df.home_wp_before < 0.9), - (play_df.home_wp_before > 0.9), - ], - [0.6, 0.9, 0.9, 0.6], - default=1, - ) - play_df["non_fumble_sack"] = (play_df["sack_vec"] == True) & ( - play_df["fumble_vec"] == False - ) - - play_df["sack_epa"] = np.where( - play_df["non_fumble_sack"] == True, play_df["qbr_epa"], np.NaN - ) - play_df["pass_epa"] = np.where( - play_df["pass"] == True, play_df["qbr_epa"], np.NaN - ) - play_df["rush_epa"] = np.where( - play_df["rush"] == True, play_df["qbr_epa"], np.NaN - ) - play_df["pen_epa"] = np.where( - play_df["penalty_flag"] == True, play_df["qbr_epa"], np.NaN - ) - - play_df["sack_weight"] = np.where( - play_df["non_fumble_sack"] == True, play_df["weight"], np.NaN - ) - play_df["pass_weight"] = np.where( - play_df["pass"] == True, play_df["weight"], np.NaN - ) - play_df["rush_weight"] = np.where( - play_df["rush"] == True, play_df["weight"], np.NaN - ) - play_df["pen_weight"] = np.where( - play_df["penalty_flag"] == True, play_df["weight"], np.NaN - ) - - play_df["action_play"] = play_df.EPA != 0 - play_df["athlete_name"] = np.select( - [ - play_df.passer_player_name.notna(), - play_df.rusher_player_name.notna(), - ], - [play_df.passer_player_name, play_df.rusher_player_name], - default=None, + play_df = play_df.with_columns( + qbr_epa = pl.when(pl.col("EPA") < -5.0) + .then(-5.0) + .when(pl.col("fumble_vec") == True) + .then(-3.5) + .otherwise(pl.col("EPA")), + weight = pl.when(pl.col("home_wp_before") < 0.1) + .then(0.6) + .when((pl.col("home_wp_before") >= 0.1) + .and_(pl.col("home_wp_before") < 0.2)) + .then(0.9) + .when((pl.col("home_wp_before") >= 0.8) + .and_(pl.col("home_wp_before") < 0.9)) + .then(0.9) + .when(pl.col("home_wp_before") > 0.9) + .then(0.6) + .otherwise(1), + non_fumble_sack = pl.when((pl.col("sack_vec") == True) + .and_(pl.col("fumble_vec") == False)) + .then(True) + .otherwise(False), + ).with_columns( + sack_epa = pl.when(pl.col("non_fumble_sack") == True) + .then(pl.col("qbr_epa")) + .otherwise(None), + pass_epa = pl.when(pl.col("pass") == True) + .then(pl.col("qbr_epa")) + .otherwise(None), + rush_epa = pl.when(pl.col("rush") == True) + .then(pl.col("qbr_epa")) + .otherwise(None), + pen_epa = pl.when(pl.col("penalty_flag") == True) + .then(pl.col("qbr_epa")) + .otherwise(None), + ).with_columns( + sack_weight = pl.when(pl.col("non_fumble_sack") == True) + .then(pl.col("weight")) + .otherwise(None), + pass_weight = pl.when(pl.col("pass") == True) + .then(pl.col("weight")) + .otherwise(None), + rush_weight = pl.when(pl.col("rush") == True) + .then(pl.col("weight")) + .otherwise(None), + pen_weight = pl.when(pl.col("penalty_flag") == True) + .then(pl.col("weight")) + .otherwise(None), + ).with_columns( + action_play = pl.col("EPA") != 0, + athlete_name = pl.when(pl.col("passer_player_name").is_not_null()) + .then(pl.col("passer_player_name")) + .when(pl.col("rusher_player_name").is_not_null()) + .then(pl.col("rusher_player_name")) + .otherwise(None), ) return play_df def __process_wpa(self, play_df): # ---- prepare variables for wp_before calculations ---- - play_df["start.ExpScoreDiff_touchback"] = np.select( - [(play_df["type.text"].isin(kickoff_vec))], - [play_df["pos_score_diff_start"] + play_df["EP_start_touchback"]], - default=0.000, - ) - play_df["start.ExpScoreDiff"] = np.select( - [ - (play_df["penalty_in_text"] == True) - & (play_df["type.text"] != "Penalty"), - (play_df["type.text"] == "Timeout") - & (play_df["lag_scoringPlay"] == True), - ], - [ - play_df["pos_score_diff_start"] - + play_df["EP_start"] - - play_df["EP_between"], - (play_df["pos_score_diff_start"] + 0.92), - ], - default=play_df["pos_score_diff_start"] + play_df.EP_start, - ) - play_df["start.ExpScoreDiff_Time_Ratio_touchback"] = play_df[ - "start.ExpScoreDiff_touchback" - ] / (play_df["start.adj_TimeSecsRem"] + 1) - play_df["start.ExpScoreDiff_Time_Ratio"] = play_df["start.ExpScoreDiff"] / ( - play_df["start.adj_TimeSecsRem"] + 1 + play_df = play_df.with_columns( + pl.when(pl.col("type.text").is_in(kickoff_vec)) + .then(pl.col("pos_score_diff_start") + pl.col("EP_start_touchback")) + .otherwise(0.000) + .alias("start.ExpScoreDiff_touchback"), + pl.when((pl.col("penalty_in_text") == True) + .and_(pl.col("type.text").is_in(["Penalty"]) == False)) + .then(pl.col("pos_score_diff_start") + pl.col("EP_start") - pl.col("EP_between")) + .when((pl.col("type.text") == "Timeout") + .and_(pl.col("lag_scoringPlay") == True)) + .then(pl.col("pos_score_diff_start") + 0.92) + .otherwise(pl.col("pos_score_diff_start") + pl.col("EP_start")) + .alias("start.ExpScoreDiff"), + ).with_columns( + (pl.col("start.ExpScoreDiff_touchback") / (pl.col("start.adj_TimeSecsRem") + 1)) + .alias("start.ExpScoreDiff_Time_Ratio_touchback"), + (pl.col("start.ExpScoreDiff") / (pl.col("start.adj_TimeSecsRem") + 1)) + .alias("start.ExpScoreDiff_Time_Ratio"), + # ---- prepare variables for wp_after calculations ---- + pl.when(((pl.col("type.text").is_in(end_change_vec)) + .or_(pl.col("downs_turnover") == True)) + .and_(pl.col("kickoff_play") == False) + .and_(pl.col("scoringPlay") == False)) + .then(pl.col("pos_score_diff_end") - pl.col("EP_end")) + .when(pl.col("type.text").is_in(kickoff_turnovers) + .and_(pl.col("scoringPlay") == False)) + .then(pl.col("pos_score_diff_end") + pl.col("EP_end")) + .when((pl.col("scoringPlay") == False) + .and_(pl.col("type.text") != "Timeout")) + .then(pl.col("pos_score_diff_end") + pl.col("EP_end")) + .when((pl.col("scoringPlay") == False) + .and_(pl.col("type.text") == "Timeout")) + .then(pl.col("pos_score_diff_end") + pl.col("EP_end")) + .when((pl.col("scoringPlay") == True) + .and_(pl.col("td_play") == True) + .and_(pl.col("type.text").is_in(defense_score_vec)) + .and_(pl.col("season") <= 2013)) + .then(pl.col("pos_score_diff_end") - 0.92) + .when((pl.col("scoringPlay") == True) + .and_(pl.col("td_play") == True) + .and_(pl.col("type.text").is_in(offense_score_vec)) + .and_(pl.col("season") <= 2013)) + .then(pl.col("pos_score_diff_end") + 0.92) + .when((pl.col("type.text") == "Timeout") + .and_(pl.col("lag_scoringPlay") == True) + .and_(pl.col("season") <= 2013)) + .then(pl.col("pos_score_diff_end") + 0.92) + .otherwise(pl.col("pos_score_diff_end")) + .alias("end.ExpScoreDiff"), + + + ).with_columns( + (pl.col("end.ExpScoreDiff") / (pl.col("end.adj_TimeSecsRem") + 1)) + .alias("end.ExpScoreDiff_Time_Ratio") ) - # ---- prepare variables for wp_after calculations ---- - play_df["end.ExpScoreDiff"] = np.select( - [ - # Flips for Turnovers that aren't kickoffs - ( - ( - (play_df["type.text"].isin(end_change_vec)) - | (play_df.downs_turnover == True) - ) - & (play_df.kickoff_play == False) - & (play_df["scoringPlay"] == False) - ), - # Flips for Turnovers that are on kickoffs - (play_df["type.text"].isin(kickoff_turnovers)) - & (play_df["scoringPlay"] == False), - (play_df["scoringPlay"] == False) & (play_df["type.text"] != "Timeout"), - (play_df["scoringPlay"] == False) & (play_df["type.text"] == "Timeout"), - (play_df["scoringPlay"] == True) - & (play_df["td_play"] == True) - & (play_df["type.text"].isin(defense_score_vec)) - & (play_df.season <= 2013), - (play_df["scoringPlay"] == True) - & (play_df["td_play"] == True) - & (play_df["type.text"].isin(offense_score_vec)) - & (play_df.season <= 2013), - (play_df["type.text"] == "Timeout") - & (play_df["lag_scoringPlay"] == True) - & (play_df.season <= 2013), - ], - [ - play_df["pos_score_diff_end"] - play_df.EP_end, - play_df["pos_score_diff_end"] + play_df.EP_end, - play_df["pos_score_diff_end"] + play_df.EP_end, - play_df["pos_score_diff_end"] + play_df.EP_end, - play_df["pos_score_diff_end"] + 0.92, - play_df["pos_score_diff_end"] + 0.92, - play_df["pos_score_diff_end"] + 0.92, - ], - default=play_df["pos_score_diff_end"], - ) - play_df["end.ExpScoreDiff_Time_Ratio"] = play_df["end.ExpScoreDiff"] / ( - play_df["end.adj_TimeSecsRem"] + 1 - ) # ---- wp_before ---- start_touchback_data = play_df[wp_start_touchback_columns] start_touchback_data.columns = wp_final_names @@ -4717,24 +3124,7 @@ def __process_wpa(self, play_df): # self.logger.info(start_data.iloc[[36]].to_json(orient="records")) dtest_start = DMatrix(start_data) WP_start = wp_model.predict(dtest_start) - play_df["wp_before"] = WP_start - play_df["wp_touchback"] = WP_start_touchback - play_df["wp_before"] = np.where( - play_df["type.text"].isin(kickoff_vec), - play_df["wp_touchback"], - play_df["wp_before"], - ) - play_df["def_wp_before"] = 1 - play_df.wp_before - play_df["home_wp_before"] = np.where( - play_df["start.pos_team.id"] == play_df["homeTeamId"], - play_df.wp_before, - play_df.def_wp_before, - ) - play_df["away_wp_before"] = np.where( - play_df["start.pos_team.id"] != play_df["homeTeamId"], - play_df.wp_before, - play_df.def_wp_before, - ) + # ---- wp_after ---- end_data = play_df[wp_end_columns] end_data.columns = wp_final_names @@ -4742,119 +3132,122 @@ def __process_wpa(self, play_df): dtest_end = DMatrix(end_data) WP_end = wp_model.predict(dtest_end) - play_df["lead_wp_before"] = play_df["wp_before"].shift(-1) - play_df["lead_wp_before2"] = play_df["wp_before"].shift(-2) - - play_df["wp_after"] = WP_end - game_complete = self.json["teamInfo"]["status"]["type"]["completed"] - play_df["wp_after"] = np.select( - [ - (play_df["type.text"] == "Timeout"), - game_complete - & ( - (play_df.lead_play_type.isna()) - | (play_df.game_play_number == max(play_df.game_play_number)) - ) - & (play_df.pos_score_diff_end > 0), - game_complete - & ( - (play_df.lead_play_type.isna()) - | (play_df.game_play_number == max(play_df.game_play_number)) - ) - & (play_df.pos_score_diff_end < 0), - (play_df.end_of_half == 1) - & (play_df["start.pos_team.id"] == play_df.lead_pos_team) - & (play_df["type.text"] != "Timeout"), - (play_df.end_of_half == 1) - & (play_df["start.pos_team.id"] != play_df["end.pos_team.id"]) - & (play_df["type.text"] != "Timeout"), - (play_df.end_of_half == 1) - & (play_df["start.pos_team_receives_2H_kickoff"] == False) - & (play_df["type.text"] == "Timeout"), - (play_df.lead_play_type.isin(["End Period", "End of Half"])) - & (play_df.change_of_pos_team == 0), - (play_df.lead_play_type.isin(["End Period", "End of Half"])) - & (play_df.change_of_pos_team == 1), - (play_df["kickoff_onside"] == True) - & (play_df["change_of_pos_team"] == True), # onside recovery - (play_df["start.pos_team.id"] != play_df["end.pos_team.id"]), - ], - [ - play_df.wp_before, - 1.0, - 0.0, - play_df.lead_wp_before, - (1 - play_df.lead_wp_before), - play_df.wp_after, - play_df.lead_wp_before, - (1 - play_df.lead_wp_before), - play_df.wp_after, - (1 - play_df.wp_after), - ], - default=play_df.wp_after, - ) - - play_df["def_wp_after"] = 1 - play_df.wp_after - play_df["home_wp_after"] = np.where( - play_df["end.pos_team.id"] == play_df["homeTeamId"], - play_df.wp_after, - play_df.def_wp_after, + play_df = play_df.with_columns( + wp_before = pl.lit(WP_start), + wp_touchback = pl.lit(WP_start_touchback), + wp_after = pl.lit(WP_end) + ).with_columns( + wp_before = pl.when(pl.col("type.text").is_in(kickoff_vec)) + .then(pl.col("wp_touchback")) + .otherwise(pl.col("wp_before")), + + ).with_columns( + def_wp_before = 1 - pl.col("wp_before"), + + ).with_columns( + home_wp_before = pl.when(pl.col("start.pos_team.id") == pl.col("homeTeamId")) + .then(pl.col("wp_before")) + .otherwise(pl.col("def_wp_before")), + away_wp_before = pl.when(pl.col("start.pos_team.id") != pl.col("homeTeamId")) + .then(pl.col("wp_before")) + .otherwise(pl.col("def_wp_before")), + ).with_columns( + lead_wp_before = pl.col("wp_before").shift(-1), + lead_wp_before2 = pl.col("wp_before").shift(-2), + ).with_columns( + wp_after = pl.when(pl.col("type.text").is_in(["Timeout"])) + .then(pl.col("wp_before")) + .when((pl.col("status_type_completed") == True) + .and_((pl.col("lead_play_type").is_null()) + .or_(pl.col("game_play_number") == pl.col("game_play_number").max())) + .and_(pl.col("pos_score_diff_end") > 0)) + .then(1.0) + .when((pl.col("status_type_completed") == True) + .and_((pl.col("lead_play_type").is_null()) + .or_(pl.col("game_play_number") == pl.col("game_play_number").max())) + .and_(pl.col("pos_score_diff_end") < 0)) + .then(0.0) + .when((pl.col("end_of_half") == True) + .and_(pl.col("start.pos_team.id") == pl.col("lead_pos_team")) + .and_(pl.col("type.text") != "Timeout")) + .then(pl.col("lead_wp_before")) + .when((pl.col("end_of_half") == True) + .and_(pl.col("start.pos_team.id") != pl.col("end.pos_team.id")) + .and_(pl.col("type.text") != "Timeout")) + .then(1 - pl.col("lead_wp_before")) + .when((pl.col("end_of_half") == True) + .and_(pl.col("start.pos_team_receives_2H_kickoff") == False) + .and_(pl.col("type.text") == "Timeout")) + .then(pl.col("wp_after")) + .when((pl.col("lead_play_type").is_in(["End Period", "End of Half"])) + .and_(pl.col("change_of_pos_team") == False)) + .then(pl.col("lead_wp_before")) + .when((pl.col("lead_play_type").is_in(["End Period", "End of Half"])) + .and_(pl.col("change_of_pos_team") == True)) + .then(1 - pl.col("lead_wp_before")) + .when((pl.col("kickoff_onside") == True) + .and_(pl.col("change_of_pos_team") == True)) + .then(pl.col("wp_after")) + .when(pl.col("start.pos_team.id") != pl.col("end.pos_team.id")) + .then(1 - pl.col("wp_after")) + .otherwise(pl.col("wp_after")), + ).with_columns( + def_wp_after = 1 - pl.col("wp_after"), + ).with_columns( + home_wp_after = pl.when(pl.col("end.pos_team.id") == pl.col("homeTeamId")) + .then(pl.col("wp_after")) + .otherwise(pl.col("def_wp_after")), + away_wp_after = pl.when(pl.col("end.pos_team.id") != pl.col("homeTeamId")) + .then(pl.col("wp_after")) + .otherwise(pl.col("def_wp_after")), + ).with_columns( + wpa = pl.col("wp_after") - pl.col("wp_before"), ) - play_df["away_wp_after"] = np.where( - play_df["end.pos_team.id"] != play_df["homeTeamId"], - play_df.wp_after, - play_df.def_wp_after, - ) - - play_df["wpa"] = play_df.wp_after - play_df.wp_before return play_df def __add_drive_data(self, play_df): - base_groups = play_df.groupby(["drive.id"], group_keys = False) - play_df["drive_start"] = np.where( - play_df["start.pos_team.id"] == play_df["homeTeamId"], - 100 - play_df["drive.start.yardLine"], - play_df["drive.start.yardLine"], - ) - play_df["drive_stopped"] = np.select([ - play_df['drive.result'].isna() - ], - [ - False - ], - default = play_df["drive.result"].str.lower().str.contains( - "punt|fumble|interception|downs", regex=True, case=False - )) - play_df["drive_start"] = play_df["drive_start"].astype(float) - play_df["drive_play_index"] = base_groups["scrimmage_play"].apply( - lambda x: x.cumsum() - ) - play_df["drive_offense_plays"] = np.where( - (play_df["sp"] == False) & (play_df["scrimmage_play"] == True), - play_df["play"].astype(int), - 0, - ) - play_df["prog_drive_EPA"] = base_groups["EPA_scrimmage"].apply( - lambda x: x.cumsum() - ) - play_df["prog_drive_WPA"] = base_groups["wpa"].apply(lambda x: x.cumsum()) - play_df["drive_offense_yards"] = np.where( - (play_df["sp"] == False) & (play_df["scrimmage_play"] == True), - play_df["statYardage"], - 0, + base_groups = play_df.groupby(["drive.id"]) + play_df = play_df.with_columns( + drive_start = pl.when(pl.col("start.pos_team.id") == pl.col("homeTeamId")) + .then(100 - pl.col("drive.start.yardLine")) + .otherwise(pl.col("drive.start.yardLine")), + drive_stopped = pl.when(pl.col("drive.result").is_null()) + .then(False) + .otherwise(pl.col("drive.result").str.to_lowercase().str.contains(r"(?i)punt|fumble|interception|downs")), + ).with_columns( + drive_start = pl.col("drive_start").cast(pl.Float32), + ).with_columns( + drive_play_index = pl.col("scrimmage_play").cumsum().over("drive.id"), + ).with_columns( + drive_offense_plays = pl.when((pl.col("sp") == False) + .and_(pl.col("scrimmage_play") == True)) + .then(pl.col("play").cast(pl.Int32)) + .otherwise(0), + prog_drive_EPA = pl.col("EPA_scrimmage").cumsum().over("drive.id"), + prog_drive_WPA = pl.col("wpa").cumsum().over("drive.id"), + drive_offense_yards = pl.when((pl.col("sp") == False) + .and_(pl.col("scrimmage_play") == True)) + .then(pl.col("statYardage")) + .otherwise(0), + ).with_columns( + drive_total_yards = pl.col("drive_offense_yards").cumsum().over("drive.id"), ) - play_df["drive_total_yards"] = play_df.groupby(["drive.id"], group_keys = False)[ - "drive_offense_yards" - ].apply(lambda x: x.cumsum()) return play_df - def __cast_box_score_column(self, column, target_type): - if (column in self.plays_json.columns): - self.plays_json[column] = self.plays_json[column].astype(target_type) + def __cast_box_score_column(self, play_df, column, target_type): + if (column in play_df.columns): + play_df = play_df.with_columns( + pl.col(column) + .cast(target_type) + .alias(column) + ) else: - self.plays_json[column] = np.NaN + play_df = play_df.with_columns( + (pl.Null).alias(column) + ) + return play_df - def create_box_score(self): + def create_box_score(self, play_df): # have to run the pipeline before pulling this in if (self.ran_pipeline == False): self.run_processing_pipeline() @@ -4916,341 +3309,496 @@ def create_box_score(self): 'havoc', ] for item in box_score_columns: - self.__cast_box_score_column(item, float) + self.__cast_box_score_column(play_df, item, pl.Float32) - pass_box = self.plays_json[(self.plays_json["pass"] == True) & (self.plays_json.scrimmage_play == True)] - rush_box = self.plays_json[(self.plays_json["rush"] == True) & (self.plays_json.scrimmage_play == True)] + pass_box = play_df.filter((pl.col("pass") == True) & (pl.col("scrimmage_play") == True)) + rush_box = play_df.filter((pl.col("rush") == True) & (pl.col("scrimmage_play") == True)) # pass_box.yds_receiving.fillna(0.0, inplace=True) - passer_box = pass_box[(pass_box["pass"] == True) & (pass_box["scrimmage_play"] == True)].fillna(0.0).groupby(by=["pos_team","passer_player_name"], as_index=False, group_keys = False).agg( - Comp = ('completion', sum), - Att = ('pass_attempt',sum), - Yds = ('yds_receiving',sum), - Pass_TD = ('pass_td', sum), - Int = ('int', sum), - YPA = ('yds_receiving', mean), - EPA = ('EPA', sum), - EPA_per_Play = ('EPA', mean), - WPA = ('wpa', sum), - SR = ('EPA_success', mean), - Sck = ('sack_vec', sum) - ).round(2) - passer_box = passer_box.replace({np.nan: None}) - qbs_list = passer_box.passer_player_name.to_list() - - def weighted_mean(s, df, wcol): - s = s[s.notna() == True] - # self.logger.info(s) - if (len(s) == 0): - return 0 - return np.average(s, weights=df.loc[s.index, wcol]) - - pass_qbr_box = self.plays_json[(self.plays_json.athlete_name.notna() == True) & (self.plays_json.scrimmage_play == True) & (self.plays_json.athlete_name.isin(qbs_list))] - pass_qbr = pass_qbr_box.groupby(by=["pos_team","athlete_name"], as_index=False, group_keys = False).agg( - qbr_epa = ('qbr_epa', partial(weighted_mean, df=pass_qbr_box, wcol='weight')), - sack_epa = ('sack_epa', partial(weighted_mean, df=pass_qbr_box, wcol='sack_weight')), - pass_epa = ('pass_epa', partial(weighted_mean, df=pass_qbr_box, wcol='pass_weight')), - rush_epa = ('rush_epa', partial(weighted_mean, df=pass_qbr_box, wcol='rush_weight')), - pen_epa = ('pen_epa', partial(weighted_mean, df=pass_qbr_box, wcol='pen_weight')), - spread = ('start.pos_team_spread', lambda x: x.iloc[0]) - ) - # self.logger.info(pass_qbr) + passer_box = pass_box.fill_null(0.0).groupby(by=["pos_team","passer_player_name"]).agg( + Comp = pl.col('completion').sum(), + Att = pl.col('pass_attempt').sum(), + Yds = pl.col('yds_receiving').sum(), + Pass_TD = pl.col('pass_td').sum(), + Int = pl.col('int').sum(), + YPA = pl.col('yds_receiving').mean(), + EPA = pl.col('EPA').sum(), + EPA_per_Play = pl.col('EPA').mean(), + WPA = pl.col('wpa').sum(), + SR = pl.col('EPA_success').mean(), + Sck = pl.col('sack_vec').sum() + ).with_columns(pl.col(pl.Float32).round(2) + ).with_columns( + pos_team = pl.col("pos_team").cast(pl.Int32) + ) + # passer_box = passer_box.replace(pl.all(), pl.Null) + qbs_list = passer_box["passer_player_name"].to_list() + + pass_qbr_box = play_df.filter( + (pl.col("athlete_name").is_not_null() == True) & + (pl.col("scrimmage_play") == True) & + (pl.col("athlete_name").is_in(qbs_list)) + ) + pass_qbr_box_df = pass_qbr_box.groupby(by=["pos_team","athlete_name"]) + pass_qbr = pass_qbr_box.groupby(by=["pos_team","athlete_name"]).agg( + qbr_epa = (pl.col("qbr_epa") * pl.col("weight")).sum() / pl.col("weight").sum(), + sack_epa = (pl.col("sack_epa") * pl.col("sack_weight")).sum() / pl.col("sack_weight").sum(), + pass_epa = (pl.col("pass_epa") * pl.col("pass_weight")).sum() / pl.col("pass_weight").sum(), + rush_epa = (pl.col("rush_epa") * pl.col("rush_weight")).sum() / pl.col("rush_weight").sum(), + pen_epa = (pl.col("pen_epa") * pl.col("pen_weight")).sum() / pl.col("pen_weight").sum(), + spread = (pl.col('start.pos_team_spread').first()) + ).fill_null(0.0).with_columns( + pos_team = pl.col("pos_team").cast(pl.Int32) + ) + # # self.logger.info(pass_qbr) dtest_qbr = DMatrix(pass_qbr[qbr_vars]) qbr_result = qbr_model.predict(dtest_qbr) - pass_qbr["exp_qbr"] = qbr_result - passer_box = pd.merge(passer_box, pass_qbr, left_on=["passer_player_name","pos_team"], right_on=["athlete_name","pos_team"]) - - rusher_box = rush_box.fillna(0.0).groupby(by=["pos_team","rusher_player_name"], as_index=False, group_keys = False).agg( - Car= ('rush', sum), - Yds= ('yds_rushed',sum), - Rush_TD = ('rush_td',sum), - YPC= ('yds_rushed', mean), - EPA= ('EPA', sum), - EPA_per_Play= ('EPA', mean), - WPA= ('wpa', sum), - SR = ('EPA_success', mean), - Fum = ('fumble_vec', sum), - Fum_Lost = ('fumble_lost', sum) - ).round(2) - rusher_box = rusher_box.replace({np.nan: None}) - - receiver_box = pass_box.groupby(by=["pos_team","receiver_player_name"], as_index=False, group_keys = False).agg( - Rec= ('completion', sum), - Tar= ('target',sum), - Yds= ('yds_receiving',sum), - Rec_TD = ('pass_td', sum), - YPT= ('yds_receiving', mean), - EPA= ('EPA', sum), - EPA_per_Play= ('EPA', mean), - WPA= ('wpa', sum), - SR = ('EPA_success', mean), - Fum = ('fumble_vec', sum), - Fum_Lost = ('fumble_lost', sum) - ).round(2) - receiver_box = receiver_box.replace({np.nan: None}) - - team_base_box = self.plays_json.groupby(by=["pos_team"], as_index=False, group_keys = False).agg( - EPA_plays = ('play', sum), - total_yards = ('statYardage', sum), - EPA_overall_total = ('EPA', sum), - ).round(2) - - team_pen_box = self.plays_json[(self.plays_json.penalty_flag == True)].groupby(by=["pos_team"], as_index=False, group_keys = False).agg( - total_pen_yards = ('statYardage', sum), - EPA_penalty = ('EPA_penalty', sum), - ).round(2) - - team_scrimmage_box = self.plays_json[(self.plays_json.scrimmage_play == True)].groupby(by=["pos_team"], as_index=False, group_keys = False).agg( - scrimmage_plays = ('scrimmage_play', sum), - EPA_overall_off = ('EPA', sum), - EPA_overall_offense = ('EPA', sum), - EPA_per_play = ('EPA', mean), - EPA_non_explosive = ('EPA_non_explosive', sum), - EPA_non_explosive_per_play = ('EPA_non_explosive', mean), - EPA_explosive = ('EPA_explosive', sum), - EPA_explosive_rate = ('EPA_explosive', mean), - passes_rate = ('pass', mean), - off_yards = ('statYardage', sum), - total_off_yards = ('statYardage', sum), - yards_per_play = ('statYardage', mean) - ).round(2) - - team_sp_box = self.plays_json[(self.plays_json.sp == True)].groupby(by=["pos_team"], as_index=False, group_keys = False).agg( - special_teams_plays = ('sp', sum), - EPA_sp = ('EPA_sp', sum), - EPA_special_teams = ('EPA_sp', sum), - EPA_fg = ('EPA_fg', sum), - EPA_punt = ('EPA_punt', sum), - kickoff_plays = ('kickoff_play', sum), - EPA_kickoff = ('EPA_kickoff', sum) - ).round(2) - - team_scrimmage_box_pass = self.plays_json[(self.plays_json["pass"] == True) & (self.plays_json["scrimmage_play"] == True)].fillna(0).groupby(by=["pos_team"], as_index=False, group_keys = False).agg( - passes = ('pass', sum), - pass_yards = ('yds_receiving', sum), - yards_per_pass = ('yds_receiving', mean), - EPA_passing_overall = ('EPA', sum), - EPA_passing_per_play = ('EPA', mean), - EPA_explosive_passing = ('EPA_explosive', sum), - EPA_explosive_passing_rate = ('EPA_explosive', mean), - EPA_non_explosive_passing = ('EPA_non_explosive', sum), - EPA_non_explosive_passing_per_play = ('EPA_non_explosive', mean), - ).round(2) - - team_scrimmage_box_rush = self.plays_json[(self.plays_json["rush"] == True) & (self.plays_json["scrimmage_play"] == True)].groupby(by=["pos_team"], as_index=False, group_keys = False).agg( - EPA_rushing_overall = ('EPA', sum), - EPA_rushing_per_play = ('EPA', mean), - EPA_explosive_rushing = ('EPA_explosive', sum), - EPA_explosive_rushing_rate = ('EPA_explosive', mean), - EPA_non_explosive_rushing = ('EPA_non_explosive', sum), - EPA_non_explosive_rushing_per_play = ('EPA_non_explosive', mean), - rushes = ('rush', sum), - rush_yards = ('yds_rushed', sum), - yards_per_rush = ('yds_rushed', mean), - rushing_power_rate = ('power_rush_attempt', mean), - ).round(2) - - team_rush_base_box = self.plays_json[(self.plays_json["scrimmage_play"] == True)].groupby(by=["pos_team"], as_index=False, group_keys = False).agg( - rushes_rate = ('rush', mean), - first_downs_created = ('first_down_created', sum), - first_downs_created_rate = ('first_down_created', mean) - ) - team_rush_power_box = self.plays_json[(self.plays_json["power_rush_attempt"] == True)].groupby(by=["pos_team"], as_index=False, group_keys = False).agg( - EPA_rushing_power = ('EPA', sum), - EPA_rushing_power_per_play = ('EPA', mean), - rushing_power_success = ('power_rush_success', sum), - rushing_power_success_rate = ('power_rush_success', mean), - rushing_power = ('power_rush_attempt', sum), - ) - - self.plays_json.opp_highlight_yards = self.plays_json.opp_highlight_yards.astype(float) - self.plays_json.highlight_yards = self.plays_json.highlight_yards.astype(float) - self.plays_json.line_yards = self.plays_json.line_yards.astype(float) - self.plays_json.second_level_yards = self.plays_json.second_level_yards.astype(float) - self.plays_json.open_field_yards = self.plays_json.open_field_yards.astype(float) - team_rush_box = self.plays_json[(self.plays_json["rush"] == True) & (self.plays_json["scrimmage_play"] == True)].fillna(0).groupby(by=["pos_team"], as_index=False, group_keys = False).agg( - rushing_stuff = ('stuffed_run', sum), - rushing_stuff_rate = ('stuffed_run', mean), - rushing_stopped = ('stopped_run', sum), - rushing_stopped_rate = ('stopped_run', mean), - rushing_opportunity = ('opportunity_run', sum), - rushing_opportunity_rate = ('opportunity_run', mean), - rushing_highlight = ('highlight_run', sum), - rushing_highlight_rate = ('highlight_run', mean), - rushing_highlight_yards = ('highlight_yards', sum), - line_yards = ('line_yards', sum), - line_yards_per_carry = ('line_yards', mean), - second_level_yards = ('second_level_yards', sum), - open_field_yards = ('open_field_yards', sum) - ).round(2) - - team_rush_opp_box = self.plays_json[(self.plays_json["rush"] == True) & (self.plays_json["scrimmage_play"] == True) & (self.plays_json.opportunity_run == True)].fillna(0).groupby(by=["pos_team"], as_index=False, group_keys = False).agg( - rushing_highlight_yards_per_opp = ('opp_highlight_yards', mean), - ).round(2) - - team_data_frames = [team_rush_opp_box, team_pen_box, team_sp_box, team_scrimmage_box_rush, team_scrimmage_box_pass, team_scrimmage_box, team_base_box, team_rush_base_box, team_rush_power_box, team_rush_box] - team_box = reduce(lambda left,right: pd.merge(left,right,on=['pos_team'], how='outer'), team_data_frames) - team_box = team_box.replace({np.nan:None}) - - situation_box_normal = self.plays_json[(self.plays_json.scrimmage_play == True)].groupby(by=["pos_team"], as_index=False, group_keys = False).agg( - EPA_success = ('EPA_success', sum), - EPA_success_rate = ('EPA_success', mean), - ) - - situation_box_rz = self.plays_json[(self.plays_json.rz_play == True)].groupby(by=["pos_team"], as_index=False, group_keys = False).agg( - EPA_success_rz = ('EPA_success', sum), - EPA_success_rate_rz = ('EPA_success', mean), - ) - - situation_box_third = self.plays_json[(self.plays_json["start.down"] == 3)].groupby(by=["pos_team"], as_index=False, group_keys = False).agg( - EPA_success_third = ('EPA_success', sum), - EPA_success_rate_third = ('EPA_success', mean), - ) - - situation_box_pass = self.plays_json[(self.plays_json["pass"] == True) & (self.plays_json.scrimmage_play == True)].groupby(by=["pos_team"], as_index=False, group_keys = False).agg( - EPA_success_pass = ('EPA_success', sum), - EPA_success_pass_rate = ('EPA_success', mean), - ) - - situation_box_rush = self.plays_json[(self.plays_json["rush"] == True) & (self.plays_json.scrimmage_play == True)].groupby(by=["pos_team"], as_index=False, group_keys = False).agg( - EPA_success_rush = ('EPA_success', sum), - EPA_success_rush_rate = ('EPA_success', mean), - ) - - situation_box_middle8 = self.plays_json[(self.plays_json["middle_8"] == True) & (self.plays_json.scrimmage_play == True)].groupby(by=["pos_team"], as_index=False, group_keys = False).agg( - middle_8 = ('middle_8', sum), - middle_8_pass_rate = ('pass', mean), - middle_8_rush_rate = ('rush', mean), - EPA_middle_8 = ('EPA', sum), - EPA_middle_8_per_play = ('EPA', mean), - EPA_middle_8_success = ('EPA_success', sum), - EPA_middle_8_success_rate = ('EPA_success', mean), - ) - - situation_box_middle8_pass = self.plays_json[(self.plays_json["pass"] == True) & (self.plays_json["middle_8"] == True) & (self.plays_json.scrimmage_play == True)].groupby(by=["pos_team"], as_index=False, group_keys = False).agg( - middle_8_pass = ('pass', sum), - EPA_middle_8_pass = ('EPA', sum), - EPA_middle_8_pass_per_play = ('EPA', mean), - EPA_middle_8_success_pass = ('EPA_success', sum), - EPA_middle_8_success_pass_rate = ('EPA_success', mean), - ) - - situation_box_middle8_rush = self.plays_json[(self.plays_json["rush"] == True) & (self.plays_json["middle_8"] == True) & (self.plays_json.scrimmage_play == True)].groupby(by=["pos_team"], as_index=False, group_keys = False).agg( - middle_8_rush = ('rush', sum), - - EPA_middle_8_rush = ('EPA', sum), - EPA_middle_8_rush_per_play = ('EPA', mean), - - EPA_middle_8_success_rush = ('EPA_success', sum), - EPA_middle_8_success_rush_rate = ('EPA_success', mean), - ) - - situation_box_early = self.plays_json[(self.plays_json.early_down == True)].groupby(by=["pos_team"], as_index=False, group_keys = False).agg( - EPA_success_early_down = ('EPA_success', sum), - EPA_success_early_down_rate = ('EPA_success', mean), - early_downs = ('early_down', sum), - early_down_pass_rate = ('pass', mean), - early_down_rush_rate = ('rush', mean), - EPA_early_down = ('EPA', sum), - EPA_early_down_per_play = ('EPA', mean), - early_down_first_down = ('first_down_created', sum), - early_down_first_down_rate = ('first_down_created', mean) - ) - - situation_box_early_pass = self.plays_json[(self.plays_json["pass"] == True) & (self.plays_json.early_down == True)].groupby(by=["pos_team"], as_index=False, group_keys = False).agg( - early_down_pass = ('pass', sum), - EPA_early_down_pass = ('EPA', sum), - EPA_early_down_pass_per_play = ('EPA', mean), - EPA_success_early_down_pass = ('EPA_success', sum), - EPA_success_early_down_pass_rate = ('EPA_success', mean), - ) - - situation_box_early_rush = self.plays_json[(self.plays_json["rush"] == True) & (self.plays_json.early_down == True)].groupby(by=["pos_team"], as_index=False, group_keys = False).agg( - early_down_rush = ('rush', sum), - EPA_early_down_rush = ('EPA', sum), - EPA_early_down_rush_per_play = ('EPA', mean), - EPA_success_early_down_rush = ('EPA_success', sum), - EPA_success_early_down_rush_rate = ('EPA_success', mean), - ) + pass_qbr = pass_qbr.with_columns( + exp_qbr = pl.lit(qbr_result) + ) + passer_box = passer_box.join(pass_qbr, + left_on=["passer_player_name", "pos_team"], + right_on=["athlete_name", "pos_team"]) + + rusher_box = rush_box.fill_null(0.0).groupby(by=["pos_team","rusher_player_name"]).agg( + Car= pl.col('rush').sum(), + Yds= pl.col('yds_rushed').sum(), + Rush_TD = pl.col('rush_td').sum(), + YPC= pl.col('yds_rushed').mean(), + EPA= pl.col('EPA').sum(), + EPA_per_Play= pl.col('EPA').mean(), + WPA= pl.col('wpa').sum(), + SR = pl.col('EPA_success').mean(), + Fum = pl.col('fumble_vec').sum(), + Fum_Lost = pl.col('fumble_lost').sum() + ).with_columns(pl.col(pl.Float32).round(2) + ).with_columns( + pos_team = pl.col("pos_team").cast(pl.Int32) + ) + # rusher_box = rusher_box.replace({np.nan: None}) + + receiver_box = pass_box.fill_null(0.0).groupby(by=["pos_team","receiver_player_name"]).agg( + Rec= pl.col('completion').sum(), + Tar= pl.col('target').sum(), + Yds= pl.col('yds_receiving').sum(), + Rec_TD = pl.col('pass_td').sum(), + YPT= pl.col('yds_receiving').mean(), + EPA= pl.col('EPA').sum(), + EPA_per_Play= pl.col('EPA').mean(), + WPA= pl.col('wpa').sum(), + SR = pl.col('EPA_success').mean(), + Fum = pl.col('fumble_vec').sum(), + Fum_Lost = pl.col('fumble_lost').sum() + ).with_columns(pl.col(pl.Float32).round(2) + ).with_columns( + pos_team = pl.col("pos_team").cast(pl.Int32) + ) + + team_base_box = play_df.groupby(by=["pos_team"]).agg( + EPA_plays = pl.col('play').sum(), + total_yards = pl.col('statYardage').sum(), + EPA_overall_total = pl.col('EPA').sum(), + ).with_columns(pl.col(pl.Float32).round(2) + ).with_columns( + pos_team = pl.col("pos_team").cast(pl.Int32) + ) + + team_pen_box = play_df.filter(pl.col("penalty_flag") == True).groupby(by=["pos_team"]).agg( + total_pen_yards = pl.col('statYardage').sum(), + EPA_penalty = pl.col('EPA_penalty').sum(), + penalty_first_downs_created = pl.col("penalty_1st_conv").sum(), + penalty_first_downs_created_rate = pl.col("penalty_1st_conv").mean(), + ).with_columns(pl.col(pl.Float32).round(2) + ).with_columns( + pos_team = pl.col("pos_team").cast(pl.Int32) + ) + + team_scrimmage_box = play_df.filter(pl.col("scrimmage_play") == True).groupby(by=["pos_team"]).agg( + scrimmage_plays = pl.col('scrimmage_play').sum(), + EPA_overall_off = pl.col('EPA').sum(), + EPA_overall_offense = pl.col('EPA').sum(), + EPA_per_play = pl.col('EPA').mean(), + EPA_non_explosive = pl.col('EPA_non_explosive').sum(), + EPA_non_explosive_per_play = pl.col('EPA_non_explosive').mean(), + EPA_explosive = pl.col('EPA_explosive').sum(), + EPA_explosive_rate = pl.col('EPA_explosive').mean(), + passes_rate = pl.col('pass').mean(), + off_yards = pl.col('statYardage').sum(), + total_off_yards = pl.col('statYardage').sum(), + yards_per_play = pl.col('statYardage').mean() + ).with_columns(pl.col(pl.Float32).round(2) + ).with_columns( + pos_team = pl.col("pos_team").cast(pl.Int32) + ) + + team_sp_box = play_df.filter(pl.col("sp") == True).groupby(by=["pos_team"]).agg( + special_teams_plays = pl.col('sp').sum(), + EPA_sp = pl.col('EPA_sp').sum(), + EPA_special_teams = pl.col('EPA_sp').sum(), + field_goals = pl.col("fg_attempt").sum(), + EPA_fg = pl.col('EPA_fg').sum(), + punt_plays = pl.col("punt_play").sum(), + EPA_punt = pl.col('EPA_punt').sum(), + kickoff_plays = pl.col('kickoff_play').sum(), + EPA_kickoff = pl.col('EPA_kickoff').sum() + ).with_columns(pl.col(pl.Float32).round(2) + ).with_columns( + pos_team = pl.col("pos_team").cast(pl.Int32) + ) + + team_scrimmage_box_pass = play_df.filter( + (pl.col("pass") == True) & (pl.col("scrimmage_play") == True) + ).fill_null(0.0).groupby(by=["pos_team"]).agg( + passes = pl.col('pass').sum(), + pass_yards = pl.col('yds_receiving').sum(), + yards_per_pass = pl.col('yds_receiving').mean(), + passing_first_downs_created = pl.col("first_down_created").sum(), + passing_first_downs_created_rate = pl.col("first_down_created").mean(), + EPA_passing_overall = pl.col('EPA').sum(), + EPA_passing_per_play = pl.col('EPA').mean(), + EPA_explosive_passing = pl.col('EPA_explosive').sum(), + EPA_explosive_passing_rate = pl.col('EPA_explosive').mean(), + EPA_non_explosive_passing = pl.col('EPA_non_explosive').sum(), + EPA_non_explosive_passing_per_play = pl.col('EPA_non_explosive').mean(), + ).with_columns(pl.col(pl.Float32).round(2) + ).with_columns( + pos_team = pl.col("pos_team").cast(pl.Int32) + ) + + team_scrimmage_box_rush = play_df.filter( + (pl.col("rush") == True) & (pl.col("scrimmage_play") == True) + ).fill_null(0.0).groupby(by=["pos_team"]).agg( + rushes = pl.col('rush').sum(), + rush_yards = pl.col('yds_rushed').sum(), + yards_per_rush = pl.col('yds_rushed').mean(), + rushing_power_rate = pl.col('power_rush_attempt').mean(), + rushing_first_downs_created = pl.col("first_down_created").sum(), + rushing_first_downs_created_rate = pl.col("first_down_created").mean(), + EPA_rushing_overall = pl.col('EPA').sum(), + EPA_rushing_per_play = pl.col('EPA').mean(), + EPA_explosive_rushing = pl.col('EPA_explosive').sum(), + EPA_explosive_rushing_rate = pl.col('EPA_explosive').mean(), + EPA_non_explosive_rushing = pl.col('EPA_non_explosive').sum(), + EPA_non_explosive_rushing_per_play = pl.col('EPA_non_explosive').mean(), + ).with_columns(pl.col(pl.Float32).round(2) + ).with_columns( + pos_team = pl.col("pos_team").cast(pl.Int32) + ) + + team_rush_base_box = play_df.filter( + (pl.col("scrimmage_play") == True) + ).fill_null(0.0).groupby(by=["pos_team"]).agg( + rushes_rate = pl.col('rush').mean(), + first_downs_created = pl.col("first_down_created").sum(), + first_downs_created_rate = pl.col("first_down_created").mean() + ).with_columns(pl.col(pl.Float32).round(2) + ).with_columns( + pos_team = pl.col("pos_team").cast(pl.Int32) + ) + + team_rush_power_box = play_df.filter( + (pl.col("power_rush_attempt") == True) & (pl.col("scrimmage_play") == True) + ).fill_null(0.0).groupby(by=["pos_team"]).agg( + EPA_rushing_power = pl.col('EPA').sum(), + EPA_rushing_power_per_play = pl.col('EPA').mean(), + rushing_power_success = pl.col('power_rush_success').sum(), + rushing_power_success_rate = pl.col('power_rush_success').mean(), + rushing_power = pl.col('power_rush_attempt').sum(), + ).with_columns(pl.col(pl.Float32).round(2) + ).with_columns( + pos_team = pl.col("pos_team").cast(pl.Int32) + ) + + play_df = play_df.with_columns( + opp_highlight_yards = pl.col("opp_highlight_yards").cast(pl.Float32), + highlight_yards = pl.col("highlight_yards").cast(pl.Float32), + line_yards = pl.col("line_yards").cast(pl.Float32), + second_level_yards = pl.col("second_level_yards").cast(pl.Float32), + open_field_yards = pl.col("open_field_yards").cast(pl.Float32), + ) + + team_rush_box = play_df.filter( + (pl.col("rush") == True) & (pl.col("scrimmage_play") == True) + ).fill_null(0.0).groupby(by=["pos_team"]).agg( + rushing_stuff = pl.col('stuffed_run').sum(), + rushing_stuff_rate = pl.col('stuffed_run').mean(), + rushing_stopped = pl.col('stopped_run').sum(), + rushing_stopped_rate = pl.col('stopped_run').mean(), + rushing_opportunity = pl.col('opportunity_run').sum(), + rushing_opportunity_rate = pl.col('opportunity_run').mean(), + rushing_highlight = pl.col('highlight_run').sum(), + rushing_highlight_rate = pl.col('highlight_run').mean(), + rushing_highlight_yards = pl.col('highlight_yards').sum(), + line_yards = pl.col('line_yards').sum(), + line_yards_per_carry = pl.col('line_yards').mean(), + second_level_yards = pl.col('second_level_yards').sum(), + open_field_yards = pl.col('open_field_yards').sum(), + ).with_columns(pl.col(pl.Float32).round(2) + ).with_columns( + pos_team = pl.col("pos_team").cast(pl.Int32) + ) + + team_rush_opp_box = play_df.filter( + (pl.col("rush") == True) & (pl.col("scrimmage_play") == True) & (pl.col("opportunity_run") == True) + ).fill_null(0.0).groupby(by=["pos_team"]).agg( + rushing_highlight_yards_per_opp = pl.col('opp_highlight_yards').mean(), + ).with_columns(pl.col(pl.Float32).round(2) + ).with_columns( + pos_team = pl.col("pos_team").cast(pl.Int32) + ) + + team_data_frames = [team_rush_opp_box, team_pen_box, team_sp_box, team_scrimmage_box_rush, team_scrimmage_box_pass, + team_scrimmage_box, team_base_box, team_rush_base_box, team_rush_power_box, team_rush_box] + team_box = reduce(lambda left, right: left.join(right, on=["pos_team"], how="outer"), team_data_frames) + + situation_box_normal = play_df.filter(pl.col("scrimmage_play") == True).groupby(by=["pos_team"]).agg( + EPA_success = pl.col('EPA_success').sum(), + EPA_success_rate = pl.col('EPA_success').mean(), + ).with_columns(pl.col(pl.Float32).round(2) + ).with_columns( + pos_team = pl.col("pos_team").cast(pl.Int32) + ) + + situation_box_rz = play_df.filter( + (pl.col("rz_play") == True) & (pl.col("scrimmage_play") == True) + ).groupby(by=["pos_team"]).agg( + EPA_success_rz = pl.col('EPA_success').sum(), + EPA_success_rate_rz = pl.col('EPA_success').mean(), + ).with_columns(pl.col(pl.Float32).round(2) + ).with_columns( + pos_team = pl.col("pos_team").cast(pl.Int32) + ) + + situation_box_third = play_df.filter( + (pl.col("start.down") == 3) & (pl.col("scrimmage_play") == True) + ).groupby(by=["pos_team"]).agg( + EPA_success_third = pl.col('EPA_success').sum(), + EPA_success_rate_third = pl.col('EPA_success').mean(), + ).with_columns(pl.col(pl.Float32).round(2) + ).with_columns( + pos_team = pl.col("pos_team").cast(pl.Int32) + ) + + situation_box_pass = play_df.filter( + (pl.col("pass") == True) & (pl.col("scrimmage_play") == True) + ).groupby(by=["pos_team"]).agg( + EPA_success_pass = pl.col('EPA_success').sum(), + EPA_success_pass_rate = pl.col('EPA_success').mean(), + ).with_columns(pl.col(pl.Float32).round(2) + ).with_columns( + pos_team = pl.col("pos_team").cast(pl.Int32) + ) + + situation_box_rush = play_df.filter( + (pl.col("rush") == True) & (pl.col("scrimmage_play") == True) + ).groupby(by=["pos_team"]).agg( + EPA_success_rush = pl.col('EPA_success').sum(), + EPA_success_rush_rate = pl.col('EPA_success').mean(), + ).with_columns(pl.col(pl.Float32).round(2) + ).with_columns( + pos_team = pl.col("pos_team").cast(pl.Int32) + ) + + situation_box_middle8 = play_df.filter( + (pl.col("middle_8") == True) & (pl.col("scrimmage_play") == True) + ).groupby(by=["pos_team"]).agg( + middle_8 = pl.col('middle_8').sum(), + middle_8_pass_rate = pl.col('pass').mean(), + middle_8_rush_rate = pl.col('rush').mean(), + EPA_middle_8 = pl.col('EPA').sum(), + EPA_middle_8_per_play = pl.col('EPA').mean(), + EPA_middle_8_success = pl.col('EPA_success').sum(), + EPA_middle_8_success_rate = pl.col('EPA_success').mean(), + ).with_columns(pl.col(pl.Float32).round(2) + ).with_columns( + pos_team = pl.col("pos_team").cast(pl.Int32), + ) + + situation_box_middle8_pass = play_df.filter( + (pl.col("pass") == True) & (pl.col("middle_8") == True) & (pl.col("scrimmage_play") == True) + ).groupby(by=["pos_team"]).agg( + middle_8_pass = pl.col('pass').sum(), + EPA_middle_8_pass = pl.col('EPA').sum(), + EPA_middle_8_pass_per_play = pl.col('EPA').mean(), + EPA_middle_8_success_pass = pl.col('EPA_success').sum(), + EPA_middle_8_success_pass_rate = pl.col('EPA_success').mean(), + ).with_columns(pl.col(pl.Float32).round(2) + ).with_columns( + pos_team = pl.col("pos_team").cast(pl.Int32), + ) + + situation_box_middle8_rush = play_df.filter( + (pl.col("rush") == True) & (pl.col("middle_8") == True) & (pl.col("scrimmage_play") == True) + ).groupby(by=["pos_team"]).agg( + middle_8_rush = pl.col('rush').sum(), + EPA_middle_8_rush = pl.col('EPA').sum(), + EPA_middle_8_rush_per_play = pl.col('EPA').mean(), + EPA_middle_8_success_rush = pl.col('EPA_success').sum(), + EPA_middle_8_success_rush_rate = pl.col('EPA_success').mean(), + ).with_columns(pl.col(pl.Float32).round(2) + ).with_columns( + pos_team = pl.col("pos_team").cast(pl.Int32), + ) + + situation_box_early = play_df.filter( + (pl.col("early_down") == True) & (pl.col("scrimmage_play") == True) + ).groupby(by=["pos_team"]).agg( + EPA_success_early_down = pl.col('EPA_success').sum(), + EPA_success_early_down_rate = pl.col('EPA_success').mean(), + early_downs = pl.col('early_down').sum(), + early_down_pass_rate = pl.col('pass').mean(), + early_down_rush_rate = pl.col('rush').mean(), + EPA_early_down = pl.col('EPA').sum(), + EPA_early_down_per_play = pl.col('EPA').mean(), + early_down_first_down = pl.col('first_down_created').sum(), + early_down_first_down_rate = pl.col('first_down_created').mean(), + ).with_columns(pl.col(pl.Float32).round(2) + ).with_columns( + pos_team = pl.col("pos_team").cast(pl.Int32), + ) + + situation_box_early_pass = play_df.filter( + (pl.col("pass") == True) & (pl.col("early_down") == True) & (pl.col("scrimmage_play") == True) + ).groupby(by=["pos_team"]).agg( + early_down_pass = pl.col('pass').sum(), + EPA_early_down_pass = pl.col('EPA').sum(), + EPA_early_down_pass_per_play = pl.col('EPA').mean(), + EPA_success_early_down_pass = pl.col('EPA_success').sum(), + EPA_success_early_down_pass_rate = pl.col('EPA_success').mean(), + ).with_columns(pl.col(pl.Float32).round(2) + ).with_columns( + pos_team = pl.col("pos_team").cast(pl.Int32), + ) + + situation_box_early_rush = play_df.filter( + (pl.col("rush") == True) & (pl.col("early_down") == True) & (pl.col("scrimmage_play") == True) + ).groupby(by=["pos_team"]).agg( + early_down_rush = pl.col('rush').sum(), + EPA_early_down_rush = pl.col('EPA').sum(), + EPA_early_down_rush_per_play = pl.col('EPA').mean(), + EPA_success_early_down_rush = pl.col('EPA_success').sum(), + EPA_success_early_down_rush_rate = pl.col('EPA_success').mean(), + ).with_columns(pl.col(pl.Float32).round(2) + ).with_columns( + pos_team = pl.col("pos_team").cast(pl.Int32), + ) + + situation_box_late = play_df.filter( + (pl.col("late_down") == True) & (pl.col("scrimmage_play") == True) + ).groupby(by=["pos_team"]).agg( + EPA_success_late_down = pl.col('EPA_success_late_down').sum(), + EPA_success_late_down_pass = pl.col('EPA_success_late_down_pass').sum(), + EPA_success_late_down_rush = pl.col('EPA_success_late_down_rush').sum(), + late_downs = pl.col('late_down').sum(), + late_down_pass = pl.col('late_down_pass').sum(), + late_down_rush = pl.col('late_down_rush').sum(), + EPA_late_down = pl.col('EPA').sum(), + EPA_late_down_per_play = pl.col('EPA').mean(), + EPA_success_late_down_rate = pl.col('EPA_success_late_down').mean(), + EPA_success_late_down_pass_rate = pl.col('EPA_success_late_down_pass').mean(), + EPA_success_late_down_rush_rate = pl.col('EPA_success_late_down_rush').mean(), + late_down_pass_rate = pl.col('late_down_pass').mean(), + late_down_rush_rate = pl.col('late_down_rush').mean(), + ).with_columns(pl.col(pl.Float32).round(2) + ).with_columns( + pos_team = pl.col("pos_team").cast(pl.Int32), + ) + + situation_box_standard = play_df.filter( + (pl.col("standard_down") == True) & (pl.col("scrimmage_play") == True) + ).groupby(by=["pos_team"]).agg( + EPA_success_standard_down = pl.col('EPA_success').sum(), + EPA_success_standard_down_rate = pl.col('EPA_success').mean(), + EPA_standard_down = pl.col('EPA').sum(), + EPA_standard_down_per_play = pl.col('EPA').mean(), + standard_downs = pl.col('standard_down').sum() + ).with_columns(pl.col(pl.Float32).round(2) + ).with_columns( + pos_team = pl.col("pos_team").cast(pl.Int32), + ) + + situation_box_passing = play_df.filter( + (pl.col("passing_down") == True) & (pl.col("scrimmage_play") == True) + ).groupby(by=["pos_team"]).agg( + EPA_success_passing_down = pl.col('EPA_success').sum(), + EPA_success_passing_down_rate = pl.col('EPA_success').mean(), + EPA_passing_down = pl.col('EPA').sum(), + EPA_passing_down_per_play = pl.col('EPA').mean(), + passing_downs = pl.col('passing_down').sum() + ).with_columns(pl.col(pl.Float32).round(2) + ).with_columns( + pos_team = pl.col("pos_team").cast(pl.Int32), + ) + + situation_data_frames = [ + situation_box_normal, situation_box_pass, situation_box_rush, + situation_box_rz, situation_box_third, situation_box_early, situation_box_early_pass, + situation_box_early_rush, situation_box_middle8, situation_box_middle8_pass, + situation_box_middle8_rush, situation_box_late, situation_box_standard, situation_box_passing + ] - situation_box_late = self.plays_json[(self.plays_json.late_down == True)].groupby(by=["pos_team"], as_index=False, group_keys = False).agg( - EPA_success_late_down = ('EPA_success_late_down', sum), - EPA_success_late_down_pass = ('EPA_success_late_down_pass', sum), - EPA_success_late_down_rush = ('EPA_success_late_down_rush', sum), - late_downs = ('late_down', sum), - late_down_pass = ('late_down_pass', sum), - late_down_rush = ('late_down_rush', sum), - EPA_late_down = ('EPA', sum), - EPA_late_down_per_play = ('EPA', mean), - EPA_success_late_down_rate = ('EPA_success_late_down', mean), - EPA_success_late_down_pass_rate = ('EPA_success_late_down_pass', mean), - EPA_success_late_down_rush_rate = ('EPA_success_late_down_rush', mean), - late_down_pass_rate = ('late_down_pass', mean), - late_down_rush_rate = ('late_down_rush', mean) + situation_box = reduce(lambda left, right: left.join(right, on=["pos_team"], how="outer"), situation_data_frames) + + play_df = play_df.with_columns( + drive_stopped = pl.col("drive_stopped").cast(pl.Float32), + drive_start = pl.col("drive_start").cast(pl.Float32), + ) + + def_base_box = play_df.filter(pl.col("scrimmage_play") == True).groupby(by=["def_pos_team"]).agg( + scrimmage_plays = pl.col('scrimmage_play').sum(), + TFL = pl.col('TFL').sum(), + TFL_pass = pl.col('TFL_pass').sum(), + TFL_rush = pl.col('TFL_rush').sum(), + havoc_total = pl.col('havoc').sum(), + havoc_total_rate = pl.col('havoc').mean(), + fumbles = pl.col('forced_fumble').sum(), + def_int = pl.col('int').sum(), + drive_stopped_rate = 100 * pl.col('drive_stopped').mean(), + ).with_columns(pl.col(pl.Float32).round(2) + ).with_columns( + def_pos_team = pl.col("def_pos_team").cast(pl.Int32), + ) + + def_box_havoc_pass = play_df.filter( + (pl.col("pass") == True) & (pl.col("scrimmage_play") == True) + ).groupby(by=["def_pos_team"]).agg( + num_pass_plays = pl.col('pass').sum(), + havoc_total_pass = pl.col('havoc').sum(), + havoc_total_pass_rate = pl.col('havoc').mean(), + sacks = pl.col('sack_vec').sum(), + sacks_rate = pl.col('sack_vec').mean(), + pass_breakups = pl.col('pass_breakup').sum() + ).with_columns(pl.col(pl.Float32).round(2) + ).with_columns( + def_pos_team = pl.col("def_pos_team").cast(pl.Int32), + ) + + def_box_havoc_rush = play_df.filter( + (pl.col("rush") == True) & (pl.col("scrimmage_play") == True) + ).groupby(by=["def_pos_team"]).agg( + havoc_total_rush = pl.col('havoc').sum(), + havoc_total_rush_rate = pl.col('havoc').mean(), + ).with_columns(pl.col(pl.Float32).round(2) + ).with_columns( + def_pos_team = pl.col("def_pos_team").cast(pl.Int32), ) - situation_box_standard = self.plays_json[self.plays_json.standard_down == True].groupby(by=["pos_team"], as_index=False, group_keys = False).agg( - EPA_success_standard_down = ('EPA_success_standard_down', sum), - EPA_success_standard_down_rate = ('EPA_success_standard_down', mean), - EPA_standard_down = ('EPA_success_standard_down', sum), - EPA_standard_down_per_play = ('EPA_success_standard_down', mean) - ) - situation_box_passing = self.plays_json[self.plays_json.passing_down == True].groupby(by=["pos_team"], as_index=False, group_keys = False).agg( - EPA_success_passing_down = ('EPA_success_passing_down', sum), - EPA_success_passing_down_rate = ('EPA_success_passing_down', mean), - EPA_passing_down = ('EPA_success_standard_down', sum), - EPA_passing_down_per_play = ('EPA_success_standard_down', mean) - ) - situation_data_frames = [situation_box_normal, situation_box_pass, situation_box_rush, situation_box_rz, situation_box_third, situation_box_early, situation_box_early_pass, situation_box_early_rush, situation_box_middle8, situation_box_middle8_pass, situation_box_middle8_rush, situation_box_late, situation_box_standard, situation_box_passing] - situation_box = reduce(lambda left,right: pd.merge(left,right,on=['pos_team'], how='outer'), situation_data_frames) - situation_box = situation_box.replace({np.nan:None}) - - self.plays_json.drive_stopped = self.plays_json.drive_stopped.astype(float) - def_base_box = self.plays_json[(self.plays_json.scrimmage_play == True)].groupby(by=["def_pos_team"], as_index=False, group_keys = False).agg( - scrimmage_plays = ('scrimmage_play', sum), - TFL = ('TFL', sum), - TFL_pass = ('TFL_pass', sum), - TFL_rush = ('TFL_rush', sum), - havoc_total = ('havoc', sum), - havoc_total_rate = ('havoc', mean), - fumbles = ('forced_fumble', sum), - def_int = ('int', sum), - drive_stopped_rate = ('drive_stopped', mean) - ) - def_base_box.drive_stopped_rate = 100 * def_base_box.drive_stopped_rate - def_base_box = def_base_box.replace({np.nan:None}) - - def_box_havoc_pass = self.plays_json[(self.plays_json.scrimmage_play == True) & (self.plays_json["pass"] == True)].groupby(by=["def_pos_team"], as_index=False, group_keys = False).agg( - num_pass_plays = ('pass', sum), - havoc_total_pass = ('havoc', sum), - havoc_total_pass_rate = ('havoc', mean), - sacks = ('sack_vec', sum), - sacks_rate = ('sack_vec', mean), - pass_breakups = ('pass_breakup', sum) - ) - def_box_havoc_pass = def_box_havoc_pass.replace({np.nan:None}) + def_data_frames = [def_base_box,def_box_havoc_pass,def_box_havoc_rush] + def_box = reduce(lambda left,right: left.join(right, on=["def_pos_team"], how="outer"), def_data_frames) + def_box_json = json.loads(def_box.write_json(row_oriented=True)) - def_box_havoc_rush = self.plays_json[(self.plays_json.scrimmage_play == True) & (self.plays_json["rush"] == True)].groupby(by=["def_pos_team"], as_index=False, group_keys = False).agg( - havoc_total_rush = ('havoc', sum), - havoc_total_rush_rate = ('havoc', mean), + turnover_box = play_df.filter(pl.col("scrimmage_play") == True).groupby(by=["pos_team"]).agg( + pass_breakups = pl.col('pass_breakup').sum(), + fumbles_lost = pl.col('fumble_lost').sum(), + fumbles_recovered = pl.col('fumble_recovered').sum(), + total_fumbles = pl.col('fumble_vec').sum(), + Int = pl.col('int').sum(), + ).with_columns(pl.col(pl.Float32).round(2) + ).with_columns( + pos_team = pl.col("pos_team").cast(pl.Int32), ) - def_box_havoc_rush = def_box_havoc_rush.replace({np.nan:None}) - - def_data_frames = [def_base_box,def_box_havoc_pass,def_box_havoc_rush] - def_box = reduce(lambda left,right: pd.merge(left,right,on=['def_pos_team'], how='outer'), def_data_frames) - def_box = def_box.replace({np.nan:None}) - def_box_json = json.loads(def_box.to_json(orient="records")) - - turnover_box = self.plays_json[(self.plays_json.scrimmage_play == True)].groupby(by=["pos_team"], as_index=False, group_keys = False).agg( - pass_breakups = ('pass_breakup', sum), - fumbles_lost = ('fumble_lost', sum), - fumbles_recovered = ('fumble_recovered', sum), - total_fumbles = ('fumble_vec', sum), - Int = ('int', sum), - ).round(2) - turnover_box = turnover_box.replace({np.nan:None}) - turnover_box_json = json.loads(turnover_box.to_json(orient="records")) + turnover_box_json = json.loads(turnover_box.write_json(row_oriented=True)) if (len(turnover_box_json) < 2): for i in range(len(turnover_box_json), 2): turnover_box_json.append({}) @@ -5283,37 +3831,38 @@ def weighted_mean(s, df, wcol): turnover_box_json[0]["turnover_luck"] = 5.0 * (turnover_box_json[0]["turnover_margin"] - turnover_box_json[0]["expected_turnover_margin"]) turnover_box_json[1]["turnover_luck"] = 5.0 * (turnover_box_json[1]["turnover_margin"] - turnover_box_json[1]["expected_turnover_margin"]) - self.plays_json.drive_start = self.plays_json.drive_start.astype(float) - drives_data = self.plays_json[(self.plays_json.scrimmage_play == True)].groupby(by=["pos_team"], as_index=False, group_keys = False).agg( - drive_total_available_yards = ('drive_start', sum), - drive_total_gained_yards = ('drive.yards', sum), - avg_field_position = ('drive_start', mean), - plays_per_drive = ('drive.offensivePlays', mean), - yards_per_drive = ('drive.yards', mean), - drives = ('drive.id', pd.Series.nunique) + drives_data = play_df.filter(pl.col("scrimmage_play") == True).groupby(by=["pos_team"]).agg( + drive_total_available_yards = pl.col('drive_start').sum(), + drive_total_gained_yards = pl.col('drive.yards').sum(), + avg_field_position = pl.col('drive_start').mean(), + plays_per_drive = pl.col('drive.offensivePlays').mean(), + yards_per_drive = pl.col('drive.yards').mean(), + drives = pl.col('drive.id').n_unique(), + drive_total_gained_yards_rate = 100 * pl.col('drive.yards').sum() / pl.col('drive_start').sum() + ).with_columns(pl.col(pl.Float32).round(2) + ).with_columns( + pos_team = pl.col("pos_team").cast(pl.Int32), ) - drives_data['drive_total_gained_yards_rate'] = (100 * drives_data.drive_total_gained_yards / drives_data.drive_total_available_yards).round(2) return { - "pass" : json.loads(passer_box.to_json(orient="records")), - "rush" : json.loads(rusher_box.to_json(orient="records")), - "receiver" : json.loads(receiver_box.to_json(orient="records")), - "team" : json.loads(team_box.to_json(orient="records")), - "situational" : json.loads(situation_box.to_json(orient="records")), + "pass" : json.loads(passer_box.write_json(row_oriented=True)), + "rush" : json.loads(rusher_box.write_json(row_oriented=True)), + "receiver" : json.loads(receiver_box.write_json(row_oriented=True)), + "team" : json.loads(team_box.write_json(row_oriented=True)), + "situational" : json.loads(situation_box.write_json(row_oriented=True)), "defensive" : def_box_json, "turnover" : turnover_box_json, - "drives" : json.loads(drives_data.to_json(orient="records")) + "drives" : json.loads(drives_data.write_json(row_oriented=True)) } def run_processing_pipeline(self): if self.ran_pipeline == False: pbp_txt = self.__helper_cfb_pbp_drives(self.json) - pbp_txt['plays']['week'] = pbp_txt['header']['week'] self.plays_json = pbp_txt['plays'] pbp_json = { "gameId": self.gameId, - "plays": np.array(self.plays_json).tolist(), + "plays":self.plays_json, "season": pbp_txt["season"], "week": pbp_txt['header']['week'], "gameInfo": pbp_txt["gameInfo"], @@ -5336,27 +3885,45 @@ def run_processing_pipeline(self): "videos": np.array(pbp_txt["videos"]).tolist(), } self.json = pbp_json - self.plays_json = pd.DataFrame(pbp_txt['plays'].to_dict(orient="records")) + self.plays_json = pbp_txt['plays'] if pbp_json.get('header').get('competitions')[0].get('playByPlaySource') != 'none': - self.plays_json = self.__add_downs_data(self.plays_json) - self.plays_json = self.__add_play_type_flags(self.plays_json) - self.plays_json = self.__add_rush_pass_flags(self.plays_json) - self.plays_json = self.__add_team_score_variables(self.plays_json) - self.plays_json = self.__add_new_play_types(self.plays_json) - self.plays_json = self.__setup_penalty_data(self.plays_json) - self.plays_json = self.__add_play_category_flags(self.plays_json) - self.plays_json = self.__add_yardage_cols(self.plays_json) - self.plays_json = self.__add_player_cols(self.plays_json) - self.plays_json = self.__after_cols(self.plays_json) - self.plays_json = self.__add_spread_time(self.plays_json) - self.plays_json = self.__process_epa(self.plays_json) - self.plays_json = self.__process_wpa(self.plays_json) - self.plays_json = self.__add_drive_data(self.plays_json) - self.plays_json = self.__process_qbr(self.plays_json) - self.plays_json = self.plays_json.replace({np.nan: None}) + self.plays_json = self.plays_json.pipe( + self.__add_downs_data + ).pipe( + self.__add_play_type_flags + ).pipe( + self.__add_rush_pass_flags + ).pipe( + self.__add_team_score_variables + ).pipe( + self.__add_new_play_types + ).pipe( + self.__setup_penalty_data + ).pipe( + self.__add_play_category_flags + ).pipe( + self.__add_yardage_cols + ).pipe( + self.__add_player_cols + ).pipe( + self.__after_cols + ).pipe( + self.__add_spread_time + ).pipe( + self.__process_epa + ).pipe( + self.__process_wpa + ).pipe( + self.__add_drive_data + ).pipe( + self.__process_qbr + ) + self.ran_pipeline = True + advBoxScore = self.create_box_score(self.plays_json) + self.plays_json = self.plays_json.to_dicts() pbp_json = { "gameId": self.gameId, - "plays": self.plays_json.to_dict(orient="records"), + "plays": self.plays_json, "season": pbp_txt["season"], "week": pbp_txt['header']['week'], "gameInfo": pbp_txt["gameInfo"], @@ -5364,6 +3931,7 @@ def run_processing_pipeline(self): "playByPlaySource": pbp_txt["playByPlaySource"], "drives": pbp_txt["drives"], "boxscore": pbp_txt["boxscore"], + "advBoxScore": advBoxScore, "header": pbp_txt["header"], "standings": pbp_txt["standings"], "leaders": np.array(pbp_txt["leaders"]).tolist(), @@ -5385,12 +3953,11 @@ def run_processing_pipeline(self): def run_cleaning_pipeline(self): if self.ran_cleaning_pipeline == False: pbp_txt = self.__helper_cfb_pbp_drives(self.json) - pbp_txt['plays']['week'] = pbp_txt['header']['week'] self.plays_json = pbp_txt['plays'] pbp_json = { "gameId": self.gameId, - "plays": np.array(self.plays_json).tolist(), + "plays": self.plays_json, "season": pbp_txt["season"], "week": pbp_txt['header']['week'], "gameInfo": pbp_txt["gameInfo"], @@ -5413,23 +3980,35 @@ def run_cleaning_pipeline(self): "videos": np.array(pbp_txt["videos"]).tolist(), } self.json = pbp_json - self.plays_json = pd.DataFrame(pbp_txt['plays'].to_dict(orient="records")) + self.plays_json = pbp_txt['plays'] if pbp_json.get('header').get('competitions')[0].get('playByPlaySource') != 'none': - self.plays_json = self.__add_downs_data(self.plays_json) - self.plays_json = self.__add_play_type_flags(self.plays_json) - self.plays_json = self.__add_rush_pass_flags(self.plays_json) - self.plays_json = self.__add_team_score_variables(self.plays_json) - self.plays_json = self.__add_new_play_types(self.plays_json) - self.plays_json = self.__setup_penalty_data(self.plays_json) - self.plays_json = self.__add_play_category_flags(self.plays_json) - self.plays_json = self.__add_yardage_cols(self.plays_json) - self.plays_json = self.__add_player_cols(self.plays_json) - self.plays_json = self.__after_cols(self.plays_json) - self.plays_json = self.__add_spread_time(self.plays_json) - self.plays_json = self.plays_json.replace({np.nan: None}) + self.plays_json = self.plays_json.pipe( + self.__add_downs_data + ).pipe( + self.__add_play_type_flags + ).pipe( + self.__add_rush_pass_flags + ).pipe( + self.__add_team_score_variables + ).pipe( + self.__add_new_play_types + ).pipe( + self.__setup_penalty_data + ).pipe( + self.__add_play_category_flags + ).pipe( + self.__add_yardage_cols + ).pipe( + self.__add_player_cols + ).pipe( + self.__after_cols + ).pipe( + self.__add_spread_time + ) + self.plays_json = self.plays_json.to_dicts() pbp_json = { "gameId": self.gameId, - "plays": self.plays_json.to_dict(orient="records"), + "plays": self.plays_json, "season": pbp_txt["season"], "week": pbp_txt['header']['week'], "gameInfo": pbp_txt["gameInfo"], diff --git a/sportsdataverse/cfb/cfb_schedule.py b/sportsdataverse/cfb/cfb_schedule.py index 644f81c..27bb419 100755 --- a/sportsdataverse/cfb/cfb_schedule.py +++ b/sportsdataverse/cfb/cfb_schedule.py @@ -6,6 +6,7 @@ from sportsdataverse.dl_utils import download, underscore def espn_cfb_schedule(dates=None, week=None, season_type=None, groups=None, limit=500, + return_as_pandas = True, **kwargs) -> pd.DataFrame: """espn_cfb_schedule - look up the college football schedule for a given season @@ -15,17 +16,16 @@ def espn_cfb_schedule(dates=None, week=None, season_type=None, groups=None, limi groups (int): Used to define different divisions. 80 is FBS, 81 is FCS. season_type (int): 2 for regular season, 3 for post-season, 4 for off-season. limit (int): number of records to return, default: 500. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. """ - cache_buster = int(time.time() * 1000) - cache_buster_url = f'&{cache_buster}' params = { 'week': week, 'dates': dates, - 'seasontype': season_type, + 'seasonType': season_type, 'groups': groups if groups is not None else '80', 'limit': limit } @@ -34,64 +34,48 @@ def espn_cfb_schedule(dates=None, week=None, season_type=None, groups=None, limi resp = download(url=url, params=params, **kwargs) ev = pl.DataFrame() - if resp is not None: - events_txt = resp.json() - events = events_txt.get('events') - if len(events) == 0: - ev = pd.DataFrame() - return ev - for event in events: - event.get('competitions')[0].get('competitors')[0].get('team').pop('links',None) - event.get('competitions')[0].get('competitors')[1].get('team').pop('links',None) - if event.get('competitions')[0].get('competitors')[0].get('homeAway') == 'home': - event = _extract_home_away_from_espn_cfb_schedule(event, 0, 'home') - event = _extract_home_away_from_espn_cfb_schedule(event, 1, 'away') - else: - event = _extract_home_away_from_espn_cfb_schedule(event, 0, 'away') - event = _extract_home_away_from_espn_cfb_schedule(event, 1, 'home') - del_keys = ['geoBroadcasts', 'headlines', 'series', 'situation', 'tickets', 'odds'] - for k in del_keys: - event.get('competitions')[0].pop(k, None) - if len(event.get('competitions')[0]['notes']) > 0: - event.get('competitions')[0]['notes_type'] = event.get('competitions')[0]['notes'][0].get("type") - event.get('competitions')[0]['notes_headline'] = event.get('competitions')[0]['notes'][0].get("headline").replace('"','') - else: - event.get('competitions')[0]['notes_type'] = '' - event.get('competitions')[0]['notes_headline'] = '' - if len(event.get('competitions')[0].get('broadcasts')) > 0: - event.get('competitions')[0]['broadcast_market'] = event.get('competitions')[0].get('broadcasts', [])[0].get('market', "") - event.get('competitions')[0]['broadcast_name'] = event.get('competitions')[0].get('broadcasts', [])[0].get('names', [])[0] - else: - event.get('competitions')[0]['broadcast_market'] = "" - event.get('competitions')[0]['broadcast_name'] = "" - event.get('competitions')[0].pop('broadcasts', None) - event.get('competitions')[0].pop('notes', None) - event.get('competitions')[0].pop('competitors', None) - x = pd.json_normalize(event.get('competitions')[0], sep = '_') - x = pl.from_pandas(x) - x = x.with_columns( - game_id = (pl.col('id').cast(pl.Int64)), - season = (event.get('season').get('year')), - season_type = (event.get('season').get('type')), - week = (event.get('week', {}).get('number')), - home_linescores = pl.when(pl.col('status_type_description') == 'Postponed').then(None).otherwise(pl.col('home_linescores')), - away_linescores = pl.when(pl.col('status_type_description') == 'Postponed').then(None).otherwise(pl.col('away_linescores')), - ) - x = x[[s.name for s in x if s.null_count() != x.height]] - # x['game_id'] = x['id'].cast(pl.Int64) - # x['season'] = event.get('season').get('year') - # x['season_type'] = event.get('season').get('type') - # x['week'] = event.get('week', {}).get('number') - - ev = pl.concat([ev, x], how = 'diagonal') + events_txt = resp.json() + events = events_txt.get('events') + if len(events) == 0: + return pd.DataFrame() if return_as_pandas else pl.DataFrame() + + for event in events: + event.get('competitions')[0].get('competitors')[0].get('team').pop('links',None) + event.get('competitions')[0].get('competitors')[1].get('team').pop('links',None) + if event.get('competitions')[0].get('competitors')[0].get('homeAway') == 'home': + event = _extract_home_away(event, 0, 'home') + event = _extract_home_away(event, 1, 'away') + else: + event = _extract_home_away(event, 0, 'away') + event = _extract_home_away(event, 1, 'home') + del_keys = ['geoBroadcasts', 'headlines', 'series', 'situation', 'tickets', 'odds'] + for k in del_keys: + event.get('competitions')[0].pop(k, None) + event.get('competitions')[0]['notes_type'] = event.get('competitions')[0]['notes'][0].get("type") if len(event.get('competitions')[0]['notes']) > 0 else '' + event.get('competitions')[0]['notes_headline'] = event.get('competitions')[0]['notes'][0].get("headline").replace('"','') if len(event.get('competitions')[0]['notes']) > 0 else '' + event.get('competitions')[0]['broadcast_market'] = event.get('competitions')[0].get('broadcasts', [])[0].get('market', "") if len(event.get('competitions')[0].get('broadcasts')) > 0 else "" + event.get('competitions')[0]['broadcast_name'] = event.get('competitions')[0].get('broadcasts', [])[0].get('names', [])[0] if len(event.get('competitions')[0].get('broadcasts')) > 0 else "" + event.get('competitions')[0].pop('broadcasts', None) + event.get('competitions')[0].pop('notes', None) + event.get('competitions')[0].pop('competitors', None) + x = pl.from_pandas(pd.json_normalize(event.get('competitions')[0], sep = '_')) + x = x.with_columns( + game_id = (pl.col('id').cast(pl.Int64)), + season = (event.get('season').get('year')), + season_type = (event.get('season').get('type')), + week = (event.get('week', {}).get('number')), + home_linescores = pl.when(pl.col('status_type_description') == 'Postponed').then(None).otherwise(pl.col('home_linescores')), + away_linescores = pl.when(pl.col('status_type_description') == 'Postponed').then(None).otherwise(pl.col('away_linescores')), + ) + x = x[[s.name for s in x if s.null_count() != x.height]] + ev = pl.concat([ev, x], how = 'diagonal') ev.columns = [underscore(c) for c in ev.columns] - return ev.to_pandas() - + return ev.to_pandas() if return_as_pandas else ev # TODO Rename this here and in `espn_cfb_schedule` -def _extract_home_away_from_espn_cfb_schedule(event, arg1, arg2): +def _extract_home_away(event, arg1, arg2): event['competitions'][0][arg2] = ( event.get('competitions')[0].get('competitors')[arg1].get('team') ) @@ -129,15 +113,15 @@ def _extract_home_away_from_espn_cfb_schedule(event, arg1, arg2): ) return event - - -def espn_cfb_calendar(season = None, groups = None, ondays = None, **kwargs) -> pd.DataFrame: +def espn_cfb_calendar(season = None, groups = None, ondays = None, + return_as_pandas = True, **kwargs) -> pd.DataFrame: """espn_cfb_calendar - look up the men's college football calendar for a given season Args: season (int): Used to define different seasons. 2002 is the earliest available season. groups (int): Used to define different divisions. 80 is FBS, 81 is FCS. ondays (boolean): Used to return dates for calendar ondays + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing calendar dates for the requested season. @@ -172,10 +156,10 @@ def espn_cfb_calendar(season = None, groups = None, ondays = None, **kwargs) -> ) full_schedule.columns = [underscore(c) for c in full_schedule.columns] full_schedule = full_schedule.rename({"week_value": "week", "season_type_value": "season_type"}) - return full_schedule.to_pandas() + return full_schedule.to_pandas() if return_as_pandas else full_schedule -def _ondays_from_espn_cfb_calendar(season): +def _ondays_cfb_calendar(season): url = f"https://sports.core.api.espn.com/v2/sports/football/leagues/college-football/seasons/{season}/types/2/calendar/ondays" resp = download(url=url) if resp is not None: @@ -197,162 +181,3 @@ def most_recent_cfb_season(): return date.year else: return date.year - 1 - - -# def espn_cfb_schedule_pandas(dates=None, week=None, season_type=None, groups=None, limit=500) -> pd.DataFrame: -# """espn_cfb_schedule - look up the college football schedule for a given season - -# Args: -# dates (int): Used to define different seasons. 2002 is the earliest available season. -# week (int): Week of the schedule. -# groups (int): Used to define different divisions. 80 is FBS, 81 is FCS. -# season_type (int): 2 for regular season, 3 for post-season, 4 for off-season. -# limit (int): number of records to return, default: 500. - -# Returns: -# pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. -# """ -# if week is None: -# week = '' -# else: -# week = '&week=' + str(week) -# if dates is None: -# dates = '' -# else: -# dates = '&dates=' + str(dates) -# if season_type is None: -# season_type = '' -# else: -# season_type = '&seasontype=' + str(season_type) -# if groups is None: -# groups = '&groups=80' -# else: -# groups = '&groups=' + str(groups) -# if limit is None: -# limit_url = '' -# else: -# limit_url = '&limit=' + str(limit) -# cache_buster = int(time.time() * 1000) -# cache_buster_url = '&'+str(cache_buster) -# url = "http://site.api.espn.com/apis/site/v2/sports/football/college-football/scoreboard?{}{}{}{}{}{}".format( -# limit_url, -# groups, -# dates, -# week, -# season_type, -# cache_buster_url -# ) -# resp = download(url=url) - -# ev = pd.DataFrame() -# if resp is not None: -# events_txt = resp.json() -# events = events_txt.get('events') -# for event in events: -# event.get('competitions')[0].get('competitors')[0].get('team').pop('links',None) -# event.get('competitions')[0].get('competitors')[1].get('team').pop('links',None) -# if event.get('competitions')[0].get('competitors')[0].get('homeAway')=='home': -# event['competitions'][0]['home'] = event.get('competitions')[0].get('competitors')[0].get('team') -# event['competitions'][0]['home']['score'] = event.get('competitions')[0].get('competitors')[0].get('score') -# event['competitions'][0]['home']['winner'] = event.get('competitions')[0].get('competitors')[0].get('winner') -# event['competitions'][0]['home']['currentRank'] = event.get('competitions')[0].get('competitors')[0].get('curatedRank', {}).get('current', '99') -# event['competitions'][0]['home']['linescores'] = event.get('competitions')[0].get('competitors')[0].get('linescores', []) -# event['competitions'][0]['home']['records'] = event.get('competitions')[0].get('competitors')[0].get('records', []) -# event['competitions'][0]['away'] = event.get('competitions')[0].get('competitors')[1].get('team') -# event['competitions'][0]['away']['score'] = event.get('competitions')[0].get('competitors')[1].get('score') -# event['competitions'][0]['away']['winner'] = event.get('competitions')[0].get('competitors')[1].get('winner') -# event['competitions'][0]['away']['currentRank'] = event.get('competitions')[0].get('competitors')[1].get('curatedRank', {}).get('current', '99') -# event['competitions'][0]['away']['linescores'] = event.get('competitions')[0].get('competitors')[1].get('linescores', []) -# event['competitions'][0]['away']['records'] = event.get('competitions')[0].get('competitors')[1].get('records', []) -# else: -# event['competitions'][0]['away'] = event.get('competitions')[0].get('competitors')[0].get('team') -# event['competitions'][0]['away']['score'] = event.get('competitions')[0].get('competitors')[0].get('score') -# event['competitions'][0]['away']['winner'] = event.get('competitions')[0].get('competitors')[0].get('winner') -# event['competitions'][0]['away']['currentRank'] = event.get('competitions')[0].get('competitors')[0].get('curatedRank', {}).get('current', '99') -# event['competitions'][0]['away']['linescores'] = event.get('competitions')[0].get('competitors')[0].get('linescores', []) -# event['competitions'][0]['away']['records'] = event.get('competitions')[0].get('competitors')[0].get('records', []) -# event['competitions'][0]['home'] = event.get('competitions')[0].get('competitors')[1].get('team') -# event['competitions'][0]['home']['score'] = event.get('competitions')[0].get('competitors')[1].get('score') -# event['competitions'][0]['home']['winner'] = event.get('competitions')[0].get('competitors')[1].get('winner') -# event['competitions'][0]['home']['currentRank'] = event.get('competitions')[0].get('competitors')[1].get('curatedRank', {}).get('current', '99') -# event['competitions'][0]['home']['linescores'] = event.get('competitions')[0].get('competitors')[1].get('linescores', []) -# event['competitions'][0]['home']['records'] = event.get('competitions')[0].get('competitors')[1].get('records', []) - -# del_keys = ['geoBroadcasts', 'headlines', 'series', 'situation', 'tickets', 'odds'] -# for k in del_keys: -# event.get('competitions')[0].pop(k, None) -# if len(event.get('competitions')[0]['notes'])>0: -# event.get('competitions')[0]['notes_type'] = event.get('competitions')[0]['notes'][0].get("type") -# event.get('competitions')[0]['notes_headline'] = event.get('competitions')[0]['notes'][0].get("headline").replace('"','') -# else: -# event.get('competitions')[0]['notes_type'] = '' -# event.get('competitions')[0]['notes_headline'] = '' -# if len(event.get('competitions')[0].get('broadcasts'))>0: -# event.get('competitions')[0]['broadcast_market'] = event.get('competitions')[0].get('broadcasts', [])[0].get('market', "") -# event.get('competitions')[0]['broadcast_name'] = event.get('competitions')[0].get('broadcasts', [])[0].get('names', [])[0] -# else: -# event.get('competitions')[0]['broadcast_market'] = "" -# event.get('competitions')[0]['broadcast_name'] = "" -# event.get('competitions')[0].pop('broadcasts', None) -# event.get('competitions')[0].pop('notes', None) -# x = pd.json_normalize(event.get('competitions')[0], sep='_') -# x['game_id'] = x['id'].astype(int) -# x['season'] = event.get('season').get('year') -# x['season_type'] = event.get('season').get('type') -# x['week'] = event.get('week', {}).get('number') -# ev = pd.concat([ev, x], axis = 0, ignore_index = True) -# ev = pd.DataFrame(ev) -# ev.columns = [underscore(c) for c in ev.columns.tolist()] -# return ev - - -# def espn_cfb_calendar_pandas(season = None, groups = None, ondays = None) -> pd.DataFrame: -# """espn_cfb_calendar - look up the men's college football calendar for a given season - -# Args: -# season (int): Used to define different seasons. 2002 is the earliest available season. -# groups (int): Used to define different divisions. 80 is FBS, 81 is FCS. -# ondays (boolean): Used to return dates for calendar ondays - -# Returns: -# pd.DataFrame: Pandas dataframe containing calendar dates for the requested season. - -# Raises: -# ValueError: If `season` is less than 2002. -# """ -# if ondays is not None: -# url = "https://sports.core.api.espn.com/v2/sports/football/leagues/college-football/seasons/{}/types/2/calendar/ondays".format(season) -# resp = download(url=url) -# txt = resp.json().get('eventDate').get('dates') -# full_schedule = pd.DataFrame(txt,columns=['dates']) -# full_schedule['dateURL'] = list(map(lambda x: x[:10].replace("-",""),full_schedule['dates'])) -# full_schedule['url']="http://site.api.espn.com/apis/site/v2/sports/football/college-football/scoreboard?dates=" -# full_schedule['url']= full_schedule['url'] + full_schedule['dateURL'] -# else: -# if season is None: -# season_url = '' -# else: -# season_url = '&dates=' + str(season) -# if groups is None: -# groups_url = '&groups=80' -# else: -# groups_url = '&groups=' + str(groups) -# url = "http://site.api.espn.com/apis/site/v2/sports/football/college-football/scoreboard?{}{}".format(season_url, groups_url) -# resp = download(url=url) -# txt = resp.json() -# txt = txt.get('leagues')[0].get('calendar') -# full_schedule = pd.DataFrame() -# for i in range(len(txt)): -# if txt[i].get('entries', None) is not None: -# reg = pd.json_normalize(data = txt[i], -# record_path = 'entries', -# meta=["label","value","startDate","endDate"], -# meta_prefix='season_type_', -# record_prefix='week_', -# errors="ignore", -# sep='_') -# full_schedule = pd.concat([full_schedule,reg], ignore_index=True) -# full_schedule['season']=season -# full_schedule.columns = [underscore(c) for c in full_schedule.columns.tolist()] -# full_schedule = full_schedule.rename(columns={"week_value": "week", "season_type_value": "season_type"}) -# return full_schedule \ No newline at end of file diff --git a/sportsdataverse/cfb/cfb_teams.py b/sportsdataverse/cfb/cfb_teams.py index 99127d6..da742ac 100755 --- a/sportsdataverse/cfb/cfb_teams.py +++ b/sportsdataverse/cfb/cfb_teams.py @@ -3,11 +3,12 @@ import json from sportsdataverse.dl_utils import download, underscore -def espn_cfb_teams(groups=None, **kwargs) -> pd.DataFrame: +def espn_cfb_teams(groups=None, return_as_pandas = True, **kwargs) -> pd.DataFrame: """espn_cfb_teams - look up the college football teams Args: groups (int): Used to define different divisions. 80 is FBS, 81 is FCS. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. @@ -28,4 +29,4 @@ def espn_cfb_teams(groups=None, **kwargs) -> pd.DataFrame: team.get('team').pop(k, None) teams = pd.json_normalize(teams, sep='_') teams.columns = [underscore(c) for c in teams.columns.tolist()] - return teams + return teams if return_as_pandas else pl.from_pandas(teams) From b93274a6ce5a613fc6f84910f0c0dfdc010b9ec6 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Mon, 10 Jul 2023 00:01:38 -0400 Subject: [PATCH 05/79] minor cfb package updates --- sportsdataverse/cfb/__init__.py | 1 + sportsdataverse/cfb/cfb_pbp.py | 25 ++++++++++++++----------- sportsdataverse/cfb/cfb_schedule.py | 6 +++--- sportsdataverse/cfb/cfb_teams.py | 2 +- 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/sportsdataverse/cfb/__init__.py b/sportsdataverse/cfb/__init__.py index 1a14707..f9fc594 100755 --- a/sportsdataverse/cfb/__init__.py +++ b/sportsdataverse/cfb/__init__.py @@ -1,4 +1,5 @@ from sportsdataverse.cfb.cfb_loaders import * +from sportsdataverse.cfb.cfb_game_rosters import * from sportsdataverse.cfb.cfb_pbp import * from sportsdataverse.cfb.cfb_schedule import * from sportsdataverse.cfb.cfb_teams import * \ No newline at end of file diff --git a/sportsdataverse/cfb/cfb_pbp.py b/sportsdataverse/cfb/cfb_pbp.py index 5f77528..fb0d595 100755 --- a/sportsdataverse/cfb/cfb_pbp.py +++ b/sportsdataverse/cfb/cfb_pbp.py @@ -46,19 +46,22 @@ class CFBPlayProcess(object): ran_cleaning_pipeline = False raw = False path_to_json = '/' - def __init__(self, gameId=0, raw=False, path_to_json='/', **kwargs): + return_keys = None + def __init__(self, gameId=0, raw=False, path_to_json='/', return_keys=None, **kwargs): self.gameId = int(gameId) # self.logger = logger self.ran_pipeline = False self.ran_cleaning_pipeline = False self.raw = raw self.path_to_json = path_to_json + self.return_keys = return_keys def espn_cfb_pbp(self, **kwargs): """espn_cfb_pbp() - Pull the game by id. Data from API endpoints: `college-football/playbyplay`, `college-football/summary` Args: game_id (int): Unique game_id, can be obtained from cfb_schedule(). + raw (bool): If True, returns the raw json from the API endpoint. If False, returns a cleaned dictionary of datasets. Returns: Dict: Dictionary of game data with keys - "gameId", "plays", "boxscore", "header", "broadcasts", @@ -633,26 +636,26 @@ def __helper_cfb_pbp(self, pbp_txt): homeTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['name']) homeTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['location']) homeTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['abbreviation']) - homeTeamNameAlt = re.sub("Stat(.+)", "St", str(homeTeamName)) + homeTeamNameAlt = re.sub("Stat(.+)", "St", homeTeamName) pbp_txt['header']['competitions'][0]['away'] = pbp_txt['header']['competitions'][0]['competitors'][1]['team'] awayTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['id']) awayTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['name']) awayTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['location']) awayTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['abbreviation']) - awayTeamNameAlt = re.sub("Stat(.+)", "St", str(awayTeamName)) + awayTeamNameAlt = re.sub("Stat(.+)", "St", awayTeamName) else: pbp_txt['header']['competitions'][0]['away'] = pbp_txt['header']['competitions'][0]['competitors'][0]['team'] awayTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['id']) awayTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['name']) awayTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['location']) awayTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['abbreviation']) - awayTeamNameAlt = re.sub("Stat(.+)", "St", str(awayTeamName)) + awayTeamNameAlt = re.sub("Stat(.+)", "St", awayTeamName) pbp_txt['header']['competitions'][0]['home'] = pbp_txt['header']['competitions'][0]['competitors'][1]['team'] homeTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['id']) homeTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['name']) homeTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['location']) homeTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['abbreviation']) - homeTeamNameAlt = re.sub("Stat(.+)", "St", str(homeTeamName)) + homeTeamNameAlt = re.sub("Stat(.+)", "St", homeTeamName) return pbp_txt, gameSpread, overUnder, homeFavorite, gameSpreadAvailable, homeTeamId,\ homeTeamMascot,homeTeamName,homeTeamAbbrev,homeTeamNameAlt,\ awayTeamId,awayTeamMascot,awayTeamName,awayTeamAbbrev,awayTeamNameAlt @@ -3861,7 +3864,7 @@ def run_processing_pipeline(self): self.plays_json = pbp_txt['plays'] pbp_json = { - "gameId": self.gameId, + "gameId": int(self.gameId), "plays":self.plays_json, "season": pbp_txt["season"], "week": pbp_txt['header']['week'], @@ -3922,7 +3925,7 @@ def run_processing_pipeline(self): advBoxScore = self.create_box_score(self.plays_json) self.plays_json = self.plays_json.to_dicts() pbp_json = { - "gameId": self.gameId, + "gameId": int(self.gameId), "plays": self.plays_json, "season": pbp_txt["season"], "week": pbp_txt['header']['week'], @@ -3948,7 +3951,7 @@ def run_processing_pipeline(self): } self.json = pbp_json self.ran_pipeline = True - return pbp_json + return self.json if self.return_keys is None else {k: self.json[k] for k in self.return_keys} def run_cleaning_pipeline(self): if self.ran_cleaning_pipeline == False: @@ -3956,7 +3959,7 @@ def run_cleaning_pipeline(self): self.plays_json = pbp_txt['plays'] pbp_json = { - "gameId": self.gameId, + "gameId": int(self.gameId), "plays": self.plays_json, "season": pbp_txt["season"], "week": pbp_txt['header']['week'], @@ -4007,7 +4010,7 @@ def run_cleaning_pipeline(self): ) self.plays_json = self.plays_json.to_dicts() pbp_json = { - "gameId": self.gameId, + "gameId": int(self.gameId), "plays": self.plays_json, "season": pbp_txt["season"], "week": pbp_txt['header']['week'], @@ -4032,4 +4035,4 @@ def run_cleaning_pipeline(self): } self.json = pbp_json self.ran_cleaning_pipeline = True - return pbp_json \ No newline at end of file + return self.json \ No newline at end of file diff --git a/sportsdataverse/cfb/cfb_schedule.py b/sportsdataverse/cfb/cfb_schedule.py index 27bb419..18ed119 100755 --- a/sportsdataverse/cfb/cfb_schedule.py +++ b/sportsdataverse/cfb/cfb_schedule.py @@ -130,7 +130,7 @@ def espn_cfb_calendar(season = None, groups = None, ondays = None, ValueError: If `season` is less than 2002. """ if ondays is not None: - full_schedule = _ondays_from_espn_cfb_calendar(season) + full_schedule = __ondays_from_espn_cfb_calendar(season) else: params = { 'dates': season, @@ -159,9 +159,9 @@ def espn_cfb_calendar(season = None, groups = None, ondays = None, return full_schedule.to_pandas() if return_as_pandas else full_schedule -def _ondays_cfb_calendar(season): +def __ondays_cfb_calendar(season, **kwargs): url = f"https://sports.core.api.espn.com/v2/sports/football/leagues/college-football/seasons/{season}/types/2/calendar/ondays" - resp = download(url=url) + resp = download(url=url, **kwargs) if resp is not None: txt = resp.json().get('eventDate').get('dates') result = pl.DataFrame(txt, schema=['dates']) diff --git a/sportsdataverse/cfb/cfb_teams.py b/sportsdataverse/cfb/cfb_teams.py index da742ac..e3ceded 100755 --- a/sportsdataverse/cfb/cfb_teams.py +++ b/sportsdataverse/cfb/cfb_teams.py @@ -13,11 +13,11 @@ def espn_cfb_teams(groups=None, return_as_pandas = True, **kwargs) -> pd.DataFra Returns: pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. """ + url = "http://site.api.espn.com/apis/site/v2/sports/football/college-football/teams" params = { "groups": groups if groups is not None else "80", "limit": 1000 } - url = "http://site.api.espn.com/apis/site/v2/sports/football/college-football/teams" resp = download(url=url, params = params, **kwargs) if resp is not None: events_txt = resp.json() From b77cd8b71f49fe5c1dc468692a28f715c756dd33 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Mon, 10 Jul 2023 00:03:00 -0400 Subject: [PATCH 06/79] Function `espn_cfb_game_rosters()` added. --- sportsdataverse/cfb/cfb_game_rosters.py | 197 ++++++++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 sportsdataverse/cfb/cfb_game_rosters.py diff --git a/sportsdataverse/cfb/cfb_game_rosters.py b/sportsdataverse/cfb/cfb_game_rosters.py new file mode 100644 index 0000000..8be545f --- /dev/null +++ b/sportsdataverse/cfb/cfb_game_rosters.py @@ -0,0 +1,197 @@ +import pandas as pd +import polars as pl +import numpy as np +import os +import json +import re +from typing import List, Callable, Iterator, Union, Optional, Dict +from sportsdataverse.dl_utils import download, underscore + +def espn_cfb_game_rosters(game_id: int, raw = False, return_as_pandas = True, **kwargs) -> pd.DataFrame: + """espn_cfb_game_rosters() - Pull the game by id. + + Args: + game_id (int): Unique game_id, can be obtained from espn_cfb_schedule(). + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + + Returns: + pd.DataFrame: Data frame of game roster data with columns: + 'athlete_id', 'athlete_uid', 'athlete_guid', 'athlete_type', + 'first_name', 'last_name', 'full_name', 'athlete_display_name', + 'short_name', 'weight', 'display_weight', 'height', 'display_height', + 'age', 'date_of_birth', 'slug', 'jersey', 'linked', 'active', + 'alternate_ids_sdr', 'birth_place_city', 'birth_place_state', + 'birth_place_country', 'headshot_href', 'headshot_alt', + 'experience_years', 'experience_display_value', + 'experience_abbreviation', 'status_id', 'status_name', 'status_type', + 'status_abbreviation', 'hand_type', 'hand_abbreviation', + 'hand_display_value', 'draft_display_text', 'draft_round', 'draft_year', + 'draft_selection', 'player_id', 'starter', 'valid', 'did_not_play', + 'display_name', 'ejected', 'athlete_href', 'position_href', + 'statistics_href', 'team_id', 'team_guid', 'team_uid', 'team_slug', + 'team_location', 'team_name', 'team_nickname', 'team_abbreviation', + 'team_display_name', 'team_short_display_name', 'team_color', + 'team_alternate_color', 'is_active', 'is_all_star', + 'team_alternate_ids_sdr', 'logo_href', 'logo_dark_href', 'game_id' + Example: + `cfb_df = sportsdataverse.cfb.espn_cfb_game_rosters(game_id=401256137)` + """ + # play by play + pbp_txt = {} + # summary endpoint for pickcenter array + summary_url = "https://sports.core.api.espn.com/v2/sports/football/leagues/college-football/events/{x}/competitions/{x}/competitors".format(x=game_id) + summary_resp = download(summary_url, **kwargs) + summary = summary_resp.json() + items = helper_cfb_game_items(summary) + team_rosters = helper_cfb_roster_items(items = items, summary_url = summary_url, **kwargs) + team_rosters = team_rosters.join(items[['team_id', 'order', 'home_away', 'winner']], how = 'left', on = 'team_id') + teams_df = helper_cfb_team_items(items = items, **kwargs) + teams_rosters = team_rosters.join(teams_df, how = 'left',on = 'team_id') + athletes = helper_cfb_athlete_items(teams_rosters = team_rosters, **kwargs) + rosters = athletes.join(teams_rosters, how = 'left', left_on = 'athlete_id', right_on = 'player_id') + rosters = rosters.with_columns( + game_id = pl.lit(game_id).cast(pl.Int64) + ) + rosters.columns = [underscore(c) for c in rosters.columns] + return rosters.to_pandas() if return_as_pandas else rosters + +def helper_cfb_game_items(summary): + items = pl.from_pandas(pd.json_normalize(summary, record_path = "items", sep = '_')) + items.columns = [col.replace("$ref", "href") for col in items.columns] + + items.columns = [underscore(c) for c in items.columns] + items = items.rename({ + "id": "team_id", + "uid": "team_uid", + "statistics_href": "team_statistics_href" + } + ) + items = items.with_columns( + team_id = pl.col("team_id").cast(pl.Int32) + ) + + return items + +def helper_cfb_team_items(items, **kwargs): + pop_cols = [ + '$ref', + 'record', + 'athletes', + 'venue', + 'groups', + 'ranks', + 'statistics', + 'leaders', + 'links', + 'notes', + 'againstTheSpreadRecords', + 'franchise', + 'events', + 'college', + 'awards', + 'coaches', + 'recruiting', + 'injuries' + ] + teams_df = pl.DataFrame() + for x in items['team_href']: + team = download(x, **kwargs).json() + for k in pop_cols: + team.pop(k, None) + team_row = pl.from_pandas(pd.json_normalize(team, sep = '_')) + teams_df = pl.concat([teams_df, team_row], how = 'vertical') + print(teams_df.columns) + teams_df.columns = [ + 'team_id', + 'team_guid', + 'team_uid', + 'team_slug', + 'team_location', + 'team_name', + 'team_nickname', + 'team_abbreviation', + 'team_display_name', + 'team_short_display_name', + 'team_color', + 'team_alternate_color', + 'is_active', + 'is_all_star', + 'logos', + 'team_alternate_ids_sdr' + ] + teams_df = teams_df.with_columns( + logo_href = pl.lit(""), + logo_dark_href = pl.lit("") + ) + + for row in range(len(teams_df['logos'])): + team = teams_df['logos'][row] + teams_df[row, 'logo_href'] = team[0]['href'] + teams_df[row, 'logo_dark_href'] = team[1]['href'] + + teams_df = teams_df.drop(['logos']) + teams_df = teams_df.with_columns( + team_id = pl.col("team_id").cast(pl.Int32) + ) + return teams_df + +def helper_cfb_roster_items(items, summary_url, **kwargs): + team_ids = list(items['team_id']) + game_rosters = pl.DataFrame() + for tm in team_ids: + team_roster_url = "{x}/{t}/roster".format(x = summary_url, t = tm) + team_roster_resp = download(team_roster_url, **kwargs) + team_roster = pl.from_pandas(pd.json_normalize(team_roster_resp.json().get('entries',[]), sep = '_')) + team_roster.columns = [col.replace("$ref", "href") for col in team_roster.columns] + team_roster.columns = [underscore(c) for c in team_roster.columns] + team_roster= team_roster.with_columns( + team_id = pl.lit(tm).cast(pl.Int32) + ) + game_rosters = pl.concat([game_rosters, team_roster], how = 'vertical') + game_rosters = game_rosters.drop(["period", "for_player_id", "active"]) + game_rosters = game_rosters.with_columns( + player_id = pl.col('player_id').cast(pl.Int64), + team_id = pl.col('team_id').cast(pl.Int32) + ) + return game_rosters + +def helper_cfb_athlete_items(teams_rosters, **kwargs): + athlete_hrefs = list(teams_rosters['athlete_href']) + game_athletes = pl.DataFrame() + pop_cols = [ + 'links', + 'injuries', + 'teams', + 'team', + 'college', + 'proAthlete', + 'statistics', + 'notes', + 'eventLog', + "$ref", + "position" + ] + for athlete_href in athlete_hrefs: + + athlete_res = download(athlete_href, **kwargs) + athlete_resp = athlete_res.json() + for k in pop_cols: + athlete_resp.pop(k, None) + athlete = pl.from_pandas(pd.json_normalize(athlete_resp, sep='_')) + athlete.columns = [col.replace("$ref", "href") for col in athlete.columns] + athlete.columns = [underscore(c) for c in athlete.columns] + + game_athletes = pl.concat([game_athletes, athlete], how = 'diagonal') + + + game_athletes = game_athletes.rename({ + "id": "athlete_id", + "uid": "athlete_uid", + "guid": "athlete_guid", + "type": "athlete_type", + "display_name": "athlete_display_name" + }) + game_athletes = game_athletes.with_columns( + athlete_id = pl.col("athlete_id").cast(pl.Int64) + ) + return game_athletes \ No newline at end of file From ecfb2ca2cb50f6ef80ad278a2b87a6f3adeeb8fd Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Mon, 10 Jul 2023 00:03:58 -0400 Subject: [PATCH 07/79] Update mbb package to use polars --- CHANGELOG.md | 2 + sportsdataverse/mbb/mbb_game_rosters.py | 112 ++++--- sportsdataverse/mbb/mbb_loaders.py | 55 ++-- sportsdataverse/mbb/mbb_pbp.py | 379 +++++++++++++----------- sportsdataverse/mbb/mbb_schedule.py | 206 ++++++++----- sportsdataverse/mbb/mbb_teams.py | 20 +- 6 files changed, 431 insertions(+), 343 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dc3a8a..5427eca 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ ## 0.0.36-7 Release: July 9, 2023 - Switched most under the hood dataframe operations to use the python `polars` library - Added `**kwargs` which pass arguments to the `dl_utils.download()` function, including `headers`, `proxy`, `timeout` (default 30s), `num_retries` (default = 15), `logger` (default = None) +- Function `espn_cfb_game_rosters()` added. +- Function `espn_nba_game_rosters()` added. ## 0.0.34-35 Release: May 7-9, 2023 - Reconfigured some imports diff --git a/sportsdataverse/mbb/mbb_game_rosters.py b/sportsdataverse/mbb/mbb_game_rosters.py index d43f690..e085a70 100755 --- a/sportsdataverse/mbb/mbb_game_rosters.py +++ b/sportsdataverse/mbb/mbb_game_rosters.py @@ -1,4 +1,5 @@ import pandas as pd +import polars as pl import numpy as np import os import json @@ -6,11 +7,12 @@ from typing import List, Callable, Iterator, Union, Optional, Dict from sportsdataverse.dl_utils import download, underscore -def espn_mbb_game_rosters(game_id: int, raw = False) -> pd.DataFrame: +def espn_mbb_game_rosters(game_id: int, raw = False, return_as_pandas = True, **kwargs) -> pd.DataFrame: """espn_mbb_game_rosters() - Pull the game by id. Args: game_id (int): Unique game_id, can be obtained from mbb_schedule(). + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Data frame of game roster data with columns: @@ -38,36 +40,39 @@ def espn_mbb_game_rosters(game_id: int, raw = False) -> pd.DataFrame: pbp_txt = {} # summary endpoint for pickcenter array summary_url = "https://sports.core.api.espn.com/v2/sports/basketball/leagues/mens-college-basketball/events/{x}/competitions/{x}/competitors".format(x=game_id) - summary_resp = download(summary_url) - summary = json.loads(summary_resp) + summary_resp = download(summary_url, **kwargs) + summary = summary_resp.json() items = helper_mbb_game_items(summary) - team_rosters = helper_mbb_roster_items(items = items, summary_url = summary_url) - team_rosters = team_rosters.merge(items[['team_id', 'order', 'home_away', 'winner']], how = 'left', on = 'team_id') - teams_df = helper_mbb_team_items(items = items) - teams_rosters = team_rosters.merge(teams_df, how = 'left',on = 'team_id') - athletes = helper_mbb_athlete_items(teams_rosters = team_rosters) - rosters = athletes.merge(teams_rosters, how = 'left', left_on = 'athlete_id', right_on = 'player_id') - rosters['game_id'] = int(game_id) - rosters.columns = [underscore(c) for c in rosters.columns.tolist()] - return rosters + team_rosters = helper_mbb_roster_items(items = items, summary_url = summary_url, **kwargs) + team_rosters = team_rosters.join(items[['team_id', 'order', 'home_away', 'winner']], how = 'left', on = 'team_id') + teams_df = helper_mbb_team_items(items = items, **kwargs) + teams_rosters = team_rosters.join(teams_df, how = 'left',on = 'team_id') + athletes = helper_mbb_athlete_items(teams_rosters = team_rosters, **kwargs) + rosters = athletes.join(teams_rosters, how = 'left', left_on = 'athlete_id', right_on = 'player_id') + rosters = rosters.with_columns( + game_id = pl.lit(game_id).cast(pl.Int64) + ) + rosters.columns = [underscore(c) for c in rosters.columns] + return rosters.to_pandas() if return_as_pandas else rosters def helper_mbb_game_items(summary): - items = pd.json_normalize(summary, record_path = "items", sep = '_') + items = pl.from_pandas(pd.json_normalize(summary, record_path = "items", sep = '_')) items.columns = [col.replace("$ref", "href") for col in items.columns] - items.columns = [underscore(c) for c in items.columns.tolist()] - items = items.rename( - columns = { + items.columns = [underscore(c) for c in items.columns] + items = items.rename({ "id": "team_id", "uid": "team_uid", "statistics_href": "team_statistics_href" } ) - items['team_id'] = items['team_id'].astype(int) + items = items.with_columns( + team_id = pl.col("team_id").cast(pl.Int32) + ) return items -def helper_mbb_team_items(items): +def helper_mbb_team_items(items, **kwargs): pop_cols = [ '$ref', 'record', @@ -84,13 +89,13 @@ def helper_mbb_team_items(items): 'events', 'college' ] - teams_df = pd.DataFrame() + teams_df = pl.DataFrame() for x in items['team_href']: - team = json.loads(download(x)) + team = download(x, **kwargs).json() for k in pop_cols: team.pop(k, None) - team_row = pd.json_normalize(team, sep = '_') - teams_df = pd.concat([teams_df, team_row], axis = 0, ignore_index = True) + team_row = pl.from_pandas(pd.json_normalize(team, sep = '_')) + teams_df = pl.concat([teams_df, team_row], how = 'vertical') teams_df.columns = [ 'team_id', @@ -110,36 +115,45 @@ def helper_mbb_team_items(items): 'logos', 'team_alternate_ids_sdr' ] + teams_df = teams_df.with_columns( + logo_href = pl.lit(""), + logo_dark_href = pl.lit("") + ) - teams_df['logos'][0] for row in range(len(teams_df['logos'])): team = teams_df['logos'][row] - teams_df.loc[row, 'logo_href'] = team[0]['href'] - teams_df.loc[row, 'logo_dark_href'] = team[1]['href'] - teams_df = teams_df.drop(['logos'], axis = 1) - teams_df['team_id'] = teams_df['team_id'].astype(int) + teams_df[row, 'logo_href'] = team[0]['href'] + teams_df[row, 'logo_dark_href'] = team[1]['href'] + + teams_df = teams_df.drop(['logos']) + teams_df = teams_df.with_columns( + team_id = pl.col("team_id").cast(pl.Int32) + ) return teams_df -def helper_mbb_roster_items(items, summary_url): +def helper_mbb_roster_items(items, summary_url, **kwargs): team_ids = list(items['team_id']) - game_rosters = pd.DataFrame() + game_rosters = pl.DataFrame() for tm in team_ids: - team_roster_url = "{x}/{t}/roster".format(x = summary_url,t = tm) - team_roster_resp = download(team_roster_url) - team_roster = pd.json_normalize(json.loads(team_roster_resp).get('entries',[]), sep = '_') + team_roster_url = "{x}/{t}/roster".format(x = summary_url, t = tm) + team_roster_resp = download(team_roster_url, **kwargs) + team_roster = pl.from_pandas(pd.json_normalize(team_roster_resp.json().get('entries',[]), sep = '_')) team_roster.columns = [col.replace("$ref", "href") for col in team_roster.columns] - team_roster.columns = [underscore(c) for c in team_roster.columns.tolist()] - team_roster['team_id'] = int(tm) - game_rosters = pd.concat([game_rosters, team_roster], axis = 0, ignore_index = True) - game_rosters = game_rosters.drop(["period", "for_player_id", "active"], axis = 1) - game_rosters['player_id'] = game_rosters['player_id'].astype(int) - game_rosters['team_id'] = game_rosters['team_id'].astype(int) + team_roster.columns = [underscore(c) for c in team_roster.columns] + team_roster= team_roster.with_columns( + team_id = pl.lit(tm).cast(pl.Int32) + ) + game_rosters = pl.concat([game_rosters, team_roster], how = 'vertical') + game_rosters = game_rosters.drop(["period", "for_player_id", "active"]) + game_rosters = game_rosters.with_columns( + player_id = pl.col('player_id').cast(pl.Int64), + team_id = pl.col('team_id').cast(pl.Int32) + ) return game_rosters - -def helper_mbb_athlete_items(teams_rosters): +def helper_mbb_athlete_items(teams_rosters, **kwargs): athlete_hrefs = list(teams_rosters['athlete_href']) - game_athletes = pd.DataFrame() + game_athletes = pl.DataFrame() pop_cols = [ 'links', 'injuries', @@ -155,23 +169,25 @@ def helper_mbb_athlete_items(teams_rosters): ] for athlete_href in athlete_hrefs: - athlete_res = download(athlete_href) - athlete_resp = json.loads(athlete_res) + athlete_res = download(athlete_href, **kwargs) + athlete_resp = athlete_res.json() for k in pop_cols: athlete_resp.pop(k, None) - athlete = pd.json_normalize(athlete_resp, sep='_') + athlete = pl.from_pandas(pd.json_normalize(athlete_resp, sep='_')) athlete.columns = [col.replace("$ref", "href") for col in athlete.columns] - athlete.columns = [underscore(c) for c in athlete.columns.tolist()] + athlete.columns = [underscore(c) for c in athlete.columns] - game_athletes = pd.concat([game_athletes, athlete], axis = 0, ignore_index = True) + game_athletes = pl.concat([game_athletes, athlete], how = 'diagonal') - game_athletes = game_athletes.rename(columns={ + game_athletes = game_athletes.rename({ "id": "athlete_id", "uid": "athlete_uid", "guid": "athlete_guid", "type": "athlete_type", "display_name": "athlete_display_name" }) - game_athletes['athlete_id'] = game_athletes['athlete_id'].astype(int) + game_athletes = game_athletes.with_columns( + athlete_id = pl.col("athlete_id").cast(pl.Int64) + ) return game_athletes \ No newline at end of file diff --git a/sportsdataverse/mbb/mbb_loaders.py b/sportsdataverse/mbb/mbb_loaders.py index ff7d2ad..59b84e5 100755 --- a/sportsdataverse/mbb/mbb_loaders.py +++ b/sportsdataverse/mbb/mbb_loaders.py @@ -1,4 +1,5 @@ import pandas as pd +import pandas as pl import json from tqdm import tqdm from typing import List, Callable, Iterator, Union, Optional @@ -6,7 +7,7 @@ from sportsdataverse.errors import SeasonNotFoundError from sportsdataverse.dl_utils import download -def load_mbb_pbp(seasons: List[int]) -> pd.DataFrame: +def load_mbb_pbp(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: """Load men's college basketball play by play data going back to 2002 Example: @@ -14,6 +15,7 @@ def load_mbb_pbp(seasons: List[int]) -> pd.DataFrame: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing the @@ -22,19 +24,17 @@ def load_mbb_pbp(seasons: List[int]) -> pd.DataFrame: Raises: ValueError: If `season` is less than 2002. """ - data = pd.DataFrame() + data = pl.DataFrame() if type(seasons) is int: seasons = [seasons] for i in tqdm(seasons): if int(i) < 2002: raise SeasonNotFoundError("season cannot be less than 2002") - i_data = pd.read_parquet(MBB_BASE_URL.format(season=i), engine='auto', columns=None) - data = pd.concat([data, i_data], axis = 0, ignore_index = True) - #Give each row a unique index - data.reset_index(drop=True, inplace=True) - return data + i_data = pl.read_parquet(MBB_BASE_URL.format(season=i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how = 'vertical') + return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data -def load_mbb_team_boxscore(seasons: List[int]) -> pd.DataFrame: +def load_mbb_team_boxscore(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: """Load men's college basketball team boxscore data Example: @@ -42,6 +42,7 @@ def load_mbb_team_boxscore(seasons: List[int]) -> pd.DataFrame: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing the @@ -50,20 +51,18 @@ def load_mbb_team_boxscore(seasons: List[int]) -> pd.DataFrame: Raises: ValueError: If `season` is less than 2002. """ - data = pd.DataFrame() + data = pl.DataFrame() if type(seasons) is int: seasons = [seasons] for i in tqdm(seasons): if int(i) < 2002: raise SeasonNotFoundError("season cannot be less than 2002") - i_data = pd.read_parquet(MBB_TEAM_BOX_URL.format(season = i), engine='auto', columns=None) - data = pd.concat([data, i_data], axis = 0, ignore_index = True) - #Give each row a unique index - data.reset_index(drop=True, inplace=True) + i_data = pl.read_parquet(MBB_TEAM_BOX_URL.format(season = i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how = 'vertical') + return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data - return data -def load_mbb_player_boxscore(seasons: List[int]) -> pd.DataFrame: +def load_mbb_player_boxscore(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: """Load men's college basketball player boxscore data Example: @@ -71,6 +70,7 @@ def load_mbb_player_boxscore(seasons: List[int]) -> pd.DataFrame: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing the @@ -79,20 +79,17 @@ def load_mbb_player_boxscore(seasons: List[int]) -> pd.DataFrame: Raises: ValueError: If `season` is less than 2002. """ - data = pd.DataFrame() + data = pl.DataFrame() if type(seasons) is int: seasons = [seasons] for i in tqdm(seasons): if int(i) < 2002: raise SeasonNotFoundError("season cannot be less than 2002") - i_data = pd.read_parquet(MBB_PLAYER_BOX_URL.format(season = i), engine='auto', columns=None) - data = pd.concat([data, i_data], axis = 0, ignore_index = True) - #Give each row a unique index - data.reset_index(drop=True, inplace=True) + i_data = pl.read_parquet(MBB_PLAYER_BOX_URL.format(season = i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how = 'vertical') + return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data - return data - -def load_mbb_schedule(seasons: List[int]) -> pd.DataFrame: +def load_mbb_schedule(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: """Load men's college basketball schedule data Example: @@ -100,6 +97,7 @@ def load_mbb_schedule(seasons: List[int]) -> pd.DataFrame: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing the @@ -108,15 +106,12 @@ def load_mbb_schedule(seasons: List[int]) -> pd.DataFrame: Raises: ValueError: If `season` is less than 2002. """ - data = pd.DataFrame() + data = pl.DataFrame() if type(seasons) is int: seasons = [seasons] for i in tqdm(seasons): if int(i) < 2002: raise SeasonNotFoundError("season cannot be less than 2002") - i_data = pd.read_parquet(MBB_TEAM_SCHEDULE_URL.format(season = i), engine='auto', columns=None) - data = pd.concat([data, i_data], axis = 0, ignore_index = True) - #Give each row a unique index - data.reset_index(drop=True, inplace=True) - - return data + i_data = pl.read_parquet(MBB_TEAM_SCHEDULE_URL.format(season = i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how = 'vertical') + return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data diff --git a/sportsdataverse/mbb/mbb_pbp.py b/sportsdataverse/mbb/mbb_pbp.py index 21ebda0..6c618ff 100755 --- a/sportsdataverse/mbb/mbb_pbp.py +++ b/sportsdataverse/mbb/mbb_pbp.py @@ -1,5 +1,6 @@ from typing import Dict import pandas as pd +import polars as pl import numpy as np import os import json @@ -7,11 +8,12 @@ from typing import List, Callable, Iterator, Union, Optional, Dict from sportsdataverse.dl_utils import download, flatten_json_iterative, key_check -def espn_mbb_pbp(game_id: int, raw = False) -> Dict: +def espn_mbb_pbp(game_id: int, raw = False, **kwargs) -> Dict: """espn_mbb_pbp() - Pull the game by id. Data from API endpoints: `mens-college-basketball/playbyplay`, `mens-college-basketball/summary` Args: game_id (int): Unique game_id, can be obtained from mbb_schedule(). + raw (bool): If True, returns the raw json from the API endpoint. If False, returns a cleaned dictionary of datasets. Returns: Dict: Dictionary of game data with keys: "gameId", "plays", "winprobability", "boxscore", "header", "broadcasts", @@ -22,12 +24,11 @@ def espn_mbb_pbp(game_id: int, raw = False) -> Dict: `mbb_df = sportsdataverse.mbb.espn_mbb_pbp(game_id=401265031)` """ # play by play - pbp_txt = {} - pbp_txt['timeouts'] = {} + pbp_txt = {'timeouts': {}} # summary endpoint for pickcenter array - summary_url = "http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/summary?event={}".format(game_id) - summary_resp = download(summary_url) - summary = json.loads(summary_resp) + summary_url = f"http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/summary?event={game_id}" + summary_resp = download(summary_url, **kwargs) + summary = summary_resp.json() incoming_keys_expected = [ 'boxscore', 'format', 'gameInfo', 'leaders', 'broadcasts', 'predictor', 'pickcenter', 'againstTheSpread', 'odds', 'winprobability', @@ -49,34 +50,71 @@ def espn_mbb_pbp(game_id: int, raw = False) -> Dict: if k in summary.keys(): pbp_json[k] = summary[k] else: - if k in dict_keys_expected: - pbp_json[k] = {} - else: - pbp_json[k] = [] + pbp_json[k] = {} if k in dict_keys_expected else [] return pbp_json for k in incoming_keys_expected: if k in summary.keys(): pbp_txt[k] = summary[k] else: - if k in dict_keys_expected: - pbp_txt[k] = {} - else: - pbp_txt[k] = [] + pbp_txt[k] = {} if k in dict_keys_expected else [] for k in ['news','shop']: - pbp_txt.pop('{}'.format(k), None) + pbp_txt.pop(f'{k}', None) - pbp_json = helper_mbb_pbp(game_id, pbp_txt) - return pbp_json + return helper_mbb_pbp(game_id, pbp_txt) def mbb_pbp_disk(game_id, path_to_json): - with open(os.path.join(path_to_json, "{}.json".format(game_id))) as json_file: + with open(os.path.join(path_to_json, f"{game_id}.json")) as json_file: pbp_txt = json.load(json_file) return pbp_txt def helper_mbb_pbp(game_id, pbp_txt): gameSpread, overUnder, homeFavorite, gameSpreadAvailable = helper_mbb_pickcenter(pbp_txt) + pbp_txt, gameSpread, overUnder, homeFavorite, gameSpreadAvailable, \ + homeTeamId, homeTeamMascot, homeTeamName,\ + homeTeamAbbrev, homeTeamNameAlt,\ + awayTeamId, awayTeamMascot, awayTeamName,\ + awayTeamAbbrev, awayTeamNameAlt = helper_mbb_game_data(pbp_txt, gameSpread, overUnder, homeFavorite, gameSpreadAvailable) + + if "plays" in pbp_txt.keys() and pbp_txt.get('header').get('competitions')[0].get('playByPlaySource') != 'none': + pbp_txt = helper_mbb_pbp_features(game_id, pbp_txt, gameSpread, homeFavorite, + gameSpreadAvailable, homeTeamId, awayTeamId, + homeTeamMascot, awayTeamMascot, homeTeamName, + awayTeamName, homeTeamAbbrev, awayTeamAbbrev, + homeTeamNameAlt, awayTeamNameAlt) + else: + pbp_txt['plays'] = pl.DataFrame() + pbp_txt['timeouts'] = { + homeTeamId: {"1": [], "2": []}, + awayTeamId: {"1": [], "2": []}, + } + # pbp_txt['plays'] = pbp_txt['plays'].replace({np.nan: None}) + return { + "gameId": int(game_id), + "plays": pbp_txt['plays'].to_dicts(), + "winprobability": np.array(pbp_txt['winprobability']).tolist(), + "boxscore": pbp_txt['boxscore'], + "header": pbp_txt['header'], + "format": pbp_txt['format'], + "broadcasts": np.array(pbp_txt['broadcasts']).tolist(), + "videos": np.array(pbp_txt['videos']).tolist(), + "playByPlaySource": pbp_txt['playByPlaySource'], + "standings": pbp_txt['standings'], + "article": pbp_txt['article'], + "leaders": np.array(pbp_txt['leaders']).tolist(), + "timeouts": pbp_txt['timeouts'], + "pickcenter": np.array(pbp_txt['pickcenter']).tolist(), + "againstTheSpread": np.array(pbp_txt['againstTheSpread']).tolist(), + "odds": np.array(pbp_txt['odds']).tolist(), + "predictor": pbp_txt['predictor'], + "espnWP": np.array(pbp_txt['espnWP']).tolist(), + "gameInfo": pbp_txt['gameInfo'], + "teamInfo": np.array(pbp_txt['teamInfo']).tolist(), + "season": np.array(pbp_txt['season']).tolist(), + } + +def helper_mbb_game_data(pbp_txt, gameSpread, overUnder, homeFavorite, gameSpreadAvailable): pbp_txt['timeouts'] = {} pbp_txt['teamInfo'] = pbp_txt['header']['competitions'][0] pbp_txt['season'] = pbp_txt['header']['season'] @@ -96,63 +134,29 @@ def helper_mbb_pbp(game_id, pbp_txt): homeTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['name']) homeTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['location']) homeTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['abbreviation']) - homeTeamNameAlt = re.sub("Stat(.+)", "St", str(homeTeamName)) + homeTeamNameAlt = re.sub("Stat(.+)", "St", homeTeamName) pbp_txt['header']['competitions'][0]['away'] = pbp_txt['header']['competitions'][0]['competitors'][1]['team'] awayTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['id']) awayTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['name']) awayTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['location']) awayTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['abbreviation']) - awayTeamNameAlt = re.sub("Stat(.+)", "St", str(awayTeamName)) + awayTeamNameAlt = re.sub("Stat(.+)", "St", awayTeamName) else: pbp_txt['header']['competitions'][0]['away'] = pbp_txt['header']['competitions'][0]['competitors'][0]['team'] awayTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['id']) awayTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['name']) awayTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['location']) awayTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['abbreviation']) - awayTeamNameAlt = re.sub("Stat(.+)", "St", str(awayTeamName)) + awayTeamNameAlt = re.sub("Stat(.+)", "St", awayTeamName) pbp_txt['header']['competitions'][0]['home'] = pbp_txt['header']['competitions'][0]['competitors'][1]['team'] homeTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['id']) homeTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['name']) homeTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['location']) homeTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['abbreviation']) - homeTeamNameAlt = re.sub("Stat(.+)", "St", str(homeTeamName)) - - if (pbp_txt['playByPlaySource'] != "none") & (len(pbp_txt['plays'])>1): - helper_mbb_pbp_features(game_id, pbp_txt, gameSpread, homeFavorite, - gameSpreadAvailable, homeTeamId, awayTeamId, - homeTeamMascot, awayTeamMascot, homeTeamName, - awayTeamName, homeTeamAbbrev, awayTeamAbbrev, - homeTeamNameAlt, awayTeamNameAlt) - else: - pbp_txt['plays'] = pd.DataFrame() - pbp_txt['timeouts'] = {} - pbp_txt['timeouts'][homeTeamId] = {"1": [], "2": []} - pbp_txt['timeouts'][awayTeamId] = {"1": [], "2": []} - pbp_txt['plays'] = pbp_txt['plays'].replace({np.nan: None}) - pbp_json = { - "gameId": game_id, - "plays" : pbp_txt['plays'].to_dict(orient='records'), - "winprobability" : np.array(pbp_txt['winprobability']).tolist(), - "boxscore" : pbp_txt['boxscore'], - "header" : pbp_txt['header'], - "format": pbp_txt['format'], - "broadcasts" : np.array(pbp_txt['broadcasts']).tolist(), - "videos" : np.array(pbp_txt['videos']).tolist(), - "playByPlaySource": pbp_txt['playByPlaySource'], - "standings" : pbp_txt['standings'], - "article" : pbp_txt['article'], - "leaders" : np.array(pbp_txt['leaders']).tolist(), - "timeouts" : pbp_txt['timeouts'], - "pickcenter" : np.array(pbp_txt['pickcenter']).tolist(), - "againstTheSpread" : np.array(pbp_txt['againstTheSpread']).tolist(), - "odds" : np.array(pbp_txt['odds']).tolist(), - "predictor" : pbp_txt['predictor'], - "espnWP" : np.array(pbp_txt['espnWP']).tolist(), - "gameInfo" : pbp_txt['gameInfo'], - "teamInfo" : np.array(pbp_txt['teamInfo']).tolist(), - "season" : np.array(pbp_txt['season']).tolist() - } - return pbp_json + homeTeamNameAlt = re.sub("Stat(.+)", "St", homeTeamName) + return pbp_txt, gameSpread, overUnder, homeFavorite, gameSpreadAvailable, homeTeamId,\ + homeTeamMascot,homeTeamName,homeTeamAbbrev,homeTeamNameAlt,\ + awayTeamId,awayTeamMascot,awayTeamName,awayTeamAbbrev,awayTeamNameAlt def helper_mbb_pickcenter(pbp_txt): # Spread definition @@ -161,12 +165,11 @@ def helper_mbb_pickcenter(pbp_txt): if "spread" in pbp_txt.get("pickcenter",{})[1].keys(): gameSpread = pbp_txt.get("pickcenter",{})[1].get("spread","") overUnder = pbp_txt.get("pickcenter",{})[1].get("overUnder","") - gameSpreadAvailable = True else: gameSpread = pbp_txt.get("pickcenter",{})[0].get("spread","") overUnder = pbp_txt.get("pickcenter",{})[0].get("overUnder","") - gameSpreadAvailable = True - # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") + gameSpreadAvailable = True + # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") else: gameSpread = 2.5 overUnder = 140.5 @@ -174,128 +177,156 @@ def helper_mbb_pickcenter(pbp_txt): gameSpreadAvailable = False return gameSpread, overUnder, homeFavorite, gameSpreadAvailable - def helper_mbb_pbp_features(game_id, pbp_txt, gameSpread, homeFavorite, gameSpreadAvailable, homeTeamId, awayTeamId, homeTeamMascot, awayTeamMascot, homeTeamName, awayTeamName, homeTeamAbbrev, awayTeamAbbrev, homeTeamNameAlt, awayTeamNameAlt): pbp_txt['plays_mod'] = [] for play in pbp_txt['plays']: p = flatten_json_iterative(play) pbp_txt['plays_mod'].append(p) - pbp_txt['plays'] = pd.json_normalize(pbp_txt,'plays_mod') - pbp_txt['plays']['season'] = pbp_txt['season']['year'] - pbp_txt['plays']['seasonType'] = pbp_txt['season']['type'] - pbp_txt['plays']["awayTeamId"] = awayTeamId - pbp_txt['plays']["awayTeamName"] = str(awayTeamName) - pbp_txt['plays']["awayTeamMascot"] = str(awayTeamMascot) - pbp_txt['plays']["awayTeamAbbrev"] = str(awayTeamAbbrev) - pbp_txt['plays']["awayTeamNameAlt"] = str(awayTeamNameAlt) - pbp_txt['plays']["homeTeamId"] = homeTeamId - pbp_txt['plays']["homeTeamName"] = str(homeTeamName) - pbp_txt['plays']["homeTeamMascot"] = str(homeTeamMascot) - pbp_txt['plays']["homeTeamAbbrev"] = str(homeTeamAbbrev) - pbp_txt['plays']["homeTeamNameAlt"] = str(homeTeamNameAlt) - # Spread definition - pbp_txt['plays']["homeTeamSpread"] = 2.5 - pbp_txt['plays']["gameSpread"] = abs(gameSpread) - pbp_txt['plays']["homeTeamSpread"] = np.where(homeFavorite == True, abs(gameSpread), -1*abs(gameSpread)) - pbp_txt['homeTeamSpread'] = np.where(homeFavorite == True, abs(gameSpread), -1*abs(gameSpread)) - pbp_txt['plays']["homeFavorite"] = homeFavorite - pbp_txt['plays']["gameSpread"] = gameSpread - pbp_txt['plays']["gameSpreadAvailable"] = gameSpreadAvailable - pbp_txt['plays'] = pbp_txt['plays'].to_dict(orient='records') - pbp_txt['plays'] = pd.DataFrame(pbp_txt['plays']) - pbp_txt['plays']['season'] = pbp_txt['header']['season']['year'] - pbp_txt['plays']['seasonType'] = pbp_txt['header']['season']['type'] - pbp_txt['plays']['game_id'] = int(game_id) - pbp_txt['plays']["homeTeamId"] = homeTeamId - pbp_txt['plays']["awayTeamId"] = awayTeamId - pbp_txt['plays']["homeTeamName"] = str(homeTeamName) - pbp_txt['plays']["awayTeamName"] = str(awayTeamName) - pbp_txt['plays']["homeTeamMascot"] = str(homeTeamMascot) - pbp_txt['plays']["awayTeamMascot"] = str(awayTeamMascot) - pbp_txt['plays']["homeTeamAbbrev"] = str(homeTeamAbbrev) - pbp_txt['plays']["awayTeamAbbrev"] = str(awayTeamAbbrev) - pbp_txt['plays']["homeTeamNameAlt"] = str(homeTeamNameAlt) - pbp_txt['plays']["awayTeamNameAlt"] = str(awayTeamNameAlt) - pbp_txt['plays']['period.number'] = pbp_txt['plays']['period.number'].apply(lambda x: int(x)) - pbp_txt['plays']['qtr'] = pbp_txt['plays']['period.number'].apply(lambda x: int(x)) - pbp_txt['plays']["homeTeamSpread"] = 2.5 - pbp_txt['plays']["gameSpread"] = abs(gameSpread) - pbp_txt['plays']["gameSpreadAvailable"] = gameSpreadAvailable - pbp_txt['plays']["homeTeamSpread"] = np.where(homeFavorite == True, abs(gameSpread), -1*abs(gameSpread)) - pbp_txt['homeTeamSpread'] = np.where(homeFavorite == True, abs(gameSpread), -1*abs(gameSpread)) - pbp_txt['plays']["homeFavorite"] = homeFavorite - pbp_txt['plays']["gameSpread"] = gameSpread - pbp_txt['plays']["homeFavorite"] = homeFavorite + pbp_txt['plays'] = pl.from_pandas(pd.json_normalize(pbp_txt,'plays_mod')) + pbp_txt['plays'] = pbp_txt['plays'].with_columns( + game_id = pl.lit(game_id).cast(pl.Int32), + id = (pl.col('id').cast(pl.Int64)), + season = pl.lit(pbp_txt['header']['season']['year']), + seasonType = pl.lit(pbp_txt['header']['season']['type']), + homeTeamId = pl.lit(homeTeamId), + homeTeamName = pl.lit(homeTeamName), + homeTeamMascot = pl.lit(homeTeamMascot), + homeTeamAbbrev = pl.lit(homeTeamAbbrev), + homeTeamNameAlt = pl.lit(homeTeamNameAlt), + awayTeamId = pl.lit(awayTeamId), + awayTeamName = pl.lit(awayTeamName), + awayTeamMascot = pl.lit(awayTeamMascot), + awayTeamAbbrev = pl.lit(awayTeamAbbrev), + awayTeamNameAlt = pl.lit(awayTeamNameAlt), + gameSpread = pl.lit(gameSpread).abs(), + homeFavorite = pl.lit(homeFavorite), + gameSpreadAvailable = pl.lit(gameSpreadAvailable), + ).with_columns( + homeTeamSpread = pl.when(pl.col('homeFavorite') == True) + .then(pl.col('gameSpread')) + .otherwise(-1*pl.col('gameSpread')), - #----- Time --------------- - pbp_txt['plays']['time'] = pbp_txt['plays']['clock.displayValue'] - pbp_txt['plays']['clock.mm'] = pbp_txt['plays']['clock.displayValue'].str.split(pat=':') - pbp_txt['plays'][['clock.minutes','clock.seconds']] = pbp_txt['plays']['clock.mm'].to_list() - pbp_txt['plays']['half'] = np.where(pbp_txt['plays']['qtr'] <= 2, "1","2") - pbp_txt['plays']['game_half'] = np.where(pbp_txt['plays']['qtr'] <= 2, "1","2") - pbp_txt['plays']['lag_qtr'] = pbp_txt['plays']['qtr'].shift(1) - pbp_txt['plays']['lead_qtr'] = pbp_txt['plays']['qtr'].shift(-1) - pbp_txt['plays']['lag_game_half'] = pbp_txt['plays']['game_half'].shift(1) - pbp_txt['plays']['lead_game_half'] = pbp_txt['plays']['game_half'].shift(-1) - pbp_txt['plays']['start.quarter_seconds_remaining'] = 60*pbp_txt['plays']['clock.minutes'].astype(int) + pbp_txt['plays']['clock.seconds'].astype(int) - pbp_txt['plays']['start.half_seconds_remaining'] = np.where( - pbp_txt['plays']['qtr'].isin([1,3]), - 600 + 60*pbp_txt['plays']['clock.minutes'].astype(int) + pbp_txt['plays']['clock.seconds'].astype(int), - 60*pbp_txt['plays']['clock.minutes'].astype(int) + pbp_txt['plays']['clock.seconds'].astype(int) - ) - pbp_txt['plays']['start.game_seconds_remaining'] = np.select( - [ - pbp_txt['plays']['qtr'] == 1, - pbp_txt['plays']['qtr'] == 2, - pbp_txt['plays']['qtr'] == 3, - pbp_txt['plays']['qtr'] == 4 - ], - [ - 1800 + 60*pbp_txt['plays']['clock.minutes'].astype(int) + pbp_txt['plays']['clock.seconds'].astype(int), - 1200 + 60*pbp_txt['plays']['clock.minutes'].astype(int) + pbp_txt['plays']['clock.seconds'].astype(int), - 600 + 60*pbp_txt['plays']['clock.minutes'].astype(int) + pbp_txt['plays']['clock.seconds'].astype(int), - 60*pbp_txt['plays']['clock.minutes'].astype(int) + pbp_txt['plays']['clock.seconds'].astype(int) - ], default = 60*pbp_txt['plays']['clock.minutes'].astype(int) + pbp_txt['plays']['clock.seconds'].astype(int) + + ).with_columns( + pl.col("period.number").cast(pl.Int32).alias("period.number"), + pl.col("period.number").cast(pl.Int32).alias("half"), + pl.col("clock.displayValue").alias("time"), + pl.col("clock.displayValue").str.split(':').list.to_struct(n_field_strategy="max_width").alias("clock.mm") + ).with_columns( + pl.col("clock.mm").struct.rename_fields(["clock.minutes", "clock.seconds"]) + ).unnest( + "clock.mm" + ).with_columns( + pl.col("clock.minutes").cast(pl.Int32), + pl.col("clock.seconds").cast(pl.Int32), + pl.when((pl.col("type.text") == "ShortTimeOut") + .and_( + pl.col("text").str.to_lowercase().str.contains(str(homeTeamAbbrev).lower()) + .or_( + pl.col("text").str.to_lowercase().str.contains(str(homeTeamAbbrev).lower()), + pl.col("text").str.to_lowercase().str.contains(str(homeTeamName).lower()), + pl.col("text").str.to_lowercase().str.contains(str(homeTeamMascot).lower()), + pl.col("text").str.to_lowercase().str.contains(str(homeTeamNameAlt).lower()) + ) + )) + .then(True) + .otherwise(False) + .alias("homeTimeoutCalled"), + pl.when((pl.col("type.text") == "ShortTimeOut") + .and_( + pl.col("text").str.to_lowercase().str.contains(str(awayTeamAbbrev).lower()) + .or_( + pl.col("text").str.to_lowercase().str.contains(str(awayTeamAbbrev).lower()), + pl.col("text").str.to_lowercase().str.contains(str(awayTeamName).lower()), + pl.col("text").str.to_lowercase().str.contains(str(awayTeamMascot).lower()), + pl.col("text").str.to_lowercase().str.contains(str(awayTeamNameAlt).lower()) + ) + )) + .then(True) + .otherwise(False) + .alias("awayTimeoutCalled"), + ).with_columns( + lag_period = pl.col("period.number").shift(1), + lead_period = pl.col("period.number").shift(-1), + lag_half = pl.col("half").shift(1), + lead_half = pl.col("half").shift(-1), + + ).with_columns( + + (60 * pl.col("clock.minutes") + pl.col("clock.seconds")).alias("start.period_seconds_remaining"), + pl.when(pl.col("period.number") == 1) + .then(1200 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) + .otherwise(60 * pl.col("clock.minutes") + pl.col("clock.seconds")) + .alias("start.game_seconds_remaining"), + ).with_columns( + pl.col("start.period_seconds_remaining").shift(-1).alias("end.period_seconds_remaining"), + pl.col("start.game_seconds_remaining").shift(-1).alias("end.game_seconds_remaining"), + ) + pbp_txt["timeouts"] = { + homeTeamId: {"1": [], "2": []}, + awayTeamId: {"1": [], "2": []}, + } + pbp_txt["timeouts"][homeTeamId]["1"] = ( + pbp_txt["plays"] + .filter( + (pl.col("homeTimeoutCalled") == True) + .and_(pl.col("period.number") <= 1) ) - # Pos Team - Start and End Id - pbp_txt['plays']['game_play_number'] = np.arange(len(pbp_txt['plays']))+1 - pbp_txt['plays']['text'] = pbp_txt['plays']['text'].astype(str) - pbp_txt['plays']['id'] = pbp_txt['plays']['id'].apply(lambda x: int(x)) - pbp_txt['plays']['end.quarter_seconds_remaining'] = pbp_txt['plays']['start.quarter_seconds_remaining'].shift(1) - pbp_txt['plays']['end.half_seconds_remaining'] = pbp_txt['plays']['start.half_seconds_remaining'].shift(1) - pbp_txt['plays']['end.game_seconds_remaining'] = pbp_txt['plays']['start.game_seconds_remaining'].shift(1) - pbp_txt['plays']['end.quarter_seconds_remaining'] = np.select( - [ - (pbp_txt['plays']['game_play_number'] == 1)| - ((pbp_txt['plays']['qtr'] == 2) & (pbp_txt['plays']['lag_qtr'] == 1))| - ((pbp_txt['plays']['qtr'] == 3) & (pbp_txt['plays']['lag_qtr'] == 2))| - ((pbp_txt['plays']['qtr'] == 4) & (pbp_txt['plays']['lag_qtr'] == 3)) - ], - [ - 600 - ], default = pbp_txt['plays']['end.quarter_seconds_remaining'] + .get_column("id") + .to_list() + ) + pbp_txt["timeouts"][homeTeamId]["2"] = ( + pbp_txt["plays"] + .filter( + (pl.col("homeTimeoutCalled") == True) + .and_(pl.col("period.number") > 1) ) - pbp_txt['plays']['end.half_seconds_remaining'] = np.select( - [ - (pbp_txt['plays']['game_play_number'] == 1)| - ((pbp_txt['plays']['game_half'] == "2") & (pbp_txt['plays']['lag_game_half'] == "1")) - ], - [ - 1200 - ], default = pbp_txt['plays']['end.half_seconds_remaining'] + .get_column("id") + .to_list() + ) + pbp_txt["timeouts"][awayTeamId]["1"] = ( + pbp_txt["plays"] + .filter( + (pl.col("awayTimeoutCalled") == True) + .and_(pl.col("period.number") <= 1) ) - pbp_txt['plays']['end.game_seconds_remaining'] = np.select( - [ - (pbp_txt['plays']['game_play_number'] == 1), - ((pbp_txt['plays']['game_half'] == "2") & (pbp_txt['plays']['lag_game_half'] == "1")) - ], - [ - 2400, - 1200 - ], default = pbp_txt['plays']['end.game_seconds_remaining'] + .get_column("id") + .to_list() + ) + pbp_txt["timeouts"][awayTeamId]["2"] = ( + pbp_txt["plays"] + .filter( + (pl.col("awayTimeoutCalled") == True) + .and_(pl.col("period.number") > 1) ) + .get_column("id") + .to_list() + ) - pbp_txt['plays']['period'] = pbp_txt['plays']['qtr'] - del pbp_txt['plays']['clock.mm'] + pbp_txt["plays"] = pbp_txt["plays"].with_row_count("game_play_number", 1) + + pbp_txt["plays"] = pbp_txt["plays"].with_columns( + pl.when((pl.col("game_play_number") == 1) + .or_((pl.col("lag_period") == 1) + .and_(pl.col("period.number") == 2))) + .then(1200) + .when((pl.col("lag_period") == 2) + .and_(pl.col("period.number") == 3)) + .then(300) + .otherwise(pl.col("end.period_seconds_remaining")) + .alias("end.period_seconds_remaining"), + pl.when((pl.col("game_play_number") == 1)) + .then(2400) + .when((pl.col("lag_period") == 1) + .and_(pl.col("period.number") == 2)) + .then(1200) + .when((pl.col("lag_period") == (pl.col("period.number") - 1)) + .and_(pl.col("period.number") >= 3)) + .then(300) + .otherwise(pl.col("end.game_seconds_remaining")) + .alias("end.game_seconds_remaining"), + ) + + return pbp_txt diff --git a/sportsdataverse/mbb/mbb_schedule.py b/sportsdataverse/mbb/mbb_schedule.py index 5f2c085..037c19e 100755 --- a/sportsdataverse/mbb/mbb_schedule.py +++ b/sportsdataverse/mbb/mbb_schedule.py @@ -6,7 +6,8 @@ from sportsdataverse.errors import SeasonNotFoundError from sportsdataverse.dl_utils import download, underscore -def espn_mbb_schedule(dates=None, groups=50, season_type=None, limit=500) -> pd.DataFrame: +def espn_mbb_schedule(dates=None, groups=50, season_type=None, limit=500, + return_as_pandas=True) -> pd.DataFrame: """espn_mbb_schedule - look up the men's college basketball scheduler for a given season Args: @@ -14,71 +15,105 @@ def espn_mbb_schedule(dates=None, groups=50, season_type=None, limit=500) -> pd. groups (int): Used to define different divisions. 50 is Division I, 51 is Division II/Division III. season_type (int): 2 for regular season, 3 for post-season, 4 for off-season. limit (int): number of records to return, default: 500. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. """ - if dates is None: - dates = '' - else: - dates = '&dates=' + str(dates) - if groups is None: - groups = '' - else: - groups = '&groups=' + str(groups) - if season_type is None: - season_type = '' - else: - season_type = '&seasontype=' + str(season_type) - url = "http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/scoreboard?limit={}{}{}{}".format(limit, dates, groups, season_type) - resp = download(url=url) + params = { + 'dates': dates, + 'seasonType': season_type, + 'groups': groups if groups is not None else '50', + 'limit': limit + } + url = "http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/scoreboard" + resp = download(url=url, params=params, **kwargs) + + ev = pl.DataFrame() + events_txt = resp.json() + events = events_txt.get('events') + if len(events) == 0: + return pd.DataFrame() if return_as_pandas else pl.DataFrame() + + for event in events: + event.get('competitions')[0].get('competitors')[0].get('team').pop('links',None) + event.get('competitions')[0].get('competitors')[1].get('team').pop('links',None) + if event.get('competitions')[0].get('competitors')[0].get('homeAway') == 'home': + event = __extract_home_away(event, 0, 'home') + event = __extract_home_away(event, 1, 'away') + else: + event = __extract_home_away(event, 0, 'away') + event = __extract_home_away(event, 1, 'home') + del_keys = ['geoBroadcasts', 'headlines', 'series', 'situation', 'tickets', 'odds'] + for k in del_keys: + event.get('competitions')[0].pop(k, None) + event.get('competitions')[0]['notes_type'] = event.get('competitions')[0]['notes'][0].get("type") if len(event.get('competitions')[0]['notes']) > 0 else '' + event.get('competitions')[0]['notes_headline'] = event.get('competitions')[0]['notes'][0].get("headline").replace('"','') if len(event.get('competitions')[0]['notes']) > 0 else '' + event.get('competitions')[0]['broadcast_market'] = event.get('competitions')[0].get('broadcasts', [])[0].get('market', "") if len(event.get('competitions')[0].get('broadcasts')) > 0 else "" + event.get('competitions')[0]['broadcast_name'] = event.get('competitions')[0].get('broadcasts', [])[0].get('names', [])[0] if len(event.get('competitions')[0].get('broadcasts')) > 0 else "" + event.get('competitions')[0].pop('broadcasts', None) + event.get('competitions')[0].pop('notes', None) + event.get('competitions')[0].pop('competitors', None) + x = pl.from_pandas(pd.json_normalize(event.get('competitions')[0], sep = '_')) + x = x.with_columns( + game_id = (pl.col('id').cast(pl.Int64)), + season = (event.get('season').get('year')), + season_type = (event.get('season').get('type')), + home_linescores = pl.when(pl.col('status_type_description') == 'Postponed').then(None).otherwise(pl.col('home_linescores')), + away_linescores = pl.when(pl.col('status_type_description') == 'Postponed').then(None).otherwise(pl.col('away_linescores')), + ) + x = x[[s.name for s in x if s.null_count() != x.height]] + ev = pl.concat([ev, x], how = 'diagonal') - ev = pd.DataFrame() - if resp is not None: - events_txt = json.loads(resp) - events = events_txt.get('events') - for event in events: - event.get('competitions')[0].get('competitors')[0].get('team').pop('links',None) - event.get('competitions')[0].get('competitors')[1].get('team').pop('links',None) - if event.get('competitions')[0].get('competitors')[0].get('homeAway')=='home': - event['competitions'][0]['home'] = event.get('competitions')[0].get('competitors')[0].get('team') - event['competitions'][0]['home']['score'] = event.get('competitions')[0].get('competitors')[0].get('score') - event['competitions'][0]['home']['winner'] = event.get('competitions')[0].get('competitors')[0].get('winner') - event['competitions'][0]['away'] = event.get('competitions')[0].get('competitors')[1].get('team') - event['competitions'][0]['away']['score'] = event.get('competitions')[0].get('competitors')[1].get('score') - event['competitions'][0]['away']['winner'] = event.get('competitions')[0].get('competitors')[1].get('winner') - else: - event['competitions'][0]['away'] = event.get('competitions')[0].get('competitors')[0].get('team') - event['competitions'][0]['away']['score'] = event.get('competitions')[0].get('competitors')[0].get('score') - event['competitions'][0]['away']['winner'] = event.get('competitions')[0].get('competitors')[0].get('winner') - event['competitions'][0]['home'] = event.get('competitions')[0].get('competitors')[1].get('team') - event['competitions'][0]['home']['score'] = event.get('competitions')[0].get('competitors')[1].get('score') - event['competitions'][0]['home']['winner'] = event.get('competitions')[0].get('competitors')[1].get('winner') + ev.columns = [underscore(c) for c in ev.columns] - del_keys = ['broadcasts','geoBroadcasts', 'headlines', 'series', 'situation', 'tickets', 'odds'] - for k in del_keys: - event.get('competitions')[0].pop(k, None) - if len(event.get('competitions')[0]['notes'])>0: - event.get('competitions')[0]['notes_type'] = event.get('competitions')[0]['notes'][0].get("type") - event.get('competitions')[0]['notes_headline'] = event.get('competitions')[0]['notes'][0].get("headline").replace('"','') - else: - event.get('competitions')[0]['notes_type'] = '' - event.get('competitions')[0]['notes_headline'] = '' - event.get('competitions')[0].pop('notes', None) - x = pd.json_normalize(event.get('competitions')[0], sep='_') - x['game_id'] = x['id'].astype(int) - x['season'] = event.get('season').get('year') - x['season_type'] = event.get('season').get('type') - ev = pd.concat([ev,x],axis=0, ignore_index=True) - ev = pd.DataFrame(ev) - ev.columns = [underscore(c) for c in ev.columns.tolist()] - return ev + return ev.to_pandas() if return_as_pandas else ev -def espn_mbb_calendar(season=None, ondays=None) -> pd.DataFrame: +def __extract_home_away(event, arg1, arg2): + event['competitions'][0][arg2] = ( + event.get('competitions')[0].get('competitors')[arg1].get('team') + ) + event['competitions'][0][arg2]['score'] = ( + event.get('competitions')[0].get('competitors')[arg1].get('score') + ) + event['competitions'][0][arg2]['winner'] = ( + event.get('competitions')[0].get('competitors')[arg1].get('winner') + ) + ## add winner back to main competitors if does not exist + event['competitions'][0]['competitors'][arg1]['winner'] = ( + event.get('competitions')[0].get('competitors')[arg1].get('winner', False) + ) + event['competitions'][0][arg2]['currentRank'] = ( + event.get('competitions')[0] + .get('competitors')[arg1] + .get('curatedRank', {}) + .get('current', 99) + ) + event['competitions'][0][arg2]['linescores'] = ( + event.get('competitions')[0] + .get('competitors')[arg1] + .get('linescores', [{'value': 'N/A'}]) + ) + ## add linescores back to main competitors if does not exist + event['competitions'][0]['competitors'][arg1]['linescores'] = ( + event.get('competitions')[0] + .get('competitors')[arg1] + .get('linescores', []) + ) + event['competitions'][0][arg2]['records'] = ( + event.get('competitions')[0] + .get('competitors')[arg1] + .get('records', []) + ) + return event + +def espn_mbb_calendar(season=None, ondays=None, + return_as_pandas = True, **kwargs) -> pd.DataFrame: """espn_mbb_calendar - look up the men's college basketball calendar for a given season Args: season (int): Used to define different seasons. 2002 is the earliest available season. ondays (boolean): Used to return dates for calendar ondays + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing @@ -90,38 +125,47 @@ def espn_mbb_calendar(season=None, ondays=None) -> pd.DataFrame: if int(season) < 2002: raise SeasonNotFoundError("season cannot be less than 2002") if ondays is not None: - url = "https://sports.core.api.espn.com/v2/sports/basketball/leagues/mens-college-basketball/seasons/{}/types/2/calendar/ondays".format(season) - resp = download(url=url) - txt = json.loads(resp).get('eventDate').get('dates') - full_schedule = pd.DataFrame(txt,columns=['dates']) - full_schedule['dateURL'] = list(map(lambda x: x[:10].replace("-",""),full_schedule['dates'])) - full_schedule['url']="http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/scoreboard?dates=" - full_schedule['url']= full_schedule['url'] + full_schedule['dateURL'] + full_schedule = __ondays_mbb_calendar(season, **kwargs) else: - url = "http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/scoreboard?dates={}".format(season) - resp = download(url=url) - txt = json.loads(resp)['leagues'][0]['calendar'] + url = f"http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/scoreboard?dates={season}" + resp = download(url=url, **kwargs) + txt = resp.json().get('leagues')[0].get('calendar') datenum = list(map(lambda x: x[:10].replace("-",""),txt)) date = list(map(lambda x: x[:10],txt)) year = list(map(lambda x: x[:4],txt)) month = list(map(lambda x: x[5:7],txt)) day = list(map(lambda x: x[8:10],txt)) - - data = {"season": season, - "datetime" : txt, - "date" : date, - "year": year, - "month": month, - "day": day, - "dateURL": datenum + data = { + "season": season, + "datetime" : txt, + "date" : date, + "year": year, + "month": month, + "day": day, + "dateURL": datenum } - full_schedule = pd.DataFrame(data) - full_schedule['url']="http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/scoreboard?dates=" - full_schedule['url']= full_schedule['url'] + full_schedule['dateURL'] - return full_schedule + full_schedule = pl.DataFrame(data) + full_schedule = full_schedule.with_columns( + url="http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/scoreboard?dates=" + + pl.col('dateURL') + ) + return full_schedule.to_pandas() if return_as_pandas else full_schedule + +def __ondays_mbb_calendar(season, **kwargs): + url = f"https://sports.core.api.espn.com/v2/sports/basketball/leagues/mens-college-basketball/seasons/{season}/types/2/calendar/ondays" + resp = download(url=url, **kwargs) + txt = resp.json().get('eventDate').get('dates') + result = pl.DataFrame(txt, schema=['dates']) + result = result.with_columns(dateURL = pl.col('dates').str.slice(0, 10)) + result = result.with_columns( + url="http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/scoreboard?dates=" + + pl.col('dateURL') + ) + + return result def most_recent_mbb_season(): - if datetime.datetime.today().month >= 10: - return datetime.datetime.today().year + 1 - else: - return datetime.datetime.today().year \ No newline at end of file + if datetime.datetime.now().month >= 10: + return datetime.datetime.now().year + 1 + else: + return datetime.datetime.now().year \ No newline at end of file diff --git a/sportsdataverse/mbb/mbb_teams.py b/sportsdataverse/mbb/mbb_teams.py index 65745d1..00d85cc 100755 --- a/sportsdataverse/mbb/mbb_teams.py +++ b/sportsdataverse/mbb/mbb_teams.py @@ -3,24 +3,24 @@ from sportsdataverse.dl_utils import download, underscore from urllib.error import URLError, HTTPError, ContentTooShortError -def espn_mbb_teams(groups=None) -> pd.DataFrame: +def espn_mbb_teams(groups=None, return_as_pandas=True) -> pd.DataFrame: """espn_mbb_teams - look up the men's college basketball teams Args: groups (int): Used to define different divisions. 50 is Division I, 51 is Division II/Division III. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing teams for the requested league. """ - if groups is None: - groups = '&groups=50' - else: - groups = '&groups=' + str(groups) - ev = pd.DataFrame() - url = "http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/teams?limit=1000{}".format(groups) - resp = download(url=url) + url = "http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/teams" + params = { + "groups": groups if groups is not None else "50", + "limit": 1000 + } + resp = download(url=url, params = params, **kwargs) if resp is not None: - events_txt = json.loads(resp) + events_txt = resp.json() teams = events_txt.get('sports')[0].get('leagues')[0].get('teams') del_keys = ['record', 'links'] @@ -29,5 +29,5 @@ def espn_mbb_teams(groups=None) -> pd.DataFrame: team.get('team').pop(k, None) teams = pd.json_normalize(teams, sep='_') teams.columns = [underscore(c) for c in teams.columns.tolist()] - return teams + return teams if return_as_pandas else pl.from_pandas(teams) From c79a0a15ef5ba91f31a5c43e1affdb199971ea00 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Mon, 10 Jul 2023 00:04:21 -0400 Subject: [PATCH 08/79] Function `espn_nba_game_rosters()` added --- sportsdataverse/nba/nba_game_rosters.py | 195 ++++++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 sportsdataverse/nba/nba_game_rosters.py diff --git a/sportsdataverse/nba/nba_game_rosters.py b/sportsdataverse/nba/nba_game_rosters.py new file mode 100644 index 0000000..9d41631 --- /dev/null +++ b/sportsdataverse/nba/nba_game_rosters.py @@ -0,0 +1,195 @@ +import pandas as pd +import polars as pl +import numpy as np +import os +import json +import re +from typing import List, Callable, Iterator, Union, Optional, Dict +from sportsdataverse.dl_utils import download, underscore + +def espn_nba_game_rosters(game_id: int, raw = False, return_as_pandas = True, **kwargs) -> pd.DataFrame: + """espn_nba_game_rosters() - Pull the game by id. + + Args: + game_id (int): Unique game_id, can be obtained from espn_nba_schedule(). + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + + Returns: + pd.DataFrame: Data frame of game roster data with columns: + 'athlete_id', 'athlete_uid', 'athlete_guid', 'athlete_type', + 'first_name', 'last_name', 'full_name', 'athlete_display_name', + 'short_name', 'weight', 'display_weight', 'height', 'display_height', + 'age', 'date_of_birth', 'slug', 'jersey', 'linked', 'active', + 'alternate_ids_sdr', 'birth_place_city', 'birth_place_state', + 'birth_place_country', 'headshot_href', 'headshot_alt', + 'experience_years', 'experience_display_value', + 'experience_abbreviation', 'status_id', 'status_name', 'status_type', + 'status_abbreviation', 'hand_type', 'hand_abbreviation', + 'hand_display_value', 'draft_display_text', 'draft_round', 'draft_year', + 'draft_selection', 'player_id', 'starter', 'valid', 'did_not_play', + 'display_name', 'ejected', 'athlete_href', 'position_href', + 'statistics_href', 'team_id', 'team_guid', 'team_uid', 'team_slug', + 'team_location', 'team_name', 'team_abbreviation', + 'team_display_name', 'team_short_display_name', 'team_color', + 'team_alternate_color', 'is_active', 'is_all_star', + 'logo_href', 'logo_dark_href', 'game_id' + Example: + `nba_df = sportsdataverse.nba.espn_nba_game_rosters(game_id=401307514)` + """ + # play by play + pbp_txt = {} + # summary endpoint for pickcenter array + summary_url = "https://sports.core.api.espn.com/v2/sports/basketball/leagues/nba/events/{x}/competitions/{x}/competitors".format(x=game_id) + summary_resp = download(summary_url, **kwargs) + summary = summary_resp.json() + items = helper_nba_game_items(summary) + team_rosters = helper_nba_roster_items(items = items, summary_url = summary_url, **kwargs) + team_rosters = team_rosters.join(items[['team_id', 'order', 'home_away', 'winner']], how = 'left', on = 'team_id') + teams_df = helper_nba_team_items(items = items, **kwargs) + teams_rosters = team_rosters.join(teams_df, how = 'left',on = 'team_id') + athletes = helper_nba_athlete_items(teams_rosters = team_rosters, **kwargs) + rosters = athletes.join(teams_rosters, how = 'left', left_on = 'athlete_id', right_on = 'player_id') + rosters = rosters.with_columns( + game_id = pl.lit(game_id).cast(pl.Int64) + ) + rosters.columns = [underscore(c) for c in rosters.columns] + return rosters.to_pandas() if return_as_pandas else rosters + +def helper_nba_game_items(summary): + items = pl.from_pandas(pd.json_normalize(summary, record_path = "items", sep = '_')) + items.columns = [col.replace("$ref", "href") for col in items.columns] + + items.columns = [underscore(c) for c in items.columns] + items = items.rename({ + "id": "team_id", + "uid": "team_uid", + "statistics_href": "team_statistics_href" + } + ) + items = items.with_columns( + team_id = pl.col("team_id").cast(pl.Int32) + ) + + return items + +def helper_nba_team_items(items, **kwargs): + pop_cols = [ + '$ref', + 'record', + 'athletes', + 'venue', + 'groups', + 'ranks', + 'statistics', + 'leaders', + 'links', + 'notes', + 'againstTheSpreadRecords', + 'franchise', + 'events', + 'college', + 'depthCharts', + 'transactions', + 'awards', + 'injuries', + 'coaches' + ] + teams_df = pl.DataFrame() + for x in items['team_href']: + team = download(x, **kwargs).json() + for k in pop_cols: + team.pop(k, None) + team_row = pl.from_pandas(pd.json_normalize(team, sep = '_')) + teams_df = pl.concat([teams_df, team_row], how = 'vertical') + + teams_df.columns = [ + 'team_id', + 'team_guid', + 'team_uid', + 'team_slug', + 'team_location', + 'team_name', + 'team_abbreviation', + 'team_display_name', + 'team_short_display_name', + 'team_color', + 'team_alternate_color', + 'is_active', + 'is_all_star', + 'logos' + ] + teams_df = teams_df.with_columns( + logo_href = pl.lit(""), + logo_dark_href = pl.lit("") + ) + for row in range(len(teams_df['logos'])): + team = teams_df['logos'][row] + teams_df[row, 'logo_href'] = team[0]['href'] + teams_df[row, 'logo_dark_href'] = team[1]['href'] + + teams_df = teams_df.drop(['logos']) + teams_df = teams_df.with_columns( + team_id = pl.col("team_id").cast(pl.Int32) + ) + return teams_df + +def helper_nba_roster_items(items, summary_url, **kwargs): + team_ids = list(items['team_id']) + game_rosters = pl.DataFrame() + for tm in team_ids: + team_roster_url = "{x}/{t}/roster".format(x = summary_url, t = tm) + team_roster_resp = download(team_roster_url, **kwargs) + team_roster = pl.from_pandas(pd.json_normalize(team_roster_resp.json().get('entries',[]), sep = '_')) + team_roster.columns = [col.replace("$ref", "href") for col in team_roster.columns] + team_roster.columns = [underscore(c) for c in team_roster.columns] + team_roster= team_roster.with_columns( + team_id = pl.lit(tm).cast(pl.Int32) + ) + game_rosters = pl.concat([game_rosters, team_roster], how = 'vertical') + game_rosters = game_rosters.drop(["period", "for_player_id", "active"]) + game_rosters = game_rosters.with_columns( + player_id = pl.col('player_id').cast(pl.Int64), + team_id = pl.col('team_id').cast(pl.Int32) + ) + return game_rosters + +def helper_nba_athlete_items(teams_rosters, **kwargs): + athlete_hrefs = list(teams_rosters['athlete_href']) + game_athletes = pl.DataFrame() + pop_cols = [ + 'links', + 'injuries', + 'teams', + 'team', + 'college', + 'proAthlete', + 'statistics', + 'notes', + 'eventLog', + "$ref", + "position" + ] + for athlete_href in athlete_hrefs: + + athlete_res = download(athlete_href, **kwargs) + athlete_resp = athlete_res.json() + for k in pop_cols: + athlete_resp.pop(k, None) + athlete = pl.from_pandas(pd.json_normalize(athlete_resp, sep='_')) + athlete.columns = [col.replace("$ref", "href") for col in athlete.columns] + athlete.columns = [underscore(c) for c in athlete.columns] + + game_athletes = pl.concat([game_athletes, athlete], how = 'diagonal') + + + game_athletes = game_athletes.rename({ + "id": "athlete_id", + "uid": "athlete_uid", + "guid": "athlete_guid", + "type": "athlete_type", + "display_name": "athlete_display_name" + }) + game_athletes = game_athletes.with_columns( + athlete_id = pl.col("athlete_id").cast(pl.Int64) + ) + return game_athletes \ No newline at end of file From 3234bc0df9e29ed9cbd796bdbd4e878d2c8dcb60 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Mon, 10 Jul 2023 00:04:51 -0400 Subject: [PATCH 09/79] Update mbb_loaders.py --- sportsdataverse/mbb/mbb_loaders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sportsdataverse/mbb/mbb_loaders.py b/sportsdataverse/mbb/mbb_loaders.py index 59b84e5..9fd043c 100755 --- a/sportsdataverse/mbb/mbb_loaders.py +++ b/sportsdataverse/mbb/mbb_loaders.py @@ -1,5 +1,5 @@ import pandas as pd -import pandas as pl +import polars as pl import json from tqdm import tqdm from typing import List, Callable, Iterator, Union, Optional From 713e07a090b295ef91ff7cc18038ce4d0674c89a Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Mon, 10 Jul 2023 01:13:00 -0400 Subject: [PATCH 10/79] Missing imports added --- sportsdataverse/cfb/cfb_loaders.py | 1 - sportsdataverse/mbb/mbb_schedule.py | 3 ++- sportsdataverse/mbb/mbb_teams.py | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/sportsdataverse/cfb/cfb_loaders.py b/sportsdataverse/cfb/cfb_loaders.py index 5e95a4b..db25781 100755 --- a/sportsdataverse/cfb/cfb_loaders.py +++ b/sportsdataverse/cfb/cfb_loaders.py @@ -30,7 +30,6 @@ def load_cfb_pbp(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: if int(i) < 2003: raise SeasonNotFoundError("season cannot be less than 2003") i_data = pl.read_parquet(CFB_BASE_URL.format(season=i), use_pyarrow=True, columns=None) - #data = data.append(i_data) data = pl.concat([data, i_data], how = 'vertical') return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data diff --git a/sportsdataverse/mbb/mbb_schedule.py b/sportsdataverse/mbb/mbb_schedule.py index 037c19e..0f15431 100755 --- a/sportsdataverse/mbb/mbb_schedule.py +++ b/sportsdataverse/mbb/mbb_schedule.py @@ -1,4 +1,5 @@ import pandas as pd +import polars as pl import numpy as np import json import datetime @@ -19,13 +20,13 @@ def espn_mbb_schedule(dates=None, groups=50, season_type=None, limit=500, Returns: pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. """ + url = "http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/scoreboard" params = { 'dates': dates, 'seasonType': season_type, 'groups': groups if groups is not None else '50', 'limit': limit } - url = "http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/scoreboard" resp = download(url=url, params=params, **kwargs) ev = pl.DataFrame() diff --git a/sportsdataverse/mbb/mbb_teams.py b/sportsdataverse/mbb/mbb_teams.py index 00d85cc..8433fbe 100755 --- a/sportsdataverse/mbb/mbb_teams.py +++ b/sportsdataverse/mbb/mbb_teams.py @@ -1,9 +1,10 @@ import pandas as pd +import polars as pl import json from sportsdataverse.dl_utils import download, underscore from urllib.error import URLError, HTTPError, ContentTooShortError -def espn_mbb_teams(groups=None, return_as_pandas=True) -> pd.DataFrame: +def espn_mbb_teams(groups=None, return_as_pandas=True, **kwargs) -> pd.DataFrame: """espn_mbb_teams - look up the men's college basketball teams Args: From f5ad90d1e311ccbaa355b2dfa757b1d401049e48 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Mon, 10 Jul 2023 01:14:29 -0400 Subject: [PATCH 11/79] simplifying the returns on the helper functions in the `mbb.espn_mbb_pbp()` function --- sportsdataverse/mbb/mbb_pbp.py | 110 +++++++++++++++++---------------- 1 file changed, 57 insertions(+), 53 deletions(-) diff --git a/sportsdataverse/mbb/mbb_pbp.py b/sportsdataverse/mbb/mbb_pbp.py index 6c618ff..73eab26 100755 --- a/sportsdataverse/mbb/mbb_pbp.py +++ b/sportsdataverse/mbb/mbb_pbp.py @@ -70,19 +70,11 @@ def mbb_pbp_disk(game_id, path_to_json): return pbp_txt def helper_mbb_pbp(game_id, pbp_txt): - gameSpread, overUnder, homeFavorite, gameSpreadAvailable = helper_mbb_pickcenter(pbp_txt) - pbp_txt, gameSpread, overUnder, homeFavorite, gameSpreadAvailable, \ - homeTeamId, homeTeamMascot, homeTeamName,\ - homeTeamAbbrev, homeTeamNameAlt,\ - awayTeamId, awayTeamMascot, awayTeamName,\ - awayTeamAbbrev, awayTeamNameAlt = helper_mbb_game_data(pbp_txt, gameSpread, overUnder, homeFavorite, gameSpreadAvailable) + init = helper_mbb_pickcenter(pbp_txt) + pbp_txt, init = helper_mbb_game_data(pbp_txt, init) if "plays" in pbp_txt.keys() and pbp_txt.get('header').get('competitions')[0].get('playByPlaySource') != 'none': - pbp_txt = helper_mbb_pbp_features(game_id, pbp_txt, gameSpread, homeFavorite, - gameSpreadAvailable, homeTeamId, awayTeamId, - homeTeamMascot, awayTeamMascot, homeTeamName, - awayTeamName, homeTeamAbbrev, awayTeamAbbrev, - homeTeamNameAlt, awayTeamNameAlt) + pbp_txt = helper_mbb_pbp_features(game_id, pbp_txt, init) else: pbp_txt['plays'] = pl.DataFrame() pbp_txt['timeouts'] = { @@ -114,19 +106,19 @@ def helper_mbb_pbp(game_id, pbp_txt): "season": np.array(pbp_txt['season']).tolist(), } -def helper_mbb_game_data(pbp_txt, gameSpread, overUnder, homeFavorite, gameSpreadAvailable): +def helper_mbb_game_data(pbp_txt, init): pbp_txt['timeouts'] = {} pbp_txt['teamInfo'] = pbp_txt['header']['competitions'][0] pbp_txt['season'] = pbp_txt['header']['season'] pbp_txt['playByPlaySource'] = pbp_txt['header']['competitions'][0]['playByPlaySource'] pbp_txt['boxscoreSource'] = pbp_txt['header']['competitions'][0]['boxscoreSource'] - pbp_txt['gameSpreadAvailable'] = gameSpreadAvailable - pbp_txt['gameSpread'] = gameSpread - pbp_txt["homeFavorite"] = homeFavorite + pbp_txt['gameSpreadAvailable'] = init["gameSpreadAvailable"] + pbp_txt['gameSpread'] = init["gameSpread"] + pbp_txt["homeFavorite"] = init["homeFavorite"] pbp_txt["homeTeamSpread"] = np.where( - homeFavorite == True, abs(gameSpread), -1 * abs(gameSpread) + init["homeFavorite"] == True, abs(init["gameSpread"]), -1 * abs(init["gameSpread"]) ) - pbp_txt["overUnder"] = overUnder + pbp_txt["overUnder"] = init["overUnder"] # Home and Away identification variables if pbp_txt['header']['competitions'][0]['competitors'][0]['homeAway']=='home': pbp_txt['header']['competitions'][0]['home'] = pbp_txt['header']['competitions'][0]['competitors'][0]['team'] @@ -154,9 +146,17 @@ def helper_mbb_game_data(pbp_txt, gameSpread, overUnder, homeFavorite, gameSprea homeTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['location']) homeTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['abbreviation']) homeTeamNameAlt = re.sub("Stat(.+)", "St", homeTeamName) - return pbp_txt, gameSpread, overUnder, homeFavorite, gameSpreadAvailable, homeTeamId,\ - homeTeamMascot,homeTeamName,homeTeamAbbrev,homeTeamNameAlt,\ - awayTeamId,awayTeamMascot,awayTeamName,awayTeamAbbrev,awayTeamNameAlt + init["homeTeamId"] = homeTeamId + init["homeTeamMascot"] = homeTeamMascot + init["homeTeamName"] = homeTeamName + init["homeTeamAbbrev"] = homeTeamAbbrev + init["homeTeamNameAlt"] = homeTeamNameAlt + init["awayTeamId"] = awayTeamId + init["awayTeamMascot"] = awayTeamMascot + init["awayTeamName"] = awayTeamName + init["awayTeamAbbrev"] = awayTeamAbbrev + init["awayTeamNameAlt"] = awayTeamNameAlt + return pbp_txt, init def helper_mbb_pickcenter(pbp_txt): # Spread definition @@ -175,9 +175,15 @@ def helper_mbb_pickcenter(pbp_txt): overUnder = 140.5 homeFavorite = True gameSpreadAvailable = False - return gameSpread, overUnder, homeFavorite, gameSpreadAvailable -def helper_mbb_pbp_features(game_id, pbp_txt, gameSpread, homeFavorite, gameSpreadAvailable, homeTeamId, awayTeamId, homeTeamMascot, awayTeamMascot, homeTeamName, awayTeamName, homeTeamAbbrev, awayTeamAbbrev, homeTeamNameAlt, awayTeamNameAlt): + return { + "gameSpread": gameSpread, + "overUnder": overUnder, + "homeFavorite": homeFavorite, + "gameSpreadAvailable": gameSpreadAvailable + } + +def helper_mbb_pbp_features(game_id, pbp_txt, init): pbp_txt['plays_mod'] = [] for play in pbp_txt['plays']: p = flatten_json_iterative(play) @@ -188,25 +194,23 @@ def helper_mbb_pbp_features(game_id, pbp_txt, gameSpread, homeFavorite, gameSpre id = (pl.col('id').cast(pl.Int64)), season = pl.lit(pbp_txt['header']['season']['year']), seasonType = pl.lit(pbp_txt['header']['season']['type']), - homeTeamId = pl.lit(homeTeamId), - homeTeamName = pl.lit(homeTeamName), - homeTeamMascot = pl.lit(homeTeamMascot), - homeTeamAbbrev = pl.lit(homeTeamAbbrev), - homeTeamNameAlt = pl.lit(homeTeamNameAlt), - awayTeamId = pl.lit(awayTeamId), - awayTeamName = pl.lit(awayTeamName), - awayTeamMascot = pl.lit(awayTeamMascot), - awayTeamAbbrev = pl.lit(awayTeamAbbrev), - awayTeamNameAlt = pl.lit(awayTeamNameAlt), - gameSpread = pl.lit(gameSpread).abs(), - homeFavorite = pl.lit(homeFavorite), - gameSpreadAvailable = pl.lit(gameSpreadAvailable), + homeTeamId = pl.lit(init["homeTeamId"]), + homeTeamName = pl.lit(init["homeTeamName"]), + homeTeamMascot = pl.lit(init["homeTeamMascot"]), + homeTeamAbbrev = pl.lit(init["homeTeamAbbrev"]), + homeTeamNameAlt = pl.lit(init["homeTeamNameAlt"]), + awayTeamId = pl.lit(init["awayTeamId"]), + awayTeamName = pl.lit(init["awayTeamName"]), + awayTeamMascot = pl.lit(init["awayTeamMascot"]), + awayTeamAbbrev = pl.lit(init["awayTeamAbbrev"]), + awayTeamNameAlt = pl.lit(init["awayTeamNameAlt"]), + gameSpread = pl.lit(init["gameSpread"]).abs(), + homeFavorite = pl.lit(init["homeFavorite"]), + gameSpreadAvailable = pl.lit(init["gameSpreadAvailable"]), ).with_columns( homeTeamSpread = pl.when(pl.col('homeFavorite') == True) .then(pl.col('gameSpread')) .otherwise(-1*pl.col('gameSpread')), - - ).with_columns( pl.col("period.number").cast(pl.Int32).alias("period.number"), pl.col("period.number").cast(pl.Int32).alias("half"), @@ -221,12 +225,12 @@ def helper_mbb_pbp_features(game_id, pbp_txt, gameSpread, homeFavorite, gameSpre pl.col("clock.seconds").cast(pl.Int32), pl.when((pl.col("type.text") == "ShortTimeOut") .and_( - pl.col("text").str.to_lowercase().str.contains(str(homeTeamAbbrev).lower()) + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamAbbrev"]).lower()) .or_( - pl.col("text").str.to_lowercase().str.contains(str(homeTeamAbbrev).lower()), - pl.col("text").str.to_lowercase().str.contains(str(homeTeamName).lower()), - pl.col("text").str.to_lowercase().str.contains(str(homeTeamMascot).lower()), - pl.col("text").str.to_lowercase().str.contains(str(homeTeamNameAlt).lower()) + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamAbbrev"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamName"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamMascot"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamNameAlt"]).lower()) ) )) .then(True) @@ -234,12 +238,12 @@ def helper_mbb_pbp_features(game_id, pbp_txt, gameSpread, homeFavorite, gameSpre .alias("homeTimeoutCalled"), pl.when((pl.col("type.text") == "ShortTimeOut") .and_( - pl.col("text").str.to_lowercase().str.contains(str(awayTeamAbbrev).lower()) + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamAbbrev"]).lower()) .or_( - pl.col("text").str.to_lowercase().str.contains(str(awayTeamAbbrev).lower()), - pl.col("text").str.to_lowercase().str.contains(str(awayTeamName).lower()), - pl.col("text").str.to_lowercase().str.contains(str(awayTeamMascot).lower()), - pl.col("text").str.to_lowercase().str.contains(str(awayTeamNameAlt).lower()) + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamAbbrev"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamName"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamMascot"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamNameAlt"]).lower()) ) )) .then(True) @@ -263,10 +267,10 @@ def helper_mbb_pbp_features(game_id, pbp_txt, gameSpread, homeFavorite, gameSpre pl.col("start.game_seconds_remaining").shift(-1).alias("end.game_seconds_remaining"), ) pbp_txt["timeouts"] = { - homeTeamId: {"1": [], "2": []}, - awayTeamId: {"1": [], "2": []}, + init["homeTeamId"]: {"1": [], "2": []}, + init["awayTeamId"]: {"1": [], "2": []}, } - pbp_txt["timeouts"][homeTeamId]["1"] = ( + pbp_txt["timeouts"][init["homeTeamId"]]["1"] = ( pbp_txt["plays"] .filter( (pl.col("homeTimeoutCalled") == True) @@ -275,7 +279,7 @@ def helper_mbb_pbp_features(game_id, pbp_txt, gameSpread, homeFavorite, gameSpre .get_column("id") .to_list() ) - pbp_txt["timeouts"][homeTeamId]["2"] = ( + pbp_txt["timeouts"][init["homeTeamId"]]["2"] = ( pbp_txt["plays"] .filter( (pl.col("homeTimeoutCalled") == True) @@ -284,7 +288,7 @@ def helper_mbb_pbp_features(game_id, pbp_txt, gameSpread, homeFavorite, gameSpre .get_column("id") .to_list() ) - pbp_txt["timeouts"][awayTeamId]["1"] = ( + pbp_txt["timeouts"][init["awayTeamId"]]["1"] = ( pbp_txt["plays"] .filter( (pl.col("awayTimeoutCalled") == True) @@ -293,7 +297,7 @@ def helper_mbb_pbp_features(game_id, pbp_txt, gameSpread, homeFavorite, gameSpre .get_column("id") .to_list() ) - pbp_txt["timeouts"][awayTeamId]["2"] = ( + pbp_txt["timeouts"][init["awayTeamId"]]["2"] = ( pbp_txt["plays"] .filter( (pl.col("awayTimeoutCalled") == True) From 738de332c658985f04b3d31527a431c58db40b28 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Mon, 10 Jul 2023 02:15:32 -0400 Subject: [PATCH 12/79] Update nba package to use polars --- sportsdataverse/nba/__init__.py | 1 + sportsdataverse/nba/nba_loaders.py | 56 ++-- sportsdataverse/nba/nba_pbp.py | 475 ++++++++++++++++------------ sportsdataverse/nba/nba_schedule.py | 209 +++++++----- sportsdataverse/nba/nba_teams.py | 22 +- 5 files changed, 438 insertions(+), 325 deletions(-) diff --git a/sportsdataverse/nba/__init__.py b/sportsdataverse/nba/__init__.py index 5305c66..31080c1 100755 --- a/sportsdataverse/nba/__init__.py +++ b/sportsdataverse/nba/__init__.py @@ -1,4 +1,5 @@ from sportsdataverse.nba.nba_loaders import * +from sportsdataverse.nba.nba_game_rosters import * from sportsdataverse.nba.nba_pbp import * from sportsdataverse.nba.nba_schedule import * from sportsdataverse.nba.nba_teams import * \ No newline at end of file diff --git a/sportsdataverse/nba/nba_loaders.py b/sportsdataverse/nba/nba_loaders.py index 1d81ce7..1f7cfc6 100755 --- a/sportsdataverse/nba/nba_loaders.py +++ b/sportsdataverse/nba/nba_loaders.py @@ -1,4 +1,5 @@ import pandas as pd +import polars as pl import json from tqdm import tqdm from typing import List, Callable, Iterator, Union, Optional @@ -6,7 +7,7 @@ from sportsdataverse.errors import SeasonNotFoundError from sportsdataverse.dl_utils import download -def load_nba_pbp(seasons: List[int]) -> pd.DataFrame: +def load_nba_pbp(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: """Load NBA play by play data going back to 2002 Example: @@ -14,6 +15,7 @@ def load_nba_pbp(seasons: List[int]) -> pd.DataFrame: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing the @@ -22,19 +24,17 @@ def load_nba_pbp(seasons: List[int]) -> pd.DataFrame: Raises: ValueError: If `season` is less than 2002. """ - data = pd.DataFrame() + data = pl.DataFrame() if type(seasons) is int: seasons = [seasons] for i in tqdm(seasons): if int(i) < 2002: raise SeasonNotFoundError("season cannot be less than 2002") - i_data = pd.read_parquet(NBA_BASE_URL.format(season=i), engine='auto', columns=None) - data = pd.concat([data, i_data], axis = 0, ignore_index = True) - #Give each row a unique index - data.reset_index(drop=True, inplace=True) - return data + i_data = pl.read_parquet(NBA_BASE_URL.format(season=i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how = 'vertical') + return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data -def load_nba_team_boxscore(seasons: List[int]) -> pd.DataFrame: +def load_nba_team_boxscore(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: """Load NBA team boxscore data Example: @@ -42,6 +42,7 @@ def load_nba_team_boxscore(seasons: List[int]) -> pd.DataFrame: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing the @@ -50,20 +51,17 @@ def load_nba_team_boxscore(seasons: List[int]) -> pd.DataFrame: Raises: ValueError: If `season` is less than 2002. """ - data = pd.DataFrame() + data = pl.DataFrame() if type(seasons) is int: seasons = [seasons] for i in tqdm(seasons): if int(i) < 2002: raise SeasonNotFoundError("season cannot be less than 2002") - i_data = pd.read_parquet(NBA_TEAM_BOX_URL.format(season = i), engine='auto', columns=None) - data = pd.concat([data, i_data], axis = 0, ignore_index = True) - #Give each row a unique index - data.reset_index(drop=True, inplace=True) + i_data = pl.read_parquet(NBA_TEAM_BOX_URL.format(season = i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how = 'vertical') + return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data - return data - -def load_nba_player_boxscore(seasons: List[int]) -> pd.DataFrame: +def load_nba_player_boxscore(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: """Load NBA player boxscore data Example: @@ -71,6 +69,7 @@ def load_nba_player_boxscore(seasons: List[int]) -> pd.DataFrame: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing the @@ -79,20 +78,17 @@ def load_nba_player_boxscore(seasons: List[int]) -> pd.DataFrame: Raises: ValueError: If `season` is less than 2002. """ - data = pd.DataFrame() + data = pl.DataFrame() if type(seasons) is int: seasons = [seasons] for i in tqdm(seasons): if int(i) < 2002: raise SeasonNotFoundError("season cannot be less than 2002") - i_data = pd.read_parquet(NBA_PLAYER_BOX_URL.format(season = i), engine='auto', columns=None) - data = pd.concat([data, i_data], axis = 0, ignore_index = True) - #Give each row a unique index - data.reset_index(drop=True, inplace=True) - - return data + i_data = pl.read_parquet(NBA_PLAYER_BOX_URL.format(season = i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how = 'vertical') + return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data -def load_nba_schedule(seasons: List[int]) -> pd.DataFrame: +def load_nba_schedule(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: """Load NBA schedule data Example: @@ -100,6 +96,7 @@ def load_nba_schedule(seasons: List[int]) -> pd.DataFrame: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing the @@ -108,15 +105,12 @@ def load_nba_schedule(seasons: List[int]) -> pd.DataFrame: Raises: ValueError: If `season` is less than 2002. """ - data = pd.DataFrame() + data = pl.DataFrame() if type(seasons) is int: seasons = [seasons] for i in tqdm(seasons): if int(i) < 2002: raise SeasonNotFoundError("season cannot be less than 2002") - i_data = pd.read_parquet(NBA_TEAM_SCHEDULE_URL.format(season = i), engine='auto', columns=None) - data = pd.concat([data, i_data], axis = 0, ignore_index = True) - #Give each row a unique index - data.reset_index(drop=True, inplace=True) - - return data + i_data = pl.read_parquet(NBA_TEAM_SCHEDULE_URL.format(season = i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how = 'vertical') + return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data diff --git a/sportsdataverse/nba/nba_pbp.py b/sportsdataverse/nba/nba_pbp.py index 92d04a1..b05647f 100755 --- a/sportsdataverse/nba/nba_pbp.py +++ b/sportsdataverse/nba/nba_pbp.py @@ -1,5 +1,6 @@ from typing import Dict import pandas as pd +import polars as pl import numpy as np import os import json @@ -7,11 +8,13 @@ from typing import List, Callable, Iterator, Union, Optional, Dict from sportsdataverse.dl_utils import download, flatten_json_iterative, key_check -def espn_nba_pbp(game_id: int, raw = False) -> Dict: +def espn_nba_pbp(game_id: int, raw = False, **kwargs) -> Dict: """espn_nba_pbp() - Pull the game by id - Data from API endpoints - `nba/playbyplay`, `nba/summary` Args: game_id (int): Unique game_id, can be obtained from nba_schedule(). + raw (bool): If True, returns the raw json from the API endpoint. If False, returns a cleaned dictionary of datasets. + Returns: Dict: Dictionary of game data with keys - "gameId", "plays", "winprobability", "boxscore", "header", "broadcasts", @@ -22,12 +25,11 @@ def espn_nba_pbp(game_id: int, raw = False) -> Dict: `nba_df = sportsdataverse.nba.espn_nba_pbp(game_id=401307514)` """ # play by play - pbp_txt = {} - pbp_txt['timeouts'] = {} + pbp_txt = {'timeouts': {}} # summary endpoint for pickcenter array - summary_url = "http://site.api.espn.com/apis/site/v2/sports/basketball/nba/summary?event={}".format(game_id) - summary_resp = download(summary_url) - summary = json.loads(summary_resp) + summary_url = f"http://site.api.espn.com/apis/site/v2/sports/basketball/nba/summary?event={game_id}" + summary_resp = download(summary_url, **kwargs) + summary = summary_resp.json() incoming_keys_expected = [ 'boxscore', 'format', 'gameInfo', 'leaders', 'seasonseries', @@ -46,51 +48,80 @@ def espn_nba_pbp(game_id: int, raw = False) -> Dict: 'leaders' ] if raw == True: - # reorder keys in raw format + # reorder keys in raw format, appending empty keys which are defined later to the end pbp_json = {} for k in incoming_keys_expected: if k in summary.keys(): pbp_json[k] = summary[k] else: - if k in dict_keys_expected: - pbp_json[k] = {} - else: - pbp_json[k] = [] + pbp_json[k] = {} if k in dict_keys_expected else [] return pbp_json for k in incoming_keys_expected: if k in summary.keys(): pbp_txt[k] = summary[k] else: - if k in dict_keys_expected: - pbp_txt[k] = {} - else: - pbp_txt[k] = [] + pbp_txt[k] = {} if k in dict_keys_expected else [] for k in ['news','shop']: - pbp_txt.pop('{}'.format(k), None) - pbp_json = helper_nba_pbp(game_id, pbp_txt) - return pbp_json + pbp_txt.pop(f'{k}', None) + + return helper_nba_pbp(game_id, pbp_txt) def nba_pbp_disk(game_id, path_to_json): - with open(os.path.join(path_to_json, "{}.json".format(game_id))) as json_file: + with open(os.path.join(path_to_json, f"{game_id}.json")) as json_file: pbp_txt = json.load(json_file) return pbp_txt def helper_nba_pbp(game_id, pbp_txt): - gameSpread, overUnder, homeFavorite, gameSpreadAvailable = helper_nba_pickcenter(pbp_txt) + init = helper_nba_pickcenter(pbp_txt) + pbp_txt, init = helper_nba_game_data(pbp_txt, init) + if "plays" in pbp_txt.keys() and pbp_txt.get('header').get('competitions')[0].get('playByPlaySource') != 'none': + pbp_txt = helper_nba_pbp_features(game_id, pbp_txt, init) + else: + pbp_txt['plays'] = pl.DataFrame() + pbp_txt['timeouts'] = { + homeTeamId: {"1": [], "2": []}, + awayTeamId: {"1": [], "2": []}, + } + return { + "gameId": game_id, + "plays": pbp_txt['plays'].to_dicts(), + "winprobability" : np.array(pbp_txt['winprobability']).tolist(), + "boxscore" : pbp_txt['boxscore'], + "header" : pbp_txt['header'], + "format": pbp_txt['format'], + "broadcasts" : np.array(pbp_txt['broadcasts']).tolist(), + "videos" : np.array(pbp_txt['videos']).tolist(), + "playByPlaySource": pbp_txt['playByPlaySource'], + "standings" : pbp_txt['standings'], + "article" : pbp_txt['article'], + "leaders" : np.array(pbp_txt['leaders']).tolist(), + "seasonseries" : np.array(pbp_txt['seasonseries']).tolist(), + "timeouts" : pbp_txt['timeouts'], + "pickcenter" : np.array(pbp_txt['pickcenter']).tolist(), + "againstTheSpread" : np.array(pbp_txt['againstTheSpread']).tolist(), + "odds" : np.array(pbp_txt['odds']).tolist(), + "predictor" : pbp_txt['predictor'], + "espnWP" : np.array(pbp_txt['espnWP']).tolist(), + "gameInfo" : pbp_txt['gameInfo'], + "teamInfo" : np.array(pbp_txt['teamInfo']).tolist(), + "season" : np.array(pbp_txt['season']).tolist() + } + +def helper_nba_game_data(pbp_txt, init): pbp_txt['timeouts'] = {} pbp_txt['teamInfo'] = pbp_txt['header']['competitions'][0] pbp_txt['season'] = pbp_txt['header']['season'] pbp_txt['playByPlaySource'] = pbp_txt['header']['competitions'][0]['playByPlaySource'] pbp_txt['boxscoreSource'] = pbp_txt['header']['competitions'][0]['boxscoreSource'] - pbp_txt['gameSpreadAvailable'] = gameSpreadAvailable - pbp_txt['gameSpread'] = gameSpread - pbp_txt["homeFavorite"] = homeFavorite + pbp_txt['gameSpreadAvailable'] = init["gameSpreadAvailable"] + pbp_txt['gameSpread'] = init["gameSpread"] + pbp_txt["homeFavorite"] = init["homeFavorite"] pbp_txt["homeTeamSpread"] = np.where( - homeFavorite == True, abs(gameSpread), -1 * abs(gameSpread) + init["homeFavorite"] == True, abs(init["gameSpread"]), -1 * abs(init["gameSpread"]) ) - pbp_txt["overUnder"] = overUnder + pbp_txt["overUnder"] = init["overUnder"] # Home and Away identification variables if pbp_txt['header']['competitions'][0]['competitors'][0]['homeAway']=='home': pbp_txt['header']['competitions'][0]['home'] = pbp_txt['header']['competitions'][0]['competitors'][0]['team'] @@ -98,212 +129,237 @@ def helper_nba_pbp(game_id, pbp_txt): homeTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['name']) homeTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['location']) homeTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['abbreviation']) - homeTeamNameAlt = re.sub("Stat(.+)", "St", str(homeTeamName)) + homeTeamNameAlt = re.sub("Stat(.+)", "St", homeTeamName) pbp_txt['header']['competitions'][0]['away'] = pbp_txt['header']['competitions'][0]['competitors'][1]['team'] awayTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['id']) awayTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['name']) awayTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['location']) awayTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['abbreviation']) - awayTeamNameAlt = re.sub("Stat(.+)", "St", str(awayTeamName)) + awayTeamNameAlt = re.sub("Stat(.+)", "St", awayTeamName) else: pbp_txt['header']['competitions'][0]['away'] = pbp_txt['header']['competitions'][0]['competitors'][0]['team'] awayTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['id']) awayTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['name']) awayTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['location']) awayTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['abbreviation']) - awayTeamNameAlt = re.sub("Stat(.+)", "St", str(awayTeamName)) + awayTeamNameAlt = re.sub("Stat(.+)", "St", awayTeamName) pbp_txt['header']['competitions'][0]['home'] = pbp_txt['header']['competitions'][0]['competitors'][1]['team'] homeTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['id']) homeTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['name']) homeTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['location']) homeTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['abbreviation']) - homeTeamNameAlt = re.sub("Stat(.+)", "St", str(homeTeamName)) - if (pbp_txt['playByPlaySource'] != "none") & (len(pbp_txt['plays'])>1): - helper_nba_pbp_features(game_id, pbp_txt, homeTeamId, awayTeamId, homeTeamMascot, awayTeamMascot, homeTeamName, awayTeamName, homeTeamAbbrev, awayTeamAbbrev, homeTeamNameAlt, awayTeamNameAlt, gameSpread, homeFavorite, gameSpreadAvailable) - else: - pbp_txt['plays'] = pd.DataFrame() - pbp_txt['timeouts'] = {} - pbp_txt['timeouts'][homeTeamId] = {"1": [], "2": []} - pbp_txt['timeouts'][awayTeamId] = {"1": [], "2": []} - pbp_txt['plays'] = pbp_txt['plays'].replace({np.nan: None}) - pbp_json = { - "gameId": game_id, - "plays" : pbp_txt['plays'].to_dict(orient='records'), - "winprobability" : np.array(pbp_txt['winprobability']).tolist(), - "boxscore" : pbp_txt['boxscore'], - "header" : pbp_txt['header'], - "format": pbp_txt['format'], - "broadcasts" : np.array(pbp_txt['broadcasts']).tolist(), - "videos" : np.array(pbp_txt['videos']).tolist(), - "playByPlaySource": pbp_txt['playByPlaySource'], - "standings" : pbp_txt['standings'], - "article" : pbp_txt['article'], - "leaders" : np.array(pbp_txt['leaders']).tolist(), - "seasonseries" : np.array(pbp_txt['seasonseries']).tolist(), - "timeouts" : pbp_txt['timeouts'], - "pickcenter" : np.array(pbp_txt['pickcenter']).tolist(), - "againstTheSpread" : np.array(pbp_txt['againstTheSpread']).tolist(), - "odds" : np.array(pbp_txt['odds']).tolist(), - "predictor" : pbp_txt['predictor'], - "espnWP" : np.array(pbp_txt['espnWP']).tolist(), - "gameInfo" : pbp_txt['gameInfo'], - "teamInfo" : np.array(pbp_txt['teamInfo']).tolist(), - "season" : np.array(pbp_txt['season']).tolist() - } - return pbp_json + homeTeamNameAlt = re.sub("Stat(.+)", "St", homeTeamName) + init["homeTeamId"] = homeTeamId + init["homeTeamMascot"] = homeTeamMascot + init["homeTeamName"] = homeTeamName + init["homeTeamAbbrev"] = homeTeamAbbrev + init["homeTeamNameAlt"] = homeTeamNameAlt + init["awayTeamId"] = awayTeamId + init["awayTeamMascot"] = awayTeamMascot + init["awayTeamName"] = awayTeamName + init["awayTeamAbbrev"] = awayTeamAbbrev + init["awayTeamNameAlt"] = awayTeamNameAlt + return pbp_txt, init -def helper_nba_pbp_features(game_id, pbp_txt, homeTeamId, awayTeamId, homeTeamMascot, awayTeamMascot, homeTeamName, awayTeamName, homeTeamAbbrev, awayTeamAbbrev, homeTeamNameAlt, awayTeamNameAlt, gameSpread, homeFavorite, gameSpreadAvailable): +def helper_nba_pbp_features(game_id, pbp_txt, init): pbp_txt['plays_mod'] = [] for play in pbp_txt['plays']: p = flatten_json_iterative(play) pbp_txt['plays_mod'].append(p) - pbp_txt['plays'] = pd.json_normalize(pbp_txt,'plays_mod') - pbp_txt['plays']['season'] = pbp_txt['season']['year'] - pbp_txt['plays']['seasonType'] = pbp_txt['season']['type'] - pbp_txt['plays']["awayTeamId"] = awayTeamId - pbp_txt['plays']["awayTeamName"] = str(awayTeamName) - pbp_txt['plays']["awayTeamMascot"] = str(awayTeamMascot) - pbp_txt['plays']["awayTeamAbbrev"] = str(awayTeamAbbrev) - pbp_txt['plays']["awayTeamNameAlt"] = str(awayTeamNameAlt) - pbp_txt['plays']["homeTeamId"] = homeTeamId - pbp_txt['plays']["homeTeamName"] = str(homeTeamName) - pbp_txt['plays']["homeTeamMascot"] = str(homeTeamMascot) - pbp_txt['plays']["homeTeamAbbrev"] = str(homeTeamAbbrev) - pbp_txt['plays']["homeTeamNameAlt"] = str(homeTeamNameAlt) - # Spread definition - pbp_txt['plays']["homeTeamSpread"] = 2.5 - pbp_txt['plays']["gameSpread"] = abs(gameSpread) - pbp_txt['plays']["homeTeamSpread"] = np.where(homeFavorite == True, abs(gameSpread), -1*abs(gameSpread)) - pbp_txt['homeTeamSpread'] = np.where(homeFavorite == True, abs(gameSpread), -1*abs(gameSpread)) - pbp_txt['plays']["homeFavorite"] = homeFavorite - pbp_txt['plays']["gameSpread"] = gameSpread - pbp_txt['plays']["gameSpreadAvailable"] = gameSpreadAvailable - pbp_txt['plays'] = pbp_txt['plays'].to_dict(orient='records') - pbp_txt['plays'] = pd.DataFrame(pbp_txt['plays']) - pbp_txt['plays']['season'] = pbp_txt['header']['season']['year'] - pbp_txt['plays']['seasonType'] = pbp_txt['header']['season']['type'] - pbp_txt['plays']['game_id'] = int(game_id) - pbp_txt['plays']["homeTeamId"] = homeTeamId - pbp_txt['plays']["awayTeamId"] = awayTeamId - pbp_txt['plays']["homeTeamName"] = str(homeTeamName) - pbp_txt['plays']["awayTeamName"] = str(awayTeamName) - pbp_txt['plays']["homeTeamMascot"] = str(homeTeamMascot) - pbp_txt['plays']["awayTeamMascot"] = str(awayTeamMascot) - pbp_txt['plays']["homeTeamAbbrev"] = str(homeTeamAbbrev) - pbp_txt['plays']["awayTeamAbbrev"] = str(awayTeamAbbrev) - pbp_txt['plays']["homeTeamNameAlt"] = str(homeTeamNameAlt) - pbp_txt['plays']["awayTeamNameAlt"] = str(awayTeamNameAlt) - pbp_txt['plays']['period.number'] = pbp_txt['plays']['period.number'].apply(lambda x: int(x)) - pbp_txt['plays']['qtr'] = pbp_txt['plays']['period.number'].apply(lambda x: int(x)) - - - pbp_txt['plays']["homeTeamSpread"] = 2.5 - if len(pbp_txt['pickcenter']) > 1: - if 'spread' in pbp_txt['pickcenter'][1].keys(): - gameSpread = pbp_txt['pickcenter'][1]['spread'] - homeFavorite = pbp_txt['pickcenter'][1]['homeTeamOdds']['favorite'] - gameSpreadAvailable = True - else: - gameSpread = pbp_txt['pickcenter'][0]['spread'] - homeFavorite = pbp_txt['pickcenter'][0]['homeTeamOdds']['favorite'] - gameSpreadAvailable = True - - else: - gameSpread = 2.5 - homeFavorite = True - gameSpreadAvailable = False - pbp_txt['plays']["gameSpread"] = abs(gameSpread) - pbp_txt['plays']["gameSpreadAvailable"] = gameSpreadAvailable - pbp_txt['plays']["homeTeamSpread"] = np.where(homeFavorite == True, abs(gameSpread), -1*abs(gameSpread)) - pbp_txt['homeTeamSpread'] = np.where(homeFavorite == True, abs(gameSpread), -1*abs(gameSpread)) - pbp_txt['plays']["homeFavorite"] = homeFavorite - pbp_txt['plays']["gameSpread"] = gameSpread - pbp_txt['plays']["homeFavorite"] = homeFavorite + pbp_txt['plays'] = pl.from_pandas(pd.json_normalize(pbp_txt,'plays_mod')) + pbp_txt['plays'] = pbp_txt['plays'].with_columns( + game_id = pl.lit(game_id).cast(pl.Int32), + id = (pl.col('id').cast(pl.Int64)), + season = pl.lit(pbp_txt['header']['season']['year']).cast(pl.Int32), + seasonType = pl.lit(pbp_txt['header']['season']['type']), + homeTeamId = pl.lit(init["homeTeamId"]).cast(pl.Int32), + homeTeamName = pl.lit(init["homeTeamName"]), + homeTeamMascot = pl.lit(init["homeTeamMascot"]), + homeTeamAbbrev = pl.lit(init["homeTeamAbbrev"]), + homeTeamNameAlt = pl.lit(init["homeTeamNameAlt"]), + awayTeamId = pl.lit(init["awayTeamId"]).cast(pl.Int32), + awayTeamName = pl.lit(init["awayTeamName"]), + awayTeamMascot = pl.lit(init["awayTeamMascot"]), + awayTeamAbbrev = pl.lit(init["awayTeamAbbrev"]), + awayTeamNameAlt = pl.lit(init["awayTeamNameAlt"]), + gameSpread = pl.lit(init["gameSpread"]).abs(), + homeFavorite = pl.lit(init["homeFavorite"]), + gameSpreadAvailable = pl.lit(init["gameSpreadAvailable"]), + ).with_columns( + homeTeamSpread = pl.when(pl.col('homeFavorite') == True) + .then(pl.col('gameSpread')) + .otherwise(-1*pl.col('gameSpread')), + ).with_columns( + pl.col("period.number").cast(pl.Int32).alias("period.number"), + pl.col("period.number").cast(pl.Int32).alias("qtr"), + pl.when(pl.col("clock.displayValue").str.contains(r":") == False) + .then('0:'+ pl.col("clock.displayValue")) + .otherwise(pl.col("clock.displayValue")) + .alias("clock.displayValue"), + ).with_columns( + pl.col("clock.displayValue").alias("time"), + pl.col("clock.displayValue").str.split(':').list.to_struct(n_field_strategy="first_non_null").alias("clock.mm") + ).with_columns( + pl.col("clock.mm").struct.rename_fields(["clock.minutes", "clock.seconds"]) + ).unnest( + "clock.mm" + ).with_columns( + pl.col("clock.minutes").cast(pl.Float32), + pl.col("clock.seconds").cast(pl.Float32), + pl.when((pl.col("type.text") == "ShortTimeOut") + .and_( + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamAbbrev"]).lower()) + .or_( + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamAbbrev"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamName"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamMascot"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamNameAlt"]).lower()) + ) + )) + .then(True) + .otherwise(False) + .alias("homeTimeoutCalled"), + pl.when((pl.col("type.text") == "ShortTimeOut") + .and_( + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamAbbrev"]).lower()) + .or_( + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamAbbrev"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamName"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamMascot"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamNameAlt"]).lower()) + ) + )) + .then(True) + .otherwise(False) + .alias("awayTimeoutCalled"), + ).with_columns( + half = pl.when(pl.col('qtr') <= 2) + .then(1) + .otherwise(2), + game_half = pl.when(pl.col('qtr') <= 2) + .then(1) + .otherwise(2), + ).with_columns( + lag_qtr = pl.col('qtr').shift(1), + lead_qtr = pl.col('qtr').shift(-1), + lag_half = pl.col('half').shift(1), + lead_half = pl.col('half').shift(-1), + ).with_columns( + (60 * pl.col("clock.minutes") + pl.col("clock.seconds")).alias("start.quarter_seconds_remaining"), + pl.when(pl.col('qtr').is_in([1,3])) + .then(720 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) + .otherwise(60 * pl.col("clock.minutes") + pl.col("clock.seconds")) + .alias("start.half_seconds_remaining"), + pl.when(pl.col('qtr') == 1) + .then(2880 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) + .when(pl.col('qtr') == 2) + .then(2160 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) + .when(pl.col('qtr') == 3) + .then(1440 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) + .otherwise(60 * pl.col("clock.minutes") + pl.col("clock.seconds")) + .alias("start.game_seconds_remaining"), + ).with_columns( + pl.col("start.quarter_seconds_remaining").shift(-1).alias("end.quarter_seconds_remaining"), + pl.col("start.half_seconds_remaining").shift(-1).alias("end.half_seconds_remaining"), + pl.col("start.game_seconds_remaining").shift(-1).alias("end.game_seconds_remaining"), + ) - #----- Time --------------- - pbp_txt['plays']['clock.displayValue'] = np.select( - [ - pbp_txt['plays']['clock.displayValue'].str.contains(":") == False - ], - [ - "0:" + pbp_txt['plays']['clock.displayValue'].apply(lambda x: str(x)) - ], default = pbp_txt['plays']['clock.displayValue'] - ) - pbp_txt['plays']['time'] = pbp_txt['plays']['clock.displayValue'] - pbp_txt['plays']['clock.mm'] = pbp_txt['plays']['clock.displayValue'].str.split(pat=':') - pbp_txt['plays'][['clock.minutes','clock.seconds']] = pbp_txt['plays']['clock.mm'].to_list() - pbp_txt['plays']['clock.minutes'] = pbp_txt['plays']['clock.minutes'].apply(lambda x: int(x)) - - pbp_txt['plays']['clock.seconds'] = pbp_txt['plays']['clock.seconds'].apply(lambda x: float(x)) - # pbp_txt['plays']['clock.mm'] = pbp_txt['plays']['clock.displayValue'].apply(lambda x: datetime.strptime(str(x),'%M:%S')) - pbp_txt['plays']['half'] = np.where(pbp_txt['plays']['qtr'] <= 2, "1","2") - pbp_txt['plays']['game_half'] = np.where(pbp_txt['plays']['qtr'] <= 2, "1","2") - pbp_txt['plays']['lag_qtr'] = pbp_txt['plays']['qtr'].shift(1) - pbp_txt['plays']['lead_qtr'] = pbp_txt['plays']['qtr'].shift(-1) - pbp_txt['plays']['lag_game_half'] = pbp_txt['plays']['game_half'].shift(1) - pbp_txt['plays']['lead_game_half'] = pbp_txt['plays']['game_half'].shift(-1) - pbp_txt['plays']['start.quarter_seconds_remaining'] = 60*pbp_txt['plays']['clock.minutes'].astype(int) + pbp_txt['plays']['clock.seconds'].astype(int) - pbp_txt['plays']['start.half_seconds_remaining'] = np.where( - pbp_txt['plays']['qtr'].isin([1,3]), - 600 + 60*pbp_txt['plays']['clock.minutes'].astype(int) + pbp_txt['plays']['clock.seconds'].astype(int), - 60*pbp_txt['plays']['clock.minutes'].astype(int) + pbp_txt['plays']['clock.seconds'].astype(int) - ) - pbp_txt['plays']['start.game_seconds_remaining'] = np.select( - [ - pbp_txt['plays']['qtr'] == 1, - pbp_txt['plays']['qtr'] == 2, - pbp_txt['plays']['qtr'] == 3, - pbp_txt['plays']['qtr'] == 4 - ], - [ - 1800 + 60*pbp_txt['plays']['clock.minutes'].astype(int) + pbp_txt['plays']['clock.seconds'].astype(int), - 1200 + 60*pbp_txt['plays']['clock.minutes'].astype(int) + pbp_txt['plays']['clock.seconds'].astype(int), - 600 + 60*pbp_txt['plays']['clock.minutes'].astype(int) + pbp_txt['plays']['clock.seconds'].astype(int), - 60*pbp_txt['plays']['clock.minutes'].astype(int) + pbp_txt['plays']['clock.seconds'].astype(int) - ], default = 60*pbp_txt['plays']['clock.minutes'].astype(int) + pbp_txt['plays']['clock.seconds'].astype(int) + pbp_txt["plays"] = pbp_txt["plays"].with_row_count("game_play_number", 1) + pbp_txt["timeouts"] = { + init["homeTeamId"]: {"1": [], "2": []}, + init["awayTeamId"]: {"1": [], "2": []}, + } + pbp_txt["timeouts"][init["homeTeamId"]]["1"] = ( + pbp_txt["plays"] + .filter( + (pl.col("homeTimeoutCalled") == True) + .and_(pl.col("period.number") <= 2) ) - # Pos Team - Start and End Id - pbp_txt['plays']['game_play_number'] = np.arange(len(pbp_txt['plays']))+1 - pbp_txt['plays']['text'] = pbp_txt['plays']['text'].astype(str) - pbp_txt['plays']['id'] = pbp_txt['plays']['id'].apply(lambda x: int(x)) - pbp_txt['plays']['end.quarter_seconds_remaining'] = pbp_txt['plays']['start.quarter_seconds_remaining'].shift(1) - pbp_txt['plays']['end.half_seconds_remaining'] = pbp_txt['plays']['start.half_seconds_remaining'].shift(1) - pbp_txt['plays']['end.game_seconds_remaining'] = pbp_txt['plays']['start.game_seconds_remaining'].shift(1) - pbp_txt['plays']['end.quarter_seconds_remaining'] = np.select( - [ - (pbp_txt['plays']['game_play_number'] == 1)| - ((pbp_txt['plays']['qtr'] == 2) & (pbp_txt['plays']['lag_qtr'] == 1))| - ((pbp_txt['plays']['qtr'] == 3) & (pbp_txt['plays']['lag_qtr'] == 2))| - ((pbp_txt['plays']['qtr'] == 4) & (pbp_txt['plays']['lag_qtr'] == 3)) - ], - [ - 600 - ], default = pbp_txt['plays']['end.quarter_seconds_remaining'] + .get_column("id") + .to_list() + ) + pbp_txt["timeouts"][init["homeTeamId"]]["2"] = ( + pbp_txt["plays"] + .filter( + (pl.col("homeTimeoutCalled") == True) + .and_(pl.col("period.number") > 2) ) - pbp_txt['plays']['end.half_seconds_remaining'] = np.select( - [ - (pbp_txt['plays']['game_play_number'] == 1)| - ((pbp_txt['plays']['game_half'] == "2") & (pbp_txt['plays']['lag_game_half'] == "1")) - ], - [ - 1200 - ], default = pbp_txt['plays']['end.half_seconds_remaining'] + .get_column("id") + .to_list() + ) + pbp_txt["timeouts"][init["awayTeamId"]]["1"] = ( + pbp_txt["plays"] + .filter( + (pl.col("awayTimeoutCalled") == True) + .and_(pl.col("period.number") <= 2) ) - pbp_txt['plays']['end.game_seconds_remaining'] = np.select( - [ - (pbp_txt['plays']['game_play_number'] == 1), - ((pbp_txt['plays']['game_half'] == "2") & (pbp_txt['plays']['lag_game_half'] == "1")) - ], - [ - 2400, - 1200 - ], default = pbp_txt['plays']['end.game_seconds_remaining'] + .get_column("id") + .to_list() + ) + pbp_txt["timeouts"][init["awayTeamId"]]["2"] = ( + pbp_txt["plays"] + .filter( + (pl.col("awayTimeoutCalled") == True) + .and_(pl.col("period.number") > 2) ) + .get_column("id") + .to_list() + ) + # Pos Team - Start and End Id + pbp_txt['plays'] = pbp_txt['plays'].with_columns( + pl.when((pl.col("game_play_number") == 1) + .or_((pl.col("lag_qtr") == 1) + .and_(pl.col("period.number") == 2)) + .or_((pl.col("lag_qtr") == 2) + .and_(pl.col("period.number") == 3)) + .or_((pl.col("lag_qtr") == 3) + .and_(pl.col("period.number") == 4))) + .then(720) + .when((pl.col("lag_qtr") == (pl.col("period.number") - 1)) + .and_(pl.col("period.number") >= 5)) + .then(300) + .otherwise(pl.col("end.quarter_seconds_remaining")) + .alias("end.quarter_seconds_remaining"), + pl.when((pl.col("game_play_number") == 1)) + .then(1440) + .when((pl.col("lag_half") == 1) + .and_(pl.col("half") == 2)) + .then(1440) + .when((pl.col("lag_qtr") == 1) + .and_(pl.col("period.number") == 2)) + .then(720) + .when((pl.col("lag_qtr") == 2) + .and_(pl.col("period.number") == 3)) + .then(1440) + .when((pl.col("lag_qtr") == 3) + .and_(pl.col("period.number") == 4)) + .then(720) + .when((pl.col("lag_qtr") == (pl.col("period.number") - 1)) + .and_(pl.col("period.number") >= 5)) + .then(300) + .otherwise(pl.col("end.half_seconds_remaining")) + .alias("end.half_seconds_remaining"), + pl.when((pl.col("game_play_number") == 1)) + .then(2880) + .when((pl.col("lag_qtr") == 1) + .and_(pl.col("period.number") == 2)) + .then(2160) + .when((pl.col("lag_qtr") == 2) + .and_(pl.col("period.number") == 3)) + .then(1440) + .when((pl.col("lag_qtr") == 3) + .and_(pl.col("period.number") == 4)) + .then(720) + .when((pl.col("lag_qtr") == (pl.col("period.number") - 1)) + .and_(pl.col("period.number") >= 5)) + .then(300) + .otherwise(pl.col("end.game_seconds_remaining")) + .alias("end.game_seconds_remaining"), + pl.col("qtr").cast(pl.Int32).alias("period"), + ) - pbp_txt['plays']['period'] = pbp_txt['plays']['qtr'] - - del pbp_txt['plays']['clock.mm'] + return pbp_txt def helper_nba_pickcenter(pbp_txt): # Spread definition @@ -312,15 +368,20 @@ def helper_nba_pickcenter(pbp_txt): if "spread" in pbp_txt.get("pickcenter",{})[1].keys(): gameSpread = pbp_txt.get("pickcenter",{})[1].get("spread","") overUnder = pbp_txt.get("pickcenter",{})[1].get("overUnder","") - gameSpreadAvailable = True else: gameSpread = pbp_txt.get("pickcenter",{})[0].get("spread","") overUnder = pbp_txt.get("pickcenter",{})[0].get("overUnder","") - gameSpreadAvailable = True - # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") + gameSpreadAvailable = True + # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") else: gameSpread = 2.5 overUnder = 190.5 homeFavorite = True gameSpreadAvailable = False - return gameSpread, overUnder, homeFavorite, gameSpreadAvailable \ No newline at end of file + + return { + "gameSpread": gameSpread, + "overUnder": overUnder, + "homeFavorite": homeFavorite, + "gameSpreadAvailable": gameSpreadAvailable, + } \ No newline at end of file diff --git a/sportsdataverse/nba/nba_schedule.py b/sportsdataverse/nba/nba_schedule.py index 6d596e0..f0d204a 100755 --- a/sportsdataverse/nba/nba_schedule.py +++ b/sportsdataverse/nba/nba_schedule.py @@ -1,81 +1,118 @@ import pandas as pd +import polars as pl import json import datetime from typing import List, Callable, Iterator, Union, Optional from sportsdataverse.errors import SeasonNotFoundError from sportsdataverse.dl_utils import download, underscore -def espn_nba_schedule(dates=None, season_type=None, limit=500) -> pd.DataFrame: +def espn_nba_schedule(dates=None, season_type=None, limit=500, + return_as_pandas = True, **kwargs) -> pd.DataFrame: """espn_nba_schedule - look up the NBA schedule for a given date from ESPN Args: dates (int): Used to define different seasons. 2002 is the earliest available season. season_type (int): season type, 1 for pre-season, 2 for regular season, 3 for post-season, 4 for all-star, 5 for off-season limit (int): number of records to return, default: 500. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Returns: pd.DataFrame: Pandas dataframe containing schedule events for the requested season. """ - if dates is None: - dates = '' - else: - dates = '&dates=' + str(dates) - if season_type is None: - season_type = '' - else: - season_type = '&seasontype=' + str(season_type) - - url = "http://site.api.espn.com/apis/site/v2/sports/basketball/nba/scoreboard?limit={}{}{}".format(limit, dates, season_type) - resp = download(url=url) - - - ev = pd.DataFrame() - if resp is not None: - events_txt = json.loads(resp) - events = events_txt.get('events') - for event in events: - event.get('competitions')[0].get('competitors')[0].get('team').pop('links',None) - event.get('competitions')[0].get('competitors')[1].get('team').pop('links',None) - if event.get('competitions')[0].get('competitors')[0].get('homeAway')=='home': - event['competitions'][0]['home'] = event.get('competitions')[0].get('competitors')[0].get('team') - event['competitions'][0]['home']['score'] = event.get('competitions')[0].get('competitors')[0].get('score') - event['competitions'][0]['home']['winner'] = event.get('competitions')[0].get('competitors')[0].get('winner') - event['competitions'][0]['away'] = event.get('competitions')[0].get('competitors')[1].get('team') - event['competitions'][0]['away']['score'] = event.get('competitions')[0].get('competitors')[1].get('score') - event['competitions'][0]['away']['winner'] = event.get('competitions')[0].get('competitors')[1].get('winner') - else: - event['competitions'][0]['away'] = event.get('competitions')[0].get('competitors')[0].get('team') - event['competitions'][0]['away']['score'] = event.get('competitions')[0].get('competitors')[0].get('score') - event['competitions'][0]['away']['winner'] = event.get('competitions')[0].get('competitors')[0].get('winner') - event['competitions'][0]['home'] = event.get('competitions')[0].get('competitors')[1].get('team') - event['competitions'][0]['home']['score'] = event.get('competitions')[0].get('competitors')[1].get('score') - event['competitions'][0]['home']['winner'] = event.get('competitions')[0].get('competitors')[1].get('winner') - - del_keys = ['broadcasts','geoBroadcasts', 'headlines', 'series', 'situation', 'tickets', 'odds'] - for k in del_keys: - event.get('competitions')[0].pop(k, None) - if len(event.get('competitions')[0]['notes'])>0: - event.get('competitions')[0]['notes_type'] = event.get('competitions')[0]['notes'][0].get("type") - event.get('competitions')[0]['notes_headline'] = event.get('competitions')[0]['notes'][0].get("headline").replace('"','') - else: - event.get('competitions')[0]['notes_type'] = '' - event.get('competitions')[0]['notes_headline'] = '' - event.get('competitions')[0].pop('notes', None) - x = pd.json_normalize(event.get('competitions')[0], sep='_') - x['game_id'] = x['id'].astype(int) - x['season'] = event.get('season').get('year') - x['season_type'] = event.get('season').get('type') - ev = pd.concat([ev,x],axis=0, ignore_index=True) - ev = pd.DataFrame(ev) - ev.columns = [underscore(c) for c in ev.columns.tolist()] - return ev - - -def espn_nba_calendar(season=None, ondays=None) -> pd.DataFrame: + url = "http://site.api.espn.com/apis/site/v2/sports/basketball/nba/scoreboard" + params = { + 'dates': dates, + 'seasonType': season_type, + 'limit': limit + } + resp = download(url=url, params=params, **kwargs) + + ev = pl.DataFrame() + events_txt = resp.json() + events = events_txt.get('events') + if len(events) == 0: + return pd.DataFrame() if return_as_pandas else pl.DataFrame() + + for event in events: + event.get('competitions')[0].get('competitors')[0].get('team').pop('links',None) + event.get('competitions')[0].get('competitors')[1].get('team').pop('links',None) + if event.get('competitions')[0].get('competitors')[0].get('homeAway') == 'home': + event = __extract_home_away(event, 0, 'home') + event = __extract_home_away(event, 1, 'away') + else: + event = __extract_home_away(event, 0, 'away') + event = __extract_home_away(event, 1, 'home') + + event.get('competitions')[0]['notes_type'] = event.get('competitions')[0]['notes'][0].get("type") if len(event.get('competitions')[0]['notes']) > 0 else '' + event.get('competitions')[0]['notes_headline'] = event.get('competitions')[0]['notes'][0].get("headline").replace('"','') if len(event.get('competitions')[0]['notes']) > 0 else '' + event.get('competitions')[0]['broadcast_market'] = event.get('competitions')[0].get('broadcasts', [])[0].get('market', "") if len(event.get('competitions')[0].get('broadcasts')) > 0 else "" + event.get('competitions')[0]['broadcast_name'] = event.get('competitions')[0].get('broadcasts', [])[0].get('names', [])[0] if len(event.get('competitions')[0].get('broadcasts')) > 0 else "" + del_keys = ['geoBroadcasts', 'headlines', 'series', 'situation', 'tickets', + 'odds', 'broadcasts', 'notes', 'competitors'] + for k in del_keys: + event.get('competitions')[0].pop(k, None) + + x = pl.from_pandas(pd.json_normalize(event.get('competitions')[0], sep = '_')) + x = x.with_columns( + game_id = (pl.col('id').cast(pl.Int64)), + season = (event.get('season').get('year')), + season_type = (event.get('season').get('type')), + home_linescores = pl.when(pl.col('status_type_description') == 'Postponed') + .then(None) + .otherwise(pl.col('home_linescores')), + away_linescores = pl.when(pl.col('status_type_description') == 'Postponed') + .then(None) + .otherwise(pl.col('away_linescores')), + ) + x = x[[s.name for s in x if s.null_count() != x.height]] + ev = pl.concat([ev, x], how = 'diagonal') + + ev.columns = [underscore(c) for c in ev.columns] + + return ev.to_pandas() if return_as_pandas else ev + +def __extract_home_away(event, arg1, arg2): + event['competitions'][0][arg2] = ( + event.get('competitions')[0].get('competitors')[arg1].get('team') + ) + event['competitions'][0][arg2]['score'] = ( + event.get('competitions')[0].get('competitors')[arg1].get('score') + ) + event['competitions'][0][arg2]['winner'] = ( + event.get('competitions')[0].get('competitors')[arg1].get('winner') + ) + ## add winner back to main competitors if does not exist + event['competitions'][0]['competitors'][arg1]['winner'] = ( + event.get('competitions')[0].get('competitors')[arg1].get('winner', False) + ) + + event['competitions'][0][arg2]['linescores'] = ( + event.get('competitions')[0] + .get('competitors')[arg1] + .get('linescores', [{'value': 'N/A'}]) + ) + ## add linescores back to main competitors if does not exist + event['competitions'][0]['competitors'][arg1]['linescores'] = ( + event.get('competitions')[0] + .get('competitors')[arg1] + .get('linescores', []) + ) + event['competitions'][0][arg2]['records'] = ( + event.get('competitions')[0] + .get('competitors')[arg1] + .get('records', []) + ) + return event + + +def espn_nba_calendar(season=None, ondays=None, return_as_pandas = True, **kwargs) -> pd.DataFrame: """espn_nba_calendar - look up the NBA calendar for a given season from ESPN Args: season (int): Used to define different seasons. 2002 is the earliest available season. + ondays (boolean): Used to return dates for calendar ondays + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing @@ -85,41 +122,51 @@ def espn_nba_calendar(season=None, ondays=None) -> pd.DataFrame: ValueError: If `season` is less than 2002. """ if ondays is not None: - url = "https://sports.core.api.espn.com/v2/sports/basketball/leagues/nba/seasons/{}/types/2/calendar/ondays".format(season) - resp = download(url=url) - txt = json.loads(resp).get('eventDate').get('dates') - full_schedule = pd.DataFrame(txt,columns=['dates']) - full_schedule['dateURL'] = list(map(lambda x: x[:10].replace("-",""),full_schedule['dates'])) - full_schedule['url']="http://site.api.espn.com/apis/site/v2/sports/basketball/nba/scoreboard?dates=" - full_schedule['url']= full_schedule['url'] + full_schedule['dateURL'] + full_schedule = __ondays_nba_calendar(season, **kwargs) else: - url = "http://site.api.espn.com/apis/site/v2/sports/basketball/nba/scoreboard?dates={}".format(season) - resp = download(url=url) - txt = json.loads(resp)['leagues'][0]['calendar'] + url = f"http://site.api.espn.com/apis/site/v2/sports/basketball/nba/scoreboard?dates={season}" + resp = download(url=url, **kwargs) + txt = resp.json().get('leagues')[0].get('calendar') datenum = list(map(lambda x: x[:10].replace("-",""),txt)) date = list(map(lambda x: x[:10],txt)) year = list(map(lambda x: x[:4],txt)) month = list(map(lambda x: x[5:7],txt)) day = list(map(lambda x: x[8:10],txt)) - - data = {"season": season, - "datetime" : txt, - "date" : date, - "year": year, - "month": month, - "day": day, - "dateURL": datenum + data = { + "season": season, + "datetime" : txt, + "date" : date, + "year": year, + "month": month, + "day": day, + "dateURL": datenum } - full_schedule = pd.DataFrame(data) - full_schedule['url']="http://site.api.espn.com/apis/site/v2/sports/basketball/nba/scoreboard?dates=" - full_schedule['url']= full_schedule['url'] + full_schedule['dateURL'] - return full_schedule + full_schedule = pl.DataFrame(data) + full_schedule = full_schedule.with_columns( + url="http://site.api.espn.com/apis/site/v2/sports/basketball/nba/scoreboard?dates=" + + pl.col('dateURL') + ) + return full_schedule.to_pandas() if return_as_pandas else full_schedule + + +def __ondays_nba_calendar(season, **kwargs): + url = f"https://sports.core.api.espn.com/v2/sports/basketball/leagues/nba/seasons/{season}/types/2/calendar/ondays" + resp = download(url=url, **kwargs) + txt = resp.json().get('eventDate').get('dates') + result = pl.DataFrame(txt, schema=['dates']) + result = result.with_columns(dateURL = pl.col('dates').str.slice(0, 10)) + result = result.with_columns( + url="http://site.api.espn.com/apis/site/v2/sports/basketball/nba/scoreboard?dates=" + + pl.col('dateURL') + ) + + return result def most_recent_nba_season(): if int(str(datetime.date.today())[5:7]) >= 10: - return int(str(datetime.date.today())[0:4]) + 1 + return int(str(datetime.date.today())[:4]) + 1 else: - return int(str(datetime.date.today())[0:4]) + return int(str(datetime.date.today())[:4]) def year_to_season(year): first_year = str(year)[2:4] diff --git a/sportsdataverse/nba/nba_teams.py b/sportsdataverse/nba/nba_teams.py index 7b7d4c6..57318ae 100755 --- a/sportsdataverse/nba/nba_teams.py +++ b/sportsdataverse/nba/nba_teams.py @@ -1,19 +1,29 @@ import pandas as pd +import polars as pl import json from sportsdataverse.dl_utils import download, underscore from urllib.error import URLError, HTTPError, ContentTooShortError -def espn_nba_teams() -> pd.DataFrame: +def espn_nba_teams(return_as_pandas=True, **kwargs) -> pd.DataFrame: """espn_nba_teams - look up NBA teams + Args: + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Returns: pd.DataFrame: Pandas dataframe containing teams for the requested league. + + Example: + `nba_df = sportsdataverse.nba.espn_nba_teams()` + """ - ev = pd.DataFrame() - url = "http://site.api.espn.com/apis/site/v2/sports/basketball/nba/teams?limit=1000" - resp = download(url=url) + url = "http://site.api.espn.com/apis/site/v2/sports/basketball/nba/teams" + params = { + "limit": 1000 + } + resp = download(url=url, params = params, **kwargs) if resp is not None: - events_txt = json.loads(resp) + events_txt = resp.json() teams = events_txt.get('sports')[0].get('leagues')[0].get('teams') del_keys = ['record', 'links'] @@ -22,5 +32,5 @@ def espn_nba_teams() -> pd.DataFrame: team.get('team').pop(k, None) teams = pd.json_normalize(teams, sep='_') teams.columns = [underscore(c) for c in teams.columns.tolist()] - return teams + return teams if return_as_pandas else pl.from_pandas(teams) From 6b1b7dc44285cdcb97dc43d1f5a85d12aea4f69f Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Mon, 10 Jul 2023 05:33:01 -0400 Subject: [PATCH 13/79] Documentation examples --- sportsdataverse/cfb/cfb_teams.py | 4 ++++ sportsdataverse/mbb/mbb_pbp.py | 2 +- sportsdataverse/mbb/mbb_teams.py | 5 ++++- sportsdataverse/nba/nba_teams.py | 1 - 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/sportsdataverse/cfb/cfb_teams.py b/sportsdataverse/cfb/cfb_teams.py index e3ceded..322fde6 100755 --- a/sportsdataverse/cfb/cfb_teams.py +++ b/sportsdataverse/cfb/cfb_teams.py @@ -12,6 +12,10 @@ def espn_cfb_teams(groups=None, return_as_pandas = True, **kwargs) -> pd.DataFra Returns: pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. + + Example: + `cfb_df = sportsdataverse.cfb.espn_cfb_teams()` + """ url = "http://site.api.espn.com/apis/site/v2/sports/football/college-football/teams" params = { diff --git a/sportsdataverse/mbb/mbb_pbp.py b/sportsdataverse/mbb/mbb_pbp.py index 73eab26..6a3f4db 100755 --- a/sportsdataverse/mbb/mbb_pbp.py +++ b/sportsdataverse/mbb/mbb_pbp.py @@ -1,4 +1,3 @@ -from typing import Dict import pandas as pd import polars as pl import numpy as np @@ -22,6 +21,7 @@ def espn_mbb_pbp(game_id: int, raw = False, **kwargs) -> Dict: Example: `mbb_df = sportsdataverse.mbb.espn_mbb_pbp(game_id=401265031)` + """ # play by play pbp_txt = {'timeouts': {}} diff --git a/sportsdataverse/mbb/mbb_teams.py b/sportsdataverse/mbb/mbb_teams.py index 8433fbe..e6e60ef 100755 --- a/sportsdataverse/mbb/mbb_teams.py +++ b/sportsdataverse/mbb/mbb_teams.py @@ -2,7 +2,6 @@ import polars as pl import json from sportsdataverse.dl_utils import download, underscore -from urllib.error import URLError, HTTPError, ContentTooShortError def espn_mbb_teams(groups=None, return_as_pandas=True, **kwargs) -> pd.DataFrame: """espn_mbb_teams - look up the men's college basketball teams @@ -13,6 +12,10 @@ def espn_mbb_teams(groups=None, return_as_pandas=True, **kwargs) -> pd.DataFrame Returns: pd.DataFrame: Pandas dataframe containing teams for the requested league. + + Example: + `mbb_df = sportsdataverse.mbb.espn_mbb_teams()` + """ url = "http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/teams" params = { diff --git a/sportsdataverse/nba/nba_teams.py b/sportsdataverse/nba/nba_teams.py index 57318ae..e0e1dfe 100755 --- a/sportsdataverse/nba/nba_teams.py +++ b/sportsdataverse/nba/nba_teams.py @@ -2,7 +2,6 @@ import polars as pl import json from sportsdataverse.dl_utils import download, underscore -from urllib.error import URLError, HTTPError, ContentTooShortError def espn_nba_teams(return_as_pandas=True, **kwargs) -> pd.DataFrame: """espn_nba_teams - look up NBA teams From 6e19ff6ed62c0ef3e3da9d0ab03e52a5de23a80d Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Mon, 10 Jul 2023 05:51:18 -0400 Subject: [PATCH 14/79] Updated nfl package to use polars --- sportsdataverse/cfb/cfb_schedule.py | 12 +- sportsdataverse/nfl/nfl_games.py | 32 +--- sportsdataverse/nfl/nfl_loaders.py | 249 ++++++++++++---------------- sportsdataverse/nfl/nfl_schedule.py | 232 +++++++++++++++----------- sportsdataverse/nfl/nfl_teams.py | 25 ++- 5 files changed, 273 insertions(+), 277 deletions(-) diff --git a/sportsdataverse/cfb/cfb_schedule.py b/sportsdataverse/cfb/cfb_schedule.py index 18ed119..43ff601 100755 --- a/sportsdataverse/cfb/cfb_schedule.py +++ b/sportsdataverse/cfb/cfb_schedule.py @@ -64,8 +64,12 @@ def espn_cfb_schedule(dates=None, week=None, season_type=None, groups=None, limi season = (event.get('season').get('year')), season_type = (event.get('season').get('type')), week = (event.get('week', {}).get('number')), - home_linescores = pl.when(pl.col('status_type_description') == 'Postponed').then(None).otherwise(pl.col('home_linescores')), - away_linescores = pl.when(pl.col('status_type_description') == 'Postponed').then(None).otherwise(pl.col('away_linescores')), + home_linescores = pl.when(pl.col('status_type_description') == 'Postponed') + .then(None) + .otherwise(pl.col('home_linescores')), + away_linescores = pl.when(pl.col('status_type_description') == 'Postponed') + .then(None) + .otherwise(pl.col('away_linescores')), ) x = x[[s.name for s in x if s.null_count() != x.height]] ev = pl.concat([ev, x], how = 'diagonal') @@ -130,13 +134,13 @@ def espn_cfb_calendar(season = None, groups = None, ondays = None, ValueError: If `season` is less than 2002. """ if ondays is not None: - full_schedule = __ondays_from_espn_cfb_calendar(season) + full_schedule = __ondays_cfb_calendar(season, **kwargs) else: + url = "http://site.api.espn.com/apis/site/v2/sports/football/college-football/scoreboard" params = { 'dates': season, 'groups': groups if groups is not None else '80' } - url = "http://site.api.espn.com/apis/site/v2/sports/football/college-football/scoreboard" resp = download(url = url, params = params, **kwargs) txt = resp.json() txt = txt.get('leagues')[0].get('calendar') diff --git a/sportsdataverse/nfl/nfl_games.py b/sportsdataverse/nfl/nfl_games.py index dbd5f07..9744aee 100755 --- a/sportsdataverse/nfl/nfl_games.py +++ b/sportsdataverse/nfl/nfl_games.py @@ -16,12 +16,11 @@ def nfl_token_gen(): response = requests.request("POST", url, headers=headers, data = payload) - access_token = json.loads(response.content)['access_token'] - return access_token + return json.loads(response.content)['access_token'] def nfl_headers_gen(): token = nfl_token_gen() - NFL_HEADERS = { + return { "Host": "api.nfl.com", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0", "Accept": "*/*", @@ -37,7 +36,6 @@ def nfl_headers_gen(): "Pragma": "no-cache", "Cache-Control": "no-cache", } - return NFL_HEADERS def nfl_game_details(game_id=None, headers=None, raw=False) -> Dict: """nfl_game_details() @@ -117,13 +115,8 @@ def nfl_game_details(game_id=None, headers=None, raw=False) -> Dict: if k in summary.keys(): pbp_txt[k] = summary.get(f"{k}") else: - if k in dict_keys_expected: - pbp_txt[k] = {} - else: - pbp_txt[k] = [] - - pbp_json = pbp_txt - return pbp_json + pbp_txt[k] = {} if k in dict_keys_expected else [] + return pbp_txt def nfl_game_schedule(season=2021, @@ -151,7 +144,7 @@ def nfl_game_schedule(season=2021, "week": week } pbp_txt = {} - summary_url = f"https://api.nfl.com/experience/v1/games" + summary_url = "https://api.nfl.com/experience/v1/games" summary_resp = requests.get(summary_url, headers=headers, params=params) @@ -171,17 +164,4 @@ def nfl_game_schedule(season=2021, 'drives', 'plays' ] - if raw == True: - return summary - - # for k in incoming_keys_expected: - # if k in summary.keys(): - # pbp_txt[k] = summary.get(f"{k}") - # else: - # if k in dict_keys_expected: - # pbp_txt[k] = {} - # else: - # pbp_txt[k] = [] - - pbp_json = summary - return pbp_json + return summary diff --git a/sportsdataverse/nfl/nfl_loaders.py b/sportsdataverse/nfl/nfl_loaders.py index d22a3a6..2304a47 100755 --- a/sportsdataverse/nfl/nfl_loaders.py +++ b/sportsdataverse/nfl/nfl_loaders.py @@ -1,4 +1,5 @@ import pandas as pd +import polars as pl import tempfile import json from tqdm import tqdm @@ -31,18 +32,16 @@ def load_nfl_pbp(seasons: List[int]) -> pd.DataFrame: Raises: ValueError: If `season` is less than 1999. """ - data = pd.DataFrame() + data = pl.DataFrame() if type(seasons) is int: seasons = [seasons] for i in tqdm(seasons): season_not_found_error(int(i), 1999) - i_data = pd.read_parquet(NFL_BASE_URL.format(season=i), engine='auto', columns=None) - data = pd.concat([data, i_data], axis = 0, ignore_index = True) - #Give each row a unique index - data.reset_index(drop=True, inplace=True) - return data + i_data = pl.read_parquet(NFL_BASE_URL.format(season=i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how = 'vertical') + return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data -def load_nfl_schedule(seasons: List[int]) -> pd.DataFrame: +def load_nfl_schedule(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: """Load NFL schedule data Example: @@ -57,7 +56,7 @@ def load_nfl_schedule(seasons: List[int]) -> pd.DataFrame: Raises: ValueError: If `season` is less than 1999. """ - data = pd.DataFrame() + data = pl.DataFrame() if type(seasons) is int: seasons = [seasons] with tempfile.TemporaryDirectory() as tempdirname: @@ -65,15 +64,14 @@ def load_nfl_schedule(seasons: List[int]) -> pd.DataFrame: season_not_found_error(int(i), 1999) schedule_url = NFL_TEAM_SCHEDULE_URL.format(season=i) #i_data = pd.read_parquet(NFL_TEAM_SCHEDULE_URL.format(season = i), engine='auto', columns=None) - i_data = read_r(download_file(schedule_url, "{}/nfl_sched_{}.rds".format(tempdirname, i)))[None] - i_data = pd.DataFrame(i_data) - data = pd.concat([data, i_data], axis = 0, ignore_index = True) - #Give each row a unique index - data.reset_index(drop=True, inplace=True) - - return data - -def load_nfl_player_stats(kicking = False) -> pd.DataFrame: + i_data = read_r( + download_file(schedule_url, f"{tempdirname}/nfl_sched_{i}.rds") + )[None] + i_data = pl.DataFrame(i_data) + data = pl.concat([data, i_data], how = "vertical") + return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data + +def load_nfl_player_stats(kicking = False, return_as_pandas = True) -> pd.DataFrame: """Load NFL player stats data Example: @@ -85,17 +83,15 @@ def load_nfl_player_stats(kicking = False) -> pd.DataFrame: Returns: pd.DataFrame: Pandas dataframe containing player stats. """ - data = pd.DataFrame() + data = pl.DataFrame() if kicking is False: - data = pd.read_parquet(NFL_PLAYER_STATS_URL, engine='auto', columns=None) + data = pl.read_parquet(NFL_PLAYER_STATS_URL, use_pyarrow=True, columns=None) else: - data = pd.read_parquet(NFL_PLAYER_KICKING_STATS_URL, engine='auto', columns=None) + data = pl.read_parquet(NFL_PLAYER_KICKING_STATS_URL, use_pyarrow=True, columns=None) - #Give each row a unique index - data.reset_index(drop=True, inplace=True) - return data + return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data -def load_nfl_ngs_passing() -> pd.DataFrame: +def load_nfl_ngs_passing(return_as_pandas = True) -> pd.DataFrame: """Load NFL NextGen Stats Passing data going back to 2016 Example: @@ -105,10 +101,10 @@ def load_nfl_ngs_passing() -> pd.DataFrame: pd.DataFrame: Pandas dataframe containing the NextGen Stats Passing data available. """ - df = pd.read_parquet(NFL_NGS_PASSING_URL, engine='auto', columns=None) - return df + return pl.read_parquet(NFL_NGS_PASSING_URL, use_pyarrow=True, columns=None).to_pandas(use_pyarrow_extension_array = True) \ + if return_as_pandas else pl.read_parquet(NFL_NGS_PASSING_URL, use_pyarrow=True, columns=None) -def load_nfl_ngs_rushing() -> pd.DataFrame: +def load_nfl_ngs_rushing(return_as_pandas = True) -> pd.DataFrame: """Load NFL NextGen Stats Rushing data going back to 2016 Example: @@ -118,10 +114,10 @@ def load_nfl_ngs_rushing() -> pd.DataFrame: pd.DataFrame: Pandas dataframe containing the NextGen Stats Rushing data available. """ - df = pd.read_parquet(NFL_NGS_RUSHING_URL, engine='auto', columns=None) - return df + return pl.read_parquet(NFL_NGS_RUSHING_URL, use_pyarrow=True, columns=None).to_pandas(use_pyarrow_extension_array = True) \ + if return_as_pandas else pl.read_parquet(NFL_NGS_RUSHING_URL, use_pyarrow=True, columns=None) -def load_nfl_ngs_receiving() -> pd.DataFrame: +def load_nfl_ngs_receiving(return_as_pandas = True) -> pd.DataFrame: """Load NFL NextGen Stats Receiving data going back to 2016 Example: @@ -131,10 +127,10 @@ def load_nfl_ngs_receiving() -> pd.DataFrame: pd.DataFrame: Pandas dataframe containing the NextGen Stats Receiving data available. """ - df = pd.read_parquet(NFL_NGS_RECEIVING_URL, engine='auto', columns=None) - return df + return pl.read_parquet(NFL_NGS_RECEIVING_URL, use_pyarrow=True, columns=None).to_pandas(use_pyarrow_extension_array = True) \ + if return_as_pandas else pl.read_parquet(NFL_NGS_RECEIVING_URL, use_pyarrow=True, columns=None) -def load_nfl_pfr_pass() -> pd.DataFrame: +def load_nfl_pfr_pass(return_as_pandas = True) -> pd.DataFrame: """Load NFL Pro-Football Reference Advanced Passing data going back to 2018 Example: @@ -145,10 +141,10 @@ def load_nfl_pfr_pass() -> pd.DataFrame: advanced passing stats data available. """ - df = pd.read_parquet(NFL_PFR_SEASON_PASS_URL, engine='auto', columns=None) - return df + return pl.read_parquet(NFL_PFR_SEASON_PASS_URL, use_pyarrow=True, columns=None).to_pandas(use_pyarrow_extension_array = True) \ + if return_as_pandas else pl.read_parquet(NFL_PFR_SEASON_PASS_URL, use_pyarrow=True, columns=None) -def load_nfl_pfr_weekly_pass(seasons: List[int]) -> pd.DataFrame: +def load_nfl_pfr_weekly_pass(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: """Load NFL Pro-Football Reference Weekly Advanced Passing data going back to 2018 Example: @@ -162,19 +158,16 @@ def load_nfl_pfr_weekly_pass(seasons: List[int]) -> pd.DataFrame: advanced passing stats data available for the requested seasons. """ - data = pd.DataFrame() + data = pl.DataFrame() if type(seasons) is int: seasons = [seasons] for i in tqdm(seasons): season_not_found_error(int(i), 2018) - i_data = pd.read_parquet(NFL_PFR_WEEK_PASS_URL.format(season=i), engine='auto', columns=None) - data = pd.concat([data, i_data], axis = 0, ignore_index = True) - - #Give each row a unique index - data.reset_index(drop=True, inplace=True) - return data + i_data = pl.read_parquet(NFL_PFR_WEEK_PASS_URL.format(season=i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how = 'vertical') + return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data -def load_nfl_pfr_rush() -> pd.DataFrame: +def load_nfl_pfr_rush(return_as_pandas = True) -> pd.DataFrame: """Load NFL Pro-Football Reference Advanced Rushing data going back to 2018 Example: @@ -185,10 +178,10 @@ def load_nfl_pfr_rush() -> pd.DataFrame: advanced rushing stats data available. """ - df = pd.read_parquet(NFL_PFR_SEASON_RUSH_URL, engine='auto', columns=None) - return df + return pl.read_parquet(NFL_PFR_SEASON_RUSH_URL, use_pyarrow=True, columns=None).to_pandas(use_pyarrow_extension_array = True) \ + if return_as_pandas else pl.read_parquet(NFL_PFR_SEASON_RUSH_URL, use_pyarrow=True, columns=None) -def load_nfl_pfr_weekly_rush(seasons: List[int]) -> pd.DataFrame: +def load_nfl_pfr_weekly_rush(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: """Load NFL Pro-Football Reference Weekly Advanced Rushing data going back to 2018 Example: @@ -202,19 +195,16 @@ def load_nfl_pfr_weekly_rush(seasons: List[int]) -> pd.DataFrame: advanced rushing stats data available for the requested seasons. """ - data = pd.DataFrame() + data = pl.DataFrame() if type(seasons) is int: seasons = [seasons] for i in tqdm(seasons): season_not_found_error(int(i), 2018) - i_data = pd.read_parquet(NFL_PFR_WEEK_RUSH_URL.format(season=i), engine='auto', columns=None) - data = pd.concat([data, i_data], axis = 0, ignore_index = True) + i_data = pl.read_parquet(NFL_PFR_WEEK_RUSH_URL.format(season=i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how = 'vertical') + return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data - #Give each row a unique index - data.reset_index(drop=True, inplace=True) - return data - -def load_nfl_pfr_rec() -> pd.DataFrame: +def load_nfl_pfr_rec(return_as_pandas = True) -> pd.DataFrame: """Load NFL Pro-Football Reference Advanced Receiving data going back to 2018 Example: @@ -225,10 +215,10 @@ def load_nfl_pfr_rec() -> pd.DataFrame: advanced receiving stats data available. """ - df = pd.read_parquet(NFL_PFR_SEASON_REC_URL, engine='auto', columns=None) - return df + return pl.read_parquet(NFL_PFR_SEASON_REC_URL, use_pyarrow=True, columns=None).to_pandas(use_pyarrow_extension_array = True) \ + if return_as_pandas else pl.read_parquet(NFL_PFR_SEASON_REC_URL, use_pyarrow=True, columns=None) -def load_nfl_pfr_weekly_rec(seasons: List[int]) -> pd.DataFrame: +def load_nfl_pfr_weekly_rec(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: """Load NFL Pro-Football Reference Weekly Advanced Receiving data going back to 2018 Example: @@ -242,19 +232,16 @@ def load_nfl_pfr_weekly_rec(seasons: List[int]) -> pd.DataFrame: advanced receiving stats data available for the requested seasons. """ - data = pd.DataFrame() + data = pl.DataFrame() if type(seasons) is int: seasons = [seasons] for i in tqdm(seasons): season_not_found_error(int(i), 2018) - i_data = pd.read_parquet(NFL_PFR_WEEK_REC_URL.format(season=i), engine='auto', columns=None) - data = pd.concat([data, i_data], axis = 0, ignore_index = True) - - #Give each row a unique index - data.reset_index(drop=True, inplace=True) - return data + i_data = pl.read_parquet(NFL_PFR_WEEK_REC_URL.format(season=i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how = 'vertical') + return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data -def load_nfl_pfr_def() -> pd.DataFrame: +def load_nfl_pfr_def(return_as_pandas = True) -> pd.DataFrame: """Load NFL Pro-Football Reference Advanced Defensive data going back to 2018 Example: @@ -265,10 +252,10 @@ def load_nfl_pfr_def() -> pd.DataFrame: advanced defensive stats data available. """ - df = pd.read_parquet(NFL_PFR_SEASON_DEF_URL, engine='auto', columns=None) - return df + return pl.read_parquet(NFL_PFR_SEASON_DEF_URL, use_pyarrow=True, columns=None).to_pandas(use_pyarrow_extension_array = True) \ + if return_as_pandas else pl.read_parquet(NFL_PFR_SEASON_DEF_URL, use_pyarrow=True, columns=None) -def load_nfl_pfr_weekly_def(seasons: List[int]) -> pd.DataFrame: +def load_nfl_pfr_weekly_def(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: """Load NFL Pro-Football Reference Weekly Advanced Defensive data going back to 2018 Example: @@ -282,20 +269,17 @@ def load_nfl_pfr_weekly_def(seasons: List[int]) -> pd.DataFrame: advanced defensive stats data available for the requested seasons. """ - data = pd.DataFrame() + data = pl.DataFrame() if type(seasons) is int: seasons = [seasons] for i in tqdm(seasons): season_not_found_error(int(i), 2018) - i_data = pd.read_parquet(NFL_PFR_WEEK_DEF_URL.format(season=i), engine='auto', columns=None) - data = pd.concat([data, i_data], axis = 0, ignore_index = True) - - #Give each row a unique index - data.reset_index(drop=True, inplace=True) - return data + i_data = pl.read_parquet(NFL_PFR_WEEK_DEF_URL.format(season=i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how = 'vertical') + return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data -def load_nfl_rosters(seasons: List[int]) -> pd.DataFrame: +def load_nfl_rosters(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: """Load NFL roster data for all seasons Example: @@ -308,19 +292,16 @@ def load_nfl_rosters(seasons: List[int]) -> pd.DataFrame: pd.DataFrame: Pandas dataframe containing rosters available for the requested seasons. """ - data = pd.DataFrame() + data = pl.DataFrame() if type(seasons) is int: seasons = [seasons] for i in tqdm(seasons): season_not_found_error(int(i), 1920) - i_data = pd.read_parquet(NFL_ROSTER_URL.format(season=i), engine='auto', columns=None) - data = pd.concat([data, i_data], axis = 0, ignore_index = True) + i_data = pl.read_parquet(NFL_ROSTER_URL.format(season=i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how = 'vertical') + return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data - #Give each row a unique index - data.reset_index(drop=True, inplace=True) - return data - -def load_nfl_weekly_rosters(seasons: List[int]) -> pd.DataFrame: +def load_nfl_weekly_rosters(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: """Load NFL weekly roster data for selected seasons Example: @@ -333,19 +314,16 @@ def load_nfl_weekly_rosters(seasons: List[int]) -> pd.DataFrame: pd.DataFrame: Pandas dataframe containing weekly rosters available for the requested seasons. """ - data = pd.DataFrame() + data = pl.DataFrame() if type(seasons) is int: seasons = [seasons] for i in tqdm(seasons): season_not_found_error(int(i), 2002) - i_data = pd.read_parquet(NFL_WEEKLY_ROSTER_URL.format(season=i), engine='auto', columns=None) - data = pd.concat([data, i_data], axis = 0, ignore_index = True) - - #Give each row a unique index - data.reset_index(drop=True, inplace=True) - return data + i_data = pl.read_parquet(NFL_WEEKLY_ROSTER_URL.format(season=i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how = 'vertical') + return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data -def load_nfl_teams() -> pd.DataFrame: +def load_nfl_teams(return_as_pandas = True) -> pd.DataFrame: """Load NFL team ID information and logos Example: @@ -357,9 +335,10 @@ def load_nfl_teams() -> pd.DataFrame: pd.DataFrame: Pandas dataframe containing teams available. """ df = pd.read_csv(NFL_TEAM_LOGO_URL, low_memory=False) - return df + return pl.read_csv(NFL_TEAM_LOGO_URL).to_pandas(use_pyarrow_extension_array = True) \ + if return_as_pandas else pl.read_csv(NFL_TEAM_LOGO_URL) -def load_nfl_players() -> pd.DataFrame: +def load_nfl_players(return_as_pandas = True) -> pd.DataFrame: """Load NFL Player ID information Example: @@ -370,10 +349,10 @@ def load_nfl_players() -> pd.DataFrame: Returns: pd.DataFrame: Pandas dataframe containing players available. """ - df = pd.read_parquet(NFL_PLAYER_URL, engine='auto', columns=None) - return df + return pl.read_parquet(NFL_PLAYER_URL, use_pyarrow=True, columns=None).to_pandas(use_pyarrow_extension_array = True) \ + if return_as_pandas else pl.read_parquet(NFL_OFFICIALS_URL, use_pyarrow=True, columns=None) -def load_nfl_snap_counts(seasons: List[int]) -> pd.DataFrame: +def load_nfl_snap_counts(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: """Load NFL snap counts data for selected seasons Example: @@ -386,19 +365,16 @@ def load_nfl_snap_counts(seasons: List[int]) -> pd.DataFrame: pd.DataFrame: Pandas dataframe containing snap counts available for the requested seasons. """ - data = pd.DataFrame() + data = pl.DataFrame() if type(seasons) is int: seasons = [seasons] for i in tqdm(seasons): season_not_found_error(int(i), 2012) - i_data = pd.read_parquet(NFL_SNAP_COUNTS_URL.format(season=i), engine='auto', columns=None) - data = pd.concat([data, i_data], axis = 0, ignore_index = True) - - #Give each row a unique index - data.reset_index(drop=True, inplace=True) - return data + i_data = pl.read_parquet(NFL_SNAP_COUNTS_URL.format(season=i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how = 'vertical') + return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data -def load_nfl_pbp_participation(seasons: List[int]) -> pd.DataFrame: +def load_nfl_pbp_participation(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: """Load NFL play-by-play participation data for selected seasons Example: @@ -411,19 +387,16 @@ def load_nfl_pbp_participation(seasons: List[int]) -> pd.DataFrame: pd.DataFrame: Pandas dataframe containing play-by-play participation data available for the requested seasons. """ - data = pd.DataFrame() + data = pl.DataFrame() if type(seasons) is int: seasons = [seasons] for i in tqdm(seasons): season_not_found_error(int(i), 2016) - i_data = pd.read_parquet(NFL_PBP_PARTICIPATION_URL.format(season=i), engine='auto', columns=None) - data = pd.concat([data, i_data], axis = 0, ignore_index = True) + i_data = pl.read_parquet(NFL_PBP_PARTICIPATION_URL.format(season=i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how = 'vertical') + return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data - #Give each row a unique index - data.reset_index(drop=True, inplace=True) - return data - -def load_nfl_injuries(seasons: List[int]) -> pd.DataFrame: +def load_nfl_injuries(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: """Load NFL injuries data for selected seasons Example: @@ -436,19 +409,16 @@ def load_nfl_injuries(seasons: List[int]) -> pd.DataFrame: pd.DataFrame: Pandas dataframe containing injuries data available for the requested seasons. """ - data = pd.DataFrame() + data = pl.DataFrame() if type(seasons) is int: seasons = [seasons] for i in tqdm(seasons): season_not_found_error(int(i), 2009) - i_data = pd.read_parquet(NFL_INJURIES_URL.format(season=i), engine='auto', columns=None) - data = pd.concat([data, i_data], axis = 0, ignore_index = True) - - #Give each row a unique index - data.reset_index(drop=True, inplace=True) - return data + i_data = pl.read_parquet(NFL_INJURIES_URL.format(season=i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how = 'vertical') + return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data -def load_nfl_depth_charts(seasons: List[int]) -> pd.DataFrame: +def load_nfl_depth_charts(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: """Load NFL Depth Chart data for selected seasons Example: @@ -461,19 +431,16 @@ def load_nfl_depth_charts(seasons: List[int]) -> pd.DataFrame: pd.DataFrame: Pandas dataframe containing depth chart data available for the requested seasons. """ - data = pd.DataFrame() + data = pl.DataFrame() if type(seasons) is int: seasons = [seasons] for i in tqdm(seasons): season_not_found_error(int(i), 2001) - i_data = pd.read_parquet(NFL_DEPTH_CHARTS_URL.format(season=i), engine='auto', columns=None) - data = pd.concat([data, i_data], axis = 0, ignore_index = True) - - #Give each row a unique index - data.reset_index(drop=True, inplace=True) - return data + i_data = pl.read_parquet(NFL_DEPTH_CHARTS_URL.format(season=i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how = 'vertical') + return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data -def load_nfl_contracts() -> pd.DataFrame: +def load_nfl_contracts(return_as_pandas = True) -> pd.DataFrame: """Load NFL Historical contracts information Example: @@ -484,11 +451,11 @@ def load_nfl_contracts() -> pd.DataFrame: Returns: pd.DataFrame: Pandas dataframe containing historical contracts available. """ - df = pd.read_parquet(NFL_CONTRACTS_URL, engine='auto', columns=None) - return df + return pl.read_parquet(NFL_CONTRACTS_URL, use_pyarrow=True, columns=None).to_pandas(use_pyarrow_extension_array = True) \ + if return_as_pandas else pl.read_parquet(NFL_CONTRACTS_URL, use_pyarrow=True, columns=None) -def load_nfl_combine() -> pd.DataFrame: +def load_nfl_combine(return_as_pandas = True) -> pd.DataFrame: """Load NFL Combine information Example: @@ -499,10 +466,10 @@ def load_nfl_combine() -> pd.DataFrame: Returns: pd.DataFrame: Pandas dataframe containing NFL combine data available. """ - df = pd.read_parquet(NFL_COMBINE_URL, engine='auto', columns=None) - return df + return pl.read_parquet(NFL_COMBINE_URL, use_pyarrow=True, columns=None).to_pandas(use_pyarrow_extension_array = True) \ + if return_as_pandas else pl.read_parquet(NFL_COMBINE_URL, use_pyarrow=True, columns=None) -def load_nfl_draft_picks() -> pd.DataFrame: +def load_nfl_draft_picks(return_as_pandas = True) -> pd.DataFrame: """Load NFL Draft picks information Example: @@ -513,10 +480,10 @@ def load_nfl_draft_picks() -> pd.DataFrame: Returns: pd.DataFrame: Pandas dataframe containing NFL Draft picks data available. """ - df = pd.read_parquet(NFL_DRAFT_PICKS_URL, engine='auto', columns=None) - return df + return pl.read_parquet(NFL_DRAFT_PICKS_URL, use_pyarrow=True, columns=None).to_pandas(use_pyarrow_extension_array = True) \ + if return_as_pandas else pl.read_parquet(NFL_DRAFT_PICKS_URL, use_pyarrow=True, columns=None) -def load_nfl_officials() -> pd.DataFrame: +def load_nfl_officials(return_as_pandas = True) -> pd.DataFrame: """Load NFL Officials information Example: @@ -527,8 +494,8 @@ def load_nfl_officials() -> pd.DataFrame: Returns: pd.DataFrame: Pandas dataframe containing officials available. """ - df = pd.read_parquet(NFL_OFFICIALS_URL, engine='auto', columns=None) - return df + return pl.read_parquet(NFL_OFFICIALS_URL, use_pyarrow=True, columns=None).to_pandas(use_pyarrow_extension_array = True) \ + if return_as_pandas else pl.read_parquet(NFL_OFFICIALS_URL, use_pyarrow=True, columns=None) ## Currently removed due to unsupported features of pyreadr's method. ## there is a list-column of nested tibbles within the data ## that is not supported by pyreadr diff --git a/sportsdataverse/nfl/nfl_schedule.py b/sportsdataverse/nfl/nfl_schedule.py index 59649d0..990effe 100755 --- a/sportsdataverse/nfl/nfl_schedule.py +++ b/sportsdataverse/nfl/nfl_schedule.py @@ -1,11 +1,14 @@ import pandas as pd +import polars as pl import json import time import datetime from sportsdataverse.dl_utils import download, underscore from urllib.error import URLError, HTTPError, ContentTooShortError -def espn_nfl_schedule(dates=None, week=None, season_type=None, limit=500) -> pd.DataFrame: +def espn_nfl_schedule(dates=None, week=None, season_type=None, groups=None, limit=500, + return_as_pandas = True, + **kwargs) -> pd.DataFrame: """espn_nfl_schedule - look up the NFL schedule for a given season Args: @@ -13,86 +16,113 @@ def espn_nfl_schedule(dates=None, week=None, season_type=None, limit=500) -> pd. week (int): Week of the schedule. season_type (int): 2 for regular season, 3 for post-season, 4 for off-season. limit (int): number of records to return, default: 500. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. """ - if week is None: - week = '' - else: - week = '&week=' + str(week) - if dates is None: - dates = '' - else: - dates = '&dates=' + str(dates) - if season_type is None: - season_type = '' - else: - season_type = '&seasontype=' + str(season_type) - if limit is None: - limit_url = '' - else: - limit_url = '&limit=' + str(limit) - cache_buster = int(time.time() * 1000) - cache_buster_url = '&'+str(cache_buster) - url = "http://site.api.espn.com/apis/site/v2/sports/football/nfl/scoreboard?{}{}{}{}{}{}".format( - limit_url, - dates, - week, - season_type, - cache_buster_url + + params = { + 'week': week, + 'dates': dates, + 'seasonType': season_type, + 'limit': limit + } + + url = "http://site.api.espn.com/apis/site/v2/sports/football/nfl/scoreboard" + resp = download(url=url, params=params, **kwargs) + + ev = pl.DataFrame() + events_txt = resp.json() + events = events_txt.get('events') + if len(events) == 0: + return pd.DataFrame() if return_as_pandas else pl.DataFrame() + + for event in events: + event.get('competitions')[0].get('competitors')[0].get('team').pop('links',None) + event.get('competitions')[0].get('competitors')[1].get('team').pop('links',None) + if event.get('competitions')[0].get('competitors')[0].get('homeAway') == 'home': + event = _extract_home_away(event, 0, 'home') + event = _extract_home_away(event, 1, 'away') + else: + event = _extract_home_away(event, 0, 'away') + event = _extract_home_away(event, 1, 'home') + del_keys = ['geoBroadcasts', 'headlines', 'series', 'situation', 'tickets', 'odds'] + for k in del_keys: + event.get('competitions')[0].pop(k, None) + event.get('competitions')[0]['notes_type'] = event.get('competitions')[0]['notes'][0].get("type") if len(event.get('competitions')[0]['notes']) > 0 else '' + event.get('competitions')[0]['notes_headline'] = event.get('competitions')[0]['notes'][0].get("headline").replace('"','') if len(event.get('competitions')[0]['notes']) > 0 else '' + event.get('competitions')[0]['broadcast_market'] = event.get('competitions')[0].get('broadcasts', [])[0].get('market', "") if len(event.get('competitions')[0].get('broadcasts')) > 0 else "" + event.get('competitions')[0]['broadcast_name'] = event.get('competitions')[0].get('broadcasts', [])[0].get('names', [])[0] if len(event.get('competitions')[0].get('broadcasts')) > 0 else "" + event.get('competitions')[0].pop('broadcasts', None) + event.get('competitions')[0].pop('notes', None) + event.get('competitions')[0].pop('competitors', None) + x = pl.from_pandas(pd.json_normalize(event.get('competitions')[0], sep = '_')) + x = x.with_columns( + game_id = (pl.col('id').cast(pl.Int64)), + season = (event.get('season').get('year')), + season_type = (event.get('season').get('type')), + week = (event.get('week', {}).get('number')), + home_linescores = pl.when(pl.col('status_type_description') == 'Postponed') + .then(None) + .otherwise(pl.col('home_linescores')), + away_linescores = pl.when(pl.col('status_type_description') == 'Postponed') + .then(None) + .otherwise(pl.col('away_linescores')), + ) + x = x[[s.name for s in x if s.null_count() != x.height]] + ev = pl.concat([ev, x], how = 'diagonal') + + ev.columns = [underscore(c) for c in ev.columns] + + return ev.to_pandas() if return_as_pandas else ev + +def _extract_home_away(event, arg1, arg2): + event['competitions'][0][arg2] = ( + event.get('competitions')[0].get('competitors')[arg1].get('team') + ) + event['competitions'][0][arg2]['score'] = ( + event.get('competitions')[0].get('competitors')[arg1].get('score') + ) + event['competitions'][0][arg2]['winner'] = ( + event.get('competitions')[0].get('competitors')[arg1].get('winner') + ) + ## add winner back to main competitors if does not exist + event['competitions'][0]['competitors'][arg1]['winner'] = ( + event.get('competitions')[0].get('competitors')[arg1].get('winner', False) ) - resp = download(url=url) + event['competitions'][0][arg2]['currentRank'] = ( + event.get('competitions')[0] + .get('competitors')[arg1] + .get('curatedRank', {}) + .get('current', 99) + ) + event['competitions'][0][arg2]['linescores'] = ( + event.get('competitions')[0] + .get('competitors')[arg1] + .get('linescores', [{'value': 'N/A'}]) + ) + ## add linescores back to main competitors if does not exist + event['competitions'][0]['competitors'][arg1]['linescores'] = ( + event.get('competitions')[0] + .get('competitors')[arg1] + .get('linescores', []) + ) + event['competitions'][0][arg2]['records'] = ( + event.get('competitions')[0] + .get('competitors')[arg1] + .get('records', []) + ) + return event - ev = pd.DataFrame() - if resp is not None: - events_txt = json.loads(resp) - events = events_txt.get('events') - for event in events: - event.get('competitions')[0].get('competitors')[0].get('team').pop('links',None) - event.get('competitions')[0].get('competitors')[1].get('team').pop('links',None) - if event.get('competitions')[0].get('competitors')[0].get('homeAway')=='home': - event['competitions'][0]['home'] = event.get('competitions')[0].get('competitors')[0].get('team') - event['competitions'][0]['home']['score'] = event.get('competitions')[0].get('competitors')[0].get('score') - event['competitions'][0]['home']['winner'] = event.get('competitions')[0].get('competitors')[0].get('winner') - event['competitions'][0]['away'] = event.get('competitions')[0].get('competitors')[1].get('team') - event['competitions'][0]['away']['score'] = event.get('competitions')[0].get('competitors')[1].get('score') - event['competitions'][0]['away']['winner'] = event.get('competitions')[0].get('competitors')[1].get('winner') - else: - event['competitions'][0]['away'] = event.get('competitions')[0].get('competitors')[0].get('team') - event['competitions'][0]['away']['score'] = event.get('competitions')[0].get('competitors')[0].get('score') - event['competitions'][0]['away']['winner'] = event.get('competitions')[0].get('competitors')[0].get('winner') - event['competitions'][0]['home'] = event.get('competitions')[0].get('competitors')[1].get('team') - event['competitions'][0]['home']['score'] = event.get('competitions')[0].get('competitors')[1].get('score') - event['competitions'][0]['home']['winner'] = event.get('competitions')[0].get('competitors')[1].get('winner') - - del_keys = ['broadcasts','geoBroadcasts', 'headlines', 'series', 'situation', 'tickets', 'odds'] - for k in del_keys: - event.get('competitions')[0].pop(k, None) - if len(event.get('competitions')[0]['notes'])>0: - event.get('competitions')[0]['notes_type'] = event.get('competitions')[0]['notes'][0].get("type") - event.get('competitions')[0]['notes_headline'] = event.get('competitions')[0]['notes'][0].get("headline").replace('"','') - else: - event.get('competitions')[0]['notes_type'] = '' - event.get('competitions')[0]['notes_headline'] = '' - event.get('competitions')[0].pop('notes', None) - x = pd.json_normalize(event.get('competitions')[0], sep='_') - x['game_id'] = x['id'].astype(int) - x['season'] = event.get('season').get('year') - x['season_type'] = event.get('season').get('type') - ev = pd.concat([ev,x],axis=0, ignore_index=True) - ev = pd.DataFrame(ev) - ev.columns = [underscore(c) for c in ev.columns.tolist()] - return ev - - - -def espn_nfl_calendar(season=None, ondays=None) -> pd.DataFrame: +def espn_nfl_calendar(season = None, ondays = None, + return_as_pandas = True, **kwargs) -> pd.DataFrame: """espn_nfl_calendar - look up the NFL calendar for a given season Args: season (int): Used to define different seasons. 2002 is the earliest available season. ondays (boolean): Used to return dates for calendar ondays + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing calendar dates for the requested season. @@ -101,21 +131,16 @@ def espn_nfl_calendar(season=None, ondays=None) -> pd.DataFrame: ValueError: If `season` is less than 2002. """ if ondays is not None: - url = "https://sports.core.api.espn.com/v2/sports/football/leagues/nfl/seasons/{}/types/2/calendar/ondays".format(season) - resp = download(url=url) - txt = json.loads(resp).get('eventDate').get('dates') - full_schedule = pd.DataFrame(txt,columns=['dates']) - full_schedule['datenum'] = list(map(lambda x: x[:10].replace("-",""),full_schedule['dates'])) + full_schedule = __ondays_nfl_calendar(season, **kwargs) else: - if season is None: - season_url = '' - else: - season_url = '&dates=' + str(season) - url = "http://site.api.espn.com/apis/site/v2/sports/football/nfl/scoreboard?{}{}".format(season_url) - resp = download(url=url) - txt = json.loads(resp) + url = "http://site.api.espn.com/apis/site/v2/sports/football/nfl/scoreboard" + params = { + 'dates': season + } + resp = download(url = url, params = params, **kwargs) + txt = resp.json() txt = txt.get('leagues')[0].get('calendar') - full_schedule = pd.DataFrame() + full_schedule = pl.DataFrame() for i in range(len(txt)): if txt[i].get('entries', None) is not None: reg = pd.json_normalize(data = txt[i], @@ -125,20 +150,35 @@ def espn_nfl_calendar(season=None, ondays=None) -> pd.DataFrame: record_prefix='week_', errors="ignore", sep='_') - full_schedule = pd.concat([full_schedule,reg], ignore_index=True) - full_schedule['season']=season - full_schedule.columns = [underscore(c) for c in full_schedule.columns.tolist()] - full_schedule = full_schedule.rename(columns={"week_value": "week", "season_type_value": "season_type"}) - return full_schedule + full_schedule = pl.concat([full_schedule, pl.from_pandas(reg)], how = 'vertical') + full_schedule = full_schedule.with_columns( + season = season + ) + full_schedule.columns = [underscore(c) for c in full_schedule.columns] + full_schedule = full_schedule.rename({"week_value": "week", "season_type_value": "season_type"}) + return full_schedule.to_pandas() if return_as_pandas else full_schedule + + +def __ondays_nfl_calendar(season, **kwargs): + url = f"https://sports.core.api.espn.com/v2/sports/football/leagues/nfl/seasons/{season}/types/2/calendar/ondays" + resp = download(url=url, **kwargs) + if resp is not None: + txt = resp.json().get('eventDate').get('dates') + result = pl.DataFrame(txt, schema=['dates']) + result = result.with_columns(dateURL = pl.col('dates').str.slice(0, 10)) + result = result.with_columns( + url="http://site.api.espn.com/apis/site/v2/sports/football/nfl/scoreboard?dates=" + + pl.col('dateURL') + ) + + return result def most_recent_nfl_season(): - today = datetime.datetime.today() + today = datetime.datetime.now() current_year = today.year current_month = today.month current_day = today.day - if current_month >= 9: - return current_year - return current_year - 1 + return current_year if current_month >= 9 else current_year - 1 def get_current_week(): @@ -154,9 +194,5 @@ def get_current_week(): current_week = int((pd.to_datetime("today") - first_game).dt.days / 7 + 1) # hardcoded week bounds because this whole date based thing has assumptions anyway - if current_week < 1: - current_week = 1 - if current_week > 22: - current_week = 22 - - return current_week \ No newline at end of file + current_week = max(current_week, 1) + return min(current_week, 22) \ No newline at end of file diff --git a/sportsdataverse/nfl/nfl_teams.py b/sportsdataverse/nfl/nfl_teams.py index f35924b..c57e7e5 100755 --- a/sportsdataverse/nfl/nfl_teams.py +++ b/sportsdataverse/nfl/nfl_teams.py @@ -1,19 +1,28 @@ import pandas as pd +import polars as pl import json -from sportsdataverse.dl_utils import download -from urllib.error import URLError, HTTPError, ContentTooShortError +from sportsdataverse.dl_utils import download, underscore -def espn_nfl_teams() -> pd.DataFrame: +def espn_nfl_teams(return_as_pandas = True, **kwargs) -> pd.DataFrame: """espn_nfl_teams - look up NFL teams + Args: + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Returns: pd.DataFrame: Pandas dataframe containing teams for the requested league. + + Example: + `nfl_df = sportsdataverse.nfl.espn_nfl_teams()` + """ - ev = pd.DataFrame() - url = "http://site.api.espn.com/apis/site/v2/sports/football/nfl/teams?limit=1000" - resp = download(url=url) + url = "http://site.api.espn.com/apis/site/v2/sports/football/nfl/teams" + params = { + "limit": 1000 + } + resp = download(url=url, params = params, **kwargs) if resp is not None: - events_txt = json.loads(resp) + events_txt = resp.json() teams = events_txt.get('sports')[0].get('leagues')[0].get('teams') del_keys = ['record', 'links'] @@ -22,5 +31,5 @@ def espn_nfl_teams() -> pd.DataFrame: team.get('team').pop(k, None) teams = pd.json_normalize(teams, sep='_') teams.columns = [underscore(c) for c in teams.columns.tolist()] - return teams + return teams if return_as_pandas else pl.from_pandas(teams) From b71f5727332df183fdd81c6c7d8f3bbf37aee62a Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Mon, 10 Jul 2023 06:31:16 -0400 Subject: [PATCH 15/79] simplifying the returns on the helper functions in the `cfb.espn_cfb_pbp()` --- sportsdataverse/cfb/cfb_pbp.py | 253 +++++++++++++++++---------------- 1 file changed, 127 insertions(+), 126 deletions(-) diff --git a/sportsdataverse/cfb/cfb_pbp.py b/sportsdataverse/cfb/cfb_pbp.py index fb0d595..8913dfa 100755 --- a/sportsdataverse/cfb/cfb_pbp.py +++ b/sportsdataverse/cfb/cfb_pbp.py @@ -135,34 +135,18 @@ def cfb_pbp_disk(self): return self.json def __helper_cfb_pbp_drives(self, pbp_txt): - pbp_txt, gameSpread, overUnder, homeFavorite, gameSpreadAvailable, \ - homeTeamId, homeTeamMascot, homeTeamName,\ - homeTeamAbbrev, homeTeamNameAlt,\ - awayTeamId, awayTeamMascot, awayTeamName,\ - awayTeamAbbrev, awayTeamNameAlt = self.__helper_cfb_pbp(pbp_txt) + pbp_txt, init = self.__helper_cfb_pbp(pbp_txt) pbp_txt["plays"] = pl.DataFrame() # negotiating the drive meta keys into columns after unnesting drive plays # concatenating the previous and current drives categories when necessary if "drives" in pbp_txt.keys() and pbp_txt.get('header').get('competitions')[0].get('playByPlaySource') != 'none': - pbp_txt = self.__helper_cfb_pbp_features(pbp_txt, \ - gameSpread, gameSpreadAvailable, \ - overUnder, homeFavorite, \ - homeTeamId, homeTeamMascot, \ - homeTeamName, homeTeamAbbrev, homeTeamNameAlt, \ - awayTeamId, awayTeamMascot, awayTeamName, \ - awayTeamAbbrev, awayTeamNameAlt) + pbp_txt = self.__helper_cfb_pbp_features(pbp_txt, init) else: pbp_txt["drives"] = {} return pbp_txt - def __helper_cfb_pbp_features(self, pbp_txt, - gameSpread, gameSpreadAvailable, - overUnder, homeFavorite, - homeTeamId, homeTeamMascot, - homeTeamName, homeTeamAbbrev, homeTeamNameAlt, - awayTeamId, awayTeamMascot, awayTeamName, - awayTeamAbbrev, awayTeamNameAlt): + def __helper_cfb_pbp_features(self, pbp_txt, init): pbp_txt["plays"] = pl.DataFrame() for key in pbp_txt.get("drives").keys(): prev_drives = pd.json_normalize( @@ -207,31 +191,31 @@ def __helper_cfb_pbp_features(self, pbp_txt, seasonType = pbp_txt.get("header").get("season").get("type"), week = pbp_txt.get("header").get("week"), status_type_completed = pbp_txt.get("header").get("competitions")[0].get("status").get("type").get("completed"), - homeTeamId = pl.lit(homeTeamId), - awayTeamId = pl.lit(awayTeamId), - homeTeamName = pl.lit(str(homeTeamName)), - awayTeamName = pl.lit(str(awayTeamName)), - homeTeamMascot = pl.lit(str(homeTeamMascot)), - awayTeamMascot = pl.lit(str(awayTeamMascot)), - homeTeamAbbrev = pl.lit(str(homeTeamAbbrev)), - awayTeamAbbrev = pl.lit(str(awayTeamAbbrev)), - homeTeamNameAlt = pl.lit(str(homeTeamNameAlt)), - awayTeamNameAlt = pl.lit(str(awayTeamNameAlt)), - homeTeamSpread = pl.when(homeFavorite == True) - .then(abs(gameSpread)) - .otherwise(-1 * abs(gameSpread)), - gameSpread = pl.lit(gameSpread), - gameSpreadAvailable = pl.lit(gameSpreadAvailable), - overUnder = pl.lit(float(overUnder)), - homeFavorite = pl.lit(homeFavorite) - ) - - pbp_txt["plays"] = pbp_txt["plays"].with_columns( + homeTeamId = pl.lit(init["homeTeamId"]), + awayTeamId = pl.lit(init["awayTeamId"]), + homeTeamName = pl.lit(str(init["homeTeamName"])), + awayTeamName = pl.lit(str(init["awayTeamName"])), + homeTeamMascot = pl.lit(str(init["homeTeamMascot"])), + awayTeamMascot = pl.lit(str(init["awayTeamMascot"])), + homeTeamAbbrev = pl.lit(str(init["homeTeamAbbrev"])), + awayTeamAbbrev = pl.lit(str(init["awayTeamAbbrev"])), + homeTeamNameAlt = pl.lit(str(init["homeTeamNameAlt"])), + awayTeamNameAlt = pl.lit(str(init["awayTeamNameAlt"])), + gameSpread = pl.lit(init["gameSpread"]).abs(), + homeFavorite = pl.lit(init["homeFavorite"]), + gameSpreadAvailable = pl.lit(init["gameSpreadAvailable"]), + overUnder = pl.lit(float(init["overUnder"])), + ).with_columns( + homeTeamSpread = pl.when(pl.col('homeFavorite') == True) + .then(pl.col('gameSpread')) + .otherwise(-1*pl.col('gameSpread')), + ).with_columns( pl.col("period.number").cast(pl.Int32), pl.col("clock.displayValue").str.split(':').list.to_struct(n_field_strategy="max_width").alias("clock.mm") ).with_columns( pl.col("clock.mm").struct.rename_fields(["clock.minutes", "clock.seconds"]) - ).unnest("clock.mm" + ).unnest( + "clock.mm" ).with_columns( pl.col("clock.minutes").cast(pl.Int32), pl.col("clock.seconds").cast(pl.Int32), @@ -295,57 +279,57 @@ def __helper_cfb_pbp_features(self, pbp_txt, pl.col("homeTeamId").cast(pl.Int32), pl.col("awayTeamId").cast(pl.Int32), pl.when(pl.col("type.text").is_in(kickoff_vec) - .and_(pl.col("start.team.id") == homeTeamId)) + .and_(pl.col("start.team.id") == init["homeTeamId"])) .then(pl.col("awayTeamId")) .when(pl.col("type.text").is_in(kickoff_vec) - .and_(pl.col("start.team.id") == awayTeamId)) + .and_(pl.col("start.team.id") == init["awayTeamId"])) .then(pl.col("homeTeamId")) .otherwise(pl.col("start.team.id")) .alias("start.pos_team.id") ).with_columns( - pl.when(pl.col("start.pos_team.id") == homeTeamId) - .then(awayTeamId) - .otherwise(homeTeamId) + pl.when(pl.col("start.pos_team.id") == init["homeTeamId"]) + .then(init["awayTeamId"]) + .otherwise(init["homeTeamId"]) .alias("start.def_pos_team.id"), - pl.when(pl.col("end.team.id") == homeTeamId) - .then(awayTeamId) - .otherwise(homeTeamId) + pl.when(pl.col("end.team.id") == init["homeTeamId"]) + .then(init["awayTeamId"]) + .otherwise(init["homeTeamId"]) .alias("end.def_pos_team.id"), pl.col("end.team.id") .alias("end.pos_team.id") ).with_columns( - pl.when(pl.col("start.pos_team.id") == homeTeamId) + pl.when(pl.col("start.pos_team.id") == init["homeTeamId"]) .then(pl.col("homeTeamName")) .otherwise(pl.col("awayTeamName")) .alias("start.pos_team.name"), - pl.when(pl.col("start.pos_team.id") == homeTeamId) + pl.when(pl.col("start.pos_team.id") == init["homeTeamId"]) .then(pl.col("awayTeamName")) .otherwise(pl.col("homeTeamName")) .alias("start.def_pos_team.name"), - pl.when(pl.col("end.pos_team.id") == homeTeamId) + pl.when(pl.col("end.pos_team.id") == init["homeTeamId"]) .then(pl.col("homeTeamName")) .otherwise(pl.col("awayTeamName")) .alias("end.pos_team.name"), - pl.when(pl.col("end.pos_team.id") == homeTeamId) + pl.when(pl.col("end.pos_team.id") == init["homeTeamId"]) .then(pl.col("awayTeamName")) .otherwise(pl.col("homeTeamName")) .alias("end.def_pos_team.name"), - pl.when(pl.col("start.pos_team.id") == homeTeamId) + pl.when(pl.col("start.pos_team.id") == init["homeTeamId"]) .then(True) .otherwise(False) .alias("start.is_home"), - pl.when(pl.col("end.pos_team.id") == homeTeamId) + pl.when(pl.col("end.pos_team.id") == init["homeTeamId"]) .then(True) .otherwise(False) .alias("end.is_home"), pl.when((pl.col("type.text") == "Timeout") .and_( - pl.col("text").str.to_lowercase().str.contains(str(homeTeamAbbrev).lower()) + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamAbbrev"]).lower()) .or_( - pl.col("text").str.to_lowercase().str.contains(str(homeTeamAbbrev).lower()), - pl.col("text").str.to_lowercase().str.contains(str(homeTeamName).lower()), - pl.col("text").str.to_lowercase().str.contains(str(homeTeamMascot).lower()), - pl.col("text").str.to_lowercase().str.contains(str(homeTeamNameAlt).lower()) + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamAbbrev"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamName"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamMascot"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamNameAlt"]).lower()) ) )) .then(True) @@ -353,12 +337,12 @@ def __helper_cfb_pbp_features(self, pbp_txt, .alias("homeTimeoutCalled"), pl.when((pl.col("type.text") == "Timeout") .and_( - pl.col("text").str.to_lowercase().str.contains(str(awayTeamAbbrev).lower()) + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamAbbrev"]).lower()) .or_( - pl.col("text").str.to_lowercase().str.contains(str(awayTeamAbbrev).lower()), - pl.col("text").str.to_lowercase().str.contains(str(awayTeamName).lower()), - pl.col("text").str.to_lowercase().str.contains(str(awayTeamMascot).lower()), - pl.col("text").str.to_lowercase().str.contains(str(awayTeamNameAlt).lower()) + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamAbbrev"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamName"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamMascot"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamNameAlt"]).lower()) ) )) .then(True) @@ -367,10 +351,10 @@ def __helper_cfb_pbp_features(self, pbp_txt, ) pbp_txt["timeouts"] = { - homeTeamId: {"1": [], "2": []}, - awayTeamId: {"1": [], "2": []}, + init["homeTeamId"]: {"1": [], "2": []}, + init["awayTeamId"]: {"1": [], "2": []}, } - pbp_txt["timeouts"][homeTeamId]["1"] = ( + pbp_txt["timeouts"][init["homeTeamId"]]["1"] = ( pbp_txt["plays"] .filter( (pl.col("homeTimeoutCalled") == True) @@ -379,7 +363,7 @@ def __helper_cfb_pbp_features(self, pbp_txt, .get_column("id") .to_list() ) - pbp_txt["timeouts"][homeTeamId]["2"] = ( + pbp_txt["timeouts"][init["homeTeamId"]]["2"] = ( pbp_txt["plays"] .filter( (pl.col("homeTimeoutCalled") == True) @@ -388,7 +372,7 @@ def __helper_cfb_pbp_features(self, pbp_txt, .get_column("id") .to_list() ) - pbp_txt["timeouts"][awayTeamId]["1"] = ( + pbp_txt["timeouts"][init["awayTeamId"]]["1"] = ( pbp_txt["plays"] .filter( (pl.col("awayTimeoutCalled") == True) @@ -397,7 +381,7 @@ def __helper_cfb_pbp_features(self, pbp_txt, .get_column("id") .to_list() ) - pbp_txt["timeouts"][awayTeamId]["2"] = ( + pbp_txt["timeouts"][init["awayTeamId"]]["2"] = ( pbp_txt["plays"] .filter( (pl.col("awayTimeoutCalled") == True) @@ -408,65 +392,65 @@ def __helper_cfb_pbp_features(self, pbp_txt, ) pbp_txt["plays"] = pbp_txt["plays"].with_columns( pl.when(( - (pbp_txt["timeouts"][homeTeamId]["1"] <= pl.col("id")) + (pbp_txt["timeouts"][init["homeTeamId"]]["1"] <= pl.col("id")) .and_(pl.col("period.number") <= 2, - len(pbp_txt["timeouts"][homeTeamId]["1"]) == 1) + len(pbp_txt["timeouts"][init["homeTeamId"]]["1"]) == 1) ).or_( - (pbp_txt["timeouts"][homeTeamId]["2"] <= pl.col("id")) + (pbp_txt["timeouts"][init["homeTeamId"]]["2"] <= pl.col("id")) .and_(pl.col("period.number") > 2, - len(pbp_txt["timeouts"][homeTeamId]["2"]) == 1) + len(pbp_txt["timeouts"][init["homeTeamId"]]["2"]) == 1) )) .then(2) .when(( - (pbp_txt["timeouts"][homeTeamId]["1"] <= pl.col("id")) + (pbp_txt["timeouts"][init["homeTeamId"]]["1"] <= pl.col("id")) .and_(pl.col("period.number") <= 2, - len(pbp_txt["timeouts"][homeTeamId]["1"]) == 2) + len(pbp_txt["timeouts"][init["homeTeamId"]]["1"]) == 2) ).or_( - (pbp_txt["timeouts"][homeTeamId]["2"] <= pl.col("id")) + (pbp_txt["timeouts"][init["homeTeamId"]]["2"] <= pl.col("id")) .and_(pl.col("period.number") > 2, - len(pbp_txt["timeouts"][homeTeamId]["2"]) == 2) + len(pbp_txt["timeouts"][init["homeTeamId"]]["2"]) == 2) )) .then(1) .when(( - (pbp_txt["timeouts"][homeTeamId]["1"] <= pl.col("id")) + (pbp_txt["timeouts"][init["homeTeamId"]]["1"] <= pl.col("id")) .and_(pl.col("period.number") <= 2, - len(pbp_txt["timeouts"][homeTeamId]["1"]) == 3) + len(pbp_txt["timeouts"][init["homeTeamId"]]["1"]) == 3) ).or_( - (pbp_txt["timeouts"][homeTeamId]["2"] <= pl.col("id")) + (pbp_txt["timeouts"][init["homeTeamId"]]["2"] <= pl.col("id")) .and_(pl.col("period.number") > 2, - len(pbp_txt["timeouts"][homeTeamId]["2"]) == 3) + len(pbp_txt["timeouts"][init["homeTeamId"]]["2"]) == 3) )) .then(0) .otherwise(3) .alias("end.homeTeamTimeouts"), pl.when(( - (pbp_txt["timeouts"][awayTeamId]["1"] <= pl.col("id")) + (pbp_txt["timeouts"][init["awayTeamId"]]["1"] <= pl.col("id")) .and_(pl.col("period.number") <= 2, - len(pbp_txt["timeouts"][awayTeamId]["1"]) == 1) + len(pbp_txt["timeouts"][init["awayTeamId"]]["1"]) == 1) ).or_( - (pbp_txt["timeouts"][awayTeamId]["2"] <= pl.col("id")) + (pbp_txt["timeouts"][init["awayTeamId"]]["2"] <= pl.col("id")) .and_(pl.col("period.number") > 2, - len(pbp_txt["timeouts"][awayTeamId]["2"]) == 1) + len(pbp_txt["timeouts"][init["awayTeamId"]]["2"]) == 1) )) .then(2) .when(( - (pbp_txt["timeouts"][awayTeamId]["1"] <= pl.col("id")) + (pbp_txt["timeouts"][init["awayTeamId"]]["1"] <= pl.col("id")) .and_(pl.col("period.number") <= 2, - len(pbp_txt["timeouts"][awayTeamId]["1"]) == 2) + len(pbp_txt["timeouts"][init["awayTeamId"]]["1"]) == 2) ).or_( - (pbp_txt["timeouts"][awayTeamId]["2"] <= pl.col("id")) + (pbp_txt["timeouts"][init["awayTeamId"]]["2"] <= pl.col("id")) .and_(pl.col("period.number") > 2, - len(pbp_txt["timeouts"][awayTeamId]["2"]) == 2) + len(pbp_txt["timeouts"][init["awayTeamId"]]["2"]) == 2) )) .then(1) .when(( - (pbp_txt["timeouts"][awayTeamId]["1"] <= pl.col("id")) + (pbp_txt["timeouts"][init["awayTeamId"]]["1"] <= pl.col("id")) .and_(pl.col("period.number") <= 2, - len(pbp_txt["timeouts"][awayTeamId]["1"]) == 3) + len(pbp_txt["timeouts"][init["awayTeamId"]]["1"]) == 3) ).or_( - (pbp_txt["timeouts"][awayTeamId]["2"] <= pl.col("id")) + (pbp_txt["timeouts"][init["awayTeamId"]]["2"] <= pl.col("id")) .and_(pl.col("period.number") > 2, - len(pbp_txt["timeouts"][awayTeamId]["2"]) == 3) + len(pbp_txt["timeouts"][init["awayTeamId"]]["2"]) == 3) )) .then(0) .otherwise(3) @@ -565,9 +549,9 @@ def __helper_cfb_pbp_features(self, pbp_txt, pbp_txt["firstHalfKickoffTeamId"] = np.where( (pbp_txt["plays"]["game_play_number"] == 1) & (pbp_txt["plays"]["type.text"].is_in(kickoff_vec)) - & (pbp_txt["plays"]["start.team.id"] == homeTeamId), - homeTeamId, - awayTeamId + & (pbp_txt["plays"]["start.team.id"] == init["homeTeamId"]), + init["homeTeamId"], + init["awayTeamId"] ) pbp_txt["firstHalfKickoffTeamId"] = pbp_txt["firstHalfKickoffTeamId"][0] @@ -617,18 +601,47 @@ def __helper_cfb_pbp_features(self, pbp_txt, return pbp_txt def __helper_cfb_pbp(self, pbp_txt): - gameSpread, overUnder, homeFavorite, gameSpreadAvailable = self.__helper_cfb_pickcenter(pbp_txt) + init = self.__helper_cfb_pickcenter(pbp_txt) + return self.__helper_cfb_game_data(pbp_txt, init) + + def __helper_cfb_pickcenter(self, pbp_txt): + # Spread definition + if len(pbp_txt.get("pickcenter",[])) > 1: + homeFavorite = pbp_txt.get("pickcenter",{})[0].get("homeTeamOdds",{}).get("favorite","") + if "spread" in pbp_txt.get("pickcenter",{})[1].keys(): + gameSpread = pbp_txt.get("pickcenter",{})[1].get("spread","") + overUnder = pbp_txt.get("pickcenter",{})[1].get("overUnder","") + else: + gameSpread = pbp_txt.get("pickcenter",{})[0].get("spread","") + overUnder = pbp_txt.get("pickcenter",{})[0].get("overUnder","") + gameSpreadAvailable = True + # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") + else: + gameSpread = 2.5 + overUnder = 140.5 + homeFavorite = True + gameSpreadAvailable = False + + return { + "gameSpread": gameSpread, + "overUnder": overUnder, + "homeFavorite": homeFavorite, + "gameSpreadAvailable": gameSpreadAvailable + } + + def __helper_cfb_game_data(self, pbp_txt, init): pbp_txt['timeouts'] = {} pbp_txt['teamInfo'] = pbp_txt['header']['competitions'][0] pbp_txt['season'] = pbp_txt['header']['season'] pbp_txt['playByPlaySource'] = pbp_txt['header']['competitions'][0]['playByPlaySource'] - pbp_txt['gameSpreadAvailable'] = gameSpreadAvailable - pbp_txt['gameSpread'] = gameSpread - pbp_txt["homeFavorite"] = homeFavorite + pbp_txt['boxscoreSource'] = pbp_txt['header']['competitions'][0]['boxscoreSource'] + pbp_txt['gameSpreadAvailable'] = init["gameSpreadAvailable"] + pbp_txt['gameSpread'] = init["gameSpread"] + pbp_txt["homeFavorite"] = init["homeFavorite"] pbp_txt["homeTeamSpread"] = np.where( - homeFavorite == True, abs(gameSpread), -1 * abs(gameSpread) + init["homeFavorite"] == True, abs(init["gameSpread"]), -1 * abs(init["gameSpread"]) ) - pbp_txt["overUnder"] = float(overUnder) + pbp_txt["overUnder"] = init["overUnder"] # Home and Away identification variables if pbp_txt['header']['competitions'][0]['competitors'][0]['homeAway']=='home': pbp_txt['header']['competitions'][0]['home'] = pbp_txt['header']['competitions'][0]['competitors'][0]['team'] @@ -656,29 +669,17 @@ def __helper_cfb_pbp(self, pbp_txt): homeTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['location']) homeTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['abbreviation']) homeTeamNameAlt = re.sub("Stat(.+)", "St", homeTeamName) - return pbp_txt, gameSpread, overUnder, homeFavorite, gameSpreadAvailable, homeTeamId,\ - homeTeamMascot,homeTeamName,homeTeamAbbrev,homeTeamNameAlt,\ - awayTeamId,awayTeamMascot,awayTeamName,awayTeamAbbrev,awayTeamNameAlt - - def __helper_cfb_pickcenter(self, pbp_txt): - # Spread definition - if len(pbp_txt.get("pickcenter",[])) > 1: - homeFavorite = pbp_txt.get("pickcenter",{})[0].get("homeTeamOdds",{}).get("favorite","") - if "spread" in pbp_txt.get("pickcenter",{})[1].keys(): - gameSpread = pbp_txt.get("pickcenter",{})[1].get("spread","") - overUnder = pbp_txt.get("pickcenter",{})[1].get("overUnder","") - gameSpreadAvailable = True - else: - gameSpread = pbp_txt.get("pickcenter",{})[0].get("spread","") - overUnder = pbp_txt.get("pickcenter",{})[0].get("overUnder","") - gameSpreadAvailable = True - # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") - else: - gameSpread = 2.5 - overUnder = 55.5 - homeFavorite = True - gameSpreadAvailable = False - return gameSpread, overUnder, homeFavorite, gameSpreadAvailable + init["homeTeamId"] = homeTeamId + init["homeTeamMascot"] = homeTeamMascot + init["homeTeamName"] = homeTeamName + init["homeTeamAbbrev"] = homeTeamAbbrev + init["homeTeamNameAlt"] = homeTeamNameAlt + init["awayTeamId"] = awayTeamId + init["awayTeamMascot"] = awayTeamMascot + init["awayTeamName"] = awayTeamName + init["awayTeamAbbrev"] = awayTeamAbbrev + init["awayTeamNameAlt"] = awayTeamNameAlt + return pbp_txt, init def __add_downs_data(self, play_df): """ From c711d9ed2fd47f8c2fd759ce12bcfcf823d4afa9 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Mon, 10 Jul 2023 09:13:49 -0400 Subject: [PATCH 16/79] Update nhl package to use polars --- sportsdataverse/nhl/__init__.py | 1 + sportsdataverse/nhl/nhl_api.py | 41 +-- sportsdataverse/nhl/nhl_loaders.py | 62 ++--- sportsdataverse/nhl/nhl_pbp.py | 380 ++++++++++++++-------------- sportsdataverse/nhl/nhl_schedule.py | 206 ++++++++++----- sportsdataverse/nhl/nhl_teams.py | 23 +- 6 files changed, 401 insertions(+), 312 deletions(-) diff --git a/sportsdataverse/nhl/__init__.py b/sportsdataverse/nhl/__init__.py index 6cbbfe8..0f27674 100755 --- a/sportsdataverse/nhl/__init__.py +++ b/sportsdataverse/nhl/__init__.py @@ -1,3 +1,4 @@ +from sportsdataverse.nhl.nhl_api import * from sportsdataverse.nhl.nhl_loaders import * from sportsdataverse.nhl.nhl_pbp import * from sportsdataverse.nhl.nhl_schedule import * diff --git a/sportsdataverse/nhl/nhl_api.py b/sportsdataverse/nhl/nhl_api.py index e9f95fa..43a2c87 100755 --- a/sportsdataverse/nhl/nhl_api.py +++ b/sportsdataverse/nhl/nhl_api.py @@ -1,5 +1,5 @@ -from typing import Dict import pandas as pd +import polars as pl import numpy as np import re import json @@ -7,7 +7,7 @@ from sportsdataverse.dl_utils import download, flatten_json_iterative, key_check -def nhl_api_pbp(game_id: int) -> Dict: +def nhl_api_pbp(game_id: int, **kwargs) -> Dict: """nhl_api_pbp() - Pull the game by id. Data from API endpoints - `nhl/playbyplay`, `nhl/summary` Args: @@ -21,13 +21,11 @@ def nhl_api_pbp(game_id: int) -> Dict: Example: `nhl_df = sportsdataverse.nhl.nhl_api_pbp(game_id=2021020079)` """ - # play by play - pbp_txt = {} # summary endpoint for pickcenter array - summary_url = "https://statsapi.web.nhl.com/api/v1/game/{}/feed/live?site=en_nhl".format(game_id) - summary_resp = download(summary_url) - summary = json.loads(summary_resp) - pbp_txt['datetime'] = summary.get("gameData").get("datetime") + summary_url = f"https://statsapi.web.nhl.com/api/v1/game/{game_id}/feed/live?site=en_nhl" + summary_resp = download(summary_url, **kwargs) + summary = summary_resp.json() + pbp_txt = {'datetime': summary.get("gameData").get("datetime")} pbp_txt['game'] = summary.get("gameData").get("game") pbp_txt['players'] = summary.get("gameData").get("players") pbp_txt['status'] = summary.get("gameData").get("status") @@ -38,27 +36,30 @@ def nhl_api_pbp(game_id: int) -> Dict: return pbp_txt -def nhl_api_schedule(start_date: str, end_date: str) -> Dict: +def nhl_api_schedule(start_date: str, end_date: str, return_as_pandas = True) -> pd.DataFrame: """nhl_api_schedule() - Pull the game by id. Data from API endpoints - `nhl/schedule` Args: game_id (int): Unique game_id, can be obtained from nhl_schedule(). Returns: - Dict: + pd.DataFrame: Pandas dataframe containing the schedule for the requested seasons. Example: `nhl_sched_df = sportsdataverse.nhl.nhl_api_schedule(start_date=2021-10-23, end_date=2021-10-28)` """ - # play by play - pbp_txt = {} # summary endpoint for pickcenter array - summary_url = "https://statsapi.web.nhl.com/api/v1/schedule?site=en_nhl&startDate={}&endDate={}".format(start_date, end_date) - summary_resp = download(summary_url) - summary = json.loads(summary_resp) - pbp_txt['dates'] = summary.get("dates") - pbp_txt_games = pd.DataFrame() + summary_url = "https://statsapi.web.nhl.com/api/v1/schedule" + params = { + "site": "en_nhl", + "startDate": start_date, + "endDate": end_date + } + summary_resp = download(summary_url, params = params, **kwargs) + summary = summary_resp.json() + pbp_txt = {'dates': summary.get("dates")} + pbp_txt_games = pl.DataFrame() for date in pbp_txt['dates']: - game = pd.json_normalize(date, record_path="games", meta=["date"]) - pbp_txt_games = pd.concat([pbp_txt_games, game], ignore_index=True) - return pbp_txt_games \ No newline at end of file + game = pl.from_pandas(pd.json_normalize(date, record_path="games", meta=["date"])) + pbp_txt_games = pl.concat([pbp_txt_games, game], how = 'vertical') + return pbp_txt_games.to_pandas() if return_as_pandas else pbp_txt_games \ No newline at end of file diff --git a/sportsdataverse/nhl/nhl_loaders.py b/sportsdataverse/nhl/nhl_loaders.py index 27e9945..75f3d1d 100755 --- a/sportsdataverse/nhl/nhl_loaders.py +++ b/sportsdataverse/nhl/nhl_loaders.py @@ -1,4 +1,5 @@ import pandas as pd +import polars as pl import json from tqdm import tqdm from typing import List, Callable, Iterator, Union, Optional @@ -6,7 +7,7 @@ from sportsdataverse.errors import SeasonNotFoundError from sportsdataverse.dl_utils import download -def load_nhl_pbp(seasons: List[int]) -> pd.DataFrame: +def load_nhl_pbp(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: """Load NHL play by play data going back to 2011 Example: @@ -14,6 +15,7 @@ def load_nhl_pbp(seasons: List[int]) -> pd.DataFrame: Args: seasons (list): Used to define different seasons. 2011 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing the play-by-plays available for the requested seasons. @@ -21,19 +23,17 @@ def load_nhl_pbp(seasons: List[int]) -> pd.DataFrame: Raises: ValueError: If `season` is less than 2011. """ - data = pd.DataFrame() + data = pl.DataFrame() if type(seasons) is int: seasons = [seasons] for i in tqdm(seasons): if int(i) < 2011: raise SeasonNotFoundError("season cannot be less than 2011") - i_data = pd.read_parquet(NHL_BASE_URL.format(season=i), engine='auto', columns=None) - data = pd.concat([data, i_data], axis = 0, ignore_index = True) - #Give each row a unique index - data.reset_index(drop=True, inplace=True) - return data + i_data = pl.read_parquet(NHL_BASE_URL.format(season=i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how = 'vertical') + return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data -def load_nhl_schedule(seasons: List[int]) -> pd.DataFrame: +def load_nhl_schedule(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: """Load NHL schedule data Example: @@ -41,6 +41,7 @@ def load_nhl_schedule(seasons: List[int]) -> pd.DataFrame: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing the schedule for the requested seasons. @@ -48,20 +49,17 @@ def load_nhl_schedule(seasons: List[int]) -> pd.DataFrame: Raises: ValueError: If `season` is less than 2002. """ - data = pd.DataFrame() + data = pl.DataFrame() if type(seasons) is int: seasons = [seasons] for i in tqdm(seasons): if int(i) < 2002: raise SeasonNotFoundError("season cannot be less than 2002") - i_data = pd.read_parquet(NHL_TEAM_SCHEDULE_URL.format(season = i), engine='auto', columns=None) - data = pd.concat([data, i_data], axis = 0, ignore_index = True) - #Give each row a unique index - data.reset_index(drop=True, inplace=True) + i_data = pl.read_parquet(NHL_TEAM_SCHEDULE_URL.format(season = i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how = 'vertical') + return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data - return data - -def load_nhl_team_boxscore(seasons: List[int]) -> pd.DataFrame: +def load_nhl_team_boxscore(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: """Load NHL team boxscore data Example: @@ -69,6 +67,7 @@ def load_nhl_team_boxscore(seasons: List[int]) -> pd.DataFrame: Args: seasons (list): Used to define different seasons. 2011 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing the @@ -77,20 +76,17 @@ def load_nhl_team_boxscore(seasons: List[int]) -> pd.DataFrame: Raises: ValueError: If `season` is less than 2011. """ - data = pd.DataFrame() + data = pl.DataFrame() if type(seasons) is int: seasons = [seasons] for i in tqdm(seasons): if int(i) < 2011: raise SeasonNotFoundError("season cannot be less than 2011") - i_data = pd.read_parquet(NHL_TEAM_BOX_URL.format(season = i), engine='auto', columns=None) - data = pd.concat([data, i_data], axis = 0, ignore_index = True) - #Give each row a unique index - data.reset_index(drop=True, inplace=True) - - return data + i_data = pl.read_parquet(NHL_TEAM_BOX_URL.format(season = i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how = 'vertical') + return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data -def load_nhl_player_boxscore(seasons: List[int]) -> pd.DataFrame: +def load_nhl_player_boxscore(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: """Load NHL player boxscore data Example: @@ -98,6 +94,7 @@ def load_nhl_player_boxscore(seasons: List[int]) -> pd.DataFrame: Args: seasons (list): Used to define different seasons. 2011 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing the @@ -106,29 +103,26 @@ def load_nhl_player_boxscore(seasons: List[int]) -> pd.DataFrame: Raises: ValueError: If `season` is less than 2011. """ - data = pd.DataFrame() + data = pl.DataFrame() if type(seasons) is int: seasons = [seasons] for i in tqdm(seasons): if int(i) < 2011: raise SeasonNotFoundError("season cannot be less than 2011") - i_data = pd.read_parquet(NHL_PLAYER_BOX_URL.format(season = i), engine='auto', columns=None) - data = pd.concat([data, i_data], axis = 0, ignore_index = True) - #Give each row a unique index - data.reset_index(drop=True, inplace=True) - - return data + i_data = pl.read_parquet(NHL_PLAYER_BOX_URL.format(season = i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how = 'vertical') + return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data -def nhl_teams() -> pd.DataFrame: +def nhl_teams(return_as_pandas = True) -> pd.DataFrame: """Load NHL team ID information and logos Example: `nhl_df = sportsdataverse.nhl.nhl_teams()` Args: + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing teams available for the requested seasons. """ - df = pd.read_csv(NHL_TEAM_LOGO_URL, low_memory=False) - return df \ No newline at end of file + return pl.read_csv(NHL_TEAM_LOGO_URL).to_pandas if return_as_pandas else pl.read_csv(NHL_TEAM_LOGO_URL) \ No newline at end of file diff --git a/sportsdataverse/nhl/nhl_pbp.py b/sportsdataverse/nhl/nhl_pbp.py index 10e1425..71cc378 100755 --- a/sportsdataverse/nhl/nhl_pbp.py +++ b/sportsdataverse/nhl/nhl_pbp.py @@ -1,5 +1,5 @@ -from typing import Dict import pandas as pd +import polars as pl import numpy as np import os import json @@ -8,7 +8,7 @@ from sportsdataverse.dl_utils import download, flatten_json_iterative, key_check -def espn_nhl_pbp(game_id: int, raw = False) -> Dict: +def espn_nhl_pbp(game_id: int, raw = False, **kwargs) -> Dict: """espn_nhl_pbp() - Pull the game by id. Data from API endpoints - `nhl/playbyplay`, `nhl/summary` Args: @@ -22,23 +22,26 @@ def espn_nhl_pbp(game_id: int, raw = False) -> Dict: Example: `nhl_df = sportsdataverse.nhl.espn_nhl_pbp(game_id=401247153)` """ - # play by play pbp_txt = {} - # summary endpoint for pickcenter array - summary_url = "http://site.api.espn.com/apis/site/v2/sports/hockey/nhl/summary?event={}".format(game_id) - summary_resp = download(summary_url) - summary = json.loads(summary_resp) - for k in ['plays', 'seasonseries', 'videos', 'broadcasts', 'pickcenter', 'onIce', 'againstTheSpread', 'odds', 'winprobability', 'teamInfo', 'espnWP', 'leaders']: + summary_url = f"http://site.api.espn.com/apis/site/v2/sports/hockey/nhl/summary?event={game_id}" + summary_resp = download(summary_url, **kwargs) + summary = summary_resp.json() + for k in ['plays', 'seasonseries', 'videos', 'broadcasts', 'pickcenter', + 'onIce', 'againstTheSpread', 'odds', 'winprobability', + 'teamInfo', 'espnWP', 'leaders']: pbp_txt[k]=key_check(obj=summary, key = k, replacement = np.array([])) - for k in ['boxscore','format', 'gameInfo', 'article', 'header', 'season', 'standings']: + for k in ['boxscore','format', 'gameInfo', 'article', + 'header', 'season', 'standings']: pbp_txt[k] = key_check(obj=summary, key = k, replacement = {}) for k in ['news','shop']: if k in pbp_txt.keys(): del pbp_txt[k] - incoming_keys_expected = ['boxscore', 'format', 'gameInfo', 'leaders', 'seasonseries', 'broadcasts', - 'pickcenter', 'againstTheSpread', 'odds', 'winprobability', - 'header', 'onIce', 'article', 'videos', 'plays', 'standings', - 'teamInfo', 'espnWP', 'season', 'timeouts'] + incoming_keys_expected = [ + 'boxscore', 'format', 'gameInfo', 'leaders', 'seasonseries', + 'broadcasts', 'pickcenter', 'againstTheSpread', 'odds', + 'winprobability', 'header', 'onIce', 'article', 'videos', + 'plays', 'standings', 'teamInfo', 'espnWP', 'season', 'timeouts' + ] if raw == True: # reorder keys in raw format, appending empty keys which are defined later to the end pbp_json = {} @@ -48,204 +51,213 @@ def espn_nhl_pbp(game_id: int, raw = False) -> Dict: else: pbp_json[k] = {} return pbp_json - pbp_json = helper_nhl_pbp(game_id, pbp_txt) - return pbp_json + return helper_nhl_pbp(game_id, pbp_txt) def nhl_pbp_disk(game_id, path_to_json): - with open(os.path.join(path_to_json, "{}.json".format(game_id))) as json_file: + with open(os.path.join(path_to_json, f"{game_id}.json")) as json_file: pbp_txt = json.load(json_file) return pbp_txt def helper_nhl_pbp(game_id, pbp_txt): - gameSpread, homeFavorite, gameSpreadAvailable = helper_nhl_pickcenter(pbp_txt) - pbp_txt['teamInfo'] = pbp_txt['header']['competitions'][0] - pbp_txt['season'] = pbp_txt['header']['season'] - pbp_txt['playByPlaySource'] = pbp_txt['header']['competitions'][0]['playByPlaySource'] - # Home and Away identification variables - homeTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['id']) - awayTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['id']) - homeTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['name']) - awayTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['name']) - homeTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['location']) - awayTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['location']) - homeTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['abbreviation']) - awayTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['abbreviation']) - homeTeamNameAlt = re.sub("Stat(.+)", "St", str(homeTeamName)) - awayTeamNameAlt = re.sub("Stat(.+)", "St", str(awayTeamName)) + init = helper_nhl_pickcenter(pbp_txt) + pbp_txt, init = helper_nhl_game_data(pbp_txt, init) if (pbp_txt['playByPlaySource'] != "none") & (len(pbp_txt['plays'])>1): - helper_nhl_pbp_features(game_id, pbp_txt, homeTeamId, awayTeamId, homeTeamMascot, awayTeamMascot, homeTeamName, awayTeamName, homeTeamAbbrev, awayTeamAbbrev, homeTeamNameAlt, awayTeamNameAlt, gameSpread, homeFavorite, gameSpreadAvailable) + pbp_txt = helper_nhl_pbp_features(game_id, pbp_txt, init) else: pbp_txt['plays'] = pd.DataFrame() pbp_txt['plays'] = pbp_txt['plays'].replace({np.nan: None}) - pbp_json = { + return { "gameId": game_id, - "plays" : pbp_txt['plays'].to_dict(orient='records'), - "boxscore" : pbp_txt['boxscore'], - "header" : pbp_txt['header'], + "plays": pbp_txt['plays'].to_dict(orient='records'), + "boxscore": pbp_txt['boxscore'], + "header": pbp_txt['header'], "format": pbp_txt['format'], - "broadcasts" : np.array(pbp_txt['broadcasts']).tolist(), - "videos" : np.array(pbp_txt['videos']).tolist(), + "broadcasts": np.array(pbp_txt['broadcasts']).tolist(), + "videos": np.array(pbp_txt['videos']).tolist(), "playByPlaySource": pbp_txt['playByPlaySource'], - "standings" : pbp_txt['standings'], - "leaders" : np.array(pbp_txt['leaders']).tolist(), - "seasonseries" : np.array(pbp_txt['seasonseries']).tolist(), - "pickcenter" : np.array(pbp_txt['pickcenter']).tolist(), - "againstTheSpread" : np.array(pbp_txt['againstTheSpread']).tolist(), - "odds" : np.array(pbp_txt['odds']).tolist(), + "standings": pbp_txt['standings'], + "leaders": np.array(pbp_txt['leaders']).tolist(), + "seasonseries": np.array(pbp_txt['seasonseries']).tolist(), + "pickcenter": np.array(pbp_txt['pickcenter']).tolist(), + "againstTheSpread": np.array(pbp_txt['againstTheSpread']).tolist(), + "odds": np.array(pbp_txt['odds']).tolist(), "onIce": np.array(pbp_txt['onIce']).tolist(), - "gameInfo" : pbp_txt['gameInfo'], - "teamInfo" : np.array(pbp_txt['teamInfo']).tolist(), - "season" : np.array(pbp_txt['season']).tolist() + "gameInfo": pbp_txt['gameInfo'], + "teamInfo": np.array(pbp_txt['teamInfo']).tolist(), + "season": np.array(pbp_txt['season']).tolist(), } - return pbp_json + +def helper_nhl_game_data(pbp_txt, init): + pbp_txt['timeouts'] = {} + pbp_txt['teamInfo'] = pbp_txt['header']['competitions'][0] + pbp_txt['season'] = pbp_txt['header']['season'] + pbp_txt['playByPlaySource'] = pbp_txt['header']['competitions'][0]['playByPlaySource'] + pbp_txt['boxscoreSource'] = pbp_txt['header']['competitions'][0]['boxscoreSource'] + pbp_txt['gameSpreadAvailable'] = init["gameSpreadAvailable"] + pbp_txt['gameSpread'] = init["gameSpread"] + pbp_txt["homeFavorite"] = init["homeFavorite"] + pbp_txt["homeTeamSpread"] = np.where( + init["homeFavorite"] == True, abs(init["gameSpread"]), -1 * abs(init["gameSpread"]) + ) + pbp_txt["overUnder"] = init["overUnder"] + # Home and Away identification variables + if pbp_txt['header']['competitions'][0]['competitors'][0]['homeAway']=='home': + pbp_txt['header']['competitions'][0]['home'] = pbp_txt['header']['competitions'][0]['competitors'][0]['team'] + homeTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['id']) + homeTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['name']) + homeTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['location']) + homeTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['abbreviation']) + homeTeamNameAlt = re.sub("Stat(.+)", "St", homeTeamName) + pbp_txt['header']['competitions'][0]['away'] = pbp_txt['header']['competitions'][0]['competitors'][1]['team'] + awayTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['id']) + awayTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['name']) + awayTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['location']) + awayTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['abbreviation']) + awayTeamNameAlt = re.sub("Stat(.+)", "St", awayTeamName) + else: + pbp_txt['header']['competitions'][0]['away'] = pbp_txt['header']['competitions'][0]['competitors'][0]['team'] + awayTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['id']) + awayTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['name']) + awayTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['location']) + awayTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['abbreviation']) + awayTeamNameAlt = re.sub("Stat(.+)", "St", awayTeamName) + pbp_txt['header']['competitions'][0]['home'] = pbp_txt['header']['competitions'][0]['competitors'][1]['team'] + homeTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['id']) + homeTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['name']) + homeTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['location']) + homeTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['abbreviation']) + homeTeamNameAlt = re.sub("Stat(.+)", "St", homeTeamName) + init["homeTeamId"] = homeTeamId + init["homeTeamMascot"] = homeTeamMascot + init["homeTeamName"] = homeTeamName + init["homeTeamAbbrev"] = homeTeamAbbrev + init["homeTeamNameAlt"] = homeTeamNameAlt + init["awayTeamId"] = awayTeamId + init["awayTeamMascot"] = awayTeamMascot + init["awayTeamName"] = awayTeamName + init["awayTeamAbbrev"] = awayTeamAbbrev + init["awayTeamNameAlt"] = awayTeamNameAlt + return pbp_txt, init def helper_nhl_pickcenter(pbp_txt): - if len(pbp_txt['pickcenter']) > 1: - if 'spread' in pbp_txt['pickcenter'][1].keys(): - gameSpread = pbp_txt['pickcenter'][1]['spread'] - homeFavorite = pbp_txt['pickcenter'][1]['homeTeamOdds']['favorite'] - gameSpreadAvailable = True + # Spread definition + if len(pbp_txt.get("pickcenter",[])) > 1: + homeFavorite = pbp_txt.get("pickcenter",{})[0].get("homeTeamOdds",{}).get("favorite","") + if "spread" in pbp_txt.get("pickcenter",{})[1].keys(): + gameSpread = pbp_txt.get("pickcenter",{})[1].get("spread","") + overUnder = pbp_txt.get("pickcenter",{})[1].get("overUnder","") else: - gameSpread = pbp_txt['pickcenter'][0]['spread'] - homeFavorite = pbp_txt['pickcenter'][0]['homeTeamOdds']['favorite'] - gameSpreadAvailable = True - + gameSpread = pbp_txt.get("pickcenter",{})[0].get("spread","") + overUnder = pbp_txt.get("pickcenter",{})[0].get("overUnder","") + gameSpreadAvailable = True + # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") else: gameSpread = 2.5 + overUnder = 190.5 homeFavorite = True gameSpreadAvailable = False - return gameSpread,homeFavorite,gameSpreadAvailable -def helper_nhl_pbp_features(game_id, pbp_txt, homeTeamId, awayTeamId, homeTeamMascot, awayTeamMascot, homeTeamName, awayTeamName, homeTeamAbbrev, awayTeamAbbrev, homeTeamNameAlt, awayTeamNameAlt, gameSpread, homeFavorite, gameSpreadAvailable): + return { + "gameSpread": gameSpread, + "overUnder": overUnder, + "homeFavorite": homeFavorite, + "gameSpreadAvailable": gameSpreadAvailable, + } + +def helper_nhl_pbp_features(game_id, pbp_txt, init): pbp_txt['plays_mod'] = [] for play in pbp_txt['plays']: p = flatten_json_iterative(play) pbp_txt['plays_mod'].append(p) - pbp_txt['plays'] = pd.json_normalize(pbp_txt,'plays_mod') - pbp_txt['plays']['season'] = pbp_txt['season']['year'] - pbp_txt['plays']['seasonType'] = pbp_txt['season']['type'] - pbp_txt['plays']["awayTeamId"] = awayTeamId - pbp_txt['plays']["awayTeamName"] = str(awayTeamName) - pbp_txt['plays']["awayTeamMascot"] = str(awayTeamMascot) - pbp_txt['plays']["awayTeamAbbrev"] = str(awayTeamAbbrev) - pbp_txt['plays']["awayTeamNameAlt"] = str(awayTeamNameAlt) - pbp_txt['plays']["homeTeamId"] = homeTeamId - pbp_txt['plays']["homeTeamName"] = str(homeTeamName) - pbp_txt['plays']["homeTeamMascot"] = str(homeTeamMascot) - pbp_txt['plays']["homeTeamAbbrev"] = str(homeTeamAbbrev) - pbp_txt['plays']["homeTeamNameAlt"] = str(homeTeamNameAlt) - # Spread definition - pbp_txt['plays']["homeTeamSpread"] = 2.5 - pbp_txt['plays']["gameSpread"] = abs(gameSpread) - pbp_txt['plays']["homeTeamSpread"] = np.where(homeFavorite == True, abs(gameSpread), -1*abs(gameSpread)) - pbp_txt['homeTeamSpread'] = np.where(homeFavorite == True, abs(gameSpread), -1*abs(gameSpread)) - pbp_txt['plays']["homeFavorite"] = homeFavorite - pbp_txt['plays']["gameSpread"] = gameSpread - pbp_txt['plays']["gameSpreadAvailable"] = gameSpreadAvailable - pbp_txt['plays'] = pbp_txt['plays'].to_dict(orient='records') - pbp_txt['plays'] = pd.DataFrame(pbp_txt['plays']) - pbp_txt['plays']['season'] = pbp_txt['header']['season']['year'] - pbp_txt['plays']['seasonType'] = pbp_txt['header']['season']['type'] - pbp_txt['plays']['game_id'] = int(game_id) - pbp_txt['plays']["homeTeamId"] = homeTeamId - pbp_txt['plays']["awayTeamId"] = awayTeamId - pbp_txt['plays']["homeTeamName"] = str(homeTeamName) - pbp_txt['plays']["awayTeamName"] = str(awayTeamName) - pbp_txt['plays']["homeTeamMascot"] = str(homeTeamMascot) - pbp_txt['plays']["awayTeamMascot"] = str(awayTeamMascot) - pbp_txt['plays']["homeTeamAbbrev"] = str(homeTeamAbbrev) - pbp_txt['plays']["awayTeamAbbrev"] = str(awayTeamAbbrev) - pbp_txt['plays']["homeTeamNameAlt"] = str(homeTeamNameAlt) - pbp_txt['plays']["awayTeamNameAlt"] = str(awayTeamNameAlt) - pbp_txt['plays']['period.number'] = pbp_txt['plays']['period.number'].apply(lambda x: int(x)) - - - pbp_txt['plays']["homeTeamSpread"] = 2.5 - if len(pbp_txt['pickcenter']) > 1: - if 'spread' in pbp_txt['pickcenter'][1].keys(): - gameSpread = pbp_txt['pickcenter'][1]['spread'] - homeFavorite = pbp_txt['pickcenter'][1]['homeTeamOdds']['favorite'] - gameSpreadAvailable = True - else: - gameSpread = pbp_txt['pickcenter'][0]['spread'] - homeFavorite = pbp_txt['pickcenter'][0]['homeTeamOdds']['favorite'] - gameSpreadAvailable = True + pbp_txt['plays'] = pl.from_pandas(pd.json_normalize(pbp_txt,'plays_mod')) + pbp_txt['plays'] = pbp_txt['plays'].with_columns( + game_id = pl.lit(game_id).cast(pl.Int32), + id = (pl.col('id').cast(pl.Int64)), + season = pl.lit(pbp_txt['header']['season']['year']).cast(pl.Int32), + seasonType = pl.lit(pbp_txt['header']['season']['type']), + homeTeamId = pl.lit(init["homeTeamId"]).cast(pl.Int32), + homeTeamName = pl.lit(init["homeTeamName"]), + homeTeamMascot = pl.lit(init["homeTeamMascot"]), + homeTeamAbbrev = pl.lit(init["homeTeamAbbrev"]), + homeTeamNameAlt = pl.lit(init["homeTeamNameAlt"]), + awayTeamId = pl.lit(init["awayTeamId"]).cast(pl.Int32), + awayTeamName = pl.lit(init["awayTeamName"]), + awayTeamMascot = pl.lit(init["awayTeamMascot"]), + awayTeamAbbrev = pl.lit(init["awayTeamAbbrev"]), + awayTeamNameAlt = pl.lit(init["awayTeamNameAlt"]), + gameSpread = pl.lit(init["gameSpread"]).abs(), + homeFavorite = pl.lit(init["homeFavorite"]), + gameSpreadAvailable = pl.lit(init["gameSpreadAvailable"]), + ).with_columns( + homeTeamSpread = pl.when(pl.col('homeFavorite') == True) + .then(pl.col('gameSpread')) + .otherwise(-1*pl.col('gameSpread')), + ).with_columns( + pl.col("period.number").cast(pl.Int32).alias("period.number"), + pl.col("period.number").cast(pl.Int32).alias("qtr"), + pl.when(pl.col("clock.displayValue").str.contains(r":") == False) + .then('0:'+ pl.col("clock.displayValue")) + .otherwise(pl.col("clock.displayValue")) + .alias("clock.displayValue"), + ).with_columns( + pl.col("clock.displayValue").alias("time"), + pl.col("clock.displayValue").str.split(':').list.to_struct(n_field_strategy="first_non_null").alias("clock.mm") + ).with_columns( + pl.col("clock.mm").struct.rename_fields(["clock.minutes", "clock.seconds"]) + ).unnest( + "clock.mm" + ).with_columns( + pl.col("clock.minutes").cast(pl.Float32), + pl.col("clock.seconds").cast(pl.Float32), + ).with_columns( + lag_period = pl.col("period.number").shift(1), + lead_period = pl.col("period.number").shift(-1), + ).with_columns( + (60 * pl.col("clock.minutes") + pl.col("clock.seconds")).alias("start.period_seconds_remaining"), + pl.when(pl.col("period.number") == 1) + .then(2400 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) + .when(pl.col("period.number") == 2) + .then(1200 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) + .when(pl.col("period.number") == 3) + .then(60 * pl.col("clock.minutes") + pl.col("clock.seconds")) + .otherwise(60 * pl.col("clock.minutes") + pl.col("clock.seconds")) + .alias("start.game_seconds_remaining"), + ).with_columns( + pl.col("start.period_seconds_remaining").shift(-1).alias("end.period_seconds_remaining"), + pl.col("start.game_seconds_remaining").shift(-1).alias("end.game_seconds_remaining"), + ) - else: - gameSpread = 2.5 - homeFavorite = True - gameSpreadAvailable = False - pbp_txt['plays']["gameSpread"] = abs(gameSpread) - pbp_txt['plays']["gameSpreadAvailable"] = gameSpreadAvailable - pbp_txt['plays']["homeTeamSpread"] = np.where(homeFavorite == True, abs(gameSpread), -1*abs(gameSpread)) - pbp_txt['homeTeamSpread'] = np.where(homeFavorite == True, abs(gameSpread), -1*abs(gameSpread)) - pbp_txt['plays']["homeFavorite"] = homeFavorite - pbp_txt['plays']["gameSpread"] = gameSpread - pbp_txt['plays']["homeFavorite"] = homeFavorite - - #----- Time --------------- - - pbp_txt['plays']['clock.displayValue'] = np.select( - [ - pbp_txt['plays']['clock.displayValue'].str.contains(":") == False - ], - [ - "0:" + pbp_txt['plays']['clock.displayValue'].apply(lambda x: str(x)) - ], default = pbp_txt['plays']['clock.displayValue'] - ) - pbp_txt['plays']['time'] = pbp_txt['plays']['clock.displayValue'] - pbp_txt['plays']['clock.mm'] = pbp_txt['plays']['clock.displayValue'].str.split(pat=':') - pbp_txt['plays'][['clock.minutes','clock.seconds']] = pbp_txt['plays']['clock.mm'].to_list() - pbp_txt['plays']['clock.minutes'] = pbp_txt['plays']['clock.minutes'].apply(lambda x: int(x)) - - pbp_txt['plays']['clock.seconds'] = pbp_txt['plays']['clock.seconds'].apply(lambda x: float(x)) - # pbp_txt['plays']['clock.mm'] = pbp_txt['plays']['clock.displayValue'].apply(lambda x: datetime.strptime(str(x),'%M:%S')) - pbp_txt['plays']['lag_period.number'] = pbp_txt['plays']['period.number'].shift(1) - pbp_txt['plays']['lead_period.number'] = pbp_txt['plays']['period.number'].shift(-1) - pbp_txt['plays']['start.period_seconds_remaining'] = 60*pbp_txt['plays']['clock.minutes'].astype(int) + pbp_txt['plays']['clock.seconds'].astype(int) - pbp_txt['plays']['start.game_seconds_remaining'] = np.select( - [ - pbp_txt['plays']['period.number'] == 1, - pbp_txt['plays']['period.number'] == 2, - pbp_txt['plays']['period.number'] == 3 - ], - [ - 2400 + 60*pbp_txt['plays']['clock.minutes'].astype(int) + pbp_txt['plays']['clock.seconds'].astype(int), - 1200 + 60*pbp_txt['plays']['clock.minutes'].astype(int) + pbp_txt['plays']['clock.seconds'].astype(int), - 60*pbp_txt['plays']['clock.minutes'].astype(int) + pbp_txt['plays']['clock.seconds'].astype(int) - ], default = 60*pbp_txt['plays']['clock.minutes'].astype(int) + pbp_txt['plays']['clock.seconds'].astype(int) - ) - # Pos Team - Start and End Id - pbp_txt['plays']['game_play_number'] = np.arange(len(pbp_txt['plays']))+1 - pbp_txt['plays']['text'] = pbp_txt['plays']['text'].astype(str) - pbp_txt['plays']['id'] = pbp_txt['plays']['id'].apply(lambda x: int(x)) - pbp_txt['plays']['end.period_seconds_remaining'] = pbp_txt['plays']['start.period_seconds_remaining'].shift(1) - pbp_txt['plays']['end.game_seconds_remaining'] = pbp_txt['plays']['start.game_seconds_remaining'].shift(1) - pbp_txt['plays']['end.period_seconds_remaining'] = np.select( - [ - (pbp_txt['plays']['game_play_number'] == 1)| - ((pbp_txt['plays']['period.number'] == 2) & (pbp_txt['plays']['lag_period.number'] == 1))| - ((pbp_txt['plays']['period.number'] == 3) & (pbp_txt['plays']['lag_period.number'] == 2)) - ], - [ - 1200 - ], default = pbp_txt['plays']['end.period_seconds_remaining'] - ) - pbp_txt['plays']['end.game_seconds_remaining'] = np.select( - [ - (pbp_txt['plays']['game_play_number'] == 1), - ((pbp_txt['plays']['period.number'] == 2) & (pbp_txt['plays']['lag_period.number'] == 1)), - ((pbp_txt['plays']['period.number'] == 3) & (pbp_txt['plays']['lag_period.number'] == 2)) - ], - [ - 3600, - 2400, - 1200 - ], default = pbp_txt['plays']['end.game_seconds_remaining'] - ) - - pbp_txt['plays']['period'] = pbp_txt['plays']['period.number'] - - del pbp_txt['plays']['clock.mm'] \ No newline at end of file + pbp_txt["plays"] = pbp_txt["plays"].with_row_count("game_play_number", 1) + + pbp_txt["plays"] = pbp_txt["plays"].with_columns( + pl.when(pl.col("game_play_number") == 1) + .then(1200) + .when((pl.col("lag_period") == 1) + .and_(pl.col("period.number") == 2)) + .then(1200) + .when((pl.col("lag_period") == 2) + .and_(pl.col("period.number") == 3)) + .then(1200) + .when((pl.col("lag_period") == pl.col("period.number") - 1) + .and_(pl.col("period.number") >= 4)) + .then(1200) + .otherwise(pl.col("end.period_seconds_remaining")) + .alias("end.period_seconds_remaining"), + pl.when(pl.col("game_play_number") == 1) + .then(3600) + .when((pl.col("lag_period") == 1) + .and_(pl.col("period.number") == 2)) + .then(2400) + .when((pl.col("lag_period") == 2) + .and_(pl.col("period.number") == 3)) + .then(1200) + .when((pl.col("lag_period") == pl.col("period.number") - 1) + .and_(pl.col("period.number") >= 4)) + .then(1200) + .otherwise(pl.col("end.game_seconds_remaining")) + .alias("end.game_seconds_remaining"), + ) + + return pbp_txt \ No newline at end of file diff --git a/sportsdataverse/nhl/nhl_schedule.py b/sportsdataverse/nhl/nhl_schedule.py index c93508a..42fa0c5 100755 --- a/sportsdataverse/nhl/nhl_schedule.py +++ b/sportsdataverse/nhl/nhl_schedule.py @@ -1,99 +1,171 @@ import pandas as pd +import polars as pl import json import datetime from sportsdataverse.dl_utils import download, underscore -def espn_nhl_schedule(dates=None, season_type=None, limit=500) -> pd.DataFrame: +def espn_nhl_schedule(dates=None, season_type=None, limit=500, + return_as_pandas = True, **kwargs) -> pd.DataFrame: """espn_nhl_schedule - look up the NHL schedule for a given date Args: dates (int): Used to define different seasons. 2002 is the earliest available season. season_type (int): season type, 1 for pre-season, 2 for regular season, 3 for post-season, 4 for all-star, 5 for off-season limit (int): number of records to return, default: 500. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Returns: pd.DataFrame: Pandas dataframe containing schedule events for the requested season. + """ - if dates is None: - dates = '' - else: - dates = '&dates=' + str(dates) - if season_type is None: - season_type = '' - else: - season_type = '&seasontype=' + str(season_type) - - url = "http://site.api.espn.com/apis/site/v2/sports/hockey/nhl/scoreboard?limit={}{}{}".format(limit, dates, season_type) - resp = download(url=url) - - ev = pd.DataFrame() - if resp is not None: - events_txt = json.loads(resp) - - events = events_txt.get('events') - for event in events: - event.get('competitions')[0].get('competitors')[0].get('team').pop('links',None) - event.get('competitions')[0].get('competitors')[1].get('team').pop('links',None) - if event.get('competitions')[0].get('competitors')[0].get('homeAway')=='home': - event['competitions'][0]['home'] = event.get('competitions')[0].get('competitors')[0].get('team') - event['competitions'][0]['away'] = event.get('competitions')[0].get('competitors')[1].get('team') - else: - event['competitions'][0]['away'] = event.get('competitions')[0].get('competitors')[0].get('team') - event['competitions'][0]['home'] = event.get('competitions')[0].get('competitors')[1].get('team') - - del_keys = ['broadcasts','geoBroadcasts', 'headlines', 'series'] - for k in del_keys: - event.get('competitions')[0].pop(k, None) - x = pd.json_normalize(event.get('competitions')[0], sep='_') - x['game_id'] = x['id'].astype(int) - x['season'] = event.get('season').get('year') - x['season_type'] = event.get('season').get('type') - ev = pd.concat([ev,x],axis=0, ignore_index=True) - ev = pd.DataFrame(ev) - ev.columns = [underscore(c) for c in ev.columns.tolist()] - return ev - -def espn_nhl_calendar(season=None) -> pd.DataFrame: + + url = "http://site.api.espn.com/apis/site/v2/sports/hockey/nhl/scoreboard" + params = { + 'dates': dates, + 'seasonType': season_type, + 'limit': limit + } + resp = download(url=url, params=params, **kwargs) + + ev = pl.DataFrame() + events_txt = resp.json() + events = events_txt.get('events') + if len(events) == 0: + return pd.DataFrame() if return_as_pandas else pl.DataFrame() + + for event in events: + event.get('competitions')[0].get('competitors')[0].get('team').pop('links',None) + event.get('competitions')[0].get('competitors')[1].get('team').pop('links',None) + if event.get('competitions')[0].get('competitors')[0].get('homeAway') == 'home': + event = __extract_home_away(event, 0, 'home') + event = __extract_home_away(event, 1, 'away') + else: + event = __extract_home_away(event, 0, 'away') + event = __extract_home_away(event, 1, 'home') + + event.get('competitions')[0]['notes_type'] = event.get('competitions')[0]['notes'][0].get("type") if len(event.get('competitions')[0]['notes']) > 0 else '' + event.get('competitions')[0]['notes_headline'] = event.get('competitions')[0]['notes'][0].get("headline").replace('"','') if len(event.get('competitions')[0]['notes']) > 0 else '' + event.get('competitions')[0]['broadcast_market'] = event.get('competitions')[0].get('broadcasts', [])[0].get('market', "") if len(event.get('competitions')[0].get('broadcasts')) > 0 else "" + event.get('competitions')[0]['broadcast_name'] = event.get('competitions')[0].get('broadcasts', [])[0].get('names', [])[0] if len(event.get('competitions')[0].get('broadcasts')) > 0 else "" + del_keys = ['geoBroadcasts', 'headlines', 'series', 'situation', 'tickets', + 'odds', 'broadcasts', 'notes', 'competitors'] + for k in del_keys: + event.get('competitions')[0].pop(k, None) + + x = pl.from_pandas(pd.json_normalize(event.get('competitions')[0], sep = '_')) + x = x.with_columns( + game_id = (pl.col('id').cast(pl.Int64)), + season = (event.get('season').get('year')), + season_type = (event.get('season').get('type')), + home_linescores = pl.when(pl.col('status_type_description') == 'Postponed') + .then(None) + .otherwise(pl.col('home_linescores')), + away_linescores = pl.when(pl.col('status_type_description') == 'Postponed') + .then(None) + .otherwise(pl.col('away_linescores')), + ) + x = x[[s.name for s in x if s.null_count() != x.height]] + ev = pl.concat([ev, x], how = 'diagonal') + + ev.columns = [underscore(c) for c in ev.columns] + + return ev.to_pandas() if return_as_pandas else ev + +def __extract_home_away(event, arg1, arg2): + event['competitions'][0][arg2] = ( + event.get('competitions')[0].get('competitors')[arg1].get('team') + ) + event['competitions'][0][arg2]['score'] = ( + event.get('competitions')[0].get('competitors')[arg1].get('score') + ) + event['competitions'][0][arg2]['winner'] = ( + event.get('competitions')[0].get('competitors')[arg1].get('winner') + ) + ## add winner back to main competitors if does not exist + event['competitions'][0]['competitors'][arg1]['winner'] = ( + event.get('competitions')[0].get('competitors')[arg1].get('winner', False) + ) + + event['competitions'][0][arg2]['linescores'] = ( + event.get('competitions')[0] + .get('competitors')[arg1] + .get('linescores', [{'value': 'N/A'}]) + ) + ## add linescores back to main competitors if does not exist + event['competitions'][0]['competitors'][arg1]['linescores'] = ( + event.get('competitions')[0] + .get('competitors')[arg1] + .get('linescores', []) + ) + event['competitions'][0][arg2]['records'] = ( + event.get('competitions')[0] + .get('competitors')[arg1] + .get('records', []) + ) + return event + +def espn_nhl_calendar(season=None, ondays=None, return_as_pandas = True, **kwargs) -> pd.DataFrame: """espn_nhl_calendar - look up the NHL calendar for a given season Args: season (int): Used to define different seasons. 2002 is the earliest available season. + ondays (boolean): Used to return dates for calendar ondays + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing calendar dates for the requested season. + pd.DataFrame: Pandas dataframe containing + calendar dates for the requested season. Raises: ValueError: If `season` is less than 2002. """ - url = "http://site.api.espn.com/apis/site/v2/sports/hockey/nhl/scoreboard?dates={}".format(season) - resp = download(url=url) - txt = json.loads(resp)['leagues'][0]['calendar'] - datenum = list(map(lambda x: x[:10].replace("-",""),txt)) - date = list(map(lambda x: x[:10],txt)) - - year = list(map(lambda x: x[:4],txt)) - month = list(map(lambda x: x[5:7],txt)) - day = list(map(lambda x: x[8:10],txt)) - - data = { - "season": season, - "datetime" : txt, - "date" : date, - "year": year, - "month": month, - "day": day, - "dateURL": datenum - } - df = pd.DataFrame(data) - df['url']="http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/scoreboard?dates=" - df['url']= df['url'] + df['dateURL'] - return df + if ondays is not None: + full_schedule = __ondays_nhl_calendar(season, **kwargs) + else: + url = f"http://site.api.espn.com/apis/site/v2/sports/hockey/nhl/scoreboard?dates={season}" + resp = download(url=url, **kwargs) + txt = resp.json().get('leagues')[0].get('calendar') + datenum = list(map(lambda x: x[:10].replace("-",""),txt)) + date = list(map(lambda x: x[:10],txt)) + year = list(map(lambda x: x[:4],txt)) + month = list(map(lambda x: x[5:7],txt)) + day = list(map(lambda x: x[8:10],txt)) + data = { + "season": season, + "datetime" : txt, + "date" : date, + "year": year, + "month": month, + "day": day, + "dateURL": datenum + } + full_schedule = pl.DataFrame(data) + full_schedule = full_schedule.with_columns( + url="http://site.api.espn.com/apis/site/v2/sports/hockey/nhl/scoreboard?dates=" + + pl.col('dateURL') + ) + return full_schedule.to_pandas() if return_as_pandas else full_schedule + +def __ondays_nhl_calendar(season, **kwargs): + url = f"https://sports.core.api.espn.com/v2/sports/hockey/leagues/nhl/seasons/{season}/types/2/calendar/ondays" + resp = download(url=url, **kwargs) + txt = resp.json().get('eventDate').get('dates') + result = pl.DataFrame(txt, schema=['dates']) + result = result.with_columns(dateURL = pl.col('dates').str.slice(0, 10)) + result = result.with_columns( + url="http://site.api.espn.com/apis/site/v2/sports/hockey/nhl/scoreboard?dates=" + + pl.col('dateURL') + ) + + return result + def most_recent_nhl_season(): if int(str(datetime.date.today())[5:7]) >= 10: - return int(str(datetime.date.today())[0:4]) + 1 + return int(str(datetime.date.today())[:4]) + 1 else: - return int(str(datetime.date.today())[0:4]) + return int(str(datetime.date.today())[:4]) def year_to_season(year): first_year = str(year)[2:4] diff --git a/sportsdataverse/nhl/nhl_teams.py b/sportsdataverse/nhl/nhl_teams.py index 4539476..89dfe40 100755 --- a/sportsdataverse/nhl/nhl_teams.py +++ b/sportsdataverse/nhl/nhl_teams.py @@ -1,19 +1,28 @@ import pandas as pd +import polars as pl import json from sportsdataverse.dl_utils import download, underscore -from urllib.error import URLError, HTTPError, ContentTooShortError -def espn_nhl_teams() -> pd.DataFrame: +def espn_nhl_teams(return_as_pandas = True, **kwargs) -> pd.DataFrame: """espn_nhl_teams - look up NHL teams + Args: + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Returns: pd.DataFrame: Pandas dataframe containing teams for the requested league. + + Example: + `nhl_df = sportsdataverse.nhl.espn_nhl_teams()` + """ - ev = pd.DataFrame() - url = "http://site.api.espn.com/apis/site/v2/sports/hockey/nhl/teams?limit=1000" - resp = download(url=url) + url = "http://site.api.espn.com/apis/site/v2/sports/hockey/nhl/teams" + params = { + "limit": 1000 + } + resp = download(url=url, params = params, **kwargs) if resp is not None: - events_txt = json.loads(resp) + events_txt = resp.json() teams = events_txt.get('sports')[0].get('leagues')[0].get('teams') del_keys = ['record', 'links'] @@ -22,5 +31,5 @@ def espn_nhl_teams() -> pd.DataFrame: team.get('team').pop(k, None) teams = pd.json_normalize(teams, sep='_') teams.columns = [underscore(c) for c in teams.columns.tolist()] - return teams + return teams if return_as_pandas else pl.from_pandas(teams) From 2caee126e33371041127fa2b2ebb2cedd15a45c1 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Mon, 10 Jul 2023 20:29:07 -0400 Subject: [PATCH 17/79] Update wbb to polars --- sportsdataverse/mbb/mbb_schedule.py | 2 +- sportsdataverse/wbb/wbb_game_rosters.py | 193 ++++++++++ sportsdataverse/wbb/wbb_loaders.py | 56 ++- sportsdataverse/wbb/wbb_pbp.py | 477 ++++++++++++++---------- sportsdataverse/wbb/wbb_schedule.py | 206 ++++++---- sportsdataverse/wbb/wbb_teams.py | 25 +- 6 files changed, 639 insertions(+), 320 deletions(-) create mode 100644 sportsdataverse/wbb/wbb_game_rosters.py diff --git a/sportsdataverse/mbb/mbb_schedule.py b/sportsdataverse/mbb/mbb_schedule.py index 0f15431..874b9dc 100755 --- a/sportsdataverse/mbb/mbb_schedule.py +++ b/sportsdataverse/mbb/mbb_schedule.py @@ -8,7 +8,7 @@ from sportsdataverse.dl_utils import download, underscore def espn_mbb_schedule(dates=None, groups=50, season_type=None, limit=500, - return_as_pandas=True) -> pd.DataFrame: + return_as_pandas=True, **kwargs) -> pd.DataFrame: """espn_mbb_schedule - look up the men's college basketball scheduler for a given season Args: diff --git a/sportsdataverse/wbb/wbb_game_rosters.py b/sportsdataverse/wbb/wbb_game_rosters.py new file mode 100644 index 0000000..8b027ae --- /dev/null +++ b/sportsdataverse/wbb/wbb_game_rosters.py @@ -0,0 +1,193 @@ +import pandas as pd +import polars as pl +import numpy as np +import os +import json +import re +from typing import List, Callable, Iterator, Union, Optional, Dict +from sportsdataverse.dl_utils import download, underscore + +def espn_wbb_game_rosters(game_id: int, raw = False, return_as_pandas = True, **kwargs) -> pd.DataFrame: + """espn_wbb_game_rosters() - Pull the game by id. + + Args: + game_id (int): Unique game_id, can be obtained from wbb_schedule(). + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + + Returns: + pd.DataFrame: Data frame of game roster data with columns: + 'athlete_id', 'athlete_uid', 'athlete_guid', 'athlete_type', + 'first_name', 'last_name', 'full_name', 'athlete_display_name', + 'short_name', 'weight', 'display_weight', 'height', 'display_height', + 'age', 'date_of_birth', 'slug', 'jersey', 'linked', 'active', + 'alternate_ids_sdr', 'birth_place_city', 'birth_place_state', + 'birth_place_country', 'headshot_href', 'headshot_alt', + 'experience_years', 'experience_display_value', + 'experience_abbreviation', 'status_id', 'status_name', 'status_type', + 'status_abbreviation', 'hand_type', 'hand_abbreviation', + 'hand_display_value', 'draft_display_text', 'draft_round', 'draft_year', + 'draft_selection', 'player_id', 'starter', 'valid', 'did_not_play', + 'display_name', 'ejected', 'athlete_href', 'position_href', + 'statistics_href', 'team_id', 'team_guid', 'team_uid', 'team_slug', + 'team_location', 'team_name', 'team_nickname', 'team_abbreviation', + 'team_display_name', 'team_short_display_name', 'team_color', + 'team_alternate_color', 'is_active', 'is_all_star', + 'team_alternate_ids_sdr', 'logo_href', 'logo_dark_href', 'game_id' + Example: + `wbb_df = sportsdataverse.wbb.espn_wbb_game_rosters(game_id=401266534)` + """ + # play by play + pbp_txt = {} + # summary endpoint for pickcenter array + summary_url = "https://sports.core.api.espn.com/v2/sports/basketball/leagues/womens-college-basketball/events/{x}/competitions/{x}/competitors".format(x=game_id) + summary_resp = download(summary_url, **kwargs) + summary = summary_resp.json() + items = helper_wbb_game_items(summary) + team_rosters = helper_wbb_roster_items(items = items, summary_url = summary_url, **kwargs) + team_rosters = team_rosters.join(items[['team_id', 'order', 'home_away', 'winner']], how = 'left', on = 'team_id') + teams_df = helper_wbb_team_items(items = items, **kwargs) + teams_rosters = team_rosters.join(teams_df, how = 'left',on = 'team_id') + athletes = helper_wbb_athlete_items(teams_rosters = team_rosters, **kwargs) + rosters = athletes.join(teams_rosters, how = 'left', left_on = 'athlete_id', right_on = 'player_id') + rosters = rosters.with_columns( + game_id = pl.lit(game_id).cast(pl.Int64) + ) + rosters.columns = [underscore(c) for c in rosters.columns] + return rosters.to_pandas() if return_as_pandas else rosters + +def helper_wbb_game_items(summary): + items = pl.from_pandas(pd.json_normalize(summary, record_path = "items", sep = '_')) + items.columns = [col.replace("$ref", "href") for col in items.columns] + + items.columns = [underscore(c) for c in items.columns] + items = items.rename({ + "id": "team_id", + "uid": "team_uid", + "statistics_href": "team_statistics_href" + } + ) + items = items.with_columns( + team_id = pl.col("team_id").cast(pl.Int32) + ) + + return items + +def helper_wbb_team_items(items, **kwargs): + pop_cols = [ + '$ref', + 'record', + 'athletes', + 'venue', + 'groups', + 'ranks', + 'statistics', + 'leaders', + 'links', + 'notes', + 'againstTheSpreadRecords', + 'franchise', + 'events', + 'college' + ] + teams_df = pl.DataFrame() + for x in items['team_href']: + team = download(x, **kwargs).json() + for k in pop_cols: + team.pop(k, None) + team_row = pl.from_pandas(pd.json_normalize(team, sep = '_')) + teams_df = pl.concat([teams_df, team_row], how = 'vertical') + + teams_df.columns = [ + 'team_id', + 'team_guid', + 'team_uid', + 'team_slug', + 'team_location', + 'team_name', + 'team_nickname', + 'team_abbreviation', + 'team_display_name', + 'team_short_display_name', + 'team_color', + 'team_alternate_color', + 'is_active', + 'is_all_star', + 'logos', + 'team_alternate_ids_sdr' + ] + teams_df = teams_df.with_columns( + logo_href = pl.lit(""), + logo_dark_href = pl.lit("") + ) + + for row in range(len(teams_df['logos'])): + team = teams_df['logos'][row] + teams_df[row, 'logo_href'] = team[0]['href'] + teams_df[row, 'logo_dark_href'] = team[1]['href'] + + teams_df = teams_df.drop(['logos']) + teams_df = teams_df.with_columns( + team_id = pl.col("team_id").cast(pl.Int32) + ) + return teams_df + +def helper_wbb_roster_items(items, summary_url, **kwargs): + team_ids = list(items['team_id']) + game_rosters = pl.DataFrame() + for tm in team_ids: + team_roster_url = "{x}/{t}/roster".format(x = summary_url, t = tm) + team_roster_resp = download(team_roster_url, **kwargs) + team_roster = pl.from_pandas(pd.json_normalize(team_roster_resp.json().get('entries',[]), sep = '_')) + team_roster.columns = [col.replace("$ref", "href") for col in team_roster.columns] + team_roster.columns = [underscore(c) for c in team_roster.columns] + team_roster= team_roster.with_columns( + team_id = pl.lit(tm).cast(pl.Int32) + ) + game_rosters = pl.concat([game_rosters, team_roster], how = 'vertical') + game_rosters = game_rosters.drop(["period", "for_player_id", "active"]) + game_rosters = game_rosters.with_columns( + player_id = pl.col('player_id').cast(pl.Int64), + team_id = pl.col('team_id').cast(pl.Int32) + ) + return game_rosters + +def helper_wbb_athlete_items(teams_rosters, **kwargs): + athlete_hrefs = list(teams_rosters['athlete_href']) + game_athletes = pl.DataFrame() + pop_cols = [ + 'links', + 'injuries', + 'teams', + 'team', + 'college', + 'proAthlete', + 'statistics', + 'notes', + 'eventLog', + "$ref", + "position" + ] + for athlete_href in athlete_hrefs: + + athlete_res = download(athlete_href, **kwargs) + athlete_resp = athlete_res.json() + for k in pop_cols: + athlete_resp.pop(k, None) + athlete = pl.from_pandas(pd.json_normalize(athlete_resp, sep='_')) + athlete.columns = [col.replace("$ref", "href") for col in athlete.columns] + athlete.columns = [underscore(c) for c in athlete.columns] + + game_athletes = pl.concat([game_athletes, athlete], how = 'diagonal') + + + game_athletes = game_athletes.rename({ + "id": "athlete_id", + "uid": "athlete_uid", + "guid": "athlete_guid", + "type": "athlete_type", + "display_name": "athlete_display_name" + }) + game_athletes = game_athletes.with_columns( + athlete_id = pl.col("athlete_id").cast(pl.Int64) + ) + return game_athletes \ No newline at end of file diff --git a/sportsdataverse/wbb/wbb_loaders.py b/sportsdataverse/wbb/wbb_loaders.py index 32dfa91..b007076 100755 --- a/sportsdataverse/wbb/wbb_loaders.py +++ b/sportsdataverse/wbb/wbb_loaders.py @@ -1,4 +1,5 @@ import pandas as pd +import polars as pl import json from tqdm import tqdm from typing import List, Callable, Iterator, Union, Optional @@ -6,7 +7,7 @@ from sportsdataverse.errors import SeasonNotFoundError from sportsdataverse.dl_utils import download -def load_wbb_pbp(seasons: List[int]) -> pd.DataFrame: +def load_wbb_pbp(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: """Load women's college basketball play by play data going back to 2002 Example: @@ -14,6 +15,7 @@ def load_wbb_pbp(seasons: List[int]) -> pd.DataFrame: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing the @@ -22,19 +24,17 @@ def load_wbb_pbp(seasons: List[int]) -> pd.DataFrame: Raises: ValueError: If `season` is less than 2002. """ - data = pd.DataFrame() + data = pl.DataFrame() if type(seasons) is int: seasons = [seasons] for i in tqdm(seasons): if int(i) < 2002: raise SeasonNotFoundError("season cannot be less than 2002") - i_data = pd.read_parquet(WBB_BASE_URL.format(season=i), engine='auto', columns=None) - data = pd.concat([data, i_data], axis = 0, ignore_index = True) - #Give each row a unique index - data.reset_index(drop=True, inplace=True) - return data + i_data = pl.read_parquet(WBB_BASE_URL.format(season=i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how = 'vertical') + return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data -def load_wbb_team_boxscore(seasons: List[int]) -> pd.DataFrame: +def load_wbb_team_boxscore(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: """Load women's college basketball team boxscore data Example: @@ -42,6 +42,7 @@ def load_wbb_team_boxscore(seasons: List[int]) -> pd.DataFrame: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing the @@ -50,20 +51,17 @@ def load_wbb_team_boxscore(seasons: List[int]) -> pd.DataFrame: Raises: ValueError: If `season` is less than 2002. """ - data = pd.DataFrame() + data = pl.DataFrame() if type(seasons) is int: seasons = [seasons] for i in tqdm(seasons): if int(i) < 2002: raise ValueError("season cannot be less than 2002") - i_data = pd.read_parquet(WBB_TEAM_BOX_URL.format(season = i), engine='auto', columns=None) - data = pd.concat([data, i_data], axis = 0, ignore_index = True) - #Give each row a unique index - data.reset_index(drop=True, inplace=True) + i_data = pl.read_parquet(WBB_TEAM_BOX_URL.format(season = i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how = 'vertical') + return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data - return data - -def load_wbb_player_boxscore(seasons: List[int]) -> pd.DataFrame: +def load_wbb_player_boxscore(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: """Load women's college basketball player boxscore data Example: @@ -71,6 +69,7 @@ def load_wbb_player_boxscore(seasons: List[int]) -> pd.DataFrame: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing the @@ -79,20 +78,17 @@ def load_wbb_player_boxscore(seasons: List[int]) -> pd.DataFrame: Raises: ValueError: If `season` is less than 2002. """ - data = pd.DataFrame() + data = pl.DataFrame() if type(seasons) is int: seasons = [seasons] for i in tqdm(seasons): if int(i) < 2002: raise ValueError("season cannot be less than 2002") - i_data = pd.read_parquet(WBB_PLAYER_BOX_URL.format(season = i), engine='auto', columns=None) - data = pd.concat([data, i_data], axis = 0, ignore_index = True) - #Give each row a unique index - data.reset_index(drop=True, inplace=True) - - return data + i_data = pl.read_parquet(WBB_PLAYER_BOX_URL.format(season = i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how = 'vertical') + return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data -def load_wbb_schedule(seasons: List[int]) -> pd.DataFrame: +def load_wbb_schedule(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: """Load women's college basketball schedule data Example: @@ -100,6 +96,7 @@ def load_wbb_schedule(seasons: List[int]) -> pd.DataFrame: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing the @@ -108,15 +105,12 @@ def load_wbb_schedule(seasons: List[int]) -> pd.DataFrame: Raises: ValueError: If `season` is less than 2002. """ - data = pd.DataFrame() + data = pl.DataFrame() if type(seasons) is int: seasons = [seasons] for i in tqdm(seasons): if int(i) < 2002: raise ValueError("season cannot be less than 2002") - i_data = pd.read_parquet(WBB_TEAM_SCHEDULE_URL.format(season = i), engine='auto', columns=None) - data = pd.concat([data, i_data], axis = 0, ignore_index = True) - #Give each row a unique index - data.reset_index(drop=True, inplace=True) - - return data + i_data = pl.read_parquet(WBB_TEAM_SCHEDULE_URL.format(season = i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how = 'vertical') + return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data diff --git a/sportsdataverse/wbb/wbb_pbp.py b/sportsdataverse/wbb/wbb_pbp.py index 6f9efd7..6d0320f 100755 --- a/sportsdataverse/wbb/wbb_pbp.py +++ b/sportsdataverse/wbb/wbb_pbp.py @@ -1,4 +1,5 @@ import pandas as pd +import polars as pl import numpy as np import os import json @@ -6,11 +7,12 @@ from typing import List, Callable, Iterator, Union, Optional, Dict from sportsdataverse.dl_utils import download, flatten_json_iterative, key_check -def espn_wbb_pbp(game_id: int, raw = False) -> Dict: +def espn_wbb_pbp(game_id: int, raw = False, **kwargs) -> Dict: """espn_wbb_pbp() - Pull the game by id. Data from API endpoints - `womens-college-basketball/playbyplay`, `womens-college-basketball/summary` Args: game_id (int): Unique game_id, can be obtained from wbb_schedule(). + raw (bool): If True, returns the raw json from the API endpoint. If False, returns a cleaned dictionary of datasets. Returns: Dict: Dictionary of game data with keys - "gameId", "plays", "winprobability", "boxscore", "header", "broadcasts", @@ -21,12 +23,11 @@ def espn_wbb_pbp(game_id: int, raw = False) -> Dict: `wbb_df = sportsdataverse.wb.espn_wbb_pbp(game_id=401266534)` """ # play by play - pbp_txt = {} - pbp_txt['timeouts'] = {} + pbp_txt = {'timeouts': {}} # summary endpoint for pickcenter array - summary_url = "http://site.api.espn.com/apis/site/v2/sports/basketball/womens-college-basketball/summary?event={}".format(game_id) - summary_resp = download(summary_url) - summary = json.loads(summary_resp) + summary_url = f"http://site.api.espn.com/apis/site/v2/sports/basketball/womens-college-basketball/summary?event={game_id}" + summary_resp = download(summary_url, **kwargs) + summary = summary_resp.json() incoming_keys_expected = [ 'boxscore', 'format', 'gameInfo', 'leaders', 'broadcasts', 'predictor', 'pickcenter', 'againstTheSpread', 'odds', 'winprobability', @@ -42,51 +43,81 @@ def espn_wbb_pbp(game_id: int, raw = False) -> Dict: 'predictor', 'article', 'header', 'season', 'standings' ] if raw == True: + # reorder keys in raw format, appending empty keys which are defined later to the end pbp_json = {} for k in incoming_keys_expected: if k in summary.keys(): pbp_json[k] = summary[k] else: - if k in dict_keys_expected: - pbp_json[k] = {} - else: - pbp_json[k] = [] + pbp_json[k] = {} if k in dict_keys_expected else [] return pbp_json for k in incoming_keys_expected: if k in summary.keys(): pbp_txt[k] = summary[k] else: - if k in dict_keys_expected: - pbp_txt[k] = {} - else: - pbp_txt[k] = [] + pbp_txt[k] = {} if k in dict_keys_expected else [] for k in ['news','shop']: - pbp_txt.pop('{}'.format(k), None) + pbp_txt.pop(f'{k}', None) - pbp_json = helper_wbb_pbp(game_id, pbp_txt) - return pbp_json + return helper_wbb_pbp(game_id, pbp_txt) -def wbb_pbp_disk(game_id, path_to_json): - with open(os.path.join(path_to_json, "{}.json".format(game_id))) as json_file: +def mbb_pbp_disk(game_id, path_to_json): + with open(os.path.join(path_to_json, f"{game_id}.json")) as json_file: pbp_txt = json.load(json_file) return pbp_txt def helper_wbb_pbp(game_id, pbp_txt): - gameSpread, overUnder, homeFavorite, gameSpreadAvailable = helper_wbb_pickcenter(pbp_txt) + init = helper_wbb_pickcenter(pbp_txt) + pbp_txt, init = helper_wbb_game_data(pbp_txt, init) + + if "plays" in pbp_txt.keys() and pbp_txt.get('header').get('competitions')[0].get('playByPlaySource') != 'none': + pbp_txt = helper_wbb_pbp_features(game_id, pbp_txt, init) + else: + pbp_txt['plays'] = pl.DataFrame() + pbp_txt['timeouts'] = { + homeTeamId: {"1": [], "2": []}, + awayTeamId: {"1": [], "2": []}, + } + # pbp_txt['plays'] = pbp_txt['plays'].replace({np.nan: None}) + return { + "gameId": int(game_id), + "plays": pbp_txt['plays'].to_dicts(), + "winprobability": np.array(pbp_txt['winprobability']).tolist(), + "boxscore": pbp_txt['boxscore'], + "header": pbp_txt['header'], + "format": pbp_txt['format'], + "broadcasts": np.array(pbp_txt['broadcasts']).tolist(), + "videos": np.array(pbp_txt['videos']).tolist(), + "playByPlaySource": pbp_txt['playByPlaySource'], + "standings": pbp_txt['standings'], + "article": pbp_txt['article'], + "leaders": np.array(pbp_txt['leaders']).tolist(), + "timeouts": pbp_txt['timeouts'], + "pickcenter": np.array(pbp_txt['pickcenter']).tolist(), + "againstTheSpread": np.array(pbp_txt['againstTheSpread']).tolist(), + "odds": np.array(pbp_txt['odds']).tolist(), + "predictor": pbp_txt['predictor'], + "espnWP": np.array(pbp_txt['espnWP']).tolist(), + "gameInfo": pbp_txt['gameInfo'], + "teamInfo": np.array(pbp_txt['teamInfo']).tolist(), + "season": np.array(pbp_txt['season']).tolist(), + } + +def helper_wbb_game_data(pbp_txt, init): pbp_txt['timeouts'] = {} pbp_txt['teamInfo'] = pbp_txt['header']['competitions'][0] pbp_txt['season'] = pbp_txt['header']['season'] pbp_txt['playByPlaySource'] = pbp_txt['header']['competitions'][0]['playByPlaySource'] pbp_txt['boxscoreSource'] = pbp_txt['header']['competitions'][0]['boxscoreSource'] - pbp_txt['gameSpreadAvailable'] = gameSpreadAvailable - pbp_txt['gameSpread'] = gameSpread - pbp_txt["homeFavorite"] = homeFavorite + pbp_txt['gameSpreadAvailable'] = init["gameSpreadAvailable"] + pbp_txt['gameSpread'] = init["gameSpread"] + pbp_txt["homeFavorite"] = init["homeFavorite"] pbp_txt["homeTeamSpread"] = np.where( - homeFavorite == True, abs(gameSpread), -1 * abs(gameSpread) + init["homeFavorite"] == True, abs(init["gameSpread"]), -1 * abs(init["gameSpread"]) ) - pbp_txt["overUnder"] = overUnder + pbp_txt["overUnder"] = init["overUnder"] # Home and Away identification variables if pbp_txt['header']['competitions'][0]['competitors'][0]['homeAway']=='home': pbp_txt['header']['competitions'][0]['home'] = pbp_txt['header']['competitions'][0]['competitors'][0]['team'] @@ -94,189 +125,37 @@ def helper_wbb_pbp(game_id, pbp_txt): homeTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['name']) homeTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['location']) homeTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['abbreviation']) - homeTeamNameAlt = re.sub("Stat(.+)", "St", str(homeTeamName)) + homeTeamNameAlt = re.sub("Stat(.+)", "St", homeTeamName) pbp_txt['header']['competitions'][0]['away'] = pbp_txt['header']['competitions'][0]['competitors'][1]['team'] awayTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['id']) awayTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['name']) awayTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['location']) awayTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['abbreviation']) - awayTeamNameAlt = re.sub("Stat(.+)", "St", str(awayTeamName)) + awayTeamNameAlt = re.sub("Stat(.+)", "St", awayTeamName) else: pbp_txt['header']['competitions'][0]['away'] = pbp_txt['header']['competitions'][0]['competitors'][0]['team'] awayTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['id']) awayTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['name']) awayTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['location']) awayTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['abbreviation']) - awayTeamNameAlt = re.sub("Stat(.+)", "St", str(awayTeamName)) + awayTeamNameAlt = re.sub("Stat(.+)", "St", awayTeamName) pbp_txt['header']['competitions'][0]['home'] = pbp_txt['header']['competitions'][0]['competitors'][1]['team'] homeTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['id']) homeTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['name']) homeTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['location']) homeTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['abbreviation']) - homeTeamNameAlt = re.sub("Stat(.+)", "St", str(homeTeamName)) - - if (pbp_txt['playByPlaySource'] != "none") & (len(pbp_txt['plays'])>1): - helper_wbb_pbp_features( - game_id, pbp_txt, homeTeamId, awayTeamId, - homeTeamMascot, awayTeamMascot, homeTeamName, - awayTeamName, homeTeamAbbrev, awayTeamAbbrev, - homeTeamNameAlt, awayTeamNameAlt, - gameSpread, homeFavorite, gameSpreadAvailable - ) - else: - pbp_txt['plays'] = pd.DataFrame() - pbp_txt['timeouts'] = {} - pbp_txt['timeouts'][homeTeamId] = {"1": [], "2": []} - pbp_txt['timeouts'][awayTeamId] = {"1": [], "2": []} - pbp_txt['plays'] = pbp_txt['plays'].replace({np.nan: None}) - pbp_json = { - "gameId": game_id, - "plays" : pbp_txt['plays'].to_dict(orient='records'), - "winprobability" : np.array(pbp_txt['winprobability']).tolist(), - "boxscore" : pbp_txt['boxscore'], - "header" : pbp_txt['header'], - "format": pbp_txt['format'], - "broadcasts" : np.array(pbp_txt['broadcasts']).tolist(), - "videos" : np.array(pbp_txt['videos']).tolist(), - "playByPlaySource": pbp_txt['playByPlaySource'], - "standings" : pbp_txt['standings'], - "article" : pbp_txt['article'], - "leaders" : np.array(pbp_txt['leaders']).tolist(), - "timeouts" : pbp_txt['timeouts'], - "pickcenter" : np.array(pbp_txt['pickcenter']).tolist(), - "againstTheSpread" : np.array(pbp_txt['againstTheSpread']).tolist(), - "odds" : np.array(pbp_txt['odds']).tolist(), - "predictor" : pbp_txt['predictor'], - "espnWP" : np.array(pbp_txt['espnWP']).tolist(), - "gameInfo" : pbp_txt['gameInfo'], - "teamInfo" : np.array(pbp_txt['teamInfo']).tolist(), - "season" : np.array(pbp_txt['season']).tolist() - } - return pbp_json - -def helper_wbb_pbp_features(game_id, pbp_txt, homeTeamId, awayTeamId, homeTeamMascot, awayTeamMascot, homeTeamName, awayTeamName, homeTeamAbbrev, awayTeamAbbrev, homeTeamNameAlt, awayTeamNameAlt, gameSpread, homeFavorite, gameSpreadAvailable): - pbp_txt['plays_mod'] = [] - for play in pbp_txt['plays']: - p = flatten_json_iterative(play) - pbp_txt['plays_mod'].append(p) - pbp_txt['plays'] = pd.json_normalize(pbp_txt,'plays_mod') - pbp_txt['plays']['season'] = pbp_txt['season']['year'] - pbp_txt['plays']['seasonType'] = pbp_txt['season']['type'] - pbp_txt['plays']["awayTeamId"] = awayTeamId - pbp_txt['plays']["awayTeamName"] = str(awayTeamName) - pbp_txt['plays']["awayTeamMascot"] = str(awayTeamMascot) - pbp_txt['plays']["awayTeamAbbrev"] = str(awayTeamAbbrev) - pbp_txt['plays']["awayTeamNameAlt"] = str(awayTeamNameAlt) - pbp_txt['plays']["homeTeamId"] = homeTeamId - pbp_txt['plays']["homeTeamName"] = str(homeTeamName) - pbp_txt['plays']["homeTeamMascot"] = str(homeTeamMascot) - pbp_txt['plays']["homeTeamAbbrev"] = str(homeTeamAbbrev) - pbp_txt['plays']["homeTeamNameAlt"] = str(homeTeamNameAlt) - # Spread definition - pbp_txt['plays']["homeTeamSpread"] = 2.5 - pbp_txt['plays']["gameSpread"] = abs(gameSpread) - pbp_txt['plays']["homeTeamSpread"] = np.where(homeFavorite == True, abs(gameSpread), -1*abs(gameSpread)) - pbp_txt['homeTeamSpread'] = np.where(homeFavorite == True, abs(gameSpread), -1*abs(gameSpread)) - pbp_txt['plays']["homeFavorite"] = homeFavorite - pbp_txt['plays']["gameSpread"] = gameSpread - pbp_txt['plays']["gameSpreadAvailable"] = gameSpreadAvailable - pbp_txt['plays'] = pbp_txt['plays'].to_dict(orient='records') - pbp_txt['plays'] = pd.DataFrame(pbp_txt['plays']) - pbp_txt['plays']['season'] = pbp_txt['header']['season']['year'] - pbp_txt['plays']['seasonType'] = pbp_txt['header']['season']['type'] - pbp_txt['plays']['game_id'] = int(game_id) - pbp_txt['plays']["homeTeamId"] = homeTeamId - pbp_txt['plays']["awayTeamId"] = awayTeamId - pbp_txt['plays']["homeTeamName"] = str(homeTeamName) - pbp_txt['plays']["awayTeamName"] = str(awayTeamName) - pbp_txt['plays']["homeTeamMascot"] = str(homeTeamMascot) - pbp_txt['plays']["awayTeamMascot"] = str(awayTeamMascot) - pbp_txt['plays']["homeTeamAbbrev"] = str(homeTeamAbbrev) - pbp_txt['plays']["awayTeamAbbrev"] = str(awayTeamAbbrev) - pbp_txt['plays']["homeTeamNameAlt"] = str(homeTeamNameAlt) - pbp_txt['plays']["awayTeamNameAlt"] = str(awayTeamNameAlt) - pbp_txt['plays']['period.number'] = pbp_txt['plays']['period.number'].apply(lambda x: int(x)) - pbp_txt['plays']['qtr'] = pbp_txt['plays']['period.number'].apply(lambda x: int(x)) - pbp_txt['plays']["homeTeamSpread"] = 2.5 - pbp_txt['plays']["gameSpread"] = abs(gameSpread) - pbp_txt['plays']["gameSpreadAvailable"] = gameSpreadAvailable - pbp_txt['plays']["homeTeamSpread"] = np.where(homeFavorite == True, abs(gameSpread), -1*abs(gameSpread)) - pbp_txt['homeTeamSpread'] = np.where(homeFavorite == True, abs(gameSpread), -1*abs(gameSpread)) - pbp_txt['plays']["homeFavorite"] = homeFavorite - pbp_txt['plays']["gameSpread"] = gameSpread - pbp_txt['plays']["homeFavorite"] = homeFavorite - - #----- Time --------------- - pbp_txt['plays']['time'] = pbp_txt['plays']['clock.displayValue'] - pbp_txt['plays']['clock.mm'] = pbp_txt['plays']['clock.displayValue'].str.split(pat=':') - pbp_txt['plays'][['clock.minutes','clock.seconds']] = pbp_txt['plays']['clock.mm'].to_list() - pbp_txt['plays']['half'] = np.where(pbp_txt['plays']['qtr'] <= 2, "1","2") - pbp_txt['plays']['game_half'] = np.where(pbp_txt['plays']['qtr'] <= 2, "1","2") - pbp_txt['plays']['lag_qtr'] = pbp_txt['plays']['qtr'].shift(1) - pbp_txt['plays']['lead_qtr'] = pbp_txt['plays']['qtr'].shift(-1) - pbp_txt['plays']['lag_game_half'] = pbp_txt['plays']['game_half'].shift(1) - pbp_txt['plays']['lead_game_half'] = pbp_txt['plays']['game_half'].shift(-1) - pbp_txt['plays']['start.quarter_seconds_remaining'] = 60*pbp_txt['plays']['clock.minutes'].astype(int) + pbp_txt['plays']['clock.seconds'].astype(int) - pbp_txt['plays']['start.half_seconds_remaining'] = np.where( - pbp_txt['plays']['qtr'].isin([1,3]), - 600 + 60*pbp_txt['plays']['clock.minutes'].astype(int) + pbp_txt['plays']['clock.seconds'].astype(int), - 60*pbp_txt['plays']['clock.minutes'].astype(int) + pbp_txt['plays']['clock.seconds'].astype(int) - ) - pbp_txt['plays']['start.game_seconds_remaining'] = np.select( - [ - pbp_txt['plays']['qtr'] == 1, - pbp_txt['plays']['qtr'] == 2, - pbp_txt['plays']['qtr'] == 3, - pbp_txt['plays']['qtr'] == 4 - ], - [ - 1800 + 60*pbp_txt['plays']['clock.minutes'].astype(int) + pbp_txt['plays']['clock.seconds'].astype(int), - 1200 + 60*pbp_txt['plays']['clock.minutes'].astype(int) + pbp_txt['plays']['clock.seconds'].astype(int), - 600 + 60*pbp_txt['plays']['clock.minutes'].astype(int) + pbp_txt['plays']['clock.seconds'].astype(int), - 60*pbp_txt['plays']['clock.minutes'].astype(int) + pbp_txt['plays']['clock.seconds'].astype(int) - ], default = 60*pbp_txt['plays']['clock.minutes'].astype(int) + pbp_txt['plays']['clock.seconds'].astype(int) - ) - # Pos Team - Start and End Id - pbp_txt['plays']['game_play_number'] = np.arange(len(pbp_txt['plays']))+1 - pbp_txt['plays']['text'] = pbp_txt['plays']['text'].astype(str) - pbp_txt['plays']['id'] = pbp_txt['plays']['id'].apply(lambda x: int(x)) - pbp_txt['plays']['end.quarter_seconds_remaining'] = pbp_txt['plays']['start.quarter_seconds_remaining'].shift(1) - pbp_txt['plays']['end.half_seconds_remaining'] = pbp_txt['plays']['start.half_seconds_remaining'].shift(1) - pbp_txt['plays']['end.game_seconds_remaining'] = pbp_txt['plays']['start.game_seconds_remaining'].shift(1) - pbp_txt['plays']['end.quarter_seconds_remaining'] = np.select( - [ - (pbp_txt['plays']['game_play_number'] == 1)| - ((pbp_txt['plays']['qtr'] == 2) & (pbp_txt['plays']['lag_qtr'] == 1))| - ((pbp_txt['plays']['qtr'] == 3) & (pbp_txt['plays']['lag_qtr'] == 2))| - ((pbp_txt['plays']['qtr'] == 4) & (pbp_txt['plays']['lag_qtr'] == 3)) - ], - [ - 600 - ], default = pbp_txt['plays']['end.quarter_seconds_remaining'] - ) - pbp_txt['plays']['end.half_seconds_remaining'] = np.select( - [ - (pbp_txt['plays']['game_play_number'] == 1)| - ((pbp_txt['plays']['game_half'] == "2") & (pbp_txt['plays']['lag_game_half'] == "1")) - ], - [ - 1200 - ], default = pbp_txt['plays']['end.half_seconds_remaining'] - ) - pbp_txt['plays']['end.game_seconds_remaining'] = np.select( - [ - (pbp_txt['plays']['game_play_number'] == 1), - ((pbp_txt['plays']['game_half'] == "2") & (pbp_txt['plays']['lag_game_half'] == "1")) - ], - [ - 2400, - 1200 - ], default = pbp_txt['plays']['end.game_seconds_remaining'] - ) - - pbp_txt['plays']['period'] = pbp_txt['plays']['qtr'] - - del pbp_txt['plays']['clock.mm'] + homeTeamNameAlt = re.sub("Stat(.+)", "St", homeTeamName) + init["homeTeamId"] = homeTeamId + init["homeTeamMascot"] = homeTeamMascot + init["homeTeamName"] = homeTeamName + init["homeTeamAbbrev"] = homeTeamAbbrev + init["homeTeamNameAlt"] = homeTeamNameAlt + init["awayTeamId"] = awayTeamId + init["awayTeamMascot"] = awayTeamMascot + init["awayTeamName"] = awayTeamName + init["awayTeamAbbrev"] = awayTeamAbbrev + init["awayTeamNameAlt"] = awayTeamNameAlt + return pbp_txt, init def helper_wbb_pickcenter(pbp_txt): # Spread definition @@ -285,15 +164,219 @@ def helper_wbb_pickcenter(pbp_txt): if "spread" in pbp_txt.get("pickcenter",{})[1].keys(): gameSpread = pbp_txt.get("pickcenter",{})[1].get("spread","") overUnder = pbp_txt.get("pickcenter",{})[1].get("overUnder","") - gameSpreadAvailable = True else: gameSpread = pbp_txt.get("pickcenter",{})[0].get("spread","") overUnder = pbp_txt.get("pickcenter",{})[0].get("overUnder","") - gameSpreadAvailable = True - # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") + gameSpreadAvailable = True + # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") else: gameSpread = 2.5 - overUnder = 120.5 + overUnder = 140.5 homeFavorite = True gameSpreadAvailable = False - return gameSpread, overUnder, homeFavorite, gameSpreadAvailable \ No newline at end of file + + return { + "gameSpread": gameSpread, + "overUnder": overUnder, + "homeFavorite": homeFavorite, + "gameSpreadAvailable": gameSpreadAvailable + } + +def helper_wbb_pbp_features(game_id, pbp_txt, init): + pbp_txt['plays_mod'] = [] + for play in pbp_txt['plays']: + p = flatten_json_iterative(play) + pbp_txt['plays_mod'].append(p) + pbp_txt['plays'] = pl.from_pandas(pd.json_normalize(pbp_txt,'plays_mod')) + pbp_txt['plays'] = pbp_txt['plays'].with_columns( + game_id = pl.lit(game_id).cast(pl.Int32), + id = (pl.col('id').cast(pl.Int64)), + season = pl.lit(pbp_txt['header']['season']['year']), + seasonType = pl.lit(pbp_txt['header']['season']['type']), + homeTeamId = pl.lit(init["homeTeamId"]), + homeTeamName = pl.lit(init["homeTeamName"]), + homeTeamMascot = pl.lit(init["homeTeamMascot"]), + homeTeamAbbrev = pl.lit(init["homeTeamAbbrev"]), + homeTeamNameAlt = pl.lit(init["homeTeamNameAlt"]), + awayTeamId = pl.lit(init["awayTeamId"]), + awayTeamName = pl.lit(init["awayTeamName"]), + awayTeamMascot = pl.lit(init["awayTeamMascot"]), + awayTeamAbbrev = pl.lit(init["awayTeamAbbrev"]), + awayTeamNameAlt = pl.lit(init["awayTeamNameAlt"]), + gameSpread = pl.lit(init["gameSpread"]).abs(), + homeFavorite = pl.lit(init["homeFavorite"]), + gameSpreadAvailable = pl.lit(init["gameSpreadAvailable"]), + ).with_columns( + homeTeamSpread = pl.when(pl.col('homeFavorite') == True) + .then(pl.col('gameSpread')) + .otherwise(-1*pl.col('gameSpread')), + ).with_columns( + pl.col("period.number").cast(pl.Int32).alias("period.number"), + pl.col("period.number").cast(pl.Int32).alias("qtr"), + pl.when(pl.col("clock.displayValue").str.contains(r":") == False) + .then('0:'+ pl.col("clock.displayValue")) + .otherwise(pl.col("clock.displayValue")) + .alias("clock.displayValue"), + ).with_columns( + pl.col("clock.displayValue").alias("time"), + pl.col("clock.displayValue").str.split(':').list.to_struct(n_field_strategy="first_non_null").alias("clock.mm") + ).with_columns( + pl.col("clock.mm").struct.rename_fields(["clock.minutes", "clock.seconds"]) + ).unnest( + "clock.mm" + ).with_columns( + pl.col("clock.minutes").cast(pl.Float32), + pl.col("clock.seconds").cast(pl.Float32), + pl.when((pl.col("type.text") == "ShortTimeOut") + .and_( + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamAbbrev"]).lower()) + .or_( + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamAbbrev"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamName"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamMascot"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamNameAlt"]).lower()) + ) + )) + .then(True) + .otherwise(False) + .alias("homeTimeoutCalled"), + pl.when((pl.col("type.text") == "ShortTimeOut") + .and_( + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamAbbrev"]).lower()) + .or_( + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamAbbrev"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamName"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamMascot"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamNameAlt"]).lower()) + ) + )) + .then(True) + .otherwise(False) + .alias("awayTimeoutCalled"), + ).with_columns( + half = pl.when(pl.col('qtr') <= 2) + .then(1) + .otherwise(2), + game_half = pl.when(pl.col('qtr') <= 2) + .then(1) + .otherwise(2), + ).with_columns( + lag_qtr = pl.col('qtr').shift(1), + lead_qtr = pl.col('qtr').shift(-1), + lag_half = pl.col('half').shift(1), + lead_half = pl.col('half').shift(-1), + ).with_columns( + (60 * pl.col("clock.minutes") + pl.col("clock.seconds")).alias("start.quarter_seconds_remaining"), + pl.when(pl.col('qtr').is_in([1,3])) + .then(600 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) + .otherwise(60 * pl.col("clock.minutes") + pl.col("clock.seconds")) + .alias("start.half_seconds_remaining"), + pl.when(pl.col('qtr') == 1) + .then(1800 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) + .when(pl.col('qtr') == 2) + .then(1200 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) + .when(pl.col('qtr') == 3) + .then(600 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) + .otherwise(60 * pl.col("clock.minutes") + pl.col("clock.seconds")) + .alias("start.game_seconds_remaining"), + ).with_columns( + pl.col("start.quarter_seconds_remaining").shift(-1).alias("end.quarter_seconds_remaining"), + pl.col("start.half_seconds_remaining").shift(-1).alias("end.half_seconds_remaining"), + pl.col("start.game_seconds_remaining").shift(-1).alias("end.game_seconds_remaining"), + ) + + + pbp_txt["plays"] = pbp_txt["plays"].with_row_count("game_play_number", 1) + pbp_txt["timeouts"] = { + init["homeTeamId"]: {"1": [], "2": []}, + init["awayTeamId"]: {"1": [], "2": []}, + } + pbp_txt["timeouts"][init["homeTeamId"]]["1"] = ( + pbp_txt["plays"] + .filter( + (pl.col("homeTimeoutCalled") == True) + .and_(pl.col("period.number") <= 2) + ) + .get_column("id") + .to_list() + ) + pbp_txt["timeouts"][init["homeTeamId"]]["2"] = ( + pbp_txt["plays"] + .filter( + (pl.col("homeTimeoutCalled") == True) + .and_(pl.col("period.number") > 2) + ) + .get_column("id") + .to_list() + ) + pbp_txt["timeouts"][init["awayTeamId"]]["1"] = ( + pbp_txt["plays"] + .filter( + (pl.col("awayTimeoutCalled") == True) + .and_(pl.col("period.number") <= 2) + ) + .get_column("id") + .to_list() + ) + pbp_txt["timeouts"][init["awayTeamId"]]["2"] = ( + pbp_txt["plays"] + .filter( + (pl.col("awayTimeoutCalled") == True) + .and_(pl.col("period.number") > 2) + ) + .get_column("id") + .to_list() + ) + # Pos Team - Start and End Id + pbp_txt['plays'] = pbp_txt['plays'].with_columns( + pl.when((pl.col("game_play_number") == 1) + .or_((pl.col("lag_qtr") == 1) + .and_(pl.col("period.number") == 2)) + .or_((pl.col("lag_qtr") == 2) + .and_(pl.col("period.number") == 3)) + .or_((pl.col("lag_qtr") == 3) + .and_(pl.col("period.number") == 4))) + .then(600) + .when((pl.col("lag_qtr") == (pl.col("period.number") - 1)) + .and_(pl.col("period.number") >= 5)) + .then(300) + .otherwise(pl.col("end.quarter_seconds_remaining")) + .alias("end.quarter_seconds_remaining"), + pl.when((pl.col("game_play_number") == 1)) + .then(1200) + .when((pl.col("lag_half") == 1) + .and_(pl.col("half") == 2)) + .then(1200) + .when((pl.col("lag_qtr") == 1) + .and_(pl.col("period.number") == 2)) + .then(600) + .when((pl.col("lag_qtr") == 2) + .and_(pl.col("period.number") == 3)) + .then(1200) + .when((pl.col("lag_qtr") == 3) + .and_(pl.col("period.number") == 4)) + .then(600) + .when((pl.col("lag_qtr") == (pl.col("period.number") - 1)) + .and_(pl.col("period.number") >= 5)) + .then(300) + .otherwise(pl.col("end.half_seconds_remaining")) + .alias("end.half_seconds_remaining"), + pl.when((pl.col("game_play_number") == 1)) + .then(2400) + .when((pl.col("lag_qtr") == 1) + .and_(pl.col("period.number") == 2)) + .then(1800) + .when((pl.col("lag_qtr") == 2) + .and_(pl.col("period.number") == 3)) + .then(1200) + .when((pl.col("lag_qtr") == 3) + .and_(pl.col("period.number") == 4)) + .then(600) + .when((pl.col("lag_qtr") == (pl.col("period.number") - 1)) + .and_(pl.col("period.number") >= 5)) + .then(300) + .otherwise(pl.col("end.game_seconds_remaining")) + .alias("end.game_seconds_remaining"), + pl.col("qtr").cast(pl.Int32).alias("period"), + ) + return pbp_txt \ No newline at end of file diff --git a/sportsdataverse/wbb/wbb_schedule.py b/sportsdataverse/wbb/wbb_schedule.py index fb107d1..9be6fc1 100755 --- a/sportsdataverse/wbb/wbb_schedule.py +++ b/sportsdataverse/wbb/wbb_schedule.py @@ -5,7 +5,8 @@ from sportsdataverse.errors import SeasonNotFoundError from sportsdataverse.dl_utils import download, underscore -def espn_wbb_schedule(dates=None, groups=50, season_type=None, limit=500) -> pd.DataFrame: +def espn_wbb_schedule(dates=None, groups=50, season_type=None, limit=500, + return_as_pandas=True, **kwargs) -> pd.DataFrame: """espn_wbb_schedule - look up the women's college basketball schedule for a given season Args: @@ -13,72 +14,106 @@ def espn_wbb_schedule(dates=None, groups=50, season_type=None, limit=500) -> pd. groups (int): Used to define different divisions. 50 is Division I, 51 is Division II/Division III. season_type (int): 2 for regular season, 3 for post-season, 4 for off-season. limit (int): number of records to return, default: 500. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. """ - if dates is None: - dates = '' - else: - dates = '&dates=' + str(dates) - if groups is None: - groups = '' - else: - groups = '&groups=' + str(groups) - if season_type is None: - season_type = '' - else: - season_type = '&seasontype=' + str(season_type) - url = "http://site.api.espn.com/apis/site/v2/sports/basketball/womens-college-basketball/scoreboard?limit={}{}{}{}".format(limit, dates, groups, season_type) - resp = download(url=url) + url = "http://site.api.espn.com/apis/site/v2/sports/basketball/womens-college-basketball/scoreboard" + params = { + 'dates': dates, + 'seasonType': season_type, + 'groups': groups if groups is not None else '50', + 'limit': limit + } + resp = download(url=url, params=params, **kwargs) + + ev = pl.DataFrame() + events_txt = resp.json() + events = events_txt.get('events') + if len(events) == 0: + return pd.DataFrame() if return_as_pandas else pl.DataFrame() + + for event in events: + event.get('competitions')[0].get('competitors')[0].get('team').pop('links',None) + event.get('competitions')[0].get('competitors')[1].get('team').pop('links',None) + if event.get('competitions')[0].get('competitors')[0].get('homeAway') == 'home': + event = __extract_home_away(event, 0, 'home') + event = __extract_home_away(event, 1, 'away') + else: + event = __extract_home_away(event, 0, 'away') + event = __extract_home_away(event, 1, 'home') + del_keys = ['geoBroadcasts', 'headlines', 'series', 'situation', 'tickets', 'odds'] + for k in del_keys: + event.get('competitions')[0].pop(k, None) + event.get('competitions')[0]['notes_type'] = event.get('competitions')[0]['notes'][0].get("type") if len(event.get('competitions')[0]['notes']) > 0 else '' + event.get('competitions')[0]['notes_headline'] = event.get('competitions')[0]['notes'][0].get("headline").replace('"','') if len(event.get('competitions')[0]['notes']) > 0 else '' + event.get('competitions')[0]['broadcast_market'] = event.get('competitions')[0].get('broadcasts', [])[0].get('market', "") if len(event.get('competitions')[0].get('broadcasts')) > 0 else "" + event.get('competitions')[0]['broadcast_name'] = event.get('competitions')[0].get('broadcasts', [])[0].get('names', [])[0] if len(event.get('competitions')[0].get('broadcasts')) > 0 else "" + event.get('competitions')[0].pop('broadcasts', None) + event.get('competitions')[0].pop('notes', None) + event.get('competitions')[0].pop('competitors', None) + x = pl.from_pandas(pd.json_normalize(event.get('competitions')[0], sep = '_')) + x = x.with_columns( + game_id = (pl.col('id').cast(pl.Int64)), + season = (event.get('season').get('year')), + season_type = (event.get('season').get('type')), + home_linescores = pl.when(pl.col('status_type_description') == 'Postponed').then(None).otherwise(pl.col('home_linescores')), + away_linescores = pl.when(pl.col('status_type_description') == 'Postponed').then(None).otherwise(pl.col('away_linescores')), + ) + x = x[[s.name for s in x if s.null_count() != x.height]] + ev = pl.concat([ev, x], how = 'diagonal') - ev = pd.DataFrame() - if resp is not None: - events_txt = json.loads(resp) - events = events_txt.get('events') - for event in events: - event.get('competitions')[0].get('competitors')[0].get('team').pop('links',None) - event.get('competitions')[0].get('competitors')[1].get('team').pop('links',None) - if event.get('competitions')[0].get('competitors')[0].get('homeAway')=='home': - event['competitions'][0]['home'] = event.get('competitions')[0].get('competitors')[0].get('team') - event['competitions'][0]['home']['score'] = event.get('competitions')[0].get('competitors')[0].get('score') - event['competitions'][0]['home']['winner'] = event.get('competitions')[0].get('competitors')[0].get('winner') - event['competitions'][0]['away'] = event.get('competitions')[0].get('competitors')[1].get('team') - event['competitions'][0]['away']['score'] = event.get('competitions')[0].get('competitors')[1].get('score') - event['competitions'][0]['away']['winner'] = event.get('competitions')[0].get('competitors')[1].get('winner') - else: - event['competitions'][0]['away'] = event.get('competitions')[0].get('competitors')[0].get('team') - event['competitions'][0]['away']['score'] = event.get('competitions')[0].get('competitors')[0].get('score') - event['competitions'][0]['away']['winner'] = event.get('competitions')[0].get('competitors')[0].get('winner') - event['competitions'][0]['home'] = event.get('competitions')[0].get('competitors')[1].get('team') - event['competitions'][0]['home']['score'] = event.get('competitions')[0].get('competitors')[1].get('score') - event['competitions'][0]['home']['winner'] = event.get('competitions')[0].get('competitors')[1].get('winner') + ev.columns = [underscore(c) for c in ev.columns] - del_keys = ['broadcasts','geoBroadcasts', 'headlines', 'series', 'situation', 'tickets', 'odds'] - for k in del_keys: - event.get('competitions')[0].pop(k, None) - if len(event.get('competitions')[0]['notes'])>0: - event.get('competitions')[0]['notes_type'] = event.get('competitions')[0]['notes'][0].get("type") - event.get('competitions')[0]['notes_headline'] = event.get('competitions')[0]['notes'][0].get("headline").replace('"','') - else: - event.get('competitions')[0]['notes_type'] = '' - event.get('competitions')[0]['notes_headline'] = '' - event.get('competitions')[0].pop('notes', None) - x = pd.json_normalize(event.get('competitions')[0], sep='_') - x['game_id'] = x['id'].astype(int) - x['season'] = event.get('season').get('year') - x['season_type'] = event.get('season').get('type') - ev = pd.concat([ev,x],axis=0, ignore_index=True) - ev = pd.DataFrame(ev) - ev.columns = [underscore(c) for c in ev.columns.tolist()] - return ev + return ev.to_pandas() if return_as_pandas else ev -def espn_wbb_calendar(season=None, ondays=None) -> pd.DataFrame: +def __extract_home_away(event, arg1, arg2): + event['competitions'][0][arg2] = ( + event.get('competitions')[0].get('competitors')[arg1].get('team') + ) + event['competitions'][0][arg2]['score'] = ( + event.get('competitions')[0].get('competitors')[arg1].get('score') + ) + event['competitions'][0][arg2]['winner'] = ( + event.get('competitions')[0].get('competitors')[arg1].get('winner') + ) + ## add winner back to main competitors if does not exist + event['competitions'][0]['competitors'][arg1]['winner'] = ( + event.get('competitions')[0].get('competitors')[arg1].get('winner', False) + ) + event['competitions'][0][arg2]['currentRank'] = ( + event.get('competitions')[0] + .get('competitors')[arg1] + .get('curatedRank', {}) + .get('current', 99) + ) + event['competitions'][0][arg2]['linescores'] = ( + event.get('competitions')[0] + .get('competitors')[arg1] + .get('linescores', [{'value': 'N/A'}]) + ) + ## add linescores back to main competitors if does not exist + event['competitions'][0]['competitors'][arg1]['linescores'] = ( + event.get('competitions')[0] + .get('competitors')[arg1] + .get('linescores', []) + ) + event['competitions'][0][arg2]['records'] = ( + event.get('competitions')[0] + .get('competitors')[arg1] + .get('records', []) + ) + return event + +def espn_wbb_calendar(season=None, ondays=None, + return_as_pandas = True, **kwargs) -> pd.DataFrame: """espn_wbb_calendar - look up the women's college basketball calendar for a given season Args: season (int): Used to define different seasons. 2002 is the earliest available season. ondays (boolean): Used to return dates for calendar ondays + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing @@ -90,38 +125,47 @@ def espn_wbb_calendar(season=None, ondays=None) -> pd.DataFrame: if int(season) < 2002: raise SeasonNotFoundError("season cannot be less than 2002") if ondays is not None: - url = "https://sports.core.api.espn.com/v2/sports/basketball/leagues/womens-college-basketball/seasons/{}/types/2/calendar/ondays".format(season) - resp = download(url=url) - txt = json.loads(resp).get('eventDate').get('dates') - full_schedule = pd.DataFrame(txt,columns=['dates']) - full_schedule['dateURL'] = list(map(lambda x: x[:10].replace("-",""),full_schedule['dates'])) - full_schedule['url']="http://site.api.espn.com/apis/site/v2/sports/basketball/womens-college-basketball/scoreboard?dates=" - full_schedule['url']= full_schedule['url'] + full_schedule['dateURL'] + full_schedule = __ondays_wbb_calendar(season, **kwargs) else: - url = "http://site.api.espn.com/apis/site/v2/sports/basketball/womens-college-basketball/scoreboard?dates={}".format(season) - resp = download(url=url) - txt = json.loads(resp)['leagues'][0]['calendar'] + url = f"http://site.api.espn.com/apis/site/v2/sports/basketball/womens-college-basketball/scoreboard?dates={season}" + resp = download(url=url, **kwargs) + txt = resp.json().get('leagues')[0].get('calendar') datenum = list(map(lambda x: x[:10].replace("-",""),txt)) date = list(map(lambda x: x[:10],txt)) year = list(map(lambda x: x[:4],txt)) month = list(map(lambda x: x[5:7],txt)) day = list(map(lambda x: x[8:10],txt)) - - data = {"season": season, - "datetime" : txt, - "date" : date, - "year": year, - "month": month, - "day": day, - "dateURL": datenum + data = { + "season": season, + "datetime" : txt, + "date" : date, + "year": year, + "month": month, + "day": day, + "dateURL": datenum } - full_schedule = pd.DataFrame(data) - full_schedule['url']="http://site.api.espn.com/apis/site/v2/sports/basketball/womens-college-basketball/scoreboard?dates=" - full_schedule['url']= full_schedule['url'] + full_schedule['dateURL'] - return full_schedule + full_schedule = pl.DataFrame(data) + full_schedule = full_schedule.with_columns( + url="http://site.api.espn.com/apis/site/v2/sports/basketball/womens-college-basketball/scoreboard?dates=" + + pl.col('dateURL') + ) + return full_schedule.to_pandas() if return_as_pandas else full_schedule + +def __ondays_wbb_calendar(season, **kwargs): + url = f"https://sports.core.api.espn.com/v2/sports/basketball/leagues/womens-college-basketball/seasons/{season}/types/2/calendar/ondays" + resp = download(url=url, **kwargs) + txt = resp.json().get('eventDate').get('dates') + result = pl.DataFrame(txt, schema=['dates']) + result = result.with_columns(dateURL = pl.col('dates').str.slice(0, 10)) + result = result.with_columns( + url="http://site.api.espn.com/apis/site/v2/sports/basketball/womens-college-basketball/scoreboard?dates=" + + pl.col('dateURL') + ) + + return result def most_recent_wbb_season(): - if datetime.datetime.today().month >= 10: - return datetime.datetime.today().year + 1 - else: - return datetime.datetime.today().year \ No newline at end of file + if datetime.datetime.now().month >= 10: + return datetime.datetime.now().year + 1 + else: + return datetime.datetime.now().year \ No newline at end of file diff --git a/sportsdataverse/wbb/wbb_teams.py b/sportsdataverse/wbb/wbb_teams.py index b0961fa..7d7bf55 100755 --- a/sportsdataverse/wbb/wbb_teams.py +++ b/sportsdataverse/wbb/wbb_teams.py @@ -1,26 +1,31 @@ import pandas as pd +import polars as pl import json from sportsdataverse.dl_utils import download, underscore from urllib.error import URLError, HTTPError, ContentTooShortError -def espn_wbb_teams(groups=None) -> pd.DataFrame: +def espn_wbb_teams(groups=None, return_as_pandas=True, **kwargs) -> pd.DataFrame: """espn_wbb_teams - look up the women's college basketball teams Args: groups (int): Used to define different divisions. 50 is Division I, 51 is Division II/Division III. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing teams for the requested league. + + Example: + `wbb_df = sportsdataverse.wbb.espn_wbb_teams()` + """ - if groups is None: - groups = '&groups=50' - else: - groups = '&groups=' + str(groups) - ev = pd.DataFrame() - url = "http://site.api.espn.com/apis/site/v2/sports/basketball/womens-college-basketball/teams?limit=1000{}".format(groups) - resp = download(url=url) + url = "http://site.api.espn.com/apis/site/v2/sports/basketball/womens-college-basketball/teams" + params = { + "groups": groups if groups is not None else "50", + "limit": 1000 + } + resp = download(url=url, params = params, **kwargs) if resp is not None: - events_txt = json.loads(resp) + events_txt = resp.json() teams = events_txt.get('sports')[0].get('leagues')[0].get('teams') del_keys = ['record', 'links'] @@ -29,5 +34,5 @@ def espn_wbb_teams(groups=None) -> pd.DataFrame: team.get('team').pop(k, None) teams = pd.json_normalize(teams, sep='_') teams.columns = [underscore(c) for c in teams.columns.tolist()] - return teams + return teams if return_as_pandas else pl.from_pandas(teams) From 1c6d8c6fdd7d9ede6a488fc115a023aa3e9e58d9 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Mon, 10 Jul 2023 20:29:31 -0400 Subject: [PATCH 18/79] Function nfl.nfl_game_rosters() added --- sportsdataverse/nfl/nfl_game_rosters.py | 200 ++++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 sportsdataverse/nfl/nfl_game_rosters.py diff --git a/sportsdataverse/nfl/nfl_game_rosters.py b/sportsdataverse/nfl/nfl_game_rosters.py new file mode 100644 index 0000000..70f550a --- /dev/null +++ b/sportsdataverse/nfl/nfl_game_rosters.py @@ -0,0 +1,200 @@ +import pandas as pd +import polars as pl +import numpy as np +import os +import json +import re +from typing import List, Callable, Iterator, Union, Optional, Dict +from sportsdataverse.dl_utils import download, underscore + +def espn_nfl_game_rosters(game_id: int, raw = False, return_as_pandas = True, **kwargs) -> pd.DataFrame: + """espn_nfl_game_rosters() - Pull the game by id. + + Args: + game_id (int): Unique game_id, can be obtained from espn_nfl_schedule(). + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + + Returns: + pd.DataFrame: Data frame of game roster data with columns: + 'athlete_id', 'athlete_uid', 'athlete_guid', 'athlete_type', + 'first_name', 'last_name', 'full_name', 'athlete_display_name', + 'short_name', 'weight', 'display_weight', 'height', 'display_height', + 'age', 'date_of_birth', 'slug', 'jersey', 'linked', 'active', + 'alternate_ids_sdr', 'birth_place_city', 'birth_place_state', + 'birth_place_country', 'headshot_href', 'headshot_alt', + 'experience_years', 'experience_display_value', + 'experience_abbreviation', 'status_id', 'status_name', 'status_type', + 'status_abbreviation', 'hand_type', 'hand_abbreviation', + 'hand_display_value', 'draft_display_text', 'draft_round', 'draft_year', + 'draft_selection', 'player_id', 'starter', 'valid', 'did_not_play', + 'display_name', 'ejected', 'athlete_href', 'position_href', + 'statistics_href', 'team_id', 'team_guid', 'team_uid', 'team_slug', + 'team_location', 'team_name', 'team_nickname', 'team_abbreviation', + 'team_display_name', 'team_short_display_name', 'team_color', + 'team_alternate_color', 'is_active', 'is_all_star', + 'team_alternate_ids_sdr', 'logo_href', 'logo_dark_href', 'game_id' + Example: + `nfl_df = sportsdataverse.nfl.espn_nfl_game_rosters(game_id=401220403)` + """ + # play by play + pbp_txt = {} + # summary endpoint for pickcenter array + summary_url = "https://sports.core.api.espn.com/v2/sports/football/leagues/nfl/events/{x}/competitions/{x}/competitors".format(x=game_id) + summary_resp = download(summary_url, **kwargs) + summary = summary_resp.json() + items = helper_nfl_game_items(summary) + team_rosters = helper_nfl_roster_items(items = items, summary_url = summary_url, **kwargs) + team_rosters = team_rosters.join(items[['team_id', 'order', 'home_away', 'winner']], how = 'left', on = 'team_id') + teams_df = helper_nfl_team_items(items = items, **kwargs) + teams_rosters = team_rosters.join(teams_df, how = 'left',on = 'team_id') + athletes = helper_nfl_athlete_items(teams_rosters = team_rosters, **kwargs) + rosters = athletes.join(teams_rosters, how = 'left', left_on = 'athlete_id', right_on = 'player_id') + rosters = rosters.with_columns( + game_id = pl.lit(game_id).cast(pl.Int64) + ) + rosters.columns = [underscore(c) for c in rosters.columns] + return rosters.to_pandas() if return_as_pandas else rosters + +def helper_nfl_game_items(summary): + items = pl.from_pandas(pd.json_normalize(summary, record_path = "items", sep = '_')) + items.columns = [col.replace("$ref", "href") for col in items.columns] + + items.columns = [underscore(c) for c in items.columns] + print(items.columns) + items = items.rename({ + "id": "team_id", + "uid": "team_uid", + "statistics_href": "team_statistics_href" + } + ) + items = items.with_columns( + team_id = pl.col("team_id").cast(pl.Int32) + ) + + return items + +def helper_nfl_team_items(items, **kwargs): + pop_cols = [ + '$ref', + 'record', + 'athletes', + 'venue', + 'groups', + 'ranks', + 'statistics', + 'leaders', + 'links', + 'notes', + 'againstTheSpreadRecords', + 'franchise', + 'events', + 'college', + 'awards', + 'coaches', + 'recruiting', + 'injuries', + 'transactions', + 'attendance' + ] + teams_df = pl.DataFrame() + for x in items['team_href']: + team = download(x, **kwargs).json() + for k in pop_cols: + team.pop(k, None) + team_row = pl.from_pandas(pd.json_normalize(team, sep = '_')) + teams_df = pl.concat([teams_df, team_row], how = 'vertical') + print(teams_df.columns) + teams_df.columns = [ + 'team_id', + 'team_guid', + 'team_uid', + 'team_slug', + 'team_location', + 'team_name', + 'team_nickname', + 'team_abbreviation', + 'team_display_name', + 'team_short_display_name', + 'team_color', + 'team_alternate_color', + 'is_active', + 'is_all_star', + 'logos', + 'team_alternate_ids_sdr' + ] + teams_df = teams_df.with_columns( + logo_href = pl.lit(""), + logo_dark_href = pl.lit("") + ) + + for row in range(len(teams_df['logos'])): + team = teams_df['logos'][row] + teams_df[row, 'logo_href'] = team[0]['href'] + teams_df[row, 'logo_dark_href'] = team[1]['href'] + + teams_df = teams_df.drop(['logos']) + teams_df = teams_df.with_columns( + team_id = pl.col("team_id").cast(pl.Int32) + ) + return teams_df + +def helper_nfl_roster_items(items, summary_url, **kwargs): + team_ids = list(items['team_id']) + game_rosters = pl.DataFrame() + for tm in team_ids: + team_roster_url = "{x}/{t}/roster".format(x = summary_url, t = tm) + team_roster_resp = download(team_roster_url, **kwargs) + team_roster = pl.from_pandas(pd.json_normalize(team_roster_resp.json().get('entries',[]), sep = '_')) + team_roster.columns = [col.replace("$ref", "href") for col in team_roster.columns] + team_roster.columns = [underscore(c) for c in team_roster.columns] + team_roster= team_roster.with_columns( + team_id = pl.lit(tm).cast(pl.Int32) + ) + game_rosters = pl.concat([game_rosters, team_roster], how = 'vertical') + game_rosters = game_rosters.drop(["period", "for_player_id", "active"]) + game_rosters = game_rosters.with_columns( + player_id = pl.col('player_id').cast(pl.Int64), + team_id = pl.col('team_id').cast(pl.Int32) + ) + return game_rosters + +def helper_nfl_athlete_items(teams_rosters, **kwargs): + athlete_hrefs = list(teams_rosters['athlete_href']) + game_athletes = pl.DataFrame() + pop_cols = [ + 'links', + 'injuries', + 'teams', + 'team', + 'college', + 'proAthlete', + 'statistics', + 'notes', + 'eventLog', + "$ref", + "position" + ] + for athlete_href in athlete_hrefs: + + athlete_res = download(athlete_href, **kwargs) + athlete_resp = athlete_res.json() + for k in pop_cols: + athlete_resp.pop(k, None) + athlete = pl.from_pandas(pd.json_normalize(athlete_resp, sep='_')) + athlete.columns = [col.replace("$ref", "href") for col in athlete.columns] + athlete.columns = [underscore(c) for c in athlete.columns] + + game_athletes = pl.concat([game_athletes, athlete], how = 'diagonal') + + + game_athletes = game_athletes.rename({ + "id": "athlete_id", + "uid": "athlete_uid", + "guid": "athlete_guid", + "type": "athlete_type", + "display_name": "athlete_display_name" + }) + game_athletes = game_athletes.with_columns( + athlete_id = pl.col("athlete_id").cast(pl.Int64) + ) + return game_athletes \ No newline at end of file From daf2c7cc8b4802b05bf1abcce9a60b2f202fe37d Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Fri, 14 Jul 2023 15:31:24 -0400 Subject: [PATCH 19/79] Update nhl_pbp.py --- sportsdataverse/nhl/nhl_pbp.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/sportsdataverse/nhl/nhl_pbp.py b/sportsdataverse/nhl/nhl_pbp.py index 71cc378..9e5a0b4 100755 --- a/sportsdataverse/nhl/nhl_pbp.py +++ b/sportsdataverse/nhl/nhl_pbp.py @@ -66,11 +66,10 @@ def helper_nhl_pbp(game_id, pbp_txt): if (pbp_txt['playByPlaySource'] != "none") & (len(pbp_txt['plays'])>1): pbp_txt = helper_nhl_pbp_features(game_id, pbp_txt, init) else: - pbp_txt['plays'] = pd.DataFrame() - pbp_txt['plays'] = pbp_txt['plays'].replace({np.nan: None}) + pbp_txt['plays'] = pl.DataFrame() return { "gameId": game_id, - "plays": pbp_txt['plays'].to_dict(orient='records'), + "plays": pbp_txt['plays'].to_dicts(), "boxscore": pbp_txt['boxscore'], "header": pbp_txt['header'], "format": pbp_txt['format'], From 3d3aa2365c8d16627fe203bae5f119440fd500e0 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Fri, 14 Jul 2023 15:31:46 -0400 Subject: [PATCH 20/79] update init files --- sportsdataverse/cfb/__init__.py | 2 +- sportsdataverse/nba/__init__.py | 2 +- sportsdataverse/nfl/__init__.py | 1 + sportsdataverse/wbb/__init__.py | 1 + sportsdataverse/wnba/__init__.py | 1 + 5 files changed, 5 insertions(+), 2 deletions(-) diff --git a/sportsdataverse/cfb/__init__.py b/sportsdataverse/cfb/__init__.py index f9fc594..9a0afcd 100755 --- a/sportsdataverse/cfb/__init__.py +++ b/sportsdataverse/cfb/__init__.py @@ -1,5 +1,5 @@ -from sportsdataverse.cfb.cfb_loaders import * from sportsdataverse.cfb.cfb_game_rosters import * +from sportsdataverse.cfb.cfb_loaders import * from sportsdataverse.cfb.cfb_pbp import * from sportsdataverse.cfb.cfb_schedule import * from sportsdataverse.cfb.cfb_teams import * \ No newline at end of file diff --git a/sportsdataverse/nba/__init__.py b/sportsdataverse/nba/__init__.py index 31080c1..9405686 100755 --- a/sportsdataverse/nba/__init__.py +++ b/sportsdataverse/nba/__init__.py @@ -1,5 +1,5 @@ -from sportsdataverse.nba.nba_loaders import * from sportsdataverse.nba.nba_game_rosters import * +from sportsdataverse.nba.nba_loaders import * from sportsdataverse.nba.nba_pbp import * from sportsdataverse.nba.nba_schedule import * from sportsdataverse.nba.nba_teams import * \ No newline at end of file diff --git a/sportsdataverse/nfl/__init__.py b/sportsdataverse/nfl/__init__.py index 38bc63e..f5d2edc 100755 --- a/sportsdataverse/nfl/__init__.py +++ b/sportsdataverse/nfl/__init__.py @@ -1,3 +1,4 @@ +from sportsdataverse.nfl.nfl_game_rosters import * from sportsdataverse.nfl.nfl_loaders import * from sportsdataverse.nfl.nfl_pbp import * from sportsdataverse.nfl.nfl_schedule import * diff --git a/sportsdataverse/wbb/__init__.py b/sportsdataverse/wbb/__init__.py index 57ef470..74f80bc 100755 --- a/sportsdataverse/wbb/__init__.py +++ b/sportsdataverse/wbb/__init__.py @@ -1,3 +1,4 @@ +from sportsdataverse.wbb.wbb_game_rosters import * from sportsdataverse.wbb.wbb_loaders import * from sportsdataverse.wbb.wbb_pbp import * from sportsdataverse.wbb.wbb_schedule import * diff --git a/sportsdataverse/wnba/__init__.py b/sportsdataverse/wnba/__init__.py index 41f71ab..0c90c4d 100755 --- a/sportsdataverse/wnba/__init__.py +++ b/sportsdataverse/wnba/__init__.py @@ -1,3 +1,4 @@ +from sportsdataverse.wnba.wnba_game_rosters import * from sportsdataverse.wnba.wnba_loaders import * from sportsdataverse.wnba.wnba_pbp import * from sportsdataverse.wnba.wnba_schedule import * From 85cad44039670f82b82bc9e48421e4c862915190 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Fri, 14 Jul 2023 15:32:07 -0400 Subject: [PATCH 21/79] error in game_seconds_remaining --- sportsdataverse/nba/nba_pbp.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/sportsdataverse/nba/nba_pbp.py b/sportsdataverse/nba/nba_pbp.py index b05647f..aebce36 100755 --- a/sportsdataverse/nba/nba_pbp.py +++ b/sportsdataverse/nba/nba_pbp.py @@ -1,4 +1,3 @@ -from typing import Dict import pandas as pd import polars as pl import numpy as np @@ -251,11 +250,11 @@ def helper_nba_pbp_features(game_id, pbp_txt, init): .otherwise(60 * pl.col("clock.minutes") + pl.col("clock.seconds")) .alias("start.half_seconds_remaining"), pl.when(pl.col('qtr') == 1) - .then(2880 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) - .when(pl.col('qtr') == 2) .then(2160 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) - .when(pl.col('qtr') == 3) + .when(pl.col('qtr') == 2) .then(1440 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) + .when(pl.col('qtr') == 3) + .then(720 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) .otherwise(60 * pl.col("clock.minutes") + pl.col("clock.seconds")) .alias("start.game_seconds_remaining"), ).with_columns( From a9bdba29f8f888fcf70169b289c497d1bdf9710c Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Fri, 14 Jul 2023 15:32:43 -0400 Subject: [PATCH 22/79] Function `wnba.wnba_game_rosters()` added. --- sportsdataverse/wnba/wnba_game_rosters.py | 196 ++++++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 sportsdataverse/wnba/wnba_game_rosters.py diff --git a/sportsdataverse/wnba/wnba_game_rosters.py b/sportsdataverse/wnba/wnba_game_rosters.py new file mode 100644 index 0000000..d58f8a4 --- /dev/null +++ b/sportsdataverse/wnba/wnba_game_rosters.py @@ -0,0 +1,196 @@ +import pandas as pd +import polars as pl +import numpy as np +import os +import json +import re +from typing import List, Callable, Iterator, Union, Optional, Dict +from sportsdataverse.dl_utils import download, underscore + +def espn_wnba_game_rosters(game_id: int, raw = False, return_as_pandas = True, **kwargs) -> pd.DataFrame: + """espn_wnba_game_rosters() - Pull the game by id. + + Args: + game_id (int): Unique game_id, can be obtained from espn_wnba_schedule(). + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + + Returns: + pd.DataFrame: Data frame of game roster data with columns: + 'athlete_id', 'athlete_uid', 'athlete_guid', 'athlete_type', + 'first_name', 'last_name', 'full_name', 'athlete_display_name', + 'short_name', 'weight', 'display_weight', 'height', 'display_height', + 'age', 'date_of_birth', 'slug', 'jersey', 'linked', 'active', + 'alternate_ids_sdr', 'birth_place_city', 'birth_place_state', + 'birth_place_country', 'headshot_href', 'headshot_alt', + 'experience_years', 'experience_display_value', + 'experience_abbreviation', 'status_id', 'status_name', 'status_type', + 'status_abbreviation', 'hand_type', 'hand_abbreviation', + 'hand_display_value', 'draft_display_text', 'draft_round', 'draft_year', + 'draft_selection', 'player_id', 'starter', 'valid', 'did_not_play', + 'display_name', 'ejected', 'athlete_href', 'position_href', + 'statistics_href', 'team_id', 'team_guid', 'team_uid', 'team_slug', + 'team_location', 'team_name', 'team_abbreviation', + 'team_display_name', 'team_short_display_name', 'team_color', + 'team_alternate_color', 'is_active', 'is_all_star', + 'logo_href', 'logo_dark_href', 'game_id' + Example: + `wnba_df = sportsdataverse.wnba.espn_wnba_game_rosters(game_id=401370395)` + """ + # play by play + pbp_txt = {} + # summary endpoint for pickcenter array + summary_url = "https://sports.core.api.espn.com/v2/sports/basketball/leagues/wnba/events/{x}/competitions/{x}/competitors".format(x=game_id) + summary_resp = download(summary_url, **kwargs) + summary = summary_resp.json() + items = helper_wnba_game_items(summary) + team_rosters = helper_wnba_roster_items(items = items, summary_url = summary_url, **kwargs) + team_rosters = team_rosters.join(items[['team_id', 'order', 'home_away', 'winner']], how = 'left', on = 'team_id') + teams_df = helper_wnba_team_items(items = items, **kwargs) + teams_rosters = team_rosters.join(teams_df, how = 'left',on = 'team_id') + athletes = helper_wnba_athlete_items(teams_rosters = team_rosters, **kwargs) + rosters = athletes.join(teams_rosters, how = 'left', left_on = 'athlete_id', right_on = 'player_id') + rosters = rosters.with_columns( + game_id = pl.lit(game_id).cast(pl.Int64) + ) + rosters.columns = [underscore(c) for c in rosters.columns] + return rosters.to_pandas() if return_as_pandas else rosters + +def helper_wnba_game_items(summary): + items = pl.from_pandas(pd.json_normalize(summary, record_path = "items", sep = '_')) + items.columns = [col.replace("$ref", "href") for col in items.columns] + + items.columns = [underscore(c) for c in items.columns] + items = items.rename({ + "id": "team_id", + "uid": "team_uid", + "statistics_href": "team_statistics_href" + } + ) + items = items.with_columns( + team_id = pl.col("team_id").cast(pl.Int32) + ) + + return items + +def helper_wnba_team_items(items, **kwargs): + pop_cols = [ + '$ref', + 'record', + 'athletes', + 'venue', + 'groups', + 'ranks', + 'statistics', + 'leaders', + 'links', + 'notes', + 'againstTheSpreadRecords', + 'franchise', + 'events', + 'college', + 'depthCharts', + 'transactions', + 'awards', + 'injuries', + 'coaches' + ] + teams_df = pl.DataFrame() + for x in items['team_href']: + team = download(x, **kwargs).json() + for k in pop_cols: + team.pop(k, None) + team_row = pl.from_pandas(pd.json_normalize(team, sep = '_')) + teams_df = pl.concat([teams_df, team_row], how = 'vertical') + + teams_df.columns = [ + 'team_id', + 'team_guid', + 'team_uid', + 'team_slug', + 'team_location', + 'team_name', + 'team_abbreviation', + 'team_display_name', + 'team_short_display_name', + 'team_color', + 'team_alternate_color', + 'is_active', + 'is_all_star', + 'logos', + 'team_alternate_ids_sdr' + ] + teams_df = teams_df.with_columns( + logo_href = pl.lit(""), + logo_dark_href = pl.lit("") + ) + for row in range(len(teams_df['logos'])): + team = teams_df['logos'][row] + teams_df[row, 'logo_href'] = team[0]['href'] + teams_df[row, 'logo_dark_href'] = team[1]['href'] + + teams_df = teams_df.drop(['logos']) + teams_df = teams_df.with_columns( + team_id = pl.col("team_id").cast(pl.Int32) + ) + return teams_df + +def helper_wnba_roster_items(items, summary_url, **kwargs): + team_ids = list(items['team_id']) + game_rosters = pl.DataFrame() + for tm in team_ids: + team_roster_url = "{x}/{t}/roster".format(x = summary_url, t = tm) + team_roster_resp = download(team_roster_url, **kwargs) + team_roster = pl.from_pandas(pd.json_normalize(team_roster_resp.json().get('entries',[]), sep = '_')) + team_roster.columns = [col.replace("$ref", "href") for col in team_roster.columns] + team_roster.columns = [underscore(c) for c in team_roster.columns] + team_roster= team_roster.with_columns( + team_id = pl.lit(tm).cast(pl.Int32) + ) + game_rosters = pl.concat([game_rosters, team_roster], how = 'vertical') + game_rosters = game_rosters.drop(["period", "for_player_id", "active"]) + game_rosters = game_rosters.with_columns( + player_id = pl.col('player_id').cast(pl.Int64), + team_id = pl.col('team_id').cast(pl.Int32) + ) + return game_rosters + +def helper_wnba_athlete_items(teams_rosters, **kwargs): + athlete_hrefs = list(teams_rosters['athlete_href']) + game_athletes = pl.DataFrame() + pop_cols = [ + 'links', + 'injuries', + 'teams', + 'team', + 'college', + 'proAthlete', + 'statistics', + 'notes', + 'eventLog', + "$ref", + "position" + ] + for athlete_href in athlete_hrefs: + + athlete_res = download(athlete_href, **kwargs) + athlete_resp = athlete_res.json() + for k in pop_cols: + athlete_resp.pop(k, None) + athlete = pl.from_pandas(pd.json_normalize(athlete_resp, sep='_')) + athlete.columns = [col.replace("$ref", "href") for col in athlete.columns] + athlete.columns = [underscore(c) for c in athlete.columns] + + game_athletes = pl.concat([game_athletes, athlete], how = 'diagonal') + + + game_athletes = game_athletes.rename({ + "id": "athlete_id", + "uid": "athlete_uid", + "guid": "athlete_guid", + "type": "athlete_type", + "display_name": "athlete_display_name" + }) + game_athletes = game_athletes.with_columns( + athlete_id = pl.col("athlete_id").cast(pl.Int64) + ) + return game_athletes \ No newline at end of file From 7dac003872639bf1f9d288fec9177d551a884274 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Fri, 14 Jul 2023 15:34:43 -0400 Subject: [PATCH 23/79] Update wnba package to polars --- sportsdataverse/wnba/wnba_loaders.py | 56 ++-- sportsdataverse/wnba/wnba_pbp.py | 454 +++++++++++++++----------- sportsdataverse/wnba/wnba_schedule.py | 203 +++++++----- sportsdataverse/wnba/wnba_teams.py | 22 +- 4 files changed, 428 insertions(+), 307 deletions(-) diff --git a/sportsdataverse/wnba/wnba_loaders.py b/sportsdataverse/wnba/wnba_loaders.py index 8765efe..1959bd6 100755 --- a/sportsdataverse/wnba/wnba_loaders.py +++ b/sportsdataverse/wnba/wnba_loaders.py @@ -1,4 +1,5 @@ import pandas as pd +import polars as pl import json from tqdm import tqdm from typing import List, Callable, Iterator, Union, Optional @@ -6,7 +7,7 @@ from sportsdataverse.errors import SeasonNotFoundError from sportsdataverse.dl_utils import download -def load_wnba_pbp(seasons: List[int]) -> pd.DataFrame: +def load_wnba_pbp(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: """Load WNBA play by play data going back to 2002 Example: @@ -14,6 +15,7 @@ def load_wnba_pbp(seasons: List[int]) -> pd.DataFrame: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing the @@ -22,19 +24,17 @@ def load_wnba_pbp(seasons: List[int]) -> pd.DataFrame: Raises: ValueError: If `season` is less than 2002. """ - data = pd.DataFrame() + data = pl.DataFrame() if type(seasons) is int: seasons = [seasons] for i in tqdm(seasons): if int(i) < 2002: raise SeasonNotFoundError("season cannot be less than 2002") - i_data = pd.read_parquet(WNBA_BASE_URL.format(season=i), engine='auto', columns=None) - data = pd.concat([data, i_data], axis = 0, ignore_index = True) - #Give each row a unique index - data.reset_index(drop=True, inplace=True) - return data + i_data = pd.read_parquet(WNBA_BASE_URL.format(season=i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how = 'vertical') + return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data -def load_wnba_team_boxscore(seasons: List[int]) -> pd.DataFrame: +def load_wnba_team_boxscore(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: """Load WNBA team boxscore data Example: @@ -42,6 +42,7 @@ def load_wnba_team_boxscore(seasons: List[int]) -> pd.DataFrame: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing the @@ -50,20 +51,17 @@ def load_wnba_team_boxscore(seasons: List[int]) -> pd.DataFrame: Raises: ValueError: If `season` is less than 2002. """ - data = pd.DataFrame() + data = pl.DataFrame() if type(seasons) is int: seasons = [seasons] for i in tqdm(seasons): if int(i) < 2002: raise ValueError("season cannot be less than 2002") - i_data = pd.read_parquet(WNBA_TEAM_BOX_URL.format(season = i), engine='auto', columns=None) - data = pd.concat([data, i_data], axis = 0, ignore_index = True) - #Give each row a unique index - data.reset_index(drop=True, inplace=True) + i_data = pd.read_parquet(WNBA_TEAM_BOX_URL.format(season = i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how = 'vertical') + return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data - return data - -def load_wnba_player_boxscore(seasons: List[int]) -> pd.DataFrame: +def load_wnba_player_boxscore(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: """Load WNBA player boxscore data Example: @@ -71,6 +69,7 @@ def load_wnba_player_boxscore(seasons: List[int]) -> pd.DataFrame: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing the @@ -79,20 +78,17 @@ def load_wnba_player_boxscore(seasons: List[int]) -> pd.DataFrame: Raises: ValueError: If `season` is less than 2002. """ - data = pd.DataFrame() + data = pl.DataFrame() if type(seasons) is int: seasons = [seasons] for i in tqdm(seasons): if int(i) < 2002: raise ValueError("season cannot be less than 2002") - i_data = pd.read_parquet(WNBA_PLAYER_BOX_URL.format(season = i), engine='auto', columns=None) - data = pd.concat([data, i_data], axis = 0, ignore_index = True) - #Give each row a unique index - data.reset_index(drop=True, inplace=True) - - return data + i_data = pd.read_parquet(WNBA_PLAYER_BOX_URL.format(season = i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how = 'vertical') + return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data -def load_wnba_schedule(seasons: List[int]) -> pd.DataFrame: +def load_wnba_schedule(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: """Load WNBA schedule data Example: @@ -100,6 +96,7 @@ def load_wnba_schedule(seasons: List[int]) -> pd.DataFrame: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing the @@ -108,15 +105,12 @@ def load_wnba_schedule(seasons: List[int]) -> pd.DataFrame: Raises: ValueError: If `season` is less than 2002. """ - data = pd.DataFrame() + data = pl.DataFrame() if type(seasons) is int: seasons = [seasons] for i in tqdm(seasons): if int(i) < 2002: raise ValueError("season cannot be less than 2002") - i_data = pd.read_parquet(WNBA_TEAM_SCHEDULE_URL.format(season = i), engine='auto', columns=None) - data = pd.concat([data, i_data], axis = 0, ignore_index = True) - #Give each row a unique index - data.reset_index(drop=True, inplace=True) - - return data + i_data = pd.read_parquet(WNBA_TEAM_SCHEDULE_URL.format(season = i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how = 'vertical') + return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data diff --git a/sportsdataverse/wnba/wnba_pbp.py b/sportsdataverse/wnba/wnba_pbp.py index 7d58819..7b23c61 100755 --- a/sportsdataverse/wnba/wnba_pbp.py +++ b/sportsdataverse/wnba/wnba_pbp.py @@ -1,5 +1,5 @@ -from typing import Dict import pandas as pd +import polars as pl import numpy as np import os import json @@ -7,7 +7,7 @@ from typing import List, Callable, Iterator, Union, Optional, Dict from sportsdataverse.dl_utils import download, flatten_json_iterative, key_check -def espn_wnba_pbp(game_id: int, raw = False) -> Dict: +def espn_wnba_pbp(game_id: int, raw = False, **kwargs) -> Dict: """espn_wnba_pbp() - Pull the game by id. Data from API endpoints - `wnba/playbyplay`, `wnba/summary` Args: @@ -22,12 +22,12 @@ def espn_wnba_pbp(game_id: int, raw = False) -> Dict: `wnba_df = sportsdataverse.wnba.espn_wnba_pbp(game_id=401370395)` """ # play by play - pbp_txt = {} - pbp_txt['timeouts'] = {} + # play by play + pbp_txt = {'timeouts': {}} # summary endpoint for pickcenter array - summary_url = "http://site.api.espn.com/apis/site/v2/sports/basketball/wnba/summary?event={}".format(game_id) - summary_resp = download(summary_url) - summary = json.loads(summary_resp) + summary_url = f"http://site.api.espn.com/apis/site/v2/sports/basketball/wnba/summary?event={game_id}" + summary_resp = download(summary_url, **kwargs) + summary = summary_resp.json() incoming_keys_expected = [ 'boxscore', 'format', 'gameInfo', 'leaders', 'seasonseries', @@ -46,52 +46,80 @@ def espn_wnba_pbp(game_id: int, raw = False) -> Dict: 'leaders' ] if raw == True: - # reorder keys in raw format + # reorder keys in raw format, appending empty keys which are defined later to the end pbp_json = {} for k in incoming_keys_expected: if k in summary.keys(): pbp_json[k] = summary[k] else: - if k in dict_keys_expected: - pbp_json[k] = {} - else: - pbp_json[k] = [] + pbp_json[k] = {} if k in dict_keys_expected else [] return pbp_json for k in incoming_keys_expected: if k in summary.keys(): pbp_txt[k] = summary[k] else: - if k in dict_keys_expected: - pbp_txt[k] = {} - else: - pbp_txt[k] = [] + pbp_txt[k] = {} if k in dict_keys_expected else [] for k in ['news','shop']: - pbp_txt.pop('{}'.format(k), None) + pbp_txt.pop(f'{k}', None) - pbp_json = helper_wnba_pbp(game_id, pbp_txt) - return pbp_json + return helper_wnba_pbp(game_id, pbp_txt) def wnba_pbp_disk(game_id, path_to_json): - with open(os.path.join(path_to_json, "{}.json".format(game_id))) as json_file: + with open(os.path.join(path_to_json, f"{game_id}.json")) as json_file: pbp_txt = json.load(json_file) return pbp_txt def helper_wnba_pbp(game_id, pbp_txt): - gameSpread, overUnder, homeFavorite, gameSpreadAvailable = helper_wnba_pickcenter(pbp_txt) + init = helper_wnba_pickcenter(pbp_txt) + pbp_txt, init = helper_wnba_game_data(pbp_txt, init) + if "plays" in pbp_txt.keys() and pbp_txt.get('header').get('competitions')[0].get('playByPlaySource') != 'none': + pbp_txt = helper_wnba_pbp_features(game_id, pbp_txt, init) + else: + pbp_txt['plays'] = pl.DataFrame() + pbp_txt['timeouts'] = { + homeTeamId: {"1": [], "2": []}, + awayTeamId: {"1": [], "2": []}, + } + return { + "gameId": game_id, + "plays": pbp_txt['plays'].to_dicts(), + "winprobability" : np.array(pbp_txt['winprobability']).tolist(), + "boxscore" : pbp_txt['boxscore'], + "header" : pbp_txt['header'], + "format": pbp_txt['format'], + "broadcasts" : np.array(pbp_txt['broadcasts']).tolist(), + "videos" : np.array(pbp_txt['videos']).tolist(), + "playByPlaySource": pbp_txt['playByPlaySource'], + "standings" : pbp_txt['standings'], + "article" : pbp_txt['article'], + "leaders" : np.array(pbp_txt['leaders']).tolist(), + "seasonseries" : np.array(pbp_txt['seasonseries']).tolist(), + "timeouts" : pbp_txt['timeouts'], + "pickcenter" : np.array(pbp_txt['pickcenter']).tolist(), + "againstTheSpread" : np.array(pbp_txt['againstTheSpread']).tolist(), + "odds" : np.array(pbp_txt['odds']).tolist(), + "predictor" : pbp_txt['predictor'], + "espnWP" : np.array(pbp_txt['espnWP']).tolist(), + "gameInfo" : pbp_txt['gameInfo'], + "teamInfo" : np.array(pbp_txt['teamInfo']).tolist(), + "season" : np.array(pbp_txt['season']).tolist() + } + +def helper_wnba_game_data(pbp_txt, init): pbp_txt['timeouts'] = {} pbp_txt['teamInfo'] = pbp_txt['header']['competitions'][0] pbp_txt['season'] = pbp_txt['header']['season'] pbp_txt['playByPlaySource'] = pbp_txt['header']['competitions'][0]['playByPlaySource'] pbp_txt['boxscoreSource'] = pbp_txt['header']['competitions'][0]['boxscoreSource'] - pbp_txt['gameSpreadAvailable'] = gameSpreadAvailable - pbp_txt['gameSpread'] = gameSpread - pbp_txt["homeFavorite"] = homeFavorite + pbp_txt['gameSpreadAvailable'] = init["gameSpreadAvailable"] + pbp_txt['gameSpread'] = init["gameSpread"] + pbp_txt["homeFavorite"] = init["homeFavorite"] pbp_txt["homeTeamSpread"] = np.where( - homeFavorite == True, abs(gameSpread), -1 * abs(gameSpread) + init["homeFavorite"] == True, abs(init["gameSpread"]), -1 * abs(init["gameSpread"]) ) - pbp_txt["overUnder"] = overUnder + pbp_txt["overUnder"] = init["overUnder"] # Home and Away identification variables if pbp_txt['header']['competitions'][0]['competitors'][0]['homeAway']=='home': pbp_txt['header']['competitions'][0]['home'] = pbp_txt['header']['competitions'][0]['competitors'][0]['team'] @@ -99,193 +127,237 @@ def helper_wnba_pbp(game_id, pbp_txt): homeTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['name']) homeTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['location']) homeTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['abbreviation']) - homeTeamNameAlt = re.sub("Stat(.+)", "St", str(homeTeamName)) + homeTeamNameAlt = re.sub("Stat(.+)", "St", homeTeamName) pbp_txt['header']['competitions'][0]['away'] = pbp_txt['header']['competitions'][0]['competitors'][1]['team'] awayTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['id']) awayTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['name']) awayTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['location']) awayTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['abbreviation']) - awayTeamNameAlt = re.sub("Stat(.+)", "St", str(awayTeamName)) + awayTeamNameAlt = re.sub("Stat(.+)", "St", awayTeamName) else: pbp_txt['header']['competitions'][0]['away'] = pbp_txt['header']['competitions'][0]['competitors'][0]['team'] awayTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['id']) awayTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['name']) awayTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['location']) awayTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['abbreviation']) - awayTeamNameAlt = re.sub("Stat(.+)", "St", str(awayTeamName)) + awayTeamNameAlt = re.sub("Stat(.+)", "St", awayTeamName) pbp_txt['header']['competitions'][0]['home'] = pbp_txt['header']['competitions'][0]['competitors'][1]['team'] homeTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['id']) homeTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['name']) homeTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['location']) homeTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['abbreviation']) - homeTeamNameAlt = re.sub("Stat(.+)", "St", str(homeTeamName)) + homeTeamNameAlt = re.sub("Stat(.+)", "St", homeTeamName) + init["homeTeamId"] = homeTeamId + init["homeTeamMascot"] = homeTeamMascot + init["homeTeamName"] = homeTeamName + init["homeTeamAbbrev"] = homeTeamAbbrev + init["homeTeamNameAlt"] = homeTeamNameAlt + init["awayTeamId"] = awayTeamId + init["awayTeamMascot"] = awayTeamMascot + init["awayTeamName"] = awayTeamName + init["awayTeamAbbrev"] = awayTeamAbbrev + init["awayTeamNameAlt"] = awayTeamNameAlt + return pbp_txt, init - if (pbp_txt['playByPlaySource'] != "none") & (len(pbp_txt['plays'])>1): - helper_wnba_pbp_features(game_id, pbp_txt, gameSpread, homeFavorite, gameSpreadAvailable, homeTeamId, awayTeamId, homeTeamMascot, awayTeamMascot, homeTeamName, awayTeamName, homeTeamAbbrev, awayTeamAbbrev, homeTeamNameAlt, awayTeamNameAlt) - else: - pbp_txt['plays'] = pd.DataFrame() - pbp_txt['plays'] = pbp_txt['plays'].replace({np.nan: None}) - pbp_json = { - "gameId": game_id, - "plays" : pbp_txt['plays'].to_dict(orient='records'), - "winprobability" : np.array(pbp_txt['winprobability']).tolist(), - "boxscore" : pbp_txt['boxscore'], - "header" : pbp_txt['header'], - "broadcasts" : np.array(pbp_txt['broadcasts']).tolist(), - "videos" : np.array(pbp_txt['videos']).tolist(), - "playByPlaySource": pbp_txt['playByPlaySource'], - "standings" : pbp_txt['standings'], - "leaders" : np.array(pbp_txt['leaders']).tolist(), - "seasonseries" : np.array(pbp_txt['seasonseries']).tolist(), - "timeouts" : pbp_txt['timeouts'], - "pickcenter" : np.array(pbp_txt['pickcenter']).tolist(), - "againstTheSpread" : np.array(pbp_txt['againstTheSpread']).tolist(), - "odds" : np.array(pbp_txt['odds']).tolist(), - "predictor" : pbp_txt['predictor'], - "espnWP" : np.array(pbp_txt['espnWP']).tolist(), - "gameInfo" : np.array(pbp_txt['gameInfo']).tolist(), - "season" : np.array(pbp_txt['season']).tolist() - } - return pbp_json - -def helper_wnba_pbp_features(game_id, pbp_txt, gameSpread, homeFavorite, gameSpreadAvailable, homeTeamId, awayTeamId, homeTeamMascot, awayTeamMascot, homeTeamName, awayTeamName, homeTeamAbbrev, awayTeamAbbrev, homeTeamNameAlt, awayTeamNameAlt): +def helper_wnba_pbp_features(game_id, pbp_txt, init): pbp_txt['plays_mod'] = [] for play in pbp_txt['plays']: p = flatten_json_iterative(play) pbp_txt['plays_mod'].append(p) - pbp_txt['plays'] = pd.json_normalize(pbp_txt,'plays_mod') - pbp_txt['plays']['season'] = pbp_txt['season']['year'] - pbp_txt['plays']['seasonType'] = pbp_txt['season']['type'] - pbp_txt['plays']["awayTeamId"] = awayTeamId - pbp_txt['plays']["awayTeamName"] = str(awayTeamName) - pbp_txt['plays']["awayTeamMascot"] = str(awayTeamMascot) - pbp_txt['plays']["awayTeamAbbrev"] = str(awayTeamAbbrev) - pbp_txt['plays']["awayTeamNameAlt"] = str(awayTeamNameAlt) - pbp_txt['plays']["homeTeamId"] = homeTeamId - pbp_txt['plays']["homeTeamName"] = str(homeTeamName) - pbp_txt['plays']["homeTeamMascot"] = str(homeTeamMascot) - pbp_txt['plays']["homeTeamAbbrev"] = str(homeTeamAbbrev) - pbp_txt['plays']["homeTeamNameAlt"] = str(homeTeamNameAlt) - # Spread definition - pbp_txt['plays']["homeTeamSpread"] = 2.5 - pbp_txt['plays']["gameSpread"] = abs(gameSpread) - pbp_txt['plays']["homeTeamSpread"] = np.where(homeFavorite == True, abs(gameSpread), -1*abs(gameSpread)) - pbp_txt['homeTeamSpread'] = np.where(homeFavorite == True, abs(gameSpread), -1*abs(gameSpread)) - pbp_txt['plays']["homeFavorite"] = homeFavorite - pbp_txt['plays']["gameSpread"] = gameSpread - pbp_txt['plays']["gameSpreadAvailable"] = gameSpreadAvailable - pbp_txt['plays'] = pbp_txt['plays'].to_dict(orient='records') - pbp_txt['plays'] = pd.DataFrame(pbp_txt['plays']) - pbp_txt['plays']['season'] = pbp_txt['header']['season']['year'] - pbp_txt['plays']['seasonType'] = pbp_txt['header']['season']['type'] - pbp_txt['plays']['game_id'] = int(game_id) - pbp_txt['plays']["homeTeamId"] = homeTeamId - pbp_txt['plays']["awayTeamId"] = awayTeamId - pbp_txt['plays']["homeTeamName"] = str(homeTeamName) - pbp_txt['plays']["awayTeamName"] = str(awayTeamName) - pbp_txt['plays']["homeTeamMascot"] = str(homeTeamMascot) - pbp_txt['plays']["awayTeamMascot"] = str(awayTeamMascot) - pbp_txt['plays']["homeTeamAbbrev"] = str(homeTeamAbbrev) - pbp_txt['plays']["awayTeamAbbrev"] = str(awayTeamAbbrev) - pbp_txt['plays']["homeTeamNameAlt"] = str(homeTeamNameAlt) - pbp_txt['plays']["awayTeamNameAlt"] = str(awayTeamNameAlt) - pbp_txt['plays']['period.number'] = pbp_txt['plays']['period.number'].apply(lambda x: int(x)) - pbp_txt['plays']['qtr'] = pbp_txt['plays']['period.number'].apply(lambda x: int(x)) - - - pbp_txt['plays']["homeTeamSpread"] = 2.5 - pbp_txt['plays']["gameSpread"] = abs(gameSpread) - pbp_txt['plays']["gameSpreadAvailable"] = gameSpreadAvailable - pbp_txt['plays']["homeTeamSpread"] = np.where(homeFavorite == True, abs(gameSpread), -1*abs(gameSpread)) - pbp_txt['homeTeamSpread'] = np.where(homeFavorite == True, abs(gameSpread), -1*abs(gameSpread)) - pbp_txt['plays']["homeFavorite"] = homeFavorite - pbp_txt['plays']["gameSpread"] = gameSpread - pbp_txt['plays']["homeFavorite"] = homeFavorite - - #----- Time --------------- + pbp_txt['plays'] = pl.from_pandas(pd.json_normalize(pbp_txt,'plays_mod')) + pbp_txt['plays'] = pbp_txt['plays'].with_columns( + game_id = pl.lit(game_id).cast(pl.Int32), + id = (pl.col('id').cast(pl.Int64)), + season = pl.lit(pbp_txt['header']['season']['year']).cast(pl.Int32), + seasonType = pl.lit(pbp_txt['header']['season']['type']), + homeTeamId = pl.lit(init["homeTeamId"]).cast(pl.Int32), + homeTeamName = pl.lit(init["homeTeamName"]), + homeTeamMascot = pl.lit(init["homeTeamMascot"]), + homeTeamAbbrev = pl.lit(init["homeTeamAbbrev"]), + homeTeamNameAlt = pl.lit(init["homeTeamNameAlt"]), + awayTeamId = pl.lit(init["awayTeamId"]).cast(pl.Int32), + awayTeamName = pl.lit(init["awayTeamName"]), + awayTeamMascot = pl.lit(init["awayTeamMascot"]), + awayTeamAbbrev = pl.lit(init["awayTeamAbbrev"]), + awayTeamNameAlt = pl.lit(init["awayTeamNameAlt"]), + gameSpread = pl.lit(init["gameSpread"]).abs(), + homeFavorite = pl.lit(init["homeFavorite"]), + gameSpreadAvailable = pl.lit(init["gameSpreadAvailable"]), + ).with_columns( + homeTeamSpread = pl.when(pl.col('homeFavorite') == True) + .then(pl.col('gameSpread')) + .otherwise(-1*pl.col('gameSpread')), + ).with_columns( + pl.col("period.number").cast(pl.Int32).alias("period.number"), + pl.col("period.number").cast(pl.Int32).alias("qtr"), + pl.when(pl.col("clock.displayValue").str.contains(r":") == False) + .then('0:'+ pl.col("clock.displayValue")) + .otherwise(pl.col("clock.displayValue")) + .alias("clock.displayValue"), + ).with_columns( + pl.col("clock.displayValue").alias("time"), + pl.col("clock.displayValue").str.split(':').list.to_struct(n_field_strategy="first_non_null").alias("clock.mm") + ).with_columns( + pl.col("clock.mm").struct.rename_fields(["clock.minutes", "clock.seconds"]) + ).unnest( + "clock.mm" + ).with_columns( + pl.col("clock.minutes").cast(pl.Float32), + pl.col("clock.seconds").cast(pl.Float32), + pl.when((pl.col("type.text") == "ShortTimeOut") + .and_( + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamAbbrev"]).lower()) + .or_( + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamAbbrev"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamName"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamMascot"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamNameAlt"]).lower()) + ) + )) + .then(True) + .otherwise(False) + .alias("homeTimeoutCalled"), + pl.when((pl.col("type.text") == "ShortTimeOut") + .and_( + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamAbbrev"]).lower()) + .or_( + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamAbbrev"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamName"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamMascot"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamNameAlt"]).lower()) + ) + )) + .then(True) + .otherwise(False) + .alias("awayTimeoutCalled"), + ).with_columns( + half = pl.when(pl.col('qtr') <= 2) + .then(1) + .otherwise(2), + game_half = pl.when(pl.col('qtr') <= 2) + .then(1) + .otherwise(2), + ).with_columns( + lag_qtr = pl.col('qtr').shift(1), + lead_qtr = pl.col('qtr').shift(-1), + lag_half = pl.col('half').shift(1), + lead_half = pl.col('half').shift(-1), + ).with_columns( + (60 * pl.col("clock.minutes") + pl.col("clock.seconds")).alias("start.quarter_seconds_remaining"), + pl.when(pl.col('qtr').is_in([1,3])) + .then(600 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) + .otherwise(60 * pl.col("clock.minutes") + pl.col("clock.seconds")) + .alias("start.half_seconds_remaining"), + pl.when(pl.col('qtr') == 1) + .then(1800 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) + .when(pl.col('qtr') == 2) + .then(1200 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) + .when(pl.col('qtr') == 3) + .then(600 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) + .otherwise(60 * pl.col("clock.minutes") + pl.col("clock.seconds")) + .alias("start.game_seconds_remaining"), + ).with_columns( + pl.col("start.quarter_seconds_remaining").shift(-1).alias("end.quarter_seconds_remaining"), + pl.col("start.half_seconds_remaining").shift(-1).alias("end.half_seconds_remaining"), + pl.col("start.game_seconds_remaining").shift(-1).alias("end.game_seconds_remaining"), + ) - pbp_txt['plays']['clock.displayValue'] = np.select( - [ - pbp_txt['plays']['clock.displayValue'].str.contains(":") == False - ], - [ - "0:" + pbp_txt['plays']['clock.displayValue'].apply(lambda x: str(x)) - ], default = pbp_txt['plays']['clock.displayValue'] - ) - pbp_txt['plays']['time'] = pbp_txt['plays']['clock.displayValue'] - pbp_txt['plays']['clock.mm'] = pbp_txt['plays']['clock.displayValue'].str.split(pat=':') - pbp_txt['plays'][['clock.minutes','clock.seconds']] = pbp_txt['plays']['clock.mm'].to_list() - pbp_txt['plays']['clock.minutes'] = pbp_txt['plays']['clock.minutes'].apply(lambda x: int(x)) - pbp_txt['plays']['clock.seconds'] = pbp_txt['plays']['clock.seconds'].apply(lambda x: float(x)) - # pbp_txt['plays']['clock.mm'] = pbp_txt['plays']['clock.displayValue'].apply(lambda x: datetime.strptime(str(x),'%M:%S')) - pbp_txt['plays']['half'] = np.where(pbp_txt['plays']['qtr'] <= 2, "1","2") - pbp_txt['plays']['game_half'] = np.where(pbp_txt['plays']['qtr'] <= 2, "1","2") - pbp_txt['plays']['lag_qtr'] = pbp_txt['plays']['qtr'].shift(1) - pbp_txt['plays']['lead_qtr'] = pbp_txt['plays']['qtr'].shift(-1) - pbp_txt['plays']['lag_game_half'] = pbp_txt['plays']['game_half'].shift(1) - pbp_txt['plays']['lead_game_half'] = pbp_txt['plays']['game_half'].shift(-1) - pbp_txt['plays']['start.quarter_seconds_remaining'] = 60*pbp_txt['plays']['clock.minutes'].astype(int) + pbp_txt['plays']['clock.seconds'].astype(int) - pbp_txt['plays']['start.half_seconds_remaining'] = np.where( - pbp_txt['plays']['qtr'].isin([1,3]), - 600 + 60*pbp_txt['plays']['clock.minutes'].astype(int) + pbp_txt['plays']['clock.seconds'].astype(int), - 60*pbp_txt['plays']['clock.minutes'].astype(int) + pbp_txt['plays']['clock.seconds'].astype(int) - ) - pbp_txt['plays']['start.game_seconds_remaining'] = np.select( - [ - pbp_txt['plays']['qtr'] == 1, - pbp_txt['plays']['qtr'] == 2, - pbp_txt['plays']['qtr'] == 3, - pbp_txt['plays']['qtr'] == 4 - ], - [ - 1800 + 60*pbp_txt['plays']['clock.minutes'].astype(int) + pbp_txt['plays']['clock.seconds'].astype(int), - 1200 + 60*pbp_txt['plays']['clock.minutes'].astype(int) + pbp_txt['plays']['clock.seconds'].astype(int), - 600 + 60*pbp_txt['plays']['clock.minutes'].astype(int) + pbp_txt['plays']['clock.seconds'].astype(int), - 60*pbp_txt['plays']['clock.minutes'].astype(int) + pbp_txt['plays']['clock.seconds'].astype(int) - ], default = 60*pbp_txt['plays']['clock.minutes'].astype(int) + pbp_txt['plays']['clock.seconds'].astype(int) + pbp_txt["plays"] = pbp_txt["plays"].with_row_count("game_play_number", 1) + pbp_txt["timeouts"] = { + init["homeTeamId"]: {"1": [], "2": []}, + init["awayTeamId"]: {"1": [], "2": []}, + } + pbp_txt["timeouts"][init["homeTeamId"]]["1"] = ( + pbp_txt["plays"] + .filter( + (pl.col("homeTimeoutCalled") == True) + .and_(pl.col("period.number") <= 2) ) - # Pos Team - Start and End Id - pbp_txt['plays']['game_play_number'] = np.arange(len(pbp_txt['plays']))+1 - pbp_txt['plays']['text'] = pbp_txt['plays']['text'].astype(str) - pbp_txt['plays']['id'] = pbp_txt['plays']['id'].apply(lambda x: int(x)) - pbp_txt['plays']['end.quarter_seconds_remaining'] = pbp_txt['plays']['start.quarter_seconds_remaining'].shift(1) - pbp_txt['plays']['end.half_seconds_remaining'] = pbp_txt['plays']['start.half_seconds_remaining'].shift(1) - pbp_txt['plays']['end.game_seconds_remaining'] = pbp_txt['plays']['start.game_seconds_remaining'].shift(1) - pbp_txt['plays']['end.quarter_seconds_remaining'] = np.select( - [ - (pbp_txt['plays']['game_play_number'] == 1)| - ((pbp_txt['plays']['qtr'] == 2) & (pbp_txt['plays']['lag_qtr'] == 1))| - ((pbp_txt['plays']['qtr'] == 3) & (pbp_txt['plays']['lag_qtr'] == 2))| - ((pbp_txt['plays']['qtr'] == 4) & (pbp_txt['plays']['lag_qtr'] == 3)) - ], - [ - 600 - ], default = pbp_txt['plays']['end.quarter_seconds_remaining'] + .get_column("id") + .to_list() + ) + pbp_txt["timeouts"][init["homeTeamId"]]["2"] = ( + pbp_txt["plays"] + .filter( + (pl.col("homeTimeoutCalled") == True) + .and_(pl.col("period.number") > 2) ) - pbp_txt['plays']['end.half_seconds_remaining'] = np.select( - [ - (pbp_txt['plays']['game_play_number'] == 1)| - ((pbp_txt['plays']['game_half'] == "2") & (pbp_txt['plays']['lag_game_half'] == "1")) - ], - [ - 1200 - ], default = pbp_txt['plays']['end.half_seconds_remaining'] + .get_column("id") + .to_list() + ) + pbp_txt["timeouts"][init["awayTeamId"]]["1"] = ( + pbp_txt["plays"] + .filter( + (pl.col("awayTimeoutCalled") == True) + .and_(pl.col("period.number") <= 2) ) - pbp_txt['plays']['end.game_seconds_remaining'] = np.select( - [ - (pbp_txt['plays']['game_play_number'] == 1), - ((pbp_txt['plays']['game_half'] == "2") & (pbp_txt['plays']['lag_game_half'] == "1")) - ], - [ - 2400, - 1200 - ], default = pbp_txt['plays']['end.game_seconds_remaining'] + .get_column("id") + .to_list() + ) + pbp_txt["timeouts"][init["awayTeamId"]]["2"] = ( + pbp_txt["plays"] + .filter( + (pl.col("awayTimeoutCalled") == True) + .and_(pl.col("period.number") > 2) ) + .get_column("id") + .to_list() + ) + # Pos Team - Start and End Id + pbp_txt['plays'] = pbp_txt['plays'].with_columns( + pl.when((pl.col("game_play_number") == 1) + .or_((pl.col("lag_qtr") == 1) + .and_(pl.col("period.number") == 2)) + .or_((pl.col("lag_qtr") == 2) + .and_(pl.col("period.number") == 3)) + .or_((pl.col("lag_qtr") == 3) + .and_(pl.col("period.number") == 4))) + .then(600) + .when((pl.col("lag_qtr") == (pl.col("period.number") - 1)) + .and_(pl.col("period.number") >= 5)) + .then(300) + .otherwise(pl.col("end.quarter_seconds_remaining")) + .alias("end.quarter_seconds_remaining"), + pl.when((pl.col("game_play_number") == 1)) + .then(1200) + .when((pl.col("lag_half") == 1) + .and_(pl.col("half") == 2)) + .then(1200) + .when((pl.col("lag_qtr") == 1) + .and_(pl.col("period.number") == 2)) + .then(600) + .when((pl.col("lag_qtr") == 2) + .and_(pl.col("period.number") == 3)) + .then(1200) + .when((pl.col("lag_qtr") == 3) + .and_(pl.col("period.number") == 4)) + .then(600) + .when((pl.col("lag_qtr") == (pl.col("period.number") - 1)) + .and_(pl.col("period.number") >= 5)) + .then(300) + .otherwise(pl.col("end.half_seconds_remaining")) + .alias("end.half_seconds_remaining"), + pl.when((pl.col("game_play_number") == 1)) + .then(2400) + .when((pl.col("lag_qtr") == 1) + .and_(pl.col("period.number") == 2)) + .then(1800) + .when((pl.col("lag_qtr") == 2) + .and_(pl.col("period.number") == 3)) + .then(1200) + .when((pl.col("lag_qtr") == 3) + .and_(pl.col("period.number") == 4)) + .then(600) + .when((pl.col("lag_qtr") == (pl.col("period.number") - 1)) + .and_(pl.col("period.number") >= 5)) + .then(300) + .otherwise(pl.col("end.game_seconds_remaining")) + .alias("end.game_seconds_remaining"), + pl.col("qtr").cast(pl.Int32).alias("period"), + ) - pbp_txt['plays']['period'] = pbp_txt['plays']['qtr'] - - del pbp_txt['plays']['clock.mm'] + return pbp_txt def helper_wnba_pickcenter(pbp_txt): # Spread definition @@ -294,16 +366,20 @@ def helper_wnba_pickcenter(pbp_txt): if "spread" in pbp_txt.get("pickcenter",{})[1].keys(): gameSpread = pbp_txt.get("pickcenter",{})[1].get("spread","") overUnder = pbp_txt.get("pickcenter",{})[1].get("overUnder","") - gameSpreadAvailable = True else: gameSpread = pbp_txt.get("pickcenter",{})[0].get("spread","") overUnder = pbp_txt.get("pickcenter",{})[0].get("overUnder","") - gameSpreadAvailable = True - # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") + gameSpreadAvailable = True + # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") else: gameSpread = 2.5 overUnder = 160.5 homeFavorite = True gameSpreadAvailable = False - return gameSpread, overUnder, homeFavorite, gameSpreadAvailable + return { + "gameSpread": gameSpread, + "overUnder": overUnder, + "homeFavorite": homeFavorite, + "gameSpreadAvailable": gameSpreadAvailable, + } \ No newline at end of file diff --git a/sportsdataverse/wnba/wnba_schedule.py b/sportsdataverse/wnba/wnba_schedule.py index ca5d873..e89e6f7 100755 --- a/sportsdataverse/wnba/wnba_schedule.py +++ b/sportsdataverse/wnba/wnba_schedule.py @@ -6,7 +6,8 @@ from sportsdataverse.errors import SeasonNotFoundError from sportsdataverse.dl_utils import download, underscore -def espn_wnba_schedule(dates=None, season_type=None, limit = 500) -> pd.DataFrame: +def espn_wnba_schedule(dates=None, season_type=None, limit = 500, + return_as_pandas = True, **kwargs) -> pd.DataFrame: """espn_wnba_schedule - look up the WNBA schedule for a given season Args: @@ -17,60 +18,93 @@ def espn_wnba_schedule(dates=None, season_type=None, limit = 500) -> pd.DataFram Returns: pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. """ - if dates is None: - dates = '' - else: - dates = '&dates=' + str(dates) - if season_type is None: - season_type = '' - else: - season_type = '&seasontype=' + str(season_type) - - url = "http://site.api.espn.com/apis/site/v2/sports/basketball/wnba/scoreboard?limit={}{}{}".format(limit, dates, season_type) - resp = download(url=url) - - ev = pd.DataFrame() - if resp is not None: - events_txt = json.loads(resp) - events = events_txt.get('events') - for event in events: - event.get('competitions')[0].get('competitors')[0].get('team').pop('links',None) - event.get('competitions')[0].get('competitors')[1].get('team').pop('links',None) - if event.get('competitions')[0].get('competitors')[0].get('homeAway')=='home': - event['competitions'][0]['home'] = event.get('competitions')[0].get('competitors')[0].get('team') - event['competitions'][0]['home']['score'] = event.get('competitions')[0].get('competitors')[0].get('score') - event['competitions'][0]['home']['winner'] = event.get('competitions')[0].get('competitors')[0].get('winner') - event['competitions'][0]['away'] = event.get('competitions')[0].get('competitors')[1].get('team') - event['competitions'][0]['away']['score'] = event.get('competitions')[0].get('competitors')[1].get('score') - event['competitions'][0]['away']['winner'] = event.get('competitions')[0].get('competitors')[1].get('winner') - else: - event['competitions'][0]['away'] = event.get('competitions')[0].get('competitors')[0].get('team') - event['competitions'][0]['away']['score'] = event.get('competitions')[0].get('competitors')[0].get('score') - event['competitions'][0]['away']['winner'] = event.get('competitions')[0].get('competitors')[0].get('winner') - event['competitions'][0]['home'] = event.get('competitions')[0].get('competitors')[1].get('team') - event['competitions'][0]['home']['score'] = event.get('competitions')[0].get('competitors')[1].get('score') - event['competitions'][0]['home']['winner'] = event.get('competitions')[0].get('competitors')[1].get('winner') - - del_keys = ['broadcasts','geoBroadcasts', 'headlines', 'series', 'situation', 'tickets', 'odds'] - for k in del_keys: - event.get('competitions')[0].pop(k, None) - if len(event.get('competitions')[0]['notes'])>0: - event.get('competitions')[0]['notes_type'] = event.get('competitions')[0]['notes'][0].get("type") - event.get('competitions')[0]['notes_headline'] = event.get('competitions')[0]['notes'][0].get("headline").replace('"','') - else: - event.get('competitions')[0]['notes_type'] = '' - event.get('competitions')[0]['notes_headline'] = '' - event.get('competitions')[0].pop('notes', None) - x = pd.json_normalize(event.get('competitions')[0], sep='_') - x['game_id'] = x['id'].astype(int) - x['season'] = event.get('season').get('year') - x['season_type'] = event.get('season').get('type') - ev = pd.concat([ev,x],axis=0, ignore_index=True) - ev = pd.DataFrame(ev) - ev.columns = [underscore(c) for c in ev.columns.tolist()] - return ev - -def espn_wnba_calendar(season=None, ondays=None) -> pd.DataFrame: + url = "http://site.api.espn.com/apis/site/v2/sports/basketball/wnba/scoreboard" + params = { + 'dates': dates, + 'seasonType': season_type, + 'limit': limit + } + resp = download(url=url, params=params, **kwargs) + + ev = pl.DataFrame() + events_txt = resp.json() + events = events_txt.get('events') + if len(events) == 0: + return pd.DataFrame() if return_as_pandas else pl.DataFrame() + + for event in events: + event.get('competitions')[0].get('competitors')[0].get('team').pop('links',None) + event.get('competitions')[0].get('competitors')[1].get('team').pop('links',None) + if event.get('competitions')[0].get('competitors')[0].get('homeAway') == 'home': + event = __extract_home_away(event, 0, 'home') + event = __extract_home_away(event, 1, 'away') + else: + event = __extract_home_away(event, 0, 'away') + event = __extract_home_away(event, 1, 'home') + + event.get('competitions')[0]['notes_type'] = event.get('competitions')[0]['notes'][0].get("type") if len(event.get('competitions')[0]['notes']) > 0 else '' + event.get('competitions')[0]['notes_headline'] = event.get('competitions')[0]['notes'][0].get("headline").replace('"','') if len(event.get('competitions')[0]['notes']) > 0 else '' + event.get('competitions')[0]['broadcast_market'] = event.get('competitions')[0].get('broadcasts', [])[0].get('market', "") if len(event.get('competitions')[0].get('broadcasts')) > 0 else "" + event.get('competitions')[0]['broadcast_name'] = event.get('competitions')[0].get('broadcasts', [])[0].get('names', [])[0] if len(event.get('competitions')[0].get('broadcasts')) > 0 else "" + del_keys = ['geoBroadcasts', 'headlines', 'series', 'situation', 'tickets', + 'odds', 'broadcasts', 'notes', 'competitors'] + for k in del_keys: + event.get('competitions')[0].pop(k, None) + + x = pl.from_pandas(pd.json_normalize(event.get('competitions')[0], sep = '_')) + x = x.with_columns( + game_id = (pl.col('id').cast(pl.Int64)), + season = (event.get('season').get('year')), + season_type = (event.get('season').get('type')), + home_linescores = pl.when(pl.col('status_type_description') == 'Postponed') + .then(None) + .otherwise(pl.col('home_linescores')), + away_linescores = pl.when(pl.col('status_type_description') == 'Postponed') + .then(None) + .otherwise(pl.col('away_linescores')), + ) + x = x[[s.name for s in x if s.null_count() != x.height]] + ev = pl.concat([ev, x], how = 'diagonal') + + ev.columns = [underscore(c) for c in ev.columns] + + return ev.to_pandas() if return_as_pandas else ev + +def __extract_home_away(event, arg1, arg2): + event['competitions'][0][arg2] = ( + event.get('competitions')[0].get('competitors')[arg1].get('team') + ) + event['competitions'][0][arg2]['score'] = ( + event.get('competitions')[0].get('competitors')[arg1].get('score') + ) + event['competitions'][0][arg2]['winner'] = ( + event.get('competitions')[0].get('competitors')[arg1].get('winner') + ) + ## add winner back to main competitors if does not exist + event['competitions'][0]['competitors'][arg1]['winner'] = ( + event.get('competitions')[0].get('competitors')[arg1].get('winner', False) + ) + + event['competitions'][0][arg2]['linescores'] = ( + event.get('competitions')[0] + .get('competitors')[arg1] + .get('linescores', [{'value': 'N/A'}]) + ) + ## add linescores back to main competitors if does not exist + event['competitions'][0]['competitors'][arg1]['linescores'] = ( + event.get('competitions')[0] + .get('competitors')[arg1] + .get('linescores', []) + ) + event['competitions'][0][arg2]['records'] = ( + event.get('competitions')[0] + .get('competitors')[arg1] + .get('records', []) + ) + return event + +def espn_wnba_calendar(season=None, ondays=None, + return_as_pandas = True, **kwargs) -> pd.DataFrame: """espn_wnba_calendar - look up the WNBA calendar for a given season Args: @@ -84,39 +118,46 @@ def espn_wnba_calendar(season=None, ondays=None) -> pd.DataFrame: ValueError: If `season` is less than 2002. """ if ondays is not None: - url = "https://sports.core.api.espn.com/v2/sports/basketball/leagues/wnba/seasons/{}/types/2/calendar/ondays".format(season) - resp = download(url=url) - txt = json.loads(resp).get('eventDate').get('dates') - full_schedule = pd.DataFrame(txt,columns=['dates']) - full_schedule['dateURL'] = list(map(lambda x: x[:10].replace("-",""),full_schedule['dates'])) - full_schedule['url']="http://site.api.espn.com/apis/site/v2/sports/basketball/wnba/scoreboard?dates=" - full_schedule['url']= full_schedule['url'] + full_schedule['dateURL'] + full_schedule = __ondays_wnba_calendar(season, **kwargs) else: - url = "http://site.api.espn.com/apis/site/v2/sports/basketball/wnba/scoreboard?dates={}".format(season) - resp = download(url=url) - txt = json.loads(resp)['leagues'][0]['calendar'] + url = f"http://site.api.espn.com/apis/site/v2/sports/basketball/wnba/scoreboard?dates={season}" + resp = download(url=url, **kwargs) + txt = resp.json().get('leagues')[0].get('calendar') datenum = list(map(lambda x: x[:10].replace("-",""),txt)) date = list(map(lambda x: x[:10],txt)) year = list(map(lambda x: x[:4],txt)) month = list(map(lambda x: x[5:7],txt)) day = list(map(lambda x: x[8:10],txt)) - - data = {"season": season, - "datetime" : txt, - "date" : date, - "year": year, - "month": month, - "day": day, - "dateURL": datenum + data = { + "season": season, + "datetime" : txt, + "date" : date, + "year": year, + "month": month, + "day": day, + "dateURL": datenum } - full_schedule = pd.DataFrame(data) - full_schedule['url']="http://site.api.espn.com/apis/site/v2/sports/basketball/wnba/scoreboard?dates=" - full_schedule['url']= full_schedule['url'] + full_schedule['dateURL'] - return full_schedule + full_schedule = pl.DataFrame(data) + full_schedule = full_schedule.with_columns( + url="http://site.api.espn.com/apis/site/v2/sports/basketball/wnba/scoreboard?dates=" + + pl.col('dateURL') + ) + return full_schedule.to_pandas() if return_as_pandas else full_schedule + + +def __ondays_wnba_calendar(season, **kwargs): + url = f"https://sports.core.api.espn.com/v2/sports/basketball/leagues/wnba/seasons/{season}/types/2/calendar/ondays" + resp = download(url=url, **kwargs) + txt = resp.json().get('eventDate').get('dates') + result = pl.DataFrame(txt, schema=['dates']) + result = result.with_columns(dateURL = pl.col('dates').str.slice(0, 10)) + result = result.with_columns( + url="http://site.api.espn.com/apis/site/v2/sports/basketball/wnba/scoreboard?dates=" + + pl.col('dateURL') + ) + + return result def most_recent_wnba_season(): today = datetime.date(datetime.now()) - if today.month >= 5: - return today.year - else: - return today.year - 1 \ No newline at end of file + return today.year if today.month >= 5 else today.year - 1 \ No newline at end of file diff --git a/sportsdataverse/wnba/wnba_teams.py b/sportsdataverse/wnba/wnba_teams.py index 19bc5aa..3826299 100755 --- a/sportsdataverse/wnba/wnba_teams.py +++ b/sportsdataverse/wnba/wnba_teams.py @@ -1,19 +1,29 @@ import pandas as pd +import polars as pl import json from sportsdataverse.dl_utils import download, underscore from urllib.error import URLError, HTTPError, ContentTooShortError -def espn_wnba_teams() -> pd.DataFrame: +def espn_wnba_teams(return_as_pandas = True, **kwargs) -> pd.DataFrame: """espn_wnba_teams - look up WNBA teams + Args: + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Returns: pd.DataFrame: Pandas dataframe containing teams for the requested league. + + Example: + `wnba_df = sportsdataverse.wnba.espn_wnba_teams()` + """ - ev = pd.DataFrame() - url = "http://site.api.espn.com/apis/site/v2/sports/basketball/wnba/teams?limit=1000" - resp = download(url=url) + url = "http://site.api.espn.com/apis/site/v2/sports/basketball/wnba/teams" + params = { + "limit": 1000 + } + resp = download(url=url, params = params, **kwargs) if resp is not None: - events_txt = json.loads(resp) + events_txt = resp.json() teams = events_txt.get('sports')[0].get('leagues')[0].get('teams') del_keys = ['record', 'links'] @@ -22,5 +32,5 @@ def espn_wnba_teams() -> pd.DataFrame: team.get('team').pop(k, None) teams = pd.json_normalize(teams, sep='_') teams.columns = [underscore(c) for c in teams.columns.tolist()] - return teams + return teams if return_as_pandas else pl.from_pandas(teams) From 9042beb26fb64f5d3d13dda0b83b854dba971ee8 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Fri, 14 Jul 2023 15:36:47 -0400 Subject: [PATCH 24/79] Update test_pbp.py --- tests/cfb/test_pbp.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/cfb/test_pbp.py b/tests/cfb/test_pbp.py index 7cb15ce..51f78a3 100755 --- a/tests/cfb/test_pbp.py +++ b/tests/cfb/test_pbp.py @@ -9,10 +9,10 @@ def generated_data(): test.run_processing_pipeline() yield test -@pytest.fixture() -def box_score(generated_data): - box = generated_data.create_box_score() - yield box +# @pytest.fixture() +# def box_score(generated_data): +# box = generated_data.create_box_score() +# yield box def test_basic_pbp(generated_data): assert generated_data.json != None @@ -20,7 +20,7 @@ def test_basic_pbp(generated_data): generated_data.run_processing_pipeline() assert len(generated_data.plays_json) > 0 assert generated_data.ran_pipeline == True - assert isinstance(generated_data.plays_json, pd.DataFrame) + assert isinstance(generated_data.plays_json, pl.DataFrame) def test_adv_box_score(box_score): assert box_score != None From 44d88d8c7661632ea66c6e66d15b40e96b59dc9d Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Fri, 14 Jul 2023 15:36:54 -0400 Subject: [PATCH 25/79] Update retrosheet.py --- sportsdataverse/mlb/retrosheet.py | 63 ++++++++++++++++--------------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/sportsdataverse/mlb/retrosheet.py b/sportsdataverse/mlb/retrosheet.py index 465f6f4..f17ce91 100755 --- a/sportsdataverse/mlb/retrosheet.py +++ b/sportsdataverse/mlb/retrosheet.py @@ -11,6 +11,7 @@ parties may contact Retrosheet at "www.retrosheet.org". """ import pandas as pd +import polars as pl from sportsdataverse.dl_utils import download from io import StringIO from tqdm import tqdm @@ -18,11 +19,11 @@ def retrosheet_ballparks() -> pd.DataFrame(): """ - Retrives the current TEAMABR.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. + Retrives the current TEAMABR.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. Args: None - + Returns: A pandas Dataframe with the biographical information of notable major league teams. """ @@ -38,11 +39,11 @@ def retrosheet_ballparks() -> pd.DataFrame(): def retrosheet_ejections() -> pd.DataFrame(): """ - Retrives the current Ejecdata.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. + Retrives the current Ejecdata.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. Args: None - + Returns: A pandas Dataframe with the ejection data of known MLB ejections. """ @@ -57,11 +58,11 @@ def retrosheet_ejections() -> pd.DataFrame(): def retrosheet_franchises() -> pd.DataFrame(): """ - Retrives the current TEAMABR.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. + Retrives the current TEAMABR.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. Args: None - + Returns: A pandas Dataframe with the biographical information of notable major league teams. """ @@ -77,11 +78,11 @@ def retrosheet_franchises() -> pd.DataFrame(): def retrosheet_people() -> pd.DataFrame(): """ - Retrives the current BioFile.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. + Retrives the current BioFile.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. Args: None - + Returns: A pandas Dataframe with the biographical information of various individuals who have played baseball. """ @@ -110,13 +111,13 @@ def retrosheet_schedule(first_season:int,last_season=None,original_2020_schedule Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. last_season (int): - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. + Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. original_2020_schedule (bool): Retrosheet keeps a record of the orignial 2020 MLB season, before the season was delayed due to the COVID-19 pandemic. - + - If this is set to True, this function will return the original 2020 MLB season, before it was altered due to the COVID-19 pandemic, if the user wants this function to return the schedule for the 2020 MLB season. - + - If this is set to False, this function will return the altered 2020 MLB season, after it was altered due to the COVID-19 pandemic, if the user wants this function to return the schedule for the 2020 MLB season. Returns: @@ -134,7 +135,7 @@ def retrosheet_schedule(first_season:int,last_season=None,original_2020_schedule print(f'Getting all of the games for the {first_season} MLB season!') elif last_season == first_season: print(f'Getting all of the games for the {first_season} MLB season!') - elif first_season > last_season: + elif first_season > last_season: last_season = first_season print(f'CAUGHT EXCEPTION!\nlast_season is greater than first_season!') print(f'Getting all of the games for the {first_season} MLB season instead.') @@ -152,7 +153,7 @@ def retrosheet_schedule(first_season:int,last_season=None,original_2020_schedule schedule_url = f"https://raw.githubusercontent.com/chadwickbureau/retrosheet/master/schedule/2020REV.TXT" else: schedule_url = f"https://raw.githubusercontent.com/chadwickbureau/retrosheet/master/schedule/{i}SKED.TXT" - + schedule_columns = ['date','game_num','day_of_week', 'road_team','road_league','road_team_game_num', 'home_team','home_league','home_team_game_num','time_of_game', @@ -164,13 +165,13 @@ def retrosheet_schedule(first_season:int,last_season=None,original_2020_schedule print(f'An error has occurred when downloading Retrosheet schedule data.\nError:\n{e}') schedule_df = pd.concat([schedule_df,season_schedule_df],ignore_index=True) del season_schedule_df - + return schedule_df - + def retrosheet_game_logs_team(first_season:int,last_season=None,game_type="regular",filter_out_seasons=True) -> pd.DataFrame(): """ - Retrives the team-level stats for MLB games in a season, or range of seasons. - THIS DOES NOT GET PLAYER STATS! + Retrives the team-level stats for MLB games in a season, or range of seasons. + THIS DOES NOT GET PLAYER STATS! Use retrosplits_game_logs_player() for player-level game stats. Args: @@ -178,7 +179,7 @@ def retrosheet_game_logs_team(first_season:int,last_season=None,game_type="regul Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. last_season (int): - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. + Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. game_type (str): Optional parameter. By default, this is set to "regular", or to put it in another way, this function call will return only regular season games. @@ -186,7 +187,7 @@ def retrosheet_game_logs_team(first_season:int,last_season=None,game_type="regul The full list of supported keywards for game_type are as follows. Case does not matter (you can set game_type to "rEgUlAr", and the function call will still work): - "regular": Regular season games. - + - "asg": All-Star games. - "playoffs": Playoff games. @@ -196,7 +197,7 @@ def retrosheet_game_logs_team(first_season:int,last_season=None,game_type="regul Returns: A pandas dataframe containing team-level stats for MLB games. - + """ game_log_df = pd.DataFrame() season_game_log_df = pd.DataFrame() @@ -204,7 +205,7 @@ def retrosheet_game_logs_team(first_season:int,last_season=None,game_type="regul last_season = int(last_season) except: last_season = None - + columns_list = [ ## Game Info 'date','game_num','day_of_week', @@ -216,21 +217,21 @@ def retrosheet_game_logs_team(first_season:int,last_season=None,game_type="regul 'game_length','day_night_indicator','completion_info','forfeit_info','protest_info', 'park_id','attendance','time_of_game','away_line_score','home_line_score', ## Away Batting stats - 'away_AB','away_H','away_2B','away_3B','away_HR','away_RBI','away_SH','away_SF', + 'away_AB','away_H','away_2B','away_3B','away_HR','away_RBI','away_SH','away_SF', 'away_HBP','away_BB','away_IBB','away_K','away_SB','away_CS','away_GDP','away_CI', 'away_LOB', ## Away Pitching - 'away_pitchers_used','away_ER','away_team_ER','away_WP','away_BK', + 'away_pitchers_used','away_ER','away_team_ER','away_WP','away_BK', ## Away Fielding - 'away_PO','away_A','away_E','away_PB','away_DP','away_TP', + 'away_PO','away_A','away_E','away_PB','away_DP','away_TP', ## Home Batting stats - 'home_AB','home_H','home_2B','home_3B','home_HR','home_RBI','home_SH','home_SF', + 'home_AB','home_H','home_2B','home_3B','home_HR','home_RBI','home_SH','home_SF', 'home_HBP','home_BB','home_IBB','home_K','home_SB','home_CS','home_GDP','home_CI', 'home_LOB', ## Home Pitching - 'home_pitchers_used','home_ER','home_team_ER','home_WP','home_BK', + 'home_pitchers_used','home_ER','home_team_ER','home_WP','home_BK', ## Home Fielding - 'home_PO','home_A','home_E','home_PB','home_DP','home_TP', + 'home_PO','home_A','home_E','home_PB','home_DP','home_TP', ## Umpires 'home_plate_umpire_id','home_plate_umpire_name','1B_umpire_id','1B_umpire_name', '2B_umpire_id','2B_umpire_name','3B_umpire_id','3B_umpire_name', @@ -267,13 +268,13 @@ def retrosheet_game_logs_team(first_season:int,last_season=None,game_type="regul 'home_batter_09_id','home_batter_09_name','home_batter_09_position', 'additional_info','acquisition_info' ] - + if last_season == None: last_season = first_season print(f'Getting all of the games for the {first_season} MLB season!') elif last_season == first_season: print(f'Getting all of the games for the {first_season} MLB season!') - elif first_season > last_season: + elif first_season > last_season: last_season = first_season print(f'CAUGHT EXCEPTION!\nlast_season is greater than first_season!') print(f'Getting all of the games for the {first_season} MLB season instead.') @@ -297,7 +298,7 @@ def retrosheet_game_logs_team(first_season:int,last_season=None,game_type="regul game_log_df = pd.concat([game_log_df,season_game_log_df],ignore_index=True) del season_game_log_df - + elif game_type.lower() == "asg" or game_type.lower() == "all star" or game_type.lower() == "all-star": game_log_url = f"https://raw.githubusercontent.com/chadwickbureau/retrosheet/master/gamelog/GLAS.TXT" try: @@ -317,7 +318,7 @@ def retrosheet_game_logs_team(first_season:int,last_season=None,game_type="regul or game_type.lower() == "post" or game_type.lower() == "postseason" \ or game_type.lower() == "october" or game_type.lower() == "november" \ or game_type.lower() == "november baseball": - + wildcard_round_url = f"https://raw.githubusercontent.com/chadwickbureau/retrosheet/master/gamelog/GLWC.TXT" divisional_round_url = f"https://raw.githubusercontent.com/chadwickbureau/retrosheet/master/gamelog/GLDV.TXT" championship_round_url = f"https://raw.githubusercontent.com/chadwickbureau/retrosheet/master/gamelog/GLLC.TXT" From 5d0fe7ddb0e511115c25c8970a34051587298461 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Fri, 14 Jul 2023 23:45:22 -0400 Subject: [PATCH 26/79] update requests for mlb module and add to init --- sportsdataverse/__init__.py | 1 + sportsdataverse/mlb/mlbam_games.py | 4 ++-- sportsdataverse/mlb/mlbam_players.py | 4 ++-- sportsdataverse/mlb/mlbam_reports.py | 2 +- sportsdataverse/mlb/mlbam_stats.py | 6 +++--- sportsdataverse/mlb/mlbam_teams.py | 6 +++--- 6 files changed, 12 insertions(+), 11 deletions(-) diff --git a/sportsdataverse/__init__.py b/sportsdataverse/__init__.py index 54cff46..c49b3fe 100755 --- a/sportsdataverse/__init__.py +++ b/sportsdataverse/__init__.py @@ -1,5 +1,6 @@ from sportsdataverse.cfb import * from sportsdataverse.mbb import * +from sportsdataverse.mlb import * from sportsdataverse.nba import * from sportsdataverse.nfl import * from sportsdataverse.nhl import * diff --git a/sportsdataverse/mlb/mlbam_games.py b/sportsdataverse/mlb/mlbam_games.py index 91c5a4e..0664347 100755 --- a/sportsdataverse/mlb/mlbam_games.py +++ b/sportsdataverse/mlb/mlbam_games.py @@ -31,7 +31,7 @@ def mlbam_schedule(season:int,gameType="R"): 'L' - League Championship 'W' - World Series - Returns: + Returns: A pandas dataframe containing MLB scheduled games. """ main_df = pd.DataFrame() @@ -59,7 +59,7 @@ def mlbam_schedule(season:int,gameType="R"): resp = download(searchURL) - resp_str = str(resp, 'UTF-8') + resp_str = str(resp.json(), 'UTF-8') resp_json = json.loads(resp_str) try: diff --git a/sportsdataverse/mlb/mlbam_players.py b/sportsdataverse/mlb/mlbam_players.py index 2ff919d..6c15159 100755 --- a/sportsdataverse/mlb/mlbam_players.py +++ b/sportsdataverse/mlb/mlbam_players.py @@ -49,7 +49,7 @@ def mlbam_search_mlb_players(search:str,isActive=""): resp = download(searchURL) - resp_str = str(resp, 'UTF-8') + resp_str = str(resp.json(), 'UTF-8') resp_json = json.loads(resp_str) result_count = int(resp_json['search_player_all']['queryResults']['totalSize']) @@ -74,7 +74,7 @@ def mlbam_player_info(playerID:int): Args: playerID (int): Required parameter. If no playerID is provided, the function wil not work. - + Returns: A pandas dataframe cointaining player information for the specified MLBAM player ID. """ diff --git a/sportsdataverse/mlb/mlbam_reports.py b/sportsdataverse/mlb/mlbam_reports.py index 5c4652f..670301a 100755 --- a/sportsdataverse/mlb/mlbam_reports.py +++ b/sportsdataverse/mlb/mlbam_reports.py @@ -51,7 +51,7 @@ def mlbam_transactions(startDate:str,endDate:str): try: resp = download(searchURL) - resp_str = str(resp, 'UTF-8') + resp_str = str(resp.json(), 'UTF-8') resp_json = json.loads(resp_str) try: diff --git a/sportsdataverse/mlb/mlbam_stats.py b/sportsdataverse/mlb/mlbam_stats.py index 95da8f7..73932e3 100755 --- a/sportsdataverse/mlb/mlbam_stats.py +++ b/sportsdataverse/mlb/mlbam_stats.py @@ -76,7 +76,7 @@ def mlbam_player_season_hitting_stats(playerID:int,season:int,gameType="R"): resp = download(searchURL) - resp_str = str(resp, 'UTF-8') + resp_str = str(resp.json(), 'UTF-8') resp_json = json.loads(resp_str) try: @@ -120,7 +120,7 @@ def mlbam_player_season_pitching_stats(playerID:int,season:int,gameType="R"): Returns: A pandas dataframe containing pitching stats for an MLB player in a given season. - + """ main_df = pd.DataFrame() @@ -262,7 +262,7 @@ def mlbam_player_career_pitching_stats(playerID:int,gameType="R"): 'F' - First Round (Wild Card) 'L' - League Championship 'W' - World Series - + Returns: A pandas dataframe containing career pitching stats for an MLB player. """ diff --git a/sportsdataverse/mlb/mlbam_teams.py b/sportsdataverse/mlb/mlbam_teams.py index 12e12f1..c5d9a94 100755 --- a/sportsdataverse/mlb/mlbam_teams.py +++ b/sportsdataverse/mlb/mlbam_teams.py @@ -25,7 +25,7 @@ def mlbam_teams(season:int,retriveAllStarRosters=False): Returns: A pandas dataframe containing information about MLB teams that played in that season. - + """ main_df = pd.DataFrame() @@ -51,7 +51,7 @@ def mlbam_teams(season:int,retriveAllStarRosters=False): resp = download(searchURL) - resp_str = str(resp, 'UTF-8') + resp_str = str(resp.json(), 'UTF-8') resp_json = json.loads(resp_str) try: @@ -122,7 +122,7 @@ def mlbam_team_roster(teamID:int,startSeason:int,endSeason:int): endSeason (int): Required parameter. This value must be greater than startSeason for this function to work. - + Returns: A pandas dataframe containg the roster(s) for the MLB team. """ From b75333b8e955bd824e067ded680e1eb83e0ba19a Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Fri, 14 Jul 2023 23:45:50 -0400 Subject: [PATCH 27/79] Function `nhl.nhl_game_rosters()` added --- sportsdataverse/nhl/nhl_game_rosters.py | 198 ++++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 sportsdataverse/nhl/nhl_game_rosters.py diff --git a/sportsdataverse/nhl/nhl_game_rosters.py b/sportsdataverse/nhl/nhl_game_rosters.py new file mode 100644 index 0000000..ecdf434 --- /dev/null +++ b/sportsdataverse/nhl/nhl_game_rosters.py @@ -0,0 +1,198 @@ +import pandas as pd +import polars as pl +import numpy as np +import os +import json +import re +from typing import List, Callable, Iterator, Union, Optional, Dict +from sportsdataverse.dl_utils import download, underscore + +def espn_nhl_game_rosters(game_id: int, raw = False, return_as_pandas = True, **kwargs) -> pd.DataFrame: + """espn_nhl_game_rosters() - Pull the game by id. + + Args: + game_id (int): Unique game_id, can be obtained from espn_nhl_schedule(). + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + + Returns: + pd.DataFrame: Data frame of game roster data with columns: + 'athlete_id', 'athlete_uid', 'athlete_guid', 'athlete_type', + 'first_name', 'last_name', 'full_name', 'athlete_display_name', + 'short_name', 'weight', 'display_weight', 'height', 'display_height', + 'age', 'date_of_birth', 'slug', 'jersey', 'linked', 'active', + 'alternate_ids_sdr', 'birth_place_city', 'birth_place_state', + 'birth_place_country', 'headshot_href', 'headshot_alt', + 'experience_years', 'experience_display_value', + 'experience_abbreviation', 'status_id', 'status_name', 'status_type', + 'status_abbreviation', 'hand_type', 'hand_abbreviation', + 'hand_display_value', 'draft_display_text', 'draft_round', 'draft_year', + 'draft_selection', 'player_id', 'starter', 'valid', 'did_not_play', + 'display_name', 'ejected', 'athlete_href', 'position_href', + 'statistics_href', 'team_id', 'team_guid', 'team_uid', 'team_slug', + 'team_location', 'team_name', 'team_abbreviation', + 'team_display_name', 'team_short_display_name', 'team_color', + 'team_alternate_color', 'is_active', 'is_all_star', + 'logo_href', 'logo_dark_href', 'game_id' + Example: + `nhl_df = sportsdataverse.nhl.espn_nhl_game_rosters(game_id=401247153)` + """ + # play by play + pbp_txt = {} + # summary endpoint for pickcenter array + summary_url = "https://sports.core.api.espn.com/v2/sports/hockey/leagues/nhl/events/{x}/competitions/{x}/competitors".format(x=game_id) + summary_resp = download(summary_url, **kwargs) + summary = summary_resp.json() + items = helper_nhl_game_items(summary) + team_rosters = helper_nhl_roster_items(items = items, summary_url = summary_url, **kwargs) + team_rosters = team_rosters.join(items[['team_id', 'order', 'home_away', 'winner']], how = 'left', on = 'team_id') + teams_df = helper_nhl_team_items(items = items, **kwargs) + teams_rosters = team_rosters.join(teams_df, how = 'left',on = 'team_id') + athletes = helper_nhl_athlete_items(teams_rosters = team_rosters, **kwargs) + rosters = athletes.join(teams_rosters, how = 'left', left_on = 'athlete_id', right_on = 'player_id') + rosters = rosters.with_columns( + game_id = pl.lit(game_id).cast(pl.Int64) + ) + rosters.columns = [underscore(c) for c in rosters.columns] + return rosters.to_pandas() if return_as_pandas else rosters + +def helper_nhl_game_items(summary): + items = pl.from_pandas(pd.json_normalize(summary, record_path = "items", sep = '_')) + items.columns = [col.replace("$ref", "href") for col in items.columns] + + items.columns = [underscore(c) for c in items.columns] + items = items.rename({ + "id": "team_id", + "uid": "team_uid", + "statistics_href": "team_statistics_href" + } + ) + items = items.with_columns( + team_id = pl.col("team_id").cast(pl.Int32) + ) + + return items + +def helper_nhl_team_items(items, **kwargs): + pop_cols = [ + '$ref', + 'record', + 'athletes', + 'venue', + 'groups', + 'ranks', + 'statistics', + 'leaders', + 'links', + 'notes', + 'againstTheSpreadRecords', + 'franchise', + 'events', + 'college', + 'depthCharts', + 'transactions', + 'awards', + 'injuries', + 'coaches', + 'ranks', + 'attendance' + ] + teams_df = pl.DataFrame() + for x in items['team_href']: + team = download(x, **kwargs).json() + for k in pop_cols: + team.pop(k, None) + team_row = pl.from_pandas(pd.json_normalize(team, sep = '_')) + teams_df = pl.concat([teams_df, team_row], how = 'vertical') + + teams_df.columns = [ + 'team_id', + 'team_guid', + 'team_uid', + 'team_slug', + 'team_location', + 'team_name', + 'team_nickname', + 'team_abbreviation', + 'team_display_name', + 'team_short_display_name', + 'team_color', + 'team_alternate_color', + 'is_active', + 'is_all_star', + 'logos', + 'team_alternate_ids_sdr' + ] + teams_df = teams_df.with_columns( + logo_href = pl.lit(""), + logo_dark_href = pl.lit("") + ) + for row in range(len(teams_df['logos'])): + team = teams_df['logos'][row] + teams_df[row, 'logo_href'] = team[0]['href'] + teams_df[row, 'logo_dark_href'] = team[1]['href'] + + teams_df = teams_df.drop(['logos']) + teams_df = teams_df.with_columns( + team_id = pl.col("team_id").cast(pl.Int32) + ) + return teams_df + +def helper_nhl_roster_items(items, summary_url, **kwargs): + team_ids = list(items['team_id']) + game_rosters = pl.DataFrame() + for tm in team_ids: + team_roster_url = "{x}/{t}/roster".format(x = summary_url, t = tm) + team_roster_resp = download(team_roster_url, **kwargs) + team_roster = pl.from_pandas(pd.json_normalize(team_roster_resp.json().get('entries',[]), sep = '_')) + team_roster.columns = [col.replace("$ref", "href") for col in team_roster.columns] + team_roster.columns = [underscore(c) for c in team_roster.columns] + team_roster= team_roster.with_columns( + team_id = pl.lit(tm).cast(pl.Int32) + ) + game_rosters = pl.concat([game_rosters, team_roster], how = 'diagonal') + game_rosters = game_rosters.with_columns( + player_id = pl.col('player_id').cast(pl.Int64), + team_id = pl.col('team_id').cast(pl.Int32) + ) + return game_rosters + +def helper_nhl_athlete_items(teams_rosters, **kwargs): + athlete_hrefs = list(teams_rosters['athlete_href']) + game_athletes = pl.DataFrame() + pop_cols = [ + 'links', + 'injuries', + 'teams', + 'team', + 'college', + 'proAthlete', + 'statistics', + 'notes', + 'eventLog', + "$ref", + "position" + ] + for athlete_href in athlete_hrefs: + + athlete_res = download(athlete_href, **kwargs) + athlete_resp = athlete_res.json() + for k in pop_cols: + athlete_resp.pop(k, None) + athlete = pl.from_pandas(pd.json_normalize(athlete_resp, sep='_')) + athlete.columns = [col.replace("$ref", "href") for col in athlete.columns] + athlete.columns = [underscore(c) for c in athlete.columns] + + game_athletes = pl.concat([game_athletes, athlete], how = 'diagonal') + + + game_athletes = game_athletes.rename({ + "id": "athlete_id", + "uid": "athlete_uid", + "guid": "athlete_guid", + "type": "athlete_type", + "display_name": "athlete_display_name" + }) + game_athletes = game_athletes.with_columns( + athlete_id = pl.col("athlete_id").cast(pl.Int64) + ) + return game_athletes \ No newline at end of file From b9e1ef0c805c4fed2258194a38047b2b31626cb3 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Fri, 14 Jul 2023 23:46:14 -0400 Subject: [PATCH 28/79] Update test_pbp to account for polars conventions --- tests/cfb/test_pbp.py | 66 +++++++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/tests/cfb/test_pbp.py b/tests/cfb/test_pbp.py index 51f78a3..bc31915 100755 --- a/tests/cfb/test_pbp.py +++ b/tests/cfb/test_pbp.py @@ -1,5 +1,6 @@ from sportsdataverse.cfb.cfb_pbp import CFBPlayProcess import pandas as pd +import polars as pl import pytest @pytest.fixture() @@ -9,10 +10,10 @@ def generated_data(): test.run_processing_pipeline() yield test -# @pytest.fixture() -# def box_score(generated_data): -# box = generated_data.create_box_score() -# yield box +@pytest.fixture() +def box_score(generated_data): + box = generated_data.create_box_score(pl.DataFrame(generated_data.plays_json, infer_schema_length=400)) + yield box def test_basic_pbp(generated_data): assert generated_data.json != None @@ -20,7 +21,7 @@ def test_basic_pbp(generated_data): generated_data.run_processing_pipeline() assert len(generated_data.plays_json) > 0 assert generated_data.ran_pipeline == True - assert isinstance(generated_data.plays_json, pl.DataFrame) + assert isinstance(pl.DataFrame(generated_data.plays_json, infer_schema_length=400), pl.DataFrame) def test_adv_box_score(box_score): assert box_score != None @@ -44,7 +45,7 @@ def dupe_fsu_play_base(): test = CFBPlayProcess(gameId = 401411109) test.espn_cfb_pbp() test.run_processing_pipeline() - yield test.plays_json + yield pl.DataFrame(test.plays_json, infer_schema_length=400) def test_fsu_play_dedupe(dupe_fsu_play_base): target_strings = [ @@ -73,23 +74,23 @@ def test_fsu_play_dedupe(dupe_fsu_play_base): for item in target_strings: print(f"Checking known test cases for dupes for play_text '{item}'") - assert len(dupe_fsu_play_base[ - (dupe_fsu_play_base["text"] == item["text"]) - & (dupe_fsu_play_base["start.down"] == item["down"]) - & (dupe_fsu_play_base["start.distance"] == item["distance"]) - & (dupe_fsu_play_base["start.yardsToEndzone"] == item["yardsToEndzone"]) - ]) == 1 + assert len(dupe_fsu_play_base.filter( + (pl.col("text") == item["text"]) + & (pl.col("start.down") == item["down"]) + & (pl.col("start.distance") == item["distance"]) + & (pl.col("start.yardsToEndzone") == item["yardsToEndzone"]) + )) == 1 print(f"No dupes for play_text '{item}'") for item in regression_cases: print(f"Checking non-dupe base cases for dupes for play_text '{item}'") - assert len(dupe_fsu_play_base[ - (dupe_fsu_play_base["text"] == item["text"]) - & (dupe_fsu_play_base["start.down"] == item["down"]) - & (dupe_fsu_play_base["start.distance"] == item["distance"]) - & (dupe_fsu_play_base["start.yardsToEndzone"] == item["yardsToEndzone"]) - ]) == 1 + assert len(dupe_fsu_play_base.filter( + (pl.col("text") == item["text"]) + & (pl.col("start.down") == item["down"]) + & (pl.col("start.distance") == item["distance"]) + & (pl.col("start.yardsToEndzone") == item["yardsToEndzone"]) + )) == 1 print(f"confirmed no dupes for regression case of play_text '{item}'") @pytest.fixture() @@ -101,7 +102,7 @@ def iu_play_base(): @pytest.fixture() def dupe_iu_play_base(iu_play_base): - yield iu_play_base.plays_json + yield pl.DataFrame(iu_play_base.plays_json, infer_schema_length=400) def test_iu_play_dedupe(dupe_iu_play_base): target_strings = [ @@ -124,28 +125,27 @@ def test_iu_play_dedupe(dupe_iu_play_base): for item in target_strings: print(f"Checking known test cases for dupes for play_text '{item}'") - assert len(dupe_iu_play_base[ - (dupe_iu_play_base["text"] == item["text"]) - & (dupe_iu_play_base["start.down"] == item["down"]) - & (dupe_iu_play_base["start.distance"] == item["distance"]) - & (dupe_iu_play_base["start.yardsToEndzone"] == item["yardsToEndzone"]) - ]) == 1 + assert len(dupe_iu_play_base.filter( + (pl.col("text") == item["text"]) + & (pl.col("start.down") == item["down"]) + & (pl.col("start.distance") == item["distance"]) + & (pl.col("start.yardsToEndzone") == item["yardsToEndzone"]) + )) == 1 print(f"No dupes for play_text '{item}'") for item in elimination_strings: print(f"Checking for strings that should have been removed by dupe check for play_text '{item}'") - assert len(dupe_iu_play_base[ - (dupe_iu_play_base["text"] == item["text"]) - & (dupe_iu_play_base["start.down"] == item["down"]) - & (dupe_iu_play_base["start.distance"] == item["distance"]) - & (dupe_iu_play_base["start.yardsToEndzone"] == item["yardsToEndzone"]) - ]) == 0 + assert len(dupe_iu_play_base.filter( + (pl.col("text") == item["text"]) + & (pl.col("start.down") == item["down"]) + & (pl.col("start.distance") == item["distance"]) + & (pl.col("start.yardsToEndzone") == item["yardsToEndzone"]) + )) == 0 print(f"Confirmed no values for play_text '{item}'") @pytest.fixture() def iu_play_base_box(iu_play_base): - box = iu_play_base.create_box_score() - yield box + yield iu_play_base.create_box_score(pl.DataFrame(iu_play_base.plays_json, infer_schema_length=400)) def test_expected_turnovers(iu_play_base_box): defense_home = iu_play_base_box["defensive"][1] From eead682d97b8caaa85580463d9f84956e6ac311e Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Fri, 14 Jul 2023 23:55:12 -0400 Subject: [PATCH 29/79] minor nfl_pbp updates (WIP) --- sportsdataverse/nfl/nfl_pbp.py | 89 +++++++++++++++------------------- tests/nfl/test_espn_pbp.py | 3 +- 2 files changed, 42 insertions(+), 50 deletions(-) diff --git a/sportsdataverse/nfl/nfl_pbp.py b/sportsdataverse/nfl/nfl_pbp.py index 3ed5a46..3f9fc9b 100755 --- a/sportsdataverse/nfl/nfl_pbp.py +++ b/sportsdataverse/nfl/nfl_pbp.py @@ -1,4 +1,5 @@ import pandas as pd +import polars as pl import numpy as np import re import os @@ -66,7 +67,7 @@ def espn_nfl_pbp(self): "gameInfo", "season" Example: - `nfl_df = sportsdataverse.nfl.NFLPlayProcess(gameId=401256137).espn_nfl_pbp()` + `nfl_df = sportsdataverse.nfl.NFLPlayProcess(gameId=401220403).espn_nfl_pbp()` """ cache_buster = int(time.time() * 1000) pbp_txt = {} @@ -74,7 +75,7 @@ def espn_nfl_pbp(self): # summary endpoint for pickcenter array summary_url = f"http://site.api.espn.com/apis/site/v2/sports/football/nfl/summary?event={self.gameId}&{cache_buster}" summary_resp = download(summary_url) - summary = json.loads(summary_resp) + summary = summary_resp.json() incoming_keys_expected = [ 'boxscore', 'format', 'gameInfo', 'drives', 'leaders', 'broadcasts', 'predictor', 'pickcenter', 'againstTheSpread', 'odds', 'winprobability', @@ -95,21 +96,14 @@ def espn_nfl_pbp(self): if k in summary.keys(): pbp_json[k] = summary[k] else: - if k in dict_keys_expected: - pbp_json[k] = {} - else: - pbp_json[k] = [] + pbp_json[k] = {} if k in dict_keys_expected else [] return pbp_json for k in incoming_keys_expected: if k in summary.keys(): pbp_txt[k] = summary[k] else: - if k in dict_keys_expected: - pbp_txt[k] = {} - else: - pbp_txt[k] = [] - + pbp_txt[k] = {} if k in dict_keys_expected else [] for k in [ "scoringPlays", "standings", @@ -127,13 +121,13 @@ def espn_nfl_pbp(self): ]: pbp_txt[k] = key_check(obj=summary, key=k) for k in ['news','shop']: - pbp_txt.pop('{}'.format(k), None) + pbp_txt.pop(f'{k}', None) self.json = pbp_txt return self.json def nfl_pbp_disk(self): - with open(os.path.join(self.path_to_json, "{}.json".format(self.gameId))) as json_file: + with open(os.path.join(self.path_to_json, f"{self.gameId}.json")) as json_file: pbp_txt = json.load(json_file) self.json = pbp_txt return self.json @@ -170,35 +164,35 @@ def __helper_nfl_pbp_features(self, pbp_txt, pbp_txt["plays"] = pd.DataFrame() for key in pbp_txt.get("drives").keys(): prev_drives = pd.json_normalize( - data=pbp_txt.get("drives").get("{}".format(key)), - record_path="plays", - meta=[ - "id", - "displayResult", - "isScore", - ["team", "shortDisplayName"], - ["team", "displayName"], - ["team", "name"], - ["team", "abbreviation"], - "yards", - "offensivePlays", - "result", - "description", - "shortDisplayResult", - ["timeElapsed", "displayValue"], - ["start", "period", "number"], - ["start", "period", "type"], - ["start", "yardLine"], - ["start", "clock", "displayValue"], - ["start", "text"], - ["end", "period", "number"], - ["end", "period", "type"], - ["end", "yardLine"], - ["end", "clock", "displayValue"], - ], - meta_prefix="drive.", - errors="ignore", - ) + data=pbp_txt.get("drives").get(f"{key}"), + record_path="plays", + meta=[ + "id", + "displayResult", + "isScore", + ["team", "shortDisplayName"], + ["team", "displayName"], + ["team", "name"], + ["team", "abbreviation"], + "yards", + "offensivePlays", + "result", + "description", + "shortDisplayResult", + ["timeElapsed", "displayValue"], + ["start", "period", "number"], + ["start", "period", "type"], + ["start", "yardLine"], + ["start", "clock", "displayValue"], + ["start", "text"], + ["end", "period", "number"], + ["end", "period", "type"], + ["end", "yardLine"], + ["end", "clock", "displayValue"], + ], + meta_prefix="drive.", + errors="ignore", + ) pbp_txt["plays"] = pd.concat( [pbp_txt["plays"], prev_drives], ignore_index=True ) @@ -237,12 +231,10 @@ def __helper_nfl_pbp_features(self, pbp_txt, pbp_txt["overUnder"] = float(overUnder) pbp_txt["homeFavorite"] = homeFavorite - # ----- Figuring out Timeouts --------- - pbp_txt["timeouts"] = {} - pbp_txt["timeouts"][homeTeamId] = {"1": [], "2": []} - pbp_txt["timeouts"][awayTeamId] = {"1": [], "2": []} - - # ----- Time --------------- + pbp_txt["timeouts"] = { + homeTeamId: {"1": [], "2": []}, + awayTeamId: {"1": [], "2": []}, + } pbp_txt["plays"]["clock.mm"] = pbp_txt["plays"][ "clock.displayValue" ].str.split(pat=":") @@ -285,7 +277,6 @@ def __helper_nfl_pbp_features(self, pbp_txt, default=60 * pbp_txt["plays"]["clock.minutes"].astype(int) + pbp_txt["plays"]["clock.seconds"].astype(int), ) - # Pos Team - Start and End Id pbp_txt["plays"]["id"] = pbp_txt["plays"]["id"].apply(lambda x: int(x)) pbp_txt["plays"] = pbp_txt["plays"].sort_values( by=["id", "start.adj_TimeSecsRem"] diff --git a/tests/nfl/test_espn_pbp.py b/tests/nfl/test_espn_pbp.py index 432be58..1b036ce 100755 --- a/tests/nfl/test_espn_pbp.py +++ b/tests/nfl/test_espn_pbp.py @@ -1,5 +1,6 @@ from sportsdataverse.nfl.nfl_pbp import NFLPlayProcess import pandas as pd +import polars as pl import pytest @pytest.fixture() @@ -15,4 +16,4 @@ def test_basic_pbp(generated_data): generated_data.run_processing_pipeline() assert len(generated_data.plays_json) > 0 assert generated_data.ran_pipeline == True - assert isinstance(generated_data.plays_json, pd.DataFrame) \ No newline at end of file + assert isinstance(generated_data.plays_json, pl.DataFrame) \ No newline at end of file From 96d45b749cc296aea4e225b1a65753d1143ee14f Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Sat, 15 Jul 2023 00:35:26 -0400 Subject: [PATCH 30/79] Update CHANGELOG.md --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5427eca..96a1831 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,12 @@ ## 0.0.36-7 Release: July 9, 2023 -- Switched most under the hood dataframe operations to use the python `polars` library +- Switched most under the hood dataframe operations to use the python `polars` library and many functions now have a parameter `return_as_pandas` which defaults to `True` but can be set to `False` to return a polars dataframe instead of a pandas dataframe. This is set this way for backward compatibility but will be changed to default to `False` in a future release. - Added `**kwargs` which pass arguments to the `dl_utils.download()` function, including `headers`, `proxy`, `timeout` (default 30s), `num_retries` (default = 15), `logger` (default = None) - Function `espn_cfb_game_rosters()` added. - Function `espn_nba_game_rosters()` added. +- Function `espn_nfl_game_rosters()` added. +- Function `espn_nhl_game_rosters()` added. +- Function `espn_wbb_game_rosters()` added. +- Function `espn_wnba_game_rosters()` added. ## 0.0.34-35 Release: May 7-9, 2023 - Reconfigured some imports From 06e5b0ef4e8bc570f471399c8e1f3e02a44e4693 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Sat, 15 Jul 2023 00:40:15 -0400 Subject: [PATCH 31/79] update docs --- .../_build/doctrees/environment.pickle | Bin 875693 -> 937165 bytes .../doctrees/sportsdataverse.cfb.doctree | Bin 72328 -> 102043 bytes .../_build/doctrees/sportsdataverse.doctree | Bin 48914 -> 57651 bytes .../doctrees/sportsdataverse.mbb.doctree | Bin 76961 -> 85580 bytes .../doctrees/sportsdataverse.mlb.doctree | Bin 166988 -> 166977 bytes .../doctrees/sportsdataverse.nba.doctree | Bin 61003 -> 85789 bytes .../doctrees/sportsdataverse.nfl.doctree | Bin 170105 -> 205651 bytes .../doctrees/sportsdataverse.nhl.doctree | Bin 75405 -> 102345 bytes .../doctrees/sportsdataverse.wbb.doctree | Bin 61843 -> 86629 bytes .../doctrees/sportsdataverse.wnba.doctree | Bin 60085 -> 83926 bytes Sphinx-docs/_build/markdown/index.md | 21 ++++ Sphinx-docs/_build/markdown/modules.md | 21 ++++ .../_build/markdown/sportsdataverse.cfb.md | 84 +++++++++++-- .../_build/markdown/sportsdataverse.mbb.md | 41 +++++-- .../_build/markdown/sportsdataverse.md | 38 +++++- .../_build/markdown/sportsdataverse.mlb.md | 8 +- .../_build/markdown/sportsdataverse.nba.md | 80 +++++++++++-- .../_build/markdown/sportsdataverse.nfl.md | 112 +++++++++++++----- .../_build/markdown/sportsdataverse.nhl.md | 92 +++++++++++--- .../_build/markdown/sportsdataverse.wbb.md | 79 ++++++++++-- .../_build/markdown/sportsdataverse.wnba.md | 76 ++++++++++-- Sphinx-docs/sportsdataverse.cfb.rst | 8 ++ Sphinx-docs/sportsdataverse.nba.rst | 8 ++ Sphinx-docs/sportsdataverse.nfl.rst | 8 ++ Sphinx-docs/sportsdataverse.nhl.rst | 8 ++ Sphinx-docs/sportsdataverse.rst | 8 ++ Sphinx-docs/sportsdataverse.wbb.rst | 8 ++ Sphinx-docs/sportsdataverse.wnba.rst | 8 ++ docs/docs/cfb/index.md | 84 +++++++++++-- docs/docs/mbb/index.md | 41 +++++-- docs/docs/mlb/index.md | 8 +- docs/docs/nba/index.md | 80 +++++++++++-- docs/docs/nfl/index.md | 112 +++++++++++++----- docs/docs/nhl/index.md | 92 +++++++++++--- docs/docs/wbb/index.md | 79 ++++++++++-- docs/docs/wnba/index.md | 76 ++++++++++-- docs/src/pages/CHANGELOG.md | 10 +- sportsdataverse/decorators.py | 12 +- 38 files changed, 1093 insertions(+), 209 deletions(-) diff --git a/Sphinx-docs/_build/doctrees/environment.pickle b/Sphinx-docs/_build/doctrees/environment.pickle index ceda09225ac39f651a205fc47aaa2dd2b6c20733..29c904b11a964ce48cf63ecf42d1d1e15bdf1125 100755 GIT binary patch literal 937165 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 abOt@Rg%eUaXDzRx6fYy{`0-@2Rr^1O#EonK delta 109301 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 Vj&~sE{BkKyZd=l}bm^R;{|8v?WRL&= diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.cfb.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.cfb.doctree index ae386211c05dbacdd9718606cb47b1ca281134c0..cd389b60400f07dc2740b34d8ae280ab7bfac95d 100755 GIT binary patch literal 102043 zcmeHw3zQsJb)Y0`q?ysz7M6_1lG|VPh|%a{8?=LEY|Az_GK%mIfn{1X-8EBP>gjH} zs`Y_j%o1Zm%1MBR0Kox6Hf$D_1e5RyY!X7UA;1y{n7|q*!43pMHY_+ji_g zR<%20H~rq5d!w)Ey{cCZhC0@9uhs2T?Ow11a#Wpqqt>yTy}NqBTFma(8{RlCztXd- ze!bO{#hn3Xg|qUmUfCH62K>6;utT4M5wG3q_+HKOtz&k_v&XAblcly*J!(zcJuC*L zmfvQdKy=KR@9hz`0B%FDqHO_we$P2G7^=tyIcEebcsmXR%NzBk-8;~ow1Q#F_dE5; zt`F_U_MTC%dDym<)rRGHkO2fd4c5AC^)d;lcI+w4(+yVo)--cKmb%yaARWS z<d3CYo3FcF~u>NNbXQy>vuFOlM`r-HyN2Nhk7$x9i&ti>-CuMYah?{+j5 z+f0O1#!ghJ*CtB!rax8!6~FfetKr%3XhRLnise<>RA$i-jqsOhzv^QGX zeidkF{4!o`b?ot}4%9S`s_h1RqK=3tttOOkWWiK1nm7Z&C7I|WI&{X|vLgfgN1WHC zcg=FJ3dpp*4Fd$?6|6xtNz>E29ae$C(21D_u+ze!ZxD)jcITK)n(-F^$~_Fqsn;Pu zuLA>yhyhXRpPeWUg4!uxb?W|_iMoSGT}IkLL?BL8=dI4KqE30cBBAp(8jJwxvj}Mz zzkxZR-Wzoo{z9^|r>NLUv1C0(0ti>WI$XIdKmvkYs}d#UN}gX3PrmEq`gl6If~C!# z6_z-p%SVAOA0`eygNT8k+udf>?^+SdDp)!pV8fFR^|L0Z?KsrWaHt!DrKLz!9}ZXL zd`u`>FqF{7LL&`&d)sc-K$lidfH@Q*D%ZvvS!Nx@C&Kln8zNwU+EcaGj8$*;f@STK zV3|RJf3gjoa)5enkFxlno@~FzAO%A(#BkFfxHyZ(!5)5cg4K5b`S4Ak$YWuHhfJJ5 zN#mEDI&KSr{}5F8-+{3YOA4ZC&Gar8w-9mRlVI){_^i>+9Rds9K)ysQ-2o^>f_0`8 ztXA9ZSbnP$Vp*_suK)~BI@o6#!3IlPF~M>-VUZC5dni~@Z<6gD>eT}@^a!B*O$WTq z3>Z4;l+|tcm19<;YwJh^8@z5Crx_J*#_ISl@3zp=e?73+Er)R{!oaf=N6Q#9*GIF7 zw#Gl`&A!gqY$ut`S3{xhq_2(Z$qX_>7g;~Q`rqgWMwr!#v_^7wyq`4I)s&%0#bVJqGS1{8`028k|!265< zQ$&G*%1oL58Q%{&jN3hG&}OC{lW{MsNk;k^iq7LgLgheVETfa;eP)t`CbQu&#QA|5 zK%6JFp~DC}Jft{3N*FEN_Yz~%M>0MTxDO%TQ96Q{GiMBVJI0UI?c-JO$n=7B;;~{i ztB$6q`^|K4+!TZ2_6i#ey&aLUa0kpx@V_=Yfq@mu)@wcI!C(cAzSSP$7xUGiE$n_u zYYv;Oge2)W`3AtR0)|Wv+yh`AwkBP06ZLdsdquwC+IiAh5e!as8x69{oCk5tzT4T1 zV!3}QkoT21KGJZ<5kQQ!s44eLsTk;*dgf?Jl z@?@!A8yEdIZ!syE%|bLGR=z%+DXm>b_XLH9=|O1C%bFYhvTz_fO(RJEF=NxRgV z^ucblYo)19Yo;XUK3>(a(Op&^9UqR4&86~WtJQ!iZ<;Exxi8mjC5umdOX#m;^IpuP zO`JW<)oNJanNqoi+3wNa-I*p7-D>$x119~@eYcL4GBkY5ClTHR|mtdl%_c&i6vt15Ebu1`BY)+F+&6espgq(Bp<0U0P{K{HxYmC1U? zcQC|Ls2k_t_3A~C|>tIHzqXM{A?Mk)oQ{ix$ z=R*OcKsd45>Vm77@;bIvgU0z42L^JiP$mF4EN7p9IbhvxLj5Og&|8=%lqLv3Gpk5` zYjUz0rzea0tz_VCkc>q&<(Q^#5#0XMA@s2g+S46T8 zHWb2SsU08&QJTGCBe5Oq&9fUUVukrpBJ7UBzXSZhbZ9%1!dksnX#y=VBh{fv5zr0o z8qQWO&4TRhmd8dumH-k5!~*wLz3NHU__j5}q0^%fc6#B0PtQ!AhPTp7qg9n0paem3 z_EN7N)e@ovrCfq?T29s{5G|uDqyiM3g_1fe2ynU<` zN`dhwnuOz1G!5k$(}tm_GRj0QX_$!wX`G1!fk1fJ5%nn}P2^@~nRpXr^i*b^Mf0ds z#1srSnIWvJL?RBj;+Yw7;*A+{;*E`Y_(rux4LXrcv(><;OG-!mAStO8V8)#&Bkeit zN3ld0nlTNr2^n={=!v|cbU{Ee_7cj4Z3j`D>oFw>JOTAwBu`j2c&?-xr4cBK#|%M% z8hwZ%jZzyo2t{Qfqfk#mqKJwk#-Ye44MdRutw1ADypo|PUc$yArXddmu3_SEqfrzS z9U#n3&3F`bAq7JQq$p3yL~(;&!yT0pLLHahg?uB9lpu^st=2Hsl&i^&OD$KFrLxYX z#xv+N`AYpvMx|&q6^Iy|0=zUj!wKB*OexQGttcQ3P%S|kp(2$VqVTH10`5L$TwasQ zxMbctF^L}pY{A7u}q+%&`L6*oIXmuy#a+I^7O72}9-^gX`yn$V&cT1~TFv%SAFDbC0-f zqLW$TbU7V?QZpG!ac2`_L>@Us_9;0vm;52hvvM^%n|W`7WiaM~-=KmlP{0BQu_9%* z;sD|dumA(swMJqVd|iV38$gU;CB!vwi0irk1z&0XOXz)Nv&B~YVeHvCNsc~TR=1fi z@G7)~&FKdF9lHT899kMD1`l+adxe&-?M(>t#Fww7fi$jQdxbt(E<3Ll3Bj(k3d^YP zJ+Xi>v<{6a7w2ACMaGq4y!U0yEp-z0u0IC0ru%2h&KsRKY4Rafe!yxm_YY**;8zxF z1!WqMVG$*+8GDDM&76GsyFKIjg3kP#XkXsOul&`E3it){Ts>~S}0U}ox)Q_ zT#c(ce#)>=q!J4gco!0{I&s#VVd^6(#9^S)wRN00{qw?7g+vi@|2X6}WNy3ErCZwS zfPyh}_sc*>8Af-%2++}lV^Q-EhS9*e`*A48V(0Exppg3r{AiuU{W_#%T-}D@a`#cJ z2GPL*2x8^DUs0s?6OhcU!p9(+=w^Kgge_sis4FUv)#e;b-QNNX+~((;N?+H-@wvv*-(tn%Ne8C&DW_WNt~Ut?YGYV42&=(Zv`aEo0v|kB@iBvWvHr zA(5i7!SU>rY;0sq`Kl|)wrp(vVNnsvZDBQ$VR0oF05;C4QBY4;N?lLi7|3@Wz~3WQtR|I4{H4dq5jFd6SRh?*mUU1ywjmD#Gp1Wu z${U*dtO`kc6yf-{jj+FqSwPr3BFJxofPDJ=Jq7sf+5UNPgijLo1s~zR2cnod!Yd48Bm9@3+&ql%h>jTHnFgQ{ z{>zX&s}WvQ03*CqEIOC6jPS=neAozo0-tDvr>A)s;lmR*iU*OVD&+3WJ#ka;ijMA= zhQ<w>Hza_{(vldN=)DsWsMWKaJKjaF7#C&$lom+n|~lzgmT}7 z)kFsJbrC_ByRrKtMx;|T{KHTv>+ttM${QNHiw}QaFyd+){yxjFP-cyAR2u!3-!KA9 zAr1qjE6cRuj}fPTUL5|CL}bB-zxMz|QinfUk?7qjvB| z2-V<~#b(C5-pmKBi2SgFp5n+j@WEL^vp8D?0@KgcYRhJxaYW!8m3}lE*J{^|u@D8B zdU6c0=Y3j5<>-DeIshf(u_r1Tow#xaS9}Ocv#IRHpnpc$K_~|O5-G;XmWL?@eY-5X z*m9+Zo7ZR@5R{3e>SSJOPVhFs;))H}YzpIagE4|)Ud zHa0ND9(e^6%CYr7Fv2hzSsYTnt^e;Z!z-}$pP`{fQZX)yfiCQSI7YAr>t+)z3gVNz;GjF z!=AQFQ>~UiY2o&C7{g09T?Z$8O|?2Rv_-50JL*cf##NpZSg5SjRPH|_nj`K%aBP#q z0AXo`rivZwi*DKg;(xEm{A8$ivsDjeeSPfZVx?-I^|3h0ll@d4J;HZ>N4Bboio{4a zC=(QAm#rYmF2O&Uw*>Pwus_CDZP|}og8vO3VMOS&RV~!%T6tb8kh5T*nlo_eU2HJK+=~3d$W9QAYDTB8mV|MEUTdiRoQp7j0xG|3NzOQ5|UkqfHY} z*1+>SN!OLEV^+OkO*U`<>d?_B9UD&MfJ0hqCC|3JR?}lAq?C2ZRNHj922lx$taV1# z>0D&h<#PyHT_v*imCxJsO}lg0c3Ad5Kb0#|y@HYIL@rY45(u6lw@akTB!P^oc_a{d zp(M~;lCSO;12uT*SU-uaE{s|kd|9F}q@bXj?CB7*xc43caxnMad+>?ed-N1_?=jEA zY?t&;putLxnD%5luj6E_kNrL#u6!1LlRLuolzAVYDBTQu((Z&qdG1O%OZL=prF_EJ zN6gW#kIJ&aD;Cy87p8hFw6$9Q%4{+{QK|}ZCw*47BflR-(ZKz4tR~_|8Oi4pfOEXOPn(|Y+zHrDr_oMm(+Fd*Uc@Z8UVJ&|-I-V%VV z6Y@^MucdcJ@Z4m^GntSaK%5eEI;}1WmzV*BVg?xrK`;u^0rmj0-nsOy2p#sA(KOLP zAJ~k-9myJMeAs99HP#W-!m;^wd){GgeDaB>w?LsBZqJ$#hSBZGA?0&>cFgb!xIGyf zd05y#FSO^~RoKMK*hP(Q+Du*d0TQlsOPF^T;n_?i;R1R#KMy@N)w8LP zi+MI5hH`W3*+fiWqDmW$n0-K=&EJLOte(w@n-e{ocnkv`x=DZWn8ngGY^|Lx;dLw} zzg613bLVA3z0RfzIefUEM!6huKb2WLmFsDd?L(sZA8@=uN%nPf{+w_|_HlEDElpwl z@9kL4X}gT{AKSfi_pZ^|ctF3-X#Q8ZXwE17e}RRV2lUJEn85>jz1ahrv9%<1x^wg& z5EVAm?=w0_vw?^h3;&plI^1vi9U@7(N>jq?h6wqs45heJiqSlM%s~L$ZyEydMFOBZ z-9OFzrrg#qmGRtIJnq#4w-WSpI?mKC)#_7p*n%%zO{4=#b&otzaIi}Q?#=N-PAXR6 zLZ1GryS@52WX~KVeX|b#C2fLKbMPyx|kvfZzd9E5>rO) zJYtFf(!_N4Emj?F_nR2L1FsCZzSC)S=*aIQOcx%(u>5 z$?0CW=wnXIJM zT#`pcgk2B84W|0DMn%LV3<^rutqw6uROAyt4i**pBtB78gr4RnDsq*ykoj`xypfTe znIj_{z$~I8FO4C~jjYotLh=Z(HQoL+M@YUT%LaEZQiMcuks=f7c%$`E-Uw9{{~&b= zDXqVgZOR`lQS@-1z-ms(Xvx1C8<^ry{(n$Nck-v+OXm&_nIy_dc5ZV33DFY#5*EPfRE_CcC8?qeuJBkrS_MM*h_PXW7* zX!JFXdzR?Nt$CvxVyeNdcNyW>b@}Wg98WUZ{~#Cbxo6^3?c1sD$R6Lw(|jTB^HseP#G{N{^<3oA1rfWs%1Q7wG z31Z$@#;(hY#xh>dD0N>hO6h`yZtm9-rTPk1)>wvYPySfO+afK|teUK`jE5qXm_>k_ zCI~q>mVwQY!WoZcP-{orDCTBi(xnw+8E+x@W>mS6!`Nj8z+)L)FC&wcw3n$r{TLQzR%T9jiLTEU}D7fE+BA@l|}HSOz`KPb}kk(gm&VBF4Ln`B5$XPKAK; zF?MARW(Wwgh-TawNB1-gXM7(xn?8jwhcmt>%LcDrq;Q7fV`eF~H48;F zekPlhKcb;X;{H2Ub4o@up0Ucr^AxA=dMKnj;?pA<7n%ViJD@p$gop-y3Crm0BN}@m zHKwclxsPaE9wTIWL}R}h&tyV!0GT2hFEIlMIrTFVB6~z5PKWe}M#GGzi4OX(XB6&8 z)==Za5slT1e+w7Uc!Rm|$q|j$5ti;bVQ1*ly+Q}VldkjLW`tpkXmCjRA{xJGhF2h> z!O+OV!v1+NqLHN07c`>rr$$mON<`yRBwXoAGZN8AB;f)^G?oCmsSyo@Tr8sTZ5YAl zHll%;h=>NW56D~p9d2=e$NI5QhXrc{+{clVZ7!m~MeJl7VF+nKj!b!_B5;3%k~ZT0 z3;xOMRc5oOQ^5cvQvQ%5pC#~dTi(EjAaZaW5-nW@kH2xnj+7Wr5S zj~OB#FUT1A$k1dOR5T}ny9M!Kp^qE0g+BVjiHumKTy)|NhVzIx>55AQ^4t(fpP8W) zcMCA;r;jY5*hdJ!asn`SyF7NEzOj$%k*y={KKxU#7odOod5og{&Uh*mHrRQJkXIOU zr*|XN@N)-a{cX8e&z*R@GhyuGMZ|h@ALqT5NlBjGin4{h6=f5&tQPyYvq0>_hs_N_ z^%+hHD!#45XGHDfBC0;Bpqsr(MD6RHeo^+YN5+;5A)kkumE_&ai1gZAMA9V>iF6MU zDU;+edghTl1dt|qd4nB0cNYzIyq8hxpi34-%#Nic{8Lhip&&V8_FemS}cN z)?mjcB9)j$fZHMnIXT#Y&5^=3gjC@6idI>ao&j_(6GSg_;&;u8fs=xKg}9al^LG@cb# z!2?P2Zr#|OIocsX%p%-zI050)Fy1k=#$;HU;~fLCY;eya#XCZrj08N+it|T|ok^dh zexZ=ZIkIK>LmrAM?k23}lni-{8ylG772XDgvW7gaGy_QXP;&qYArJf#p8w1~(bJz}6UTuVA=5)1wi(Z4LUI6^LLQzOK*;N#kr3HK9&tLPhdf?qM$<$Geeg31 zcO+}5@!^oirHp?I7xMTGbK{dk9`7bBGlx7LHo`E5JUFC$A&);Y!z&Q-U})rFVgI}s z@<`I?3mWqH8zZR}CFJoa30Jz(jD$QANw|O^k8=Ut)R2clE*A1w0_Em5 zj@$FbI;5pJFTswjjQ*G8qCfW|Y{o(?*l__oW(aod&lv2;(qSY;9amN6ofBL8r3U`D6Y#{);^K=$lJx&#W ze4>-K&!JWoWb24~7yikthxC4cF6oyrias6gXr>)}cB;f|)DYBWtZ(LGJ$K{r&V=EP zqr`f1ALqT5NlBjGin4{hm5O7zJezn(6mzpM>C%evkUuB*W>mQ%W$ZEo z;PDWysgcP_TFoVSJY=;bqRT4D8V?auD~N|khpY}UOFU#~ors4F;}gY0=*bulQENb0iy~MF7>6rxxrhM3P5QLF zOTu}j?hMkh>s61q&jhMwHu>nb-i@*Z-PU^!G_mMyy%+Vntry793oh()Q!i%HZ|PMi z27a*!3DlYShF;9O?}lp+!s(a?JFP05OgP)?d9To`E0wDa3(h-)JxD16gE9ackG2^; z$532x!0%b;5;+7)K~aqPVobO4zDUQejMre_RNK1~Zk4f)@4dllc=lMSZTU{cZ&h5l zUTW{w9b4sd^-aMLD4jN z)?2i4y~z%93-F8~QIuyL+wXRo6**eRc(y8(L$pgv)7w`vwl9Ta=Ru(y3UJ&A!^klX zN#odZv{FRV*o)q#E6wl<@Qk5RkY|I!o?^Vx71XkA+eB0FQE$lY=7wAhn}MQtrMa6R z5$vWH5sO0WLRK)FP5=|HI>4$CV2U&_PcKQG3alZ*+sXpr>czBiuZbR`(**i-dd z&z%7!lNumZh{Xb=`=DHg0O^Rqi0rAG&Jha{9A)+$1@^Az8W6hRWBvAnZM@dPGuulO zWTj6WX-&Fz)jz^S5!S=5r8V6b)7aF#lxR26yuV7(7k5Pt8 z(GXS!(<$5U1)}lD)NeYQ7i~&IVuZZyPNw%oBq;ZTeHyYtOm!*H_Ko;R+ZzhH7rMb! z6Z{Dq3h;~}QIuz)=_k39xuvQ6a@u5+8--e$KG3|?*uE5wy(u5Ze!~dE$T1E{ z<5=Gb>ceJu1$f5LD9E$o6V%U{8)BNE{u!|-?*#ShMt~{Oz(D08PXD|(K~;8)`%gGb zqan9I6VzZ+ictu2447}!TGgKWMVK3+4gV?T5m4C1X02ZZ>}Nk~rAf2sPSJkv=7Me`^^q<3a$*wk}?TPZX-Br&y?-CPkZFhAxJ^ ziUp9n!L_)~6&xy+THQNZbe8v=9J9PIywsTg)IUIs0gQ{`zvg+h5U@{s1Zg7ZJo>z7LQkS-AwfG&|#&#Oz00TN7? z9LFcpCG?b{OL&(nsz+|9c5G1WmC4qLidSuQ?4r8lxp{O+1m-OCNjWNPk*dphb!(+I z6<00mqb(QeCH_HIF~7paBzZD#RxyDr8{E4%Dkef7Sg?b`kaEzh%IidXpKLf2Sl#`cVphHv3YmuSu9r0R7?Rb74flb$t3hS7L zyboG2ryBBqAvL*q$w)*R^7TlqH1jfxh7=WmhLn`gt05l;B$$SL0-s1j($oBC$ZKt{ z-K_ATxu|x`V`+uq%|bI?5)rg;wPg`7jjP4g;e0|R>WO;K{x>cYMbzPsW!Xitxb((G zRG~g=4|z5959J*3<_#N6lN+NSWE~U*O|3Mq2r!GV9}hq4+_}IAFh%b&P

ivNS)0 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 literal 72328 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 literal 48914 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 Pr)%WR$cA53&!ztlko`SI diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.mbb.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.mbb.doctree index c00cace7031c7c2becca89ad934f13541f9cdd5a..e82b5ce2f4cdfcb5e30cb51de572cc2ec06bd540 100755 GIT binary patch literal 85580 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 literal 76961 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 HIh_9gwYB^| diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.mlb.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.mlb.doctree index c3b52f1672a933d0df0878fe21ca07ed904fc7cb..99ac763069820c1d8c7d4336dc40c00959fd5f66 100755 GIT binary patch delta 200 zcmX>zgX`c7t_@Rl8Lg)u3};lIyiZqS@`X^<&AfX5F7jwLPMONcz{mgslLO2xw)!(F zwK1-puGP-yJN-Z#qr&uuZHyl10*=dXwKFm@>9K5&-OiZKxZS0TQJ&gX_!;t_@Rlxom<`OG=CKimenT7lz7D-m9x2!wDCflEEH3B}1$SA+q_uF6SlA zsf-Ma3?MK$*VB5d52I2WL@hg)) z%XURprcB1|wq1;R?9)GRF+JMuGl`K+m1#RORA$c(#%*j6ncxGA+wD20>h*waW|;i3 PN_z5vDxR%YOzb@XA1g+U diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.nba.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.nba.doctree index 6ab2cb4a5e17a1b18a2697d36d935fd45032e3a1..35688340a0016d54808842b166c0a694ae41898f 100755 GIT binary patch literal 85789 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 literal 61003 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 XJ9QX+xbqmPhU@sUG#}J3mpJvGVl&~z diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.nfl.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.nfl.doctree index c5769708b1442f745befe1ab6eca8e01925476f3..68555defcebd5b2884a001f377218231068f8f03 100755 GIT binary patch literal 205651 zcmeFa37i~9bw4i2I@hu!U$XI9_W0CFme!JNV@tMtTRtRYBiq=S*UYONSFrlZBh+UV3oP;E}X za(d0Pr|+B|ik8$1`x~{X`e-m6&4DLIOXW(j9#p4qn~s*@di*6&@Ai=>WyHyI#$U|7Do3L#)D}r2BqfT z5FEhf*dKo0G;1x8J2F~0S%CI6r%Q)NOY)MT(jn18&d1JZex+Ourgu(_6rzDbvso{X zOf`Xj4Y&!{3#x?)x>{VEnp9s#psDp>44;{b7Bvgw_|tT0d1ojkbvde#H$p_Pr@3BnkzH-L{5 zw$GayDIH23A|zT`A-+cg*H4X1)QVFTXd{!_O5>K2%_eBr25Q*O(h10Z=|teUbP`DV z6!?D{{9g_K*8o4IGZ-K1>G9G!XvG4(Ks3-$R12bb?5Ry~!jBe# z{?^M(<%Xj9lZA49Iy#*=K0lW#8OG^!bQ(RlzRh!`g*%(m1?=aWp=)@fqrG;0jn%mu z2JCLuD~$H=g1Zm=}vr=fLCLstOnw+K_>qnD*1T1TCctdbxf#;D+PIy!?MPh-z~+Eh>8;^-y* z>KSi`lajU+3L2BueEcq&W>Da4@`LFJMhU2LbX8*ILu+zFy#E}$l&YJ0HOKkM8 z!4Q9LIFa6TqQq&Q02?+R9E8yp6E+0DEW=;&jWW!B&8fOE!B-P04dJNCmy26+@9D$@~oVJeNADF_a&d|Z@^`13+|^1CUnN7BU=&87>MZPtTFF<~I-$*JmSbE**Y>!Z0_1Z}u7sXlI_I@wC~C!FfaXl^c6)wlJkO5YO` z6}ai>;bVvZqkS@{7QvS04}deS36;-gE9qgQiQ!U+f%K}=Jzzh@RR7}L-5)Z;wHNg6I&S$ zGlrns4wo8Mn+)oOX05LET{L&AAPiR~*-Nctqq(&f4GS;EB7=hVl4xPM%0fVGcrRdc zkRZyxnS`&m6Nb?`R+y?Z!56Mf1!g0nBO6nb813d86NP&7_Nf{MoG-+VeyZNKEFE}s z;|r*cZWbV~El zaS;5`7n7$K+UY$bPOp9cQ8Tji-(un4P?wo9o40nY-~%sBF8rPs4L9Vg;=yh!8Ni z;eF*`|0p0Z)6sHqnJ-jFONQNU7eVU$t5iBSWkJ?E?pA8bB-X8*3vWfR_L)Xu^pW`px024@(Q9 z#bZ;I3Uk>?A4j+9Eu~fHW^SJg68>{^JD7__8arC6Hg&{R0DK6UEI4nlAZ!xLk;j(#b_FRg87_8}l9)<(>^s3nxpkOD2 zi#RLL4@;uuFmIQuWpr@!F{hu@8M^}fVdJ5Exfx98z4G2mAVS?T!2NQJ-J5H+fQWZpZnRL%jRd*cNE7_YpqLx0*Cui@ENYCF0))4Q)(j8CXY1V1NUc_ZDz6^P zQCQ4vU9P}m;v53$6cXb@Moitnr)m{MPSsNbG~po14)Kn`%>u@aP#ykFX~bI6h#@bH zIGt%kcpUx#G#R?#WVmEqFrcKX$&{oY9tuwh^BleX_esD)e!pLhR^7Z6yJ|2zAOCm` ze9_JGq5OHQo6|!qG}uojQlt@@rEbkjggJb4l^}q6w2Bc3Pl7)}QEslypEG9{!;@fC zP8JnPv?Ygt5sW-EkpNONQ%Mjm8ymU(JOGC8>UBB!ZOZ&Q z9{Y~*;p%k*tH;Wa!eP-n7KW6P`hs(W{6!EcmK&3m!a;t2V08ngS5;(xP#!Neu_p1H zx>IWTP22$%#-TA#r~qu##_}WOdb5N#8kMPW{2OBS?I2!3RJ1oBMj?~8kMU5bfOP?wckX4@n7RL zlmK+h(=*Ug{9-;aKsRHJ0*TiPW6iv{-2^uZY0}*~2!lzQz9N&z2YqV<6-uSz^QaL) zUA4as{eky^chbUQxtOnlEb&2BhoMCvH*{@yw{mS3VxO!vXy&6Epm889K)%YO4cRr# zpfJIyvrD1u>MB+~{YHLi2 zth7>Gem}Rm+{hQWV$&BGgTQf88BN(Mwegyi2K!Ph)c49ed`enP(`mR`Gegm0VS~h6 zpl&ETdn;PB!SELR<7{Wm>GW7Ae)b^B7phKNv(d5dUBhrF9fTj9b7z z7{osq#F!DyXb?*)Z>}!r$l^S5&;7zbDiqir+(h#(M)$R5@kh zDe0Jr8`3!wH$(&UX-CwjoHX$?xn<&;C}Z9w_bmR5x{ECZ$4x$<+bVGrCtPt&PMr8g zj-2>La~^(UwZS?1z>{qt>=MgrYLdDKu}P zP(;Ns=TJN-9Yk>hxIiaSd}WTJ_@X#jIZn3${tTL$(ZCR`lLZB1MbBT+Oa1->RrtTows_J)m{ zHf=b2!&z(gB)aK>h;-BC4dtf4h$Q&}FG-qi`fZ@)E_&P;$RbX zgpZzL_bENKmmDN61k@}MD_9|8FSvjeq(lKF4qA!~(Mkl!Gr-ymTyPyrTk!P?;pd>L zXc43~aEhA_KaXG8(irXgqH2xSr(!m`^rtKU9W2#LtN7ZlY^wtwh_;u43P3n))ts0- z5HxROTG_TWA=87aw~QRoa2iSL;EBygeV!xJLC#AB6N?#~_F;QZ$ZcLL{_$#c2wehR6jmM;D23HKV?&dJ#L7I0| zIzH*wl~L@j$<&HNY-*5_oL>rs9P+~@ST__dX3uI%hqA9+7_zq2y58edYK0P&SgOE> zkoal}=gC%rDZ(LA>DF2dBqf~o&%)w`L=_5u1SxBwMP!NO!ua@NE@PQM#x#2P1Sqe} zq=&!4bpkSRuC*|pI}aa$a+E#~e*=ZW-@=D6Si*T&v)igSo-q&SLlM>@o-XG@izRCx zh2OYixDYNKWrWPoOd5&`U<#Ha*6b+zd>VcUTQ>G*T3eiA+200*T&KOa zI6jM(JxK(8x9qQj2B%tf)xs9b{%k1M8_SL@5|*7*gIV@_;dV!sT~q*;U22@aW&Z&* zgDm?G@rqe?cGUyRevLLn38P>uiLwPn7x)H6I>oHk{hTxoSa7#4$47a7s$QAlxabf_ zbY_i@>d9??czwxCFfD?A*liJAW6ZMwnz z0W~2kU{6!))pVfWT?uRBVpY8 zq3hDkfIWpF;3ktbT-(P3XYSVZ28Sod@wpwSv$Eg*bGvESPLzAxhDZ(LArm;+$0ZBORpT!w4 zNk#fT1AYeNm^uTh7PicQ?}l={nE|myVg{t4!)Czu!fouj%rl^<0A@frBF&C+nE@{X z4Wb$FV!UEAAiGMR0Tq#<(#xVVV&Nt`n+0-k>k*<{&ol0=C$z+iB_6sU4o|V3(}IVU zSk7rlOP)5wuBILy5wCtQyw*NewH;=tEpT2`xI>m;rz0H;jAqNb z2IBl5`>8B&*w?8J$FabHQu;ALS~H_NvPZ=YuQOQSbTN_UEe5b{ssfGb;M|qc1JMQX z3f(=k1{|_792*9PJ}0#{MTBISN|yBV5v>j7v#rW6pI~nd1*I#ac8^IlXBp+^NOeqh zA8Z-ryJXoJ))6yzydo}3A(_@6Oj*2K@{=qJ8!Vz^5xxwoiMdkqhRBOfP4tNf73bWf zx-!qXr#tPv1uMZf-G;A(=BG}!s%b5g?LjE#FxjrMYVov@Cd8(QS(c>AX4z+=fYWB# z@OVhlnVG{ObD7!kiTgLN#7$yXUrwXW+}Y7)i{z)UIfLORc&o&68vAl4S}Zoh$PQnF$k{mr%0Eu*wAm^Y zZOq0P^n8Yriphb6jLF6L$C*h}DRAHE0w`|_4N){)DM%hZN`X>DrQjw<-+LO}EEbPy zOIG94&yd_IzBRY{+(K~Pxua)~t}kwakBu9H(e)#n&MK}iZak;3dE1Pq_dB-(cA>a}X=YWLUecI z(*tW_$o%6C2zMJ&|se?HKIcvTXFs8HgX1++!Kk!&{^&qJxBAjNIkeA00cQ^no<`w8 zBPcX#RpIT@w*mUY&Iia1!Rg0vNDF@sm)XOyJiD&Aa$WAaYp=gyV1E$o6{YbyLtyb~ zWMnY>BjA1&}n&nKkbI^8-^j0qlbhGXgUo&HNg99t?kpf1DkC?v`?P@cftmA$DeIG;`=8 z#^e(GJs?6TDliMia3EgO(8n)V} zSAe@S(w~O_5mcG&xFuy)+=gQ$q+*OIyCFA6I_hSDF=a2sM>_Fumzrt*QEKNo5a0pe zIqXqWn)5gi0fNFyscYeV$T)0p#XlJ>9lV+$B${@nIR7+^?T?!LqkxsAvzO#D_@JDg zLYj13gJ9uvLJ%zddy}F|5d01hCk4R|;S~#l*;Q|X;PY{0C|G_xsQM+xtZ#Rq z2LUh(%(SX_DrHF9df6`L#qpzdk4gPzw)7KL<+g{v56iOA!TyH7nxI;i8!eA@ zxCnh)?wf-6nqDa=cr`1b+TUOMCB@WYy$&-vTG+1GUDI-%E7|b3TuKDt zld+mkELT!p{j*$~q4}wnOEs;-94;fXte0Dzvgl%WzEWfE zpIDT~9i_DA=ft0v!!1fH(n?}5yqkBo6Qaph%{@z&V5{bG9juyLO{?M3SIu#e$QSXY zI=bRPbLE_BYIIjN)5+^~UOOkA$G_|lmeJb82)_5S!_{*a#w&CS5O{MSOSU0J?SvK$ z#|gydSS3obO|}(u+v0T%(W&XNcZ@l{Sxl|EOFV%JXCls~JaxbQgL1I2@ZvpErIY6- z+seC#WZ7AuW3knD@qQ2!^@^4sOnH1*@{}wO+v+C~Vyf;VNB4BxHNPB%s z>q~UF(QN&}Wbo@&221wj*~L&Ft_#&9Pb?69aG}95+AqhomVAuRtXn3@>xm zMFa>4x}F7-4U^r)yzO8ZTUYyH>zZkBWJAb<|?}ilBHF4?1fA$;p0}FN((T*rs~Bt zUqcr%R?{uH(!o#@(6~b>o<1O1rcWzKfTs^M0=pat@YI3ONr*tH0&t7WQQT%3`KwTF zgW)Ujk8etaHyM-1q8$!nW2aozY`!GmI`-mS_4*)qMM@RlVubJdMkB<-3wno2VIOSN zDL|T%9N{c0o!?5F|D_k_=5WDW*Plaqhv9NX_n3vWSeI&3w&MdNjKA{|2G1;VHNeEz z`&}S5lCd!&NGAKk0==JcTMgrP zp|&RotQ~|Zu^@TP4iO|@1uvzj)yt!W3iDjq6r2<>dz6rdHvA*hz`-!WKb~&-te9_@ zTwlmr?+_$x74faM!!5}@X`Q$_3Zeg;#QdjTV&*ZyYq1c8&=0_+Lrid%a|kUxI$DEi z2GM_wgei#Li0+z85G^Z=h0%ZG#W@e7-^*i+gj} zU~u=JP~KtKTCVT#&OeN;wpNuMjA@%Q=Q0#+?4-=A~&oDu$|ImSe zWtiB>@sL`~s4%{ypKlg)38~KjQBp{~4zE~9&8~VAQeP$~mtrv4EagW_@X{;qsQR3w zX)7_R)~R$5ShEx{OUUaL4$+G9#+KX;Fao#2+rw{Lmz(9e>MN{6#~yEABFjei&QQFq zi@zo0J~~mkMAV(~t28bw@V-H^=%2b);t8+EYGQ%+8fR@`EgsVH*+;Y%<)Q=cnQ@mT2x4K?bf&0tf5_ep}E(I}8+l0``&#$@X=n$ac?pG4$}SUlzZtpVb8m~a_apZ9+GJu*lUAjVJ*r7T z04fN+j8I>yAeL*3pAAd=>~eInbC;o$O*vjzYXUobE5T?pDDthobgmUdY`(?h zydORVb#XA&04M)jKKo)T(LaH-Z9%0R$&;<1K6-JS8n8*r633Iv#ykbMl&aoE&kka;@YY< z3uIo0B8Qj7;vZ)i$JA6j7wc8TZM!2RZsw6u*I5^?Y3~Rq6Z>`&*;~Ct#ut-t1woO$ zg^A3rw_F2Bx;`3+1W*I9Pr-s12U3(WK&xyx=NLp3TcMQ0c?|Wk9^5_!YN;l(>}!`k zO%nd3mxN8FL~QY+Ou}tXH^{pKuGz%^lX}nTC>8w{3Gko21Zb)#o+R)!CO{_@C0YBZ zDE5$2QF}=a&fkjJnWy6Kzo#Y55@o$0fm9f>_wmW`e>Q%J1da}4TK z?|eQgsXxSQnf#+PBMgQ;L-OYj!z$T?Yq6Rb3~NDVG{n{NR9AKoU1DW01(DhcgAm$ubAjaS>0dM&#k|w<6EJwUWuQCR!eBFzC4y$~b zv7BziHCROx)E6B}@t~Gunm(gwu!=_D0}cduv>@~ooPMgnch%ExQEr3b6Zpp&8{ttD zZ!)fW672v5YBx@@ta`%DL%MM)7McZx4P_6)k6gyMKOFx!b8ikETz_*Ily?|99&40b z81`HB)UHNlx=$scoa`kOQzbC&J&_5;KDhWWA^G_X6O{UZ0|Co0v6JILwU|d?d`a)# zEa(zcZ-;q%&`mM?}^Mzepp(!s@1E%Rj7EA_*Hssap<~C}>19C2s2M(8*#22W$C504ztOStt8DRT8sMf8xPuWeSCDkFj%jmm)-inmMLrn*9scn} zt2no~X6CPn+up2Wa$sG=kJ;8Sxe!n$^|K_hPkV`sV_BNWKEXt0*ITZEBwZg3#6Cd{ z4U69t3R|ur_ z6ue?cE4%6q(yEp(RmU6o$wH$6N1geCt?tX0G$I|~t!nv_R^Mztxak7u&h$yo4zHdk z%g)&HB~8*T`$*LaCF3b_-;#fnW`yCa+a-VgIIEIPcnww)!&xm0llr)PsbFO=1#-F# z3V|c;x_qf-rC|kVI3+)T=1x1k3;+!^!?c+?bfIHxw=m(PPaEP{1Za{}q~8F|>#fq# zx%9$QyRv-gem7Z)?1()6liWLK%*Mh0??cV<+={gU}FS8L-vQE2SW9`Coq|RO3A!+2}caM@m}ks(d%ff)1xHoN4#Xs1CDQEAp&W>1D6gUjq6$` zle+{w=4VLSYps(}Wej@xg%|f6dU>3&oNn;@ofiKB4efrU$7mdKT7y%P{ z*_|Qi#oalU@h|t0WnU|F{NZevABTT@(HG7zu9R8{?166gDFw& zOQDcQl=~(t4QrIkDfvaY@3hm)5am)cO#7+Fe?c&_*6-YB2&N%`aJQ)6#x7#byoh>sHll=cG$+wMlFY(>iW6XseMY}6W-bdjd z-;iS|7NTf(1zb9W9OpVNNr>~85bv%=;uPtu&gJnSI3rcXs$#H$fFj1e#+-e+_r;*HsYF2uY$QSO7`4*Zi*0Ws3Lidb&9;^4+k zVqQD*5cT$`c4Zy&B&^%KgvH}tt{?136DBOX+HwUXx%wy|_691TN8BrHC4{fkO4qno zOm9$7I^AYlUE<#RptThDJ|C}G+{>5wtBl3Bw|a5UIR2W zJiH*8rcXawe5(<-h!HU3+wNp^jq%@w==L6z`e1k${_%}&Il~xF-9Zeui*7s0y4qOp zG^3kVx3Y&XCqcc`OHe$r<$}Vt#plS#bgEr zrHgI0)g`k1TWBpswtt6LEV5-+y@_nq>t$K(G$Gc^4r{Ul8Xi)OzkIrEQZl~ls-Vl}fen*D{9 z!IWtBaVX>w%`P~~HcMHfSx(6>nmy7^FGDm-%`oj`S~UA#OgQONg&xf&sYt)0*-chy z>0I_dnjMC=d2Aq$iSt!+=gggh|JcwA`xw@KA9Nr!tW~XS32Q$E<+>i$Vq-*DONNMr zSf7F0u3_y7GKG7sn1GW`S#0;1w5?$gl@8tp~jQHKPdcH_FPn zs-pN2f1j z?510ArHi2^r28C7@eqOJnLeFp(Wyq@b_W7{!Kctn@bakw2oJ(TwYNz}ZQeWaS17l^ z@ICm)Ilkm!6K^s`HKH93Bjm~qd1Tq|-8fM3Q^fKoyjV8J3LfqEFqC%~D;K&aj8nOU z=(*Esltum~vHuM(_RSv0J-1(F?Ayl<9|a^SpHYDF{g49z%P6pu<3}fosS$>c^xVyY zE^5Gx-Dj9@pu$ox(oA#nsZEJ4YBl>Fvll?XVWcKAFy4efv*FQIsycGxPD^wSRSx3||O6LSW$Dt+uxO$q|Nz`Xr= ztS{5mNIZ*q`y_MFZ=~bz?XuIj^y=%b?8N?CH(83jeWEz~2ym=}=B5H1s&Op<#}QDj z>i`EfMgSZ%*Rb&XD7ftkaIBXA$7H2&FgJ2A$NsXz+RY8v8#iy>EapznQgTiTe~e8V z4F4Z*VKU~fLvJeNoMDfGN%jXyvTd{T*ZQtO*BnFs(oYn5=ZECYU&&-X5C1rav^>81 zH8MoITf8K1>Lq5VpUlMB_8_th zC)c^LE^K1&2q^RU6cX8XFOhMqf-4A$>?$U*bpHj_ifbTA*GB`90BRtgfebASGhkh~ zRUYT0^&#P5b7&?KkEmG0s}Wrwzbi?-mLzq*m!wQ(TMCR!GfA~owza$?%79v|jE+p|9x3wV zpA%YcS0+|~{__L3X~7QQNAxm1bkN+}UOKtTy$ znG{_>;C}~k&`PPt@QQ)J?5Z~)uv&NoULTz5SZ~5|smVgKT&wy*!du!Qt2&x!lIj2; zE~eL-02j~tSL@6HYWmruNZ=M}*-8s$5U&~lLE1eH@eb7q7x@eM9@>?|dH+Fg%7EMw! ze7dmxvzSUqvh@81UjRDxF^cL{?$&Fxu_pi7s2Egw^Z50(bEr7%hnJcTd8mR;U*TA9%%@?PA zfMd0Kt`Ss%(PmJDi$bGTZ3w5S!&;mhKg+Tk{m}x)-!RX+e9rzp$U1dLn)4BP=BOPU|NboBXBe$U@jHT zoT5Pe>`jW|X(;l+@CN+jY(q?)ML7IA;DONa7X6#^hDBpC-=Qt5*DnFhQig_EE}ox z1h+Bjh7$PmNZ|K+3Eb3CT)%oZ6L?z)xjF+<0Zg~wWR-aTn8Z8nC0|AVKp%{cXqtn=Lsu=DZth*p%6^A zuJ864c#_TMvcf=|k{=9otesv47>JsYZjE&$?|+zZ(x(UUEW$uZD$;Kl=v=F`W)=oI z3)*H|l@)`562;j^80bHs>r-JM)wmWI=<`sn>o5>DM!-O1e;EAmMYzqK<+d=;H4+B8 zu3lTetyXJ}6e^Y6u3#*v2i4IacS&(y0sa780(~p5(<;qCU|}i{%X^NvyP)ZnT0(!ASDIPA9 zEYqhY4Y1J&>|+GXPZJ$O!$QztUIxKrw4pm$!8G7w*&KKWdSee$W) zJ~1|b4|&NCa0mASily%oOW*Ngi6aJF&2Yc(w-`%yL%@eLaqlyvQI9|FK!BqK4?8@{ z+d`wmw=HO1$ic9eD}yN*#tl%&1H%}z(y(F}oRS}g z(X`XcfMHNGObe--R)I_4YNYF!aMC9%@hoB(Nh;EB4C9qnY3W?{e<{mL-DD}2vLuSL zj~K?~(A-oELp81i!&nRDx{hIBV_?c>OIgVNuvmE=+;+tKf`B+c|`2?_8GG#Y^~F#={t-s};Efx^VsmvPHXtaxA-V#)&uf+XgO zT?hvT3d*p`Y^w`)F$LnFl^6T)ieVS*syEn$x$*+uN$m?=_^rGE0v#Y1=E@6f>?{W^ zUJ6~9J`vb~ixCgXkU%03wRk%dPWn_Oo<-CmNk#gN zTKtz)S~{2gM=gHjCQE@@B#N_-sKpzhxv8jyYFrCyaUYcHI%(9~L0L0B*aY z7VgU=cC~o}wZtvDS*0}9;Yr5>Q-k4&_{TYnAun?l%vG#kj^}L$yV@Ev#ej=UD=*Od z=mfVomt=g7my9`Xu^tN%Zm|(AlS4%#F!5P=L7XMP7FQuz@0AyMN5dW_IO81{rk@IP?Iycs}*X^E&L4XQ%dwVIb5(3K#hqD=*p=Q1iZ_o>K0Z?l^3kJvmC(q z5!b%tGM@X;?7~PRS3$SZAk~0mGnXm=;ox?gcJ^7{+2Iob(AxJc}4cl8W>j z!`N<>md<7WF^ntSWGOI=L~-^J!*~LQc`AmX8rOnhJObsqj$vS91cpKOhsDZ|qA3Em zAI9%OZF@6VJ1~i{LqyycO6;<}SzIC*-K$WV>F`d}-@)*i&T6be$JsDdu}(R_8}9;O zWLkN_+>cHWj8~GBU+yJk9yGoX3lW0x61a3gFnm^CaMltaj6X-ZG|_vkyrBAG7{;5u z_~sbKpE73CX9A^&p(dg~aVW)u43cL0w4`Ae8i5xv0w#vhtCbfh%XGkt*dzav+9L*jp_Wp#m!7fm!v46LQ~rn8 z`KcE>9J}BOhSW$l zq&2V1H#mZpHx{VSPay|d}4m*M5V`({og!7N_7hP++a{lqlVwI-PQeI)d z-}%Q;i%`?gKmId&gOi0y1f0)5ez%>5cJi;6P`u7RZk0*;`Nto%x7Q{UbIP;lyCqdgh4!%hgXCI-DCyNUVW8P31yLCw?t9t@x2tk?=enmt}A>#S=y3oel6)3aMLrn5 z4*&QfqMTdAlU_~SCLp2){+8|NVh7emq}n?I%G5qYB73`+$T-HQiR?ipGP~Y#4J7IM zXdw0pY9QfirWU@eN1G+>2R`-U(B#ijPkh>|Crnd|r}}=J^+eL4$*qfmO;O(4y^WMz zmENp``X8mP?Ek-0#TUE;tR-wppJgxC**E-HWnk2`-mqqx)t)OF+FEOA{QM)>p#&N`F9_Kb9!C zN%HESC{S_=pN7@U%0$70mBAFuvll?XT|3f^OdXEm3chb=J~>hFudJ2MiGrV4X;>2loRVLn;CFU<84?B544*D+|12g7k}Q3{ z69p%nWE#dU*|4r83XX%ec_a!F#o5P1!7ZS8sfhyBxRyl0MNqEmi2`hlNEDD8!La8` z;5LU$+OBcF);Cdbo!cREU9VUAUg349&4b}iXQfuy=`3cD>~qNYeGuK=9T>P>;#e#TTUP$HGse9THBS>z$;>vrq5DdD7as0 z;E!5_nw}c?3wwi;g-QgRQv>g{)6hV1FQIs)23lp3o*MYLy}dS>m{X=z>0^&-QqWTa zW7L=ZOAS0~XFfSK@LkqQ=hVPYtu(Bu0Zz#;H8AHCbEuo6GR%+~pl0}VVf$w>HIQWK z`<)s%(JHN(J^o}6+UAiONEBxuQv**2MNdr)sK&LV1}=tjT~7^QV?=6z+z19ZU&;p~ zfURv)1KunBcgYhQ*;_KlYY2~w6h`;vnzh`f4IAY-C_SiI`d{HwQKJXLYn>HaA*Qp( zD`lOvgR|h09q42affk8$H90U&JrQ{I1V^lH!$OoCD8Qvdav;MV0z2agxq$=Ns;qkm ztkhzufjhip##00Pn4rx0PPgDn(L+tV4Tn-3)u5h8pWL+6fJWdMjKHi<4g3j;d@y_i z{_#x>aBdNXehqQ^e?L`7gbN8sKN(!t6oCj7pdf`0Ed^at1RG8jDT0l7#Zm<9sy8Wu#61Lf=#_7dAk!WK zB-BBYAaM@?Yy9tFmf#xb?sPB8o+a2O%g%=7TGXqtG(qAP0eX_6gY*Za`D2NKTO_ak zi2^03@Mf%LRwfE+Rt8fr)4fp0HBoS4}0j+Z&uLR3hM{CB z?D;Z27+n$t-fR7LxgD3@^?If674AZ99t^K@R%(Tv&LXFj_0&^14=!1PPWBM!p-5NL z1SRT=F|WSh*wr(z5Tyx5;L;&YkYNu&8_pB*1P8HMS@#gw$;A={&-9WSPZaEDqB7?@ z-GVD+4>bWd9ZGR*gZd(Ua?=t88i70`FzXWqZ$yy~h7aH$-$ViD7D4IP61V>+5(V!j zk-gJPWE|R52mR_K=f0r zAF`wgy10j+i?W-QNrK0yul}vWzWQEjUl~aPwe-Iexf**2vR0(Lv|o}4e(og#(`;aZ z;Bh7bC$mA)@-Z7Y9=_v1fCKX1a(I-RFdAjPZBZu7UXr&*c#nYdlc8L(Ir8!@pO?O*o0RsLBOthlOVWEo&8cN91QCDMzhdt_+|&raX(>5qtZcY z;OWWD-di5s3$w@8ZG}oBnDxgBZHEp{_m}J`fh%R%=-!z+R>p{9$SAf+5HPEyD6RcCe}*lPJFMGhP4wpC8HDP z-%y_l1b_6!YO7pYA1X4h5Wmg$Q*$87>`A*C)H4++woF6exT^Q^9aE(k5v2Y{%nSj5=e!!}(tP zbNuvN#&o*H(eP7ERA)Ps;-Mc&HGO)}@KcSziHv}WpI(1SeY~+{fIDlsp^;jxvSv%} z>aiS7kn3{jxaP#KY~&8(k36JczZNQ$T&-CO>NK*42UxERg{!r0i54(AV!-onYHaU8 zF~dS!{Np^fO>IMb?&-u~JIr*4V5l4H3ZA-KsS?fYB$}F+XgJ8hJ0C?;VWP3?B-bvI zqK|eV0n{!&C!D1B{KXm0SJ7vR;UslhEGiY!S2vX8UP_XCiI?O|rCI`vy^u*R3C%PR zvUA;!KWSwTfU(Tw6~k{Nh9B@^*i;pSt6s+#c2X6RjgP9JM3t)8OL7cKOqc*AkwUv- zP{K`tf->$kDY{@#p8#=$Uj~&9KZ#cigJM^`!JsaTVFljs(*{rYsgr}PwH38&7Htg0 zW5}&IkF3e9zc4phs1^&2;n7+>7#^z^CW7J1!3W%iS2V;+4+%#yYIlw|83h}kMO^+ni#BPK^^*t zO)MO;x)jMP)8;`T4}j??D-A0)!72G+6DQm0Wxyt=8K!O2qjnu$`YrUe!)mG$ViQR! z(r;|y605XyF8hy7Y-Ng;KGnvsi9~Vs5u5labbTr|p&HkMO?(5&bsd|)#t3YJ><nl8;qA^Ur$WJ5 zFsCs2Re9SJ;R!Q%JU<&c0Uphs#2|68s80x9-B2QaA&L0;ULxjR{oPoIfQftI(gB#* z=mkt5XRPfeCh=M%OPIu&K9~el7lR{S@5METBkpIcrW+ZhgP|s%S2>j80VByWefH4c z2#vr!4g`1vAaoKOd8z;+fY6A(+TaThqud6=591$ae1S(syvYlpyl98R*tkMOr~|%n zVIx=AS14BsBb6X~?E4L3_-kGao1yLS{ZuT_> zsgE+&?W2b4KS{|)|53JIa3EmOe>*w83PVhWFmR-cZWeTjfLE*$5%A%7#UfyK)$0hj zUTZXidJiLDPM|AmFGOR154U5kg>Fr^i}tAabXj)BJQ&%FsZp_ff@WbA6^kFF`Cur) zX33L(RIKC?o`==MqT)5q>cJcdeZ|EQtws4W+OiA&jPRfqiSH(Z*IF4&$=Gj)Lay5` zZ?h9f-VMhIBy79HUvvfS%C^f!tkQH_?-dyOZM&?s2sM4%<$d-BCkvGbIB&b0w$soy z9C``GVcX@a=FEWwJ@Bt|MmPXhFh#ZsCw>3rTkH+D3CElft=b=ZR1=2|kz7fgsuoLG zu&^Obms=Ow@UWrv2Xj1s%+7o=%JpH^O6L_gU$)Y)qFkJkAIkMTJG~4j7d6AD3)?@7 zC|8on?>EXdXRT==yJXC|vj6f4rfBIcilJPI;_M^Jbv`sV73ETmYeBhAf^uC)xv((; zemewcSgnBm^Zg5sVg*MCrf04;@ zJ#TwrR6k@wUe^nVYQPZ~;tr-b%AN0w+_-u3=5?DkY}&Y{6DaH|lKnO>+4CU(LM%j3 z*rjmk5aeIoHI5S?vRkk@w*PF>MPBwr|fG0@jx2q@(vFu;{a z!sS@atb|ifw=$RlD4h(2T;bHS?F5p+ElwZ-PQ_nzk4%H0mdKIDx)(NcXuIrag9l7N zgmId^d`+y6L;GN7o`44*D+|16@YNtV9fDC*a(($eQBui@Gi6!ptY(bBDI3`I>8XCG12BcQpd zD5`2)3yS(n@PWFHqGDqNib`$-L;ilv{VhPh7HWG|Xqt=dV_YdwRN*2Sdl=JxmeWDF zWF1VpgW)3l?2iU04@UVJ5WHiGVVxB@)?~ zm&iCu#T5iaHp)b1*ITZEBwZg3L;|RRNoeV<8PHOM>BVriyecMnfwiC~Iw@(}T=3R|nw93gnuPQ!FCm$lheCP<6OxnWkvx4g4+*4c z-lkoJawCA9-%l%4rh+T#^;(_n`rbo`+a5G=m7sxHp+Xh%B%3!ULhp-sHA3d{yOPvj zlce73B`H&#mI5R1WRhyDPHTPn0Wf2Etc;opTYrLsIvXCUIDrJz^ z=VF!ERe;a0NEr~LmC=z&-6KVw{BuIf?aE|o>tRs-!;HSYsoYLdci9Q#z~u%!of~58 zWTmb4k{rz!-l(j^70nlZKNOUpqDj#O&7Zqop!xIgilO=JsyArq>S1Nw?c2blgd5*z({2rc3A%ySvGpkOu_nkZ^n@Qqmuean6Km?rLAGc zY^~(af4Q`hO}GZDnU(nd#a0GWpw0`RkSo5w(@r26cI5;T@O}J6SJtlJ`^8wL>GOaW zHs*)#?`aWgI=;W(-r!`R5&>s?|9N&A8nW#r6fb-@0^cV$g2B~C!EKJa zENqAG4=a2he5D+@*J`PBBd7$U+DbIxANdxOo>O?rFf?v3JPH3e<9$3IFgx5xCgT%$ z`(083H+iQ7l-nB13T)c2aZ~3js5es|oa@yG9Go~43sF|!EVy*Y3OvO%E8xU6(?A1! z2(Q5=QDR`bV`9LCR1CY=;Uz7OU0lUPly2R$+<+$7D;!F3#DV%CeJav&0~&!1j6j!i z1E`a|$qn3&B8UBO_{Z5+n0ksh^j_k&2e|<^)`ii`kQ;C$pv>!YNM!eTiHu`jTtQG| zcQTRL^_FWON!LdMkpOBSpQDVG)T08c_c%bJq0LeY$&&Ad>_DQSBuOCAN2y*xuoObL365J%@-PZI*M_Y9nJVt>!`3JtUs1zqi4+!!peIqhOI6$ z`#?;9@!QfwFg$hcnYL+usAO9&tt1i7!D?c7s%0a39}i_b#>!v{v~(mC0w>$`tskdZ zX;^_6PRS35Im=Ej0}w;aFzulZQRrCOaweSgDM37oKunT~^c#rTWtEoBr5A46m4%i& z-DD|V7?CK>J_0d^Kyy=p7}dBIAm#^PGrJDNU}FRjL-vP(1V7^T8OCp0Am(%l#Gpxy z9m$x3LxkZ_`tcf^flGm|>*LbIhR;Ra8w{U=f1HCU4!q3t?82mZH*a|&(a zIUYI1?RYq&Xoth7xGY0#&H7yJQBGVAr9x#(?EC{__c1Sa&GCW92Yw&QJB*Jl?y++s zhY%ZgQiZa*zb3YS>BTmWeYs*{X!T_6Nb<%E7!ifn!Z>s9eoW)Nq4wyYu+hOh@NB z2f!ws4kF<~TGM|I7b2YxeVuM5?P2n{vTXFc848o*eHx3Gk4fn%;q9gVE)5Nfps$gP z`mab*LJF_OYGM)e>^Q)2kCnlc2zoaZa$S*BwG&8QD8dOOtVqINbgk{milpbpDovji zyn;x-6-oEB2sM30(yQ$aP8KQ=a9)x07j_!jS{yH-c&$ikl}Y-Fq`$Sd*CrEl%Cstd z>`_e$0vW*_$2U`7_HRYf*X+zEuSoh5Yo#*~_NbMH6$s;${D82Z+UaEg!l)TOUD*Cv z1j3RmeZPUQ!#A3Su}e0rD?r#`&^CuRy$B#IQJj4Q!ghe-r2=89aV!O&SuX5x9;KFj3viXFYD( zu%SmQlwOV^9}HiLf1E))Q(JL0%!`TJo~%%EVO_+TnN}z{5m2V~Pf28N^b#3I#JGZ> z$lkz2X4hM;fh1iY4MYN{f!L>DMT`|nV;N9XV_qL`zR;&j#5s0Ne0X{DjzDX5I3SzVyzN1cIPKYCItaL|EE1WM}Jmoqhr;B;o?mm(@$wR=+6RKg0VOR zEZ!nGA{I+0-U)1;1Y+UB6e5Zi!C}I6I4*e_}TZ*;O1~j-jo~w=78aWJaM8p>&URSS;!WjVYw%P4-Gt1?H=p3`U z+|X#H04EDT&cGrANEEEK{M^#Sg*%(m1ytIbjZURtuk>eTmkyQwQrw8QZfG3dM|^d0 z>jpSzw6K3Gf6@J9p;^i|Yx%HIt8QI==IS-fPZ%oAiIx|G#%R7!t=5|K&d66z%c>U= zqr#^UU!__zsCuhZz$B7{Xp-C%4DS&DP3V|!=x(@FyD-A3B$SE1{%#_NrUplnk9^I^agyGFqNgYiUWe zs8WXad=@Ix%6SUU!kP#yTu6;jB$?xMZj6)}WDZ614?y=!NAo5RPDhI;53XknK}e+K z{t$6H2dfzjH^LuVuEXqKBXxQy6pa=EvF5?aU^-ergrY;?PHSO!1JQ-~e7Ra~=JWUx1Iv(zg$B8apjoOFA)YwPz0k0r6$`}# z8>Lmzoih=NY3*VTttGCx(DZO+NxuN+Vd9coffvLLLtKYpgV^9tEdlcoY@~{VjG%8N3NXU;W9g{K_f_DX;7Qmv@;jh4r zM%U$s^yXpJ+7_=pgw}V(YiI5{Iqv5{Hv)C$xRZ34*EGTPHPLYo57myei7vbve4}!q zQoaM$U_8VVWFC+#5kkm3$KY5yFA-RQSJP>{f zqo`YfBqeEf;}uKNu&b6N4RZ%qh16UGL$jq09*DM$)&uB}{7CIUzA;*>vln%_yQ|8_ zJ>R|1aGYkUuS-L~2BM!78({IOhXi1*{K*yZX9Nt<`h!#|7WYkAHOjs_`gU11>tdzO zQYi^Ck1a2s$3>NzbrLQO+Em3(crS9+Pn&vTn=Vhct*X^6wzUFWblbMR7U~qXm88TB zOO>laqHRr;zGiAf1z>8V@O@3qJE0L|YTkuc%+#=}6jQ@TVpcPAY0#Lg<}vLHYMeEU zPTjwQLnEdXI=v3e%PLE?38rPKNtVJ@^QLK1KE-t**=Mtxl#j`>(X(d+5-^$V#GEKr z!mlG&{WK?omM%^=C91_OrsPAwLAOoGkD*RsN=QP?lzfD%K%yy$y+-I6dUwGPT%Wsc z3f{tw?>4}UU2f!Hu3Q|>U0!Gwa%1({MDCL7uFeJ3;$#ipehIiNF4H`75|Z;H2k~>y zI=a)CnwTim4=SAYq7LaOur=zqDwwH$%|#?en6U#pS`p_;_yWsJZdIe0CveC)prp== z4f!%pYzcU3E2DYqa;iS|t!+CGW9>9#4HX@VjpR2~jjV+$v$xQkN|GJr*w>`5_0C(5rMhn&4NRX?IGz;+h z?jnr_oV&!^PlncHdMo+a*e8SGCaDlC-||^yVi+m@b!}US1w;ZpqC<%ys+g;hY+ zwTa*cIMC$!NeJ~=3u^m<`i)ga4=7BQNB54xf-)js7zd?qG;b&c+VjOCwV)mpf!7*q z=l*havR)f0jFcgVb`S}n;lJrR)@sIWb*#qP7!+Xf(#hH13p7gVQt(n2bKKyH(A%gt z6UI%e-7w6?&1<30d*0PO@97?NG-W1=!{vI0&J3V-N zMUZ0eCUa`T*H^ud3AC+CxYNGXE)&;AJQaQp86ntX2YSrKvE0_g#nzs*)OcXg%@@4n z#pAEfFd^ARYmOUy_1GsJN^xDKUhvUX>^r5a_L6+;h;RqMKk`&v_mK#!018Ts#iZCi zLGc?92BC((#Vdvyva8;phFfWV_F8mPXu?K=k*Q_?>uxrBT zsqBMT4=q-h@yDb|VETQ`lgWN1+XFi+c%&>FJ#$82LBTi!@h*TBfN~{|TA?u?%tCw> z_35(V1gWKu)YyIm#c_BV)`5hTW{1n*UYGH>D5bMGL^sodXSm1;F3v=3&?U_vDgc9# zAXZ#`#|_33Ip&g*npZP>_nV_T7AA>As%CF|AJmlQ$5!3Iu+QHt67tKh-l)`?ibu!mdF9!QWyB|$i z9ATA(o*|0`Qc4iv;aEq^Lj4-#)NKpprZB=(&93fYHtPwlJeh5lr~qtMg3z69);Z8& zWV4=(SIlOytKQhG%TQ-KHdQ{pkxsA!3l*Od&1z$YX=%%^V2{k3=Fq#=I`r(o&30LK zhI}KF*{s&*8Ilc8|48v4-iCF=tj{*M*KO-#RR&>nX2w5?bq2FFS8>s1wl$&xur&!H zcD6OohIWvxc@ADNTf?q;V{5j+p#_<}q13mYB`g|cha(v0xu|WIq|COZ=ts2pK6KQA zj{deef8lk!gR_KIua;$_12g2s0KZwS##?3UJ*|dfJA4r9h*^zSz`br;jiini#$qN# z&uqjr7i4A|Au0eHkswfK8}T`42ib_v;}x?J?5a05VtaimA%@C||9XW`NrXFaP)?7V zk)-+)5aFW}B_N>TlwVEn3GcHkDf1oAFYLygM;nm1sKdn;IHB6><6G>~CSa!9 zc4-gP$z3xpEM}L^=8BQoE{O`jF3CRdwM$db2(nB2@QT?bc4f6oCKVmb13txOl4LN2}W$9xI41HRjX4Z+8&YE+uC4SVZ3!`ts1P$ z)j%o-8~jV+OWN1v)~?+Phlh6v|g6W646Xoe`^q z5`G1%iMc16Giwdk3UzMmp;}e^NA2iX`cbgFkbgD#{E?N)bkgxq$fJ{ntu(Bi#3}i8(gk*U89IrYk+GAGPBhpp-L)eAnCQ6H zAIvVi(aLU07e3Xm3rDRqtX;?{nO%58eM*fp_QmAsc00WcT}aKy*oBM5&!fn3etnEq1>R1cmqj7Sv&DJQsXpIOq5tGjfV@LnE~suIpQ~C3hpQ znxvK_cKGh6UQD>pgp;ABUgdMB)A!H$PrFvbO7s+xp#dLuSN)6epiyZ zjU;uem!wREIusbWiAk!hLap`X2i9pWkCjm&RO?UBl1hvdV`F=?SV&NaBpIBVidAA) z0lqOEDFX_)GCDG;d!)#de@>K@bP!<=%={EHB5dS~6cO zyTLPG8Iv#%L@KKwJmOwv6$Fjgk^q&bX`fYig~X^uWkYP*;&}vo$0)xfqm#!|p!uEg zdTnY_WE?8xiE?x6IUDeecWlng7aDm=F>EDISKuSFDf#d)C`GzcVoyH&qbxgvQIqI3 zzBVc;`Jf(w1@T~*RPsUoBFz%Z$b3mM=AV2}LJ9u~tBEBa_Vil0m_;Ab;O_xNk^iPr z6bsmIViMrrOj-PgRTe2w*`rX%1N;8Dm4+2K<&;d|v|WN?{`oeg&yt{^W|-DlM=bRO z#dn!-v>sX_jZ8}i&Bvca|Iin+dsbOlN$H*^K_QRsIm1fB+C7|-U-xXb)60NsQ!_Gl z&x*L=Z|NnT24qu%=q%%&*=M_~{H65SPQO0eW2Is3Gfv5`&#HEM8TyQxk+IJX)u!f_ z?pjDcik=c*%}#p0m7$bQy4SChUS*|W?Icdguao}FPA@|zQ8O}j(h})1wRF(}_LJx# z{>|*5zqc}z(m{Xg*Fm4Q(y(?Ar(|@{A>bVfp4hd?(KqbWGIS3$B4hXTIwkuPE4wKj z_@90q_&X~NYX@>lMhDJsmy$i~f^K^j)QpT>m^~$Xx}6bQO7>(Z3IKreGDQwn91_Me zw5IZ(Z6%nZ4MfUEIPIUslx&g{-|v*{F1tW_o{|ke3224(N=-fKx|4NrI>sV);`5|y zqB#4Rbo~mr+^I=d)w-6X>-(Wx_mi&J8o+tjTdF8VWJ%Y*hui*1R}sP{BwgiCW3@V@ z*X&7G`EYCUR{oSwj~v9|b5UOh!{<1wEka!c-^~vAfOW#%y!j4E*PBbQq=53SER61Y z+7)9zQRz*14d=0AB1QDa@PHPXhzB>ZG;BQ>pMo?g&$O(|oyG1>)*8+ARAyc7Y<8 zU(Nkpbwf>-pCW;N!b_m0ZNgK^A0~d=tv~U|GJA0jSb8t2R$}@liRl|&Vlwp(Gj?BP zVsg?ulBkc~VLzbWk?UqJ$y0*D$$-orDX(ivP`HRtP$m~liY_U^xm!g_a2{TKjzvpo7V-WkEvldpxg4#!D!kPti~UY9(bpf9_D2BAx`CghHhWutqt z?GTm%ojkY&-sjtZ)5ojh=mfUi{uGy5OFnKz>l2iYYyCkQ0G5qgE4lK|#wm$}Yp|MF zHqPs?-K4rQzj-;!(&2T$L24AMTGtZA9s%XLAH`y8L=;Q1WdYPtC}vpY1}^hL?FACh zli1rQ-U-LYp9B}N4LmrV^JNdB0z6tLLYzPuO5=(n}j`_44c2Qb^c=dzf zmw9(58!&w-22{CF!NGj#Y45R$f!`w5y?z$AYuXRvOlKC%m>=jr=smDS#-ZKr+#>wQyQbnhjW*-w;o*3ACtZMRd?92<$f)W<+$ zQv(6jw3a~NH{g0X1Oja&Xl$g$ridtj>^O@8e#f1>L^Nb5GF~fU&71^l=9@ zcP}Vlba_-xQQ<$LDh`JKfPXyqAI|8^%p~}J-fECw+g;EhW#i*hW)BjPS@s-%mAc`} zUfsZRKA*utCv3+g+85vwgwZbNGuj6!2=Aybw3I&oI|=epFG2FGkS56QF+rxAI>aRT zMlq6k`s9O;@@%9=;Iq_IMmF;LUH^$+N<^lWrkr8JWw zkd^*k=eUg2k_Z<4Zu8=p?~lEdv1vCuraEDU`67o>+*pwuebfo1uhhw2lBcl6v<&DZ zSv=Piw(y9cpp17-iY_VaCWwPz%_+QMuqL}|fi)QflW?BvkHIsGPL27<64qn}s1?(U zKQG`$6F;;9nuzmR0L|r3sTHRxL2k5GZ9*=oAxvG%ePyuj0|`I}J{SMET5*OzK3kSx z2;{|?5w;*2v>=ev(Q)axV=Qr{niM_5KbMowm#gJwKJNlH^5avL>J5btfIOn*ysfeW z0uj55pH$yiI~v5PDvt(}G;Ojly0O{;M@A9cSegFL9+?T|HgE5IIIMi zYL+VvI3<;-Wf4xqX2%gUre8VDk4RmkpRD*h!Clr|K2>c387EnjZlk@mC_h1DgnEb-(PN);GKkp2An2=6nO!8V`)8@tks+OO0X}e zOmCkT4U`HM`E>#87(M`c)o2!)Qw{OZf@nc87?~P}`j)|&vg~b#P%oq%;Leg}y)YUe zQ`0YqmX)iMQ_Xx(9jz5XqSMimspi=F^VT=YaDBY_V?MxBF7j}mMX?gpLC)CJ(dKlt zyf)P|Ygxc*nT}S)eu-5Peg)Nf8P%)WAW9!^{tzo1tw0_cAgB_Er~*7qM~77k)$u9N zTab7WzRwG))6tQ%N;qE(CgF5qAX8?NUI@cz4A=yTl#B2l0XT{301WwPSz)SK%d1<_ zyfP@&Va2J5$-GoN5G=b<22)tC7An|v%h?@}Znc5kuWn5?f~jIHe_#StYDI7W);b4Z z4pd2|C8N#!zH*~XD!Lrss$CeVAS1k$i}G@yL`#901`L5hvs|k}rHApy@?&Kq@)=B8{Mq&p;1jZAIwvL*eC1@++w5;W(;5bE@N@h_tuuDJxb-%O-!`UF$W%aSq7#X~y$Q&=oG~tA=#&9`kjxjQ&=0pWZENK=-UyPSKZ@E-uwdmGJ5XaAzBS99#?^`|x8P97`YmZXoAKk%;A1LUf>iJn3#3VOc9t&NS*q=16n3)Oce0vy zvRZet8h5hVcCwmaFVIO?mNjoFG;e*WQrcD;FD;1{^0Oc1#IPW~^k-a7cO^cbMGMLe zn0BXMfI-nQ7X!@puYlT&kB|79I<25l4W> z*?5@~8~gp)z=&iVx+q_VHAR6VGx0)#r8==xdSmIq@DW+VU^&pre`?!%8?KL?Nt0=#_d^n^;-O}X(K++(#|B~GLcOiVU{TR3`qXG&`4y0YvhB- z%0u|^_!9W|C4O9RD12OkA4eVrAIIXy?q%>%z>l+5z{h#`5gZO5A%4922>5tCethak z`1m}29C0*!9D^UfSP36b;K%2Wg^w@e$J*oI<4pXxZxB9Sj319244dOCbui67rs4Ilr4A1AGWkJItvmuunUH~4YHdiXd7 zKYlO_AOBx(SB%pz5CjnoN}^B%=v`8wD1cI=pn#4JiG~JY$3k9A92EQnI(Ot1NYL;B z8a~1Ucmp$Qudyv*9YLbm;(T|`-X+>P#qwCKMMEiVx*sp66X@omT52vXIMZ5tG#x(MrLbSJDR9a@w(7 zSWcdHtOsKYU%}P)v4v&K!ZK%JA)l6g=GcZQ9$`MG)qQqwa(~58r70t4-?g_nZe>Dn zoR22;`L%*IVLn9j60NQ=apC0Jf806-gsAot4t98>bSCwGmcJpWOKwD zcyY8)*sv%u#FgiRg5TiEESz}h3}hD{22dnL5(3=&zJwNko~Mo`o*A}(izc2hsEQ^m zo%Rj>8wf(qF~Yz@_ebz>2$+BrTQ(?@0o8@hdS$bcwq7&~;&ciQo?Mf*z|AiO}>9CngG2!;@|Ign@az$}Ka#Sg;vKc`MDw{F+1uIird zneUIjPu+KJ->Osft5c`W`PFi78@Ttj1q&D8zi4%Rs#vPrGLkP8D%CQmKPqh822^815YJs7Esm$Os(v3>c8poz&K z)#7V{Td+9xhwo^z(t@}{q9s##C|{#lTpJDMWQB^0q9wc@d!ohVQYC2anI6qYL-|Id zRvMjdK>e}4i%Nw$)@|unIbW~CO}HMY4b_#@yhxt78_R4+fg&W<0UA1 zGmsjWOn^pYM3qS>N~N{VwZqsKM2h-`W;Bvee5S|p0sM=>tO~95 zUk)Rgbuc-LVLTseu{gu<%PRaOS1-X})tIhneMIVlqL*X|~!5EJviB@94*kH~64&tL&G@ zpTwcz(+Z{HrG+&!Rr?Ofo z)U)|YAv;y8PSvv*J!TJ-8pZ6sV1@#q;c!(vyJaFfp=#=e!s`+&Sth&)zdW@~;I#$H=GHrz}`Dxo$)m{c2iLm}EAL~CA%l93W>vlJ>^ zVTiU>n=5)zZ7@-*88wrG!7c!|3Y;Z1oQHQ{)sTx>7hUB9`gd=8qYLccz3ojy8=4YH z=p~u(ZZ8;Lflp_`+ZlFNDkIc&7)9OYkct~NY8)RO#wJxdY)^R*t3~mT)FHW2wB=@g zFUENq_u+Y6%5Zeh)C~B%&q<#Q?*4zlbU6#-&9}B6skc;*^ITGCd$h zAK~{rByL297|KZoqxfF=P`mhsccFHV>HXnR)Qwa_JzV@~@nc%M3eOsXoA4_#ZS=x9 zh^-aqNHi8dBY|4mf~K&qhF^3z{;#Z($rT(fK2rR$5e|rAVsM22iP^AHJt|A(6SSD~ z`mV@}dv@QaXw;*jF6ivO+S^P6@NNAb5% zo8K|1Jj&qcF&Fnsygi_O|De}CTm(Pqw}$nh{91|`>OsCuf^jv^m%!lF~>b? z7dlJppx_QJ=aoHy8oYwp&sg^QaD>`sqgUJT!G0JMQ53U*L1cFk_+U?H>Vs*r@p%-( z1z;N-!UX6VJ{M8UZme@Ayd1wgbvfkK8G!VcQg!BO6mt_*@&>O;>Pmqg;B~B$PD(*| z_$URUh)RL20G(3{Z?6$?gei&<`JdN-UEFaJ*uaxEX3^&D z*xF45^Fl&%#tTgz!Elusgvw4cGz}5Vuwg9F@wnI!)hHMoAut*Kqih*-4{k784m`qW zIhf2hN^!RagdNVGA^nb?6V`xU5zc|UKRiJjKq(2XFAgD5Y3T0Nc4L?jE z{avC`_CBlkbkxniWtFn0;^v>~Z%0ix|A^Wv9RyIE7ruf$s-t-jCm;YQyp*~Yegi9> z3BQJ4&gNXNKTLIUnf6NY`cpTyKdRRs0aljIo|0p*M%IT9IxgXED7?+$dga0E;_N|h z_%QgS;U7VE`1g3VU=e(LTwZ+wu4XyFJqP=AF~}ESaZ7F@sQ87q_Gwf$EfCAQ)8Li` zU9o)RlwXfS+ou8g={ju>Z4Z-aqnZAOwlSb!)>sd0S0`FqgtaaAW&dVl$*`=9e^9G{ z2v5Rnx(RBHdG#l#JsHYw2x?V{T7ud&kfPh57Nv@yme6BC?ZF5n^V*U@ZR&(3<^qIV zD3XGOlFi|7p>Bo(kcGRk~YN!`~_?2*SHCn{KRN_7OXGpi($f>n~u;ct~b1qC3h^l7|eR*7Au zTO}oAxOh)=e8OC?g?*ryo7ft<)^&Z!=XG{pthuu;>%%o~mSue>B}ybD+?Xp@na?JK zaPB_s`31@6Wmk&QlC+8d4wpBv+3F-XC|sNyeo3ZaOKOjR>UK646GSJ`5|s2MejF0H z4mH&~6%T5YEwQ3Tw{~V0dA`nT{lxS5okIgSR-GKhW#8Sb_AA~Wotwcr42nQY3#^&wIGt+6DQ+j( zR2!e^rup!tIQND#-NoN(^Ke%2utaNSyI7)kOqr7|1(>lF9Nc3&R;%354@MT*w z!>4Y;Q|NJHBXs2V47XkN$U8DDF9dX8-D7wdl)>Qns4}&9KEH*LDBbgEiy5D0M7hHE zAvI>c&+qv#*E-EOtn+9`?FL()-E-;g>_uejv!%*-wT8?t>`CI9W`9VkS~x)s5#X06 z_cjk~5(93OmmMY`76H4Z&&TQ<{5gg>36rKLjoa{sn}=w~=wR{GiwWWvdO^%N>pCVn zdKWH7I{=ps6QEnwY=C3USjED-V$g-JMzLhF*otAc1kEiw#p|-+?;fvO^R-S9tEwK4 z#w{Ki9M1p70J_^D73Y5mOXazn)_ZdPHzuIrAb@kfLMI^_p$uU3)O&H8>G}^LZkh0d z_~qFzxx`&K`zOlb&^Pvo^`k>m1?4Dh&tA#AD7FBVIZ;_$Xbr2XaPt!yA?|R}3f266 z*lLo8c_#9gI$5InMXLE1ylSp@7o4p42T1SGT`rlu%EG+HEaj%m^S23%$GpJMy9Ul& ze1m~u?@+vJ5N`(qZl2bLrK?9PvtT#vUos*_XBvv!h3d=4DTuY z=yvJf`LS^XwmRm=8g<_=YMaUGVw}g_nRpn*MRXBJooX!bd|+hUJ* z5;X0!Vi9*C;z3h*0f>#d4w}MCF&lGSbO{v9BQ7#wS?pI_v>D23h>KL2TH>O^AVv3a z5y};D5osfH?+=IDW==Q}7j4)r;-Y85UcG9qG?p(jXN4JFQ;_75LiV}vg;<_UIKzu& z=5^w#ro7d8VR{&-`z5GtW2PPcF_RqYx`>)yOEAC23uf*Hz5)|b)buL2bZ`Stb&i@; z-3)Ql+fgvZOwI&LgLo zfV&(7aQ9GXCOCSOfp6sW&xl(l{5*d7MozrQ7&(b@I7Ci+JBXZ$CKELa(bNB-+JDEZ z_IkHLfBsvL-l5yUJ;^|^cP`#d2tS{0f~bGp zL4c*3SjDj@N{pq@xun0Rr*es+j#wh1sHfo-i=x<7Z=$G+q<35hrW(cESTPUBDftFc zr#lByaVcHIQqMFOo0f>yw`X^Q1-JqBkl!?a=Blg-$phH-4Q@BBM znr0}}uG?tgcF_QErHe1O5kJ)`KGej+g7$18Pb^qmzr*X{h73;!WshWq8a79xi}9&M zzPBgxO&=oM!%mVDpOmx3moI@5z<9kcUY~_GNN{gR6!_1r1r86u&n=D1(dCkik@_Mq zX9-CUS|!C5$Q>FcrYmG%pEr5w3vWu4_e0k5Iv}GDr{TB>O0A#h3F3%|$OO8dp$>f+3 zu3XAG$#VSi4EyGb!@>|=#w%cqI}^K$O$UXT++3sUGtAw99kqOAA{gGbWy{u$xc_cL zH*?*`Q7b&%s}*?o`7}&Ktj&73bO=B9II=cz$Iw!3HYn&5;iqAlXsFojM(DW7N{l*Z zc;Uslo^1@IbYDa9j$sVi%?_z}5KFC)K0cuU&h^9u9LWUe>n?KfZSeK%Fdo7M+ufgC z2Mf@sX4%nELAFpDXD72XgjE=U4WV1MW=r*KqZnj^JRHIn)Eikzlkn$8jB7L6Ot9a8 zkY~c@;+M0%&@~m$KY2FQ_L$h}37WXjXZx??oo&{uE*zQO2~Z}sOpt|MkZ~G?D+oe1 z!638iE!RLo*GB_U0BRr&Qo0rvJK*V9cg`}(A+f?Ci{lvTWi5FAG^nK-%sOBD@Jd4X zE-!?2r9`6Y|N(D0H@b;IZ8#}OaQ*g3qV~(aVzPcGXR}bl(6s)*zMS9Q0H!Y~QB=3)&L#8N6a_AG_)e+jpUIR%-e2 zMsBJ!w$GdFJI&+;8COjdl#njCzSC0*JzrK%{JXV3*onPw$h6VR=8D*hH&TMvJJbZd z7!}PwN^`+jy&uc^`LlWooA5`NO@h_)I&R67SHDB{rORBsp+1M~i=bc*+=mF+O<49V zWd8|hf+1vAWoil8zX~b357|+!2-!&+SwQ(HDkgZiZP%4&cZ>BC7h`|RGRtdPHqrh; z*~7xKQKK^9na+x(qLz7LZWxHC@$ws^a^1^3&v(5Id92EIcaJ-XF`#a6NX6qA z!c_OY;sj_cl8yi@QXAREROo4td@Tqr639afY~JCsI+ws;WaQFCFk$co*LGt`Q>s4?sUk% z_39ipXlr5XyzW{^3d4Xt8K1t=;W)b_+hjaRf&rjqsRRBJS5O^;&X+ozoya%cuDeHw zeoGxrH=&!p)M2-^z}5+UF3EMN!}V54ad*!h8n2}eMr6{LI!s#2>wt_t20C;JJwY6i z#8QWqtc}xkOPm0XOC3zMqu;#AYpqbmE9&0#v$E9T&)k3%yom|RzVRjtp{xepM3t$9 zH+cfaitc$6lq+}>8hu#E|3f~WwdGAtk-Ui*R@<+0@Jb^GrtmYUXPNL*_~jX;%?DqD z;rel2cw>l|Sm0nfL)he%wsg#OpP(C1O&BbbN?maoF4LzH;ABf%n+ z&QfIq>+&Nk53w%KbYorG%Sli%PkQ0RshA%yaMCTmq6fnmq~CK$#bZoDKYb*MQ86(A zUuFVyqOY4p4QKaeQN!9HW*=7Lm$QA)H54b%mqU674ee~rfkV|r%+Wgm%A_7kkY&9f z<17bP5QJp=&L8bnv_!-|IU3Ep$P;MFw}Nu=!z z-nJ_mZggMK&>;p8YxP!$(#)#}x+}b((=`*>DQF3yF6y9KZy4sHgo7zf9$dc(n~wG5SsdTt8dv<`=+_|kChYZ+of zx?tkeT83DDQ0V!xmf>~Q{$QuxUMi!8c;Vkfb~Hb> z4;LYC+p_fx;dpkMT=vHBzp$j4@F%>$UZ+IN)iNv1=&+Y8hd+{l8_tPH1nb@lA7b*l zTKF)qoMF8PzkH*f-(m$RF8?E3Iz&CL3m^2_8A9-NSYEG%4_cOqX!{7S+Vg1pP)KAm zMX_i*2Gm-IR6N!pOm#;sZhB+Uc1*zUs3o*G=f)1=99*4k>Ho8^a9{_bo(a$K0-uLB zyu!F_;!K8U+wkUU_wdHA>JZiRByVL8_YhFKy@2Au3?nm~iKC8$|vhF`c^X+KNT z!2+B6Wi9;!8--AKA7;}{U}MUw-+|3XtysiI*FH}+{v8y|=1qu6UViwu3Cq3(HgAQp z8Uhlrp-tx*$Q~I!iKWeiKjMY< z3U20$kz%kvAz^PH-dy0ju3IyUT?IG?tzdXBz%Snb=hs+83UGc0mkt5WnT`{W$@&>W zoI|k06ylujHp`gEG!f(+=7j(ca@Im3n^}qlIWe$SIi%u24&kahaB(vn3vyxtenYLH z1v$@~l^`c!3uYnA*^anp!n3^K=V1=7GFu(aFl`&=JiD7P$6k2|bb6G#vW~k5tesw9 z@lc0@g`v)+3@oQm=QQ@Hj{;(ApaOb?I>J^$&_bnj3U$Q52C1a;r5DsC)Tu!QDb%Us z6$^FPRc}Haecq-vT`zVJ==jasa3NiUIr_YfD)fArw~3(T(nl|QkaL$zJE!wDvXK(2 zruC^A`IKqiM*Jwv1q*N9EbHeV-Y9Iszrbv|32#h!^*g-zkQIygxZ3CN=5L{3Ha{W} z-k7lLTX=IHl+_U4s4}&LH@8EI?!z0DE5aMnM&@VV0k@sPo3x3Sy<~&i1vi~0m%TCk zA(k`~KEVs@723?@yp6Cc;OZ}g^8a56c>BPnqj?)HudA@;kA(H_y|Ct9<$q%$3Tu7? zmkwc#&%BMQogt`Mvl2vm&)di>6CusPn45D@$3vP`kjQ3=Vj)cosO1i+ct}H->W*66 z^u|J(n1H9KCA5%ccIIvN#{2&)1T<$N>Y4C#FYtBMLl=50L$qx`)79LHt?J#*+oa{K z?BO#Bs7t+o;=v5p3=C#2Vn8_sGlZ#+>S0Tu>Ujh+!ahP+LZx&HX2dWCsido=7t|$~ zsR9-h%uL}G3uf3=Z-N>15~Aw(cxkMZFMICqh3NJbDKwVHFsNkf<276 zL#CaxH-ShUkCMTm=LPTzNapHIXE6gp;mG3>K0`R7?`QCS-H;U4RV?y5g86^E zVCD|tFEA0sB2U4kLoDL@x}kX843WsnRUnv5wBzfBVi_jlkky!%Zyd5564@+DEDni* zw9FwDk3$Gg-N%Ys&{!N26Yxvc3U+UkFW?cH2`(LF0FI!3bZeV)WY0j{;NTlC__%}2 zi;VuOC>@Ig$@ENy}&Ator@y|Md&-yann<| zghRJOF)18+IbN}Fh+Xw29NI3IBTbD?`R>Kp=)8ChGVdY;Qiu0oA?Nc!IQK!Dr4JkS zaOh1kZS;aUTDr!XDKVR~BCa7~nVNo68Uq&m{I#ryfAFI)2>%ymlL&s&&a~;}$v2<2 zR>ykG$HyTlm~_{ZZ@z4oWIOqWB!OLTIr#>EiEHF+oqY5CM84^k+dZuFJNf3jCUnzJ zzWI%{z}E9!xFpw;ZWWPZ0NDMbOP(^7B#F z#_0g^$JtAMOtz!nWXHKyDB~4%?-jFh^38TAm@PCBxbwxRCM^3#cDxrF-9UDzGPRH$ zO-Ru_*@1Ee*+HWZBRl>aZaa}3n#9BacV z)U`}laaK7Me$KIj!C>9T%WVwt5_{>c@Lg;g^Aq_CeGu~Q&%C*lFn_rh<~-JW5hfyD z~qNa*@#APPVYq~S-`!mtD03%5by zn6znhW)ptb75`;fs{Y~Ydj)u9bs0To#X=^e)AlYQ$0 z!TIV$8Z~aV{sbjCB~cUYU3}3pfuMk72_dR7Wo*o8iA?M&zz1WL3^No zF3yDzE&>)r2$%4R5yI@MH-xZyPdm7La7a_F0gGg&*sH32>EZ1@^sp|f3$plGalTT| zbzW1)FN4-iw_A4V_*R)Vx@+#JV;$_46*WhuH;gc0wnwDZVP@^Mvc~=-vqCGp2eX+6 zB=bA1)v+>a4?t26lKF#nNj8$1B$-HN{3Witvqduhb0XjL5y72K^COx6(S&X~$^0E_ zfvr?3m*h$^KWUZJ8Pn)RG8>UeCz=0fEw2MI`pD_fCG-SwLAILU0X z9sMSmk6Wwvw)B#__lj8|nU8^jd63K|Ec-?>zaARhKr*W`wUEp+kfM8%8RZI+nMNPR zhQ1JPQX}#Q5Q4e)y^uZc)WS#s<1}5lGnn8 zlD??}N?Lc2gf}YRIKTCb?g{FB)E*_T_TY?59upBk9l)hS5PY>eLG4_3<_l_}ZulZB z7}3>NIn&jy0u%A_tzO9Sc=-hku5>%Bls=4se83?Uk8-I!(nquyc^wll$^_`NVaMb( z9thbR^7;XUJQKbRzkG=sURxwe@1xrGfV_6Ax(H%AAg>(-D6{(kg6w~LLB@HO7-a8b zklFQ?YapTPqk-5asDU)B=~{R|u*XC0oNb)AHjQ_3ADo5CW^FVau8PlQDB_yi7$g}1?|%}!k9)zZ>obzj-(m1N=`*#2k3M7j zDSfu5GL|o-IQ0qBmXO`Si~ugK1cq$pkN-6zX{8}MgFHlSq+iDDpO13 zpMezJNB$^RME<0WEa+Shx1A#Y%Vp$$Wv#mD(rUFanlG2LdxP;;AWdH615 zFNRx{dTD=?Mt(f$siBeP8y z%EG_AunXXoal24wuYZ`W_r*Vg?U4!p1;2a)cn$>yi=QVb3<3P2SiA;K0Vt6USjjzJ zO2MfcjLz>?y6EWLXQT|o6VxW(>#|KAPi+%zMc}X(eh_N9Cs0-THC5>;uPW(X8aD#| zl2yrW2zZyK+WT~A)Z_0s2;c$YV-An!-R564L)7{R%B84vi+j{cP$k0FuXxp)hpqp@Dr@&v zxsMxz=nD?1xL-+V>h4T4WidcQ6kpdY}gZtLx*c zI>%6T)_YY)@5&gi9?7a>HvqgVQ_X$4GHUREg8&{L9^~*SFAMbx-?E@tA+N%ol9xve zc@TI~mvjnOh3f*Tq=xyRs2DkC>E%`RMyEqP*r$@w_!Hj z1gfUI`W>j=XT>7!74 z``|VWDQ#9(yHAGhZS!_H8LG}^ma^A|k6?{px`kKFD_EV&`Bk(%_byxjb2;!2VmW*> z!xv=hv=3T4nrH=~U7dvbJp%hby};%%>tmRRV%GnFO9!XaXQGvAW(ZlILb2Wxtt`95 z*zjwwTJwnYSFEabhn2gzF@S#VkczvOgr)AW#cgveVvPy-cP2oOSbH_m3Ru$XfM+>o zU4Izs`5FB3jgUAL=CMWxsDBf!9bnXbu-SvC^>k{JQ@d=F&8cmYGSTXSpEl9j zE`b_gE~P46>{TVbOJn4E0jrYT5b!QdwfE`LsK*-}1n@|4gTte|EYv%E%Yx>`BXSkv z9>k#3F`Xh;;mbfO>2K==b%|VG0$5PwdK+G`$dz67CUVs$TJgm{zM-q%L@P?@B6ig$ zT6uBjOYr(8Xs~q4Zx3GYm1*a6qLrE_5xwfusq_eWFos{Gp zsEp|BpG!c@?L@0LLh~|Yy^&hv`Yv1K+SC@&%!8UaZI7Nd(Q1EOjVY5bJ=1G-u#(e7=z4it(dY9k zQ$EQx(d-J^L)wRB+USlsT!j~JyhLDocuJFqb;PN^OY_Ep+(%?Z{exTuQurmzCK2S$ z#XD8LXRVI)Y_G>5DfpzW?^OAvU6Sp%JCXz=Qp-D4@RztNGh4^q4IZxNn?4q~hwpyJ z-3>qqTF^~D?(QgSfvtzSaY?Sn-JNWe6nCTDq47HI&WKFa?oB@{$KBlo1@j=aOj!1f)cOiEx`EVEWojX{ zJ_IScC$&(nAhl@pVHC8m*h7@XqSZQFxCdgPTI<9vTkF`=*3wMPg>tt|cgR9G zJ{wsn^Kmx8aHbawy7^cRN_!fE!O46OVm{^rd&Myh0ytTFl*6Oklu=*vEsHW`_LRIl z!i5AHm<+BHc`QN%NF`}2U7QPfTmvkKJg(yvBahitZ^+|IO>`e^2F(taD%r_uVY(b- z+fc(Bn$cNp9vMj!)CGOKD=kmqm~YSSF4Y@1lq!vzHe~0^lBfvUHGR0T^T&6|w9%b& z&mW_nv>ZWkL|UT=A1nOjg4A&R)xHvK{@VkT+SO zjF;TKSIi29ya5X4K_Q#4>>GuAA2hmwLRMvJp^$Hf6x~zEC|6L(H2N@N@(#F7@pKys z`HJ1ud?CB%(%o6}*?-w2g?uGHZ^H26?)dV^SI&4Y70&v@54kyAXnki%xY+H@>Tu>a+{hP800&# zSi~S-;msi1#U*^FySz~26!Oa%ROzFOQuZ(g@Jk(1@u-*DB7L-rQOGd?WhOvpoaP~g z{2qin6aFv!@@3(8ZIP~gC)M^(f_nO$$W1`@hH8i;Lz8c0K) zu7$7e5rvFhJ#9U3F9_s+r*`<3S3BsY7OAqYv34-MWThQ}{LOmU6$}Y)98j~aw zX^g+bN1E9pjo+5YH+`sZr^ozAsYx?_{==r?Kn8!MFYin{lTSs{)8+YQL@l*PyGUeH?YQgu~eO|JGM$oIGxvcVa7@0D;ZGfqf3l5j=}pZhg3Y?rItt^ z?P8>HOu$)8fKJ!VL(+H#Ac89*D3@~N_}{uHvpDcpwH%mWH}r?om(zU&f6>Omnt$1cf6A(JE%g^a(% zwRW~B{I}mrUrUQ^>bj3v4Ayxg=K#`PEiQo$-iX6tWSSbPD-x*77+t_!-7?{S z^D3H;AGXPxCDiq^+JtP$VV}#(nlAi>|qSxr#YnJQ7^Sc`e+xUkYfTC zGXXkzHxDV~OAzu*coBYesIN#@o=3I)lc12VCCIMvf{b%OTtN`Bs~BW(SnpZn;0-3i1Qe_p^4xJFlv$*bf7O|U`^l?OO^~x^W z>dw@*(&%HgT(}!twROjxW~98d`v`(JdqJR^4Wy3W$RKbs8-$jR*}(pAyMq8u)!ydt zC^uo$%Y4hCOqe|-FOTpZfdr-w-a-N=yx`*Rp~50GfK-yM(#5$@$6o_1Xx;JG@rqH$ z?5a1^@kQ!@fpUH(sO8vewtacy)7{Ujh)L;!G)^h@zS7t}7&>-bnlIOb`Ey9c&!8pK zy&^kV{G?19-9A@`RA_CMV2TejDv&r%A>~(T%@{}gM_EOGj#$AH{sFT|aKtM$ji8|9 znk5SsYDEiQ=G7J~X%u3nnR zRi}blzEQ0;U*D8PUARLO0j_l6Cs>6?3#-k1!eWKTVUkS9We-M6N|kb{62t`#ux=!T z^1pP!XV?X=gyWv_)8$5Pf4)2&=!J+5sZUQ$Rcno0eKKEbJb${{2{X$n!`U8@y)e8B zwI~zbiC@lvH@SoJWk6xb-OelEvf}f6?~?*zBUo3jR=S_i{!1^kd92vPL`2KI5iT8Q zxiegk1JJ9-6ezqac3yAzK9o(|+i8w_{dy(b_pnOaBMu&?#Grb&LnQO_P3Vy!Y9u8K8FG{EGD4$uEmefw(&b0+*Z{BrKwy0#&K^JS`G zJ5p`8sL*UwSJY{CGgZ($LC}293mRR!5Ss5YXzV)4wTn>n(JmB#+GQfo&hKE;ete`D zF{#rsk*Ppm-B8F49K)1q5q@>3RD)2lKQiP@beA^Ho$0=(zO{P5oOGOCRd^j$_)xD3 z>#Bl$##%^kb1%`la8)5}d{hMyRjOi7$w>+^U;^Dh*35~d5Dp5YlJSx*&V{7d0ay@8 zaV}mll7e0JhNL(*5o6YU`H3x_`~)1txMWYGnMV)rI%t|NEz&zn@qvVMSA0m^h_)IE zJ6}%Ae2%pb*ola%W!mW0IVB=mPsv=9gk0Fa)*qxdqpD0T;piSn(S10Ia#5LZ!y{=U3#zV!+vI(;2}h6KEl)Q7Y5$@BzT53BznAXPGn~-vGmi5bn@XOiz*D+ZF{aTd6$kyn6;}Q{+4Ecrv^=y8BzEsYSmV?g2ou^TS z5BI9D-b?WHu!lf;hhFlGuImUfPj{Ip1937{eb}q&dRt>$assQmz1ML4CzO2jAF*BI zAi$#kR&i`*Rt$r3aBK;cT!NmfpqLc&?8Pe<^suX52R*fFy%E%U81!%fT?9R9K2{WV zK5yRH2kn<`{p?}TgiJeUVUTRD#8j+`cH|Q@*0%m2jRXsWULfn_9|kEr!Wqma5eCi0 z`(^L3R>wLO8$nVq^RDlgz27d$wn>L1nKtR*FLCYEr=thuOE{Q+H5?i@qR{3Uy_G+Z z$T!`hy9aoFn{@uhgl_sKoqx0z*t!*kOEO}^cF*Y0JZhB`_u$;2@!F(gLc0zb zN_f9Nu$I>W8GX!j=n{H@I3gnaz8aQQz+iqCYvXj?iqjq@+tF{@W5v_;-j-f+_oknf zO*+e>xG3Cq6G9xsAMH_#rcOf9s>D5U6~_CUFU_Mp*+MeYT-?L>PVFKG`NF|%y* z4bUE0oWA2OjOc}K4@<)mo{O5537_GtU>F^e`#ld_3kKxbyzIs>FY)@ruN}0nLpf@|D;;|%QpFZxyxRjWHT}*(^Kyug1lgOtAmeNXR}h5kjSMoo-f|5j zbbT}s1)v7f;FHk84tOx$lNArvivyHCM!;4F6Kr)}Paq_8!L41~THN_e zuB|-QHm2FhwLzISddVEEF;ESbVAs~RG=muZ48KTo!T7bKWVQVHH3dnS!EELMzqZX< z9V?kI0!cmiwe#(gZ2THYGVyEpOWf(2Eq?9VM84^^-JMDBGSsO#4}mp5gyNNty6FT%gx7qIuv!Fti?%Xc^WKB}lYud`UEcYvL61 z*9a%JoDjR6DrN=>eh(>#+8;#apPJ%}pL=nzx z4(HJZAd!vckMU?RNDp#I#aRqOKYe_O@n|stKcO~TwlKC=whJEZ?6Z5rqm3ZsnQ)^Q z^16m1XEV&;Y|CAAM02@RT^PjP2~Z~WJc4Y87i65j;0l6}ZD)|B`yr@STmuPR9}PqS zsDUOP?S>9`G}wog72VlFp_$iW3VEWDOq*(MB2aJe0#(;TB!!;CKy9msSGjX&jx`r` z>p{*+^C|>V*$X6H^AMyG1IbDA2u~l)Ljh@;w{35}R1aWZ;I+7${gPU(T4Q?xZzekC zW;Ai-pkB{5iupy-}*pQy*iOb z4TY^gK}p`6sEM{LI9etU6p$<-L{+AYMtLxiiCqQw_==KYH}<;d5L5F=$jv_+N^Vys zU0YW|`ZqH9_M(dU2KK03z)~2H!5Qa<7#mq>t34&>d4)GBGjZa1g~u=BfR1keB;fQ;Cv4?HHI)!<{za+VJ2(g@pfz;DSkPKb0C7f`80HFg-y5svzZ6X z@8QCgvA^iEHX?F~8>|@=dq)?i`mN^LwTV-E`)6kF~&7 zev?abWqxn8N{Xw#J2YO*uMwGa=C^7suLCmrAned3^aOE463p*v*2d|&C2R+7C^gxR zelx$Xw?Y}OsC(1T3iJCKHy{P`Yr?W`%%BLUN6zZ!$UBhMz-i%Y>i7FJ}&p-1T|ldN6=L z$;%Hz<)Rq(2F?U4_m*2iHI+L6fPb3 z;%B+?#ZI+mUY!<}hEHOVh%(;gNEy3`N<_6k^+JnBwLfAIrH=(M);I?26Ar0(EJ|&V zJ~qWzHXX%cXh)&Qf}I9eRJGnuQYr;n=}!p)@`o)PhPwScH)* z>SS+N<3mnhwqh-QIok?dPm!Tq3F#g5v;$Jtt?D8W>74*&UXLfpj`f0!vmsnT5VB(! zWOlvf8c68+Xdnte4a7EucG2c8#srJ&u?x8#8@c38WD9-F+9Pqomm-U~B~dpNxEB(* z=X-&x>m#oCdjrZ&gi7!N=)IIK`O}>=;B-$x)%Tz#L&GEuNXteu6o1JJwx&< zjUYei&CQKCb8{^FE?BwEEd`w~M>;jF+HYsy?vZJu7tIm-#v3NVyRFjU76V=KwloEd zYkNS}#($NiLLz)SW|QFBybg0R<<;-l`x90y;`XD@vG>QIV7BEBg7e^qIhnBRTkQQ; zP*y|it;*CAd%p@&bRT=8ToHSdHnPz0&)_z>Hf;`bI#I^n*t@d&QBK)>r2yxbPE_F- zHHvLqAD6u?{5h5+6aE)3kk_iLxi&Nm$|oh1hA3Ni9MAR*viV5X#lcQXPh{{8;FoWB z@_Vcqh0zNky+e4i*)fb3^YZ za)9(jE!>mdD=z9FoMLURwz3)5MyTk>5-wd!)xE~6x;$LsEriDoT?y$O`p0(naLHIh z2#31KLRsAjs&?R2ZM~H-8X0BPw)YpV`Gk;<<|D4pau8tAe5*JX>WG05`iped^i(dP z&a0uA6zaSNuUM$Vu6h&dTq0fC13|E_Jd+c!^@Zl*54y>s;*TAt?+7t_MFoW|w4Jo?!lnn@`Rt5&`n>Su=pe$2))48Nq8>Fb$P=$;}$xq>30(TBzLZ-d)T6v>v|;=F4b zXtU&vwO^p%pHPmt;R{jYGU1G~;$e(P4*dM_Lm1Bcc@>P2Vq%rT^}Ym(?)nHPQ@&k& z%PcHZcrCTWYrI;5N06_;M8vSX3N9TY$SWNg7Uzl^D44fnsfdEv<3_=_N=tAw4|-w7 zIhwaJpwh<%rRZS{-uoO<@ko_gB7Lkv0etCvOu$`CfX-}myvc3bmMuM6uJCz;98QeE zFJC^8*A}P7KSQ-Wrj^>!-q_R8Y66$4i`b-h0+gx!7D4t6FUUAo5`*kf2AN%Nxdsxt zJ{pK^f*MEzmac{49q?<~NESOgy%#GMeogp3<%O?qK#`031;e-PiiMlpDY#BSfOxFO zQj~HI4zvEe6u+GNv#y-DZecN`xA}=^^SE*n&OXYCt)rCFo|2P!!YKe+j=GT3x&`5~ zK`Kcj=;B<+ye)tQk$GG3ijjHj%1Y)bIm6+Ti@=jTF+r~3YzF&}bPF88zKLkMW*}bX z(~@EYQsDm_Pr->jc=hc`5Y@qJf1p*0(0auc)bz!T;7c3<%W0PjZig@REUpnZqT>>! z5;AVgO^r?o0etXJdv-zmS*ePSS!crYeRQ?B8kA>;Ou?#{g(^N*)QGBBOnWm@v=r6> z)=Ka;U|8~645H%*^>b3$QGlTt9ZOGbN&yS&pWr}M5u-+DG#}(SGBvZkP#vp7fh!Z) z>bR|tBYQ60eI>kK=gL}j4EBivtG8=m$GF@dovvq>9Ud#^VfQJp$rcQNLO$8@b4wAm z_n;}nD6X@;2Q5*$;fefYu(PmZ%f@Uie}LZ5zA-zMZxnNlYA&o-D?3iwd=kAgYZ%^z zy_&rXJ71|(8}tgI*qgAIP(_79A^wzV&A82JsemSKGalcIfVz$;GGSFFoHMlm%UOJ{ z=q=Gvw)PSr*dNlNdAY2i2^u>$uFwf@$83@-cXXW;rCXJ_t(WI@ZOUWYC1)(pqY}Fl zoYamLb2!xA48KgImh5ktlZm!kQ5@ztSH57}XRScM-cXFEXt%Qu$RD;r@ znA&O){3`0huJu;>w!KE<(CjVPJr{0*rT8Y5#kG}wY?owfB`!&8r2+Q3VNo;wmtMEu z*d=z*T2zp(T5CDVM(ol$XhJH4XA`!4obmcbWBgrV!P>cfgSBA1k*`ezjb?Owj@-HW=EUHK?~P;c zj$=;W03<0Dnqdp)(1o!sR9}^0!cv-!Yl!&Ew%{G!!y!w9NY) z4B>y!Yd)+m>REST9#0d=ZfD-i_M5AtA?%5x*X9jFJi3(st}L*+1;*#*gkoqff{9j} zaxD%`o%kHWydlF33W+zLmwFanm4t1_3d!c<}^Pf)6Nfk=ZC-k z8HtK1EV8-~pMazuF2q;tl58#nm!!E6;H7i>-QutRhh0(!?gJGd-7qEm^^XxUv*)k> z+Fm1@zy2#Ki>trBT8T^2TFJp*Kh!R!2;E5T0$>_HpL8s{tYWDE2Ql>h-~Kj}A{6 zKASNw5S{GD!MRa|Z1Lvb4u$U6n`fRpIP_DfPTZj<6qrx-SKQ#VI`q0ff9_6jn@ag| z=>@QkHz<#fWV2TXLADr_r-spGg_R8D@eLc;bXyN^fsRzVZ&Xh0xJlKq#o%S$Y*nwkh`b{3E{&q zHyyqn4EINuj@1HajofJUmRxV}L5DMS5_54@JL|9KkgISMv z@Fs7yo~iSH!kCDpU`!;uzQ*JU5KqSB2YAJd3A;)$CcLk8YDg{&>Qj{*&i{gyQ}LrS_<8e2 zx>KK?oXpo|6bE}=mvR)58g-o&xL4Z}-ALPp8?8y!N(||%^xCRIu>t*@M5a2soAeL) zicTyEw9>lhpp99TAN#9qIajiBYFI-?%dwFBrpl3(a8KtY)W?ftWysl`A7+JhfX%$7 z5Y;YfS*Mv;SbaGitcCiXD;01TAv;&AQhNS0^#ZuNF*^oFk&g!1>S!Ym=WG?IH{bvz z&ch$xFk(FSet=~!REYm2t$J9-l4iott7-TOz)?&@OQ^@;5-QtnQR+prP)kvjS;gD@`vhjWxDs@v~H| zfEC2WOw5?Fmtr3%HHz7N!Au=&?Xh@!XW`h5*<;aq*6{*DCl)#3buqxza_nvm7S4_3 z>kW8jD0}SwQX#0a$1&Z7Ggrdr)z#_RSbz`L8*nxStfFEMlvy6^vQe50s?(SSWL=#M zu7Oi6uAYL3jg_FfKd3#s!sLO3snXcKF<3rE;`0-r^!3Iy#US>4p+F_51qG;Um6h{A zsWMfoj^;;8Fh4efg3$0k=So&;R@=&Wm6b8b!&0SVjXlO`^ZJ`fUEbuSE_%Pg#h>?5 zYa04ZtKHC+7GHNVw0X~)y5}w3gN7!pc4+6DdkvqkR1J4Q-@WZGsZ)OUw)-|_;V;%P z#UG+`iodCYioZr>C&fEz><#~xboKpR>gsz^byeGV@K67LQ5xQS*DZ#g1}#BGPk@~? zeB!<|e&Pl6>wsi^j*mu=+RBJ7s3I{eqeTduGjUIDhJiEiVwAxda z>JKcsS&Cwu_kr;EYY`-BL&%HeA>0SVm(eV6NX2!PTERzGvF((u+EenrBf=eE_^6al zTSf#+0I4L4qKj)k$#6O#LA>A@c*S@@cGVkRa0d;}UJGjS4cJpKI^76hO-I^@wZE+jb1n>#Gj}*(sW@1*7b?? ze34plu^4PZa*_1&WWk9&OCPAjVhO(WK-D%vkbrw!uSnx+lnzY9-dg?3Z79&hG~V0Iww8873;N9A;@RN{DXRISHj_oA565y&l}h&~NIt;=i~O zbYd%hEYr@Btc&6a@2fqO>u>iVNyZ%oSWTE3DS1IDQW^m}fq zG!>Nbr7S+n|D`(x(aQNAtDuaMyD&1xCxVxh{1evZu=K}ZYBJ?ly#t;D4b^F+bEZri zy>d<>os{ag8X|oW^D;0+vmr%5OQc{~-oV&;im^~OA1gzDL~Rr2w8;sU!cM#*tc&o*I$R&SvE!Py#Y5AH^$XR@hZ<%*u8+7@%VxC-vP!21b@0K%gC@ zqBb~^-r5#-AKc>6&~XOw)!c2)IQS}W+D}40s#f7kv4VZGJEpP6 zpbMr=C#uXXmM4TdbZvRw2$=}WL!dCrQ|1cLf#nfN!SYCyeJ#&>Ks;HV_u>_^JnYJ9 zd2}g>MOADot~iGX$|eu$(^K)8aDI@Ec3cnb#Qm5o+QYJF?5d?`>|n_C$z~f4Z@_bz zj!hURd8#UeO06fVCZCQUy(A7hwVtM$g@&}8rkX}--zaxP2q}EhDJ{W*mITr$MQLda zLNeh$1GwovQ?hOGaMgd3DcIqv!%*YSb}R8?)Z=XI z!R4(5riHmX_Eamu#%vW(nW?isykFk+^tLq-sDm?)U4nUT*~~uAJH-ju*huis}%@;&0iH;?Pg$*Jnk>W%#%{ zO^l)JAWDAN;~sGO=}gB=_!_2$`e+g_tmsMnFG+MAo>HqP8wKY;oY|mGF-Gv7OyGF~^>b~ss6x*Z^ zgaPbKCYOv3M9NN-_6KlsJ=SFNAtsrU@h<|UVNAR z;cuk<;e@E%9$tq%;Eigw0O#hy3ZFu0d_1VZepwEDeI)Dn9wQnbvcsd*Dy|#aus!>1 zo^>sN^QLP!<<)w8Ytg@AqN8^WKgd`t_zN?H)# z2_M}kx52x=Ye?#~Qgu9lq!z#d4D|-|Gg2e-@d{3x#g}hk)f~hxXQQR7J#LpAfbMSp$ zbcMq0v8OSl+HTQ2*|$Eh;AwRtjaug1`V*97XQC$B4$f$qKu|zp4n_ASGO?=w-+_se z!4X;)9b#%83Ay=aL&@#Rq-*PPNPj7lZ!ao0lhj;x0lJyQMpoKtPs#Uk3fBbqX0o-~ z-Zh+si~HH=UE+a}!li^%l9tfLah7`7_;iJxOk&!ubthm!9QrHpigD=ds)eX#WF#l5 zJLF97j}!?geD+?FfI>!Mf=$86luMJP z#*WjsY?)8=;9H@Y(?>)*J@}V0?HoGxq8<3^0uw!`9)X3TU}qFPD1VVwhH<9vlNB>_ zrnVE|6)53*F`EQExXoW(_z0zuP(vyk{%B}b^M&Qj#k$2Q;-x!URMU{ zt9D5?28v7aW1t?l%jHl#U*L&wFq1cQ8RX}*XuUBv<_N}3gOvWZ69X@y=gS z#@@FSr{Ler)GR%45yKLv;7wTekAi~m<0+R1q%wEJTgWAzX@&= zh0x~hVv0H!-lBnmmp_eHYuS7o3SK_kO6AL+4Ee}z9DW5gG7~=Hte8mO%{c`x>c&=% zK=HrGiw`5)qSz5x;DO_QE{)h>C%_R4&pg{%C*jfm5}7^+ui?l}q##9^M}}e^fp~Bm zBjszs#B@1d;~eG2?5XVTRJGpNL}@l=Ph+>LGob+V zR243f{NIT43;F3XoF@ua-AsVCVf>#UW%-_$vT&yA+nDI+T}z?09*0Y4sbgANN`H69 z@BgIRM-01v;wUIf^=neTr@WMpQ*>PUa3}V5 zylM_4OfRxlFr7p&o!|wNu6M}X9?M{I(mO)bNAIv5Q18eKv8UwJuoyCdrHgZ+hW7v#L=9hoSBx5FSG}Qz&x5zu=HT5RsOm^kL>I*H z!O6Vjt}ylbd55)|e5JI*lTz0iV_CE+x}C-?$h6UIo$cF@fWm8G3)QhY9Jg4RK!2v~ zveqPOEiCSu)+dO>ZT&$<$$%EJR%>SVUcfA&a{52q&AkO-5a94TTpz1rBj7! zv2>q=6tiIIP_VFcgdwwZpGG)4vveXUSUTA#{+4ddslw8&#Vcm%*p=1NDLKR8_hFT4 zbW&mrmwV2HOur}sM4NZpg%1; zR+N??nSCbA0=mwjSP?g!t(Pg-rn4Mivx2xq)2yQ+Tg`EsGwBg zy^xYJ?`1dKHhV9L4Jz#QUM#X<)rn1w;m#Cp8OfVFh!rvgiy+SG*h3DYj3t6-Mn|T{4GC&V6)Ae2 zKiiVal`5r1F6Ux>+4_O_>s*Gz4S+|qnwM2JKp;Af_(`>mwV?q{rPoo$q-j(6v3>c8 zpvi_`9EDmQLj?mTUOaXZtUTUuQxk_;m{=+}v`Wm0 z@cVnB72*yaHQwA4EvpBO2EP5X8Lb62e!5XA*CB*pYFU8eg4n*0dh_*7{t}+md!oUK zpc2&b@D5K%Kgg!=8~Mp8_+vSV$wA)x;Ljoc=X9+Mf30j($8w`kNBkAU4S?R)W}4B# z;N3MJ<@B)*88TusNZ=HM;}o zR_fUN>ef^}m@ZUvw@ji+tqE>{r8Dpz=2%LmC1Z`;{!+a}D!Llq?~@-bV?}r=m*!+g ziB>>m>d*!9_yS(YbR~Z*H(r9>Urp$(>+lMM)Pov611*fX6`;wN!x^*kPf*dd*sDNu zDiffHv}*KOYfg$*O$AfH>$5$-aP}MsS%G&(^H6r|98tE|XiU|&Z{B?1zyYvojbgPj zQmsvFE(H5G!;|&R&FC;p!3qK?U?7@=W@^9^!F8^49oua_*FU6a5Jsh45-8MV}hv)6pR2jr)jD66L<9c+s9> zbq~vT56g28%Wn_MYY)q356c7Ai#{DwvErQbtItbueI|Su`@^V;p%_*zjT^oV5X?sd z(j{1oou?Tsea;1Y_w3wr8QL`Tl)xYg=PDeAB&gCK#bRZ`zr{aCSw`U-;5uA`efKc@ zcs_pY#}C$vzl2v$;)g{8*#j-*H1(y)&Xf?0AiTHTT@Cs3ToFU81n*G4#i}fTvt`1= z2H@jJ{MfJrK1T54t4rbI8~E|WGWd8BKkgoak5}Wzoy+0lRrqoH3i!ASKWQnsyDqAMXGMduQzXekhFFg(e!P7Hd_067 z51tGk@4}B?tEr zXy%oK81A#M+gde@%blv$56S@Sx*#d0N2jBeaX*+jaVy745GSG+2xBGP!C*%`N8V!m z4m}6q1o+L9qt&u>t=Xf8CT_~72eljrg;bX4U@d<(Qe&WeSTZmE%YtYKLb0*xG=@vf zX!%&RTn7IZ>Hy*R(u?^0*Ms92PC{GkI;eg}b1#GyQ{Z|9!nZ)d!)$O3KL!u!Zj|n4 zoOdxEcT_?F*>b7Q+=jaJA@bQ#h%ooTFO}e&>~sZUIVcRcBGV9KLulEMu0_}R=-$Mp z(>w4cPKHErZ$hv|%GXVJ5aFYMj&L4^Qk{oaFy|qN4wkWQ9O!7dyv<^g<)zA4dAa~0 UW_1E1)1d5+Du+0w)UzA%XC1K1(2nrErA?mzgnxbyn6T6ZnO@wht*npikDy6_R3+k zQJ2O2A%BIx^49LSKO7B()v)GipQ5pLv(XCMl~Pzb=(XD3RNb!?nx*oA(yZ6TVo+-N zUhgo1W6yk7m!O5_PK;JGO8{Tk^^cE+i?T!hanTCik3G@yTD9(V_jIO9(MTx_Th-}K z2>r+Y9#^fjv2QEOwNkqciSRsBFU_&1Rh3Rty-WkBEpG<%bfT4EX%;_q{q_Eszs5h^ zKhED6U3&SheJ`$tS9PW*FNbdKYgZ0VR=k7sw|QjW4Mc%SfU>Av(m5u?6I#{YE}!Z zx1zIId6(#UB^IKr>}J(t{?+*~b)|->RY0#L(NMKs@eXSY&&EO~&ZCvUwyi30+IY0Q zS*o_W(OImo&o5*k1pC;HHnCijwoLwtJz=+mguNMP!NHAgvomO{!hJ{r_>ZklHIM8< z#s!e;vxUZt4MH5*Sf<#VZX!c31q8i9FmNuktY|}PAV#7Kv?|B96vnG{&K5Q}wsfP- zu`VHK0zC1TftiLcHg15p%Ryk46SN(NFYEA2v0Vje5O!LIz=$0D>X| zWQSg>Rs}tvQGPU?cO=U&7L>*HqHFY`vH+12dA1lw%1WMJAVj{K$oNz?GNPsRZb|14 z?DZ$0*N+k{PNJ@2)6Gu39Ck`E10q_wOSBD7W~*Pawc1R!`bFOAXtcBttLjm`D*uy0 zHi12WrUcp|FcO8;gF;}YAyV0`d5imd? zAw1HAAv8qetV>x!kP%+kWi3U+AcDCrOtp`MJi}plWEZPu52F8n10ueqt8-G|U&tbp zm#MP^7k>mQ{1DLd4DR!bVHOVYF=PRzYWcG_V+~29q@#k78whM;19s12)csFS<#kwHQPJQ+Mb_^DBhGau6k$u03el1OC=hZI%s*ITk35c zfy<@V@ShM=ketarnw{Kb@1$l8>fGu7n_^A>-wmc@jp$tI|7Qy41s4)cvIhfas^EMC zFph%gL_TeSdBN1ds&}XibC+(kUOW~{^|EiURxkSkhn^T$XBXLcYHy1v*KMF2@ISM+ zhM-8b()AyTR?xUDcM+__WCnF!u-E7XGaxkDA0p!!CLSOHyI?C|c@$o+?Kw7dGTSOO zeVxxXFG+NR%5Pk>2+5ZKyEf0AW7SheLrHd zSt}ixK7zmdCs<0mGdEXi9of&t#b|WNfbA#|GI&)WNDjtE3*cux&1j3j0Up>zVC`rs zdPBUMLg1`3yRV=`6D+th)=KsuLDqULu@->_Bpr>GO=xP3-ckS$?toH4KtyW^P6PQ+ z5t=>Qcw!8~ZvPN!T%Vk71|e3w@#vU^I5dLWc+UWpK-&t8@Ol-Ekz&mJV)6tB2-q%0 zreZbp=5#K2$)#Y@?;7DY6i!mYExQUg)~mO5*l>Xfz(k>3su!lcLSs4v_3u^KutDx` zmwgZC_2XNnM)PUHvkYXRCUtmo! z{Mm=V?_<`DdIKMz27*W6k3{RwuV>1C*9dC-T{K$+HU;5{vv3l^=u#^js)l~yfOiCi z?j}sWy0U4aunC238y}_{huh^w%fn~iD^b866g@Dq7oFne~6eJ3%-wkoat>2f?+8C zB$V$=2Y;ru&l&&sl1T>9ezM@SS}aoaPpSP2jRU~}p?gD5-0^NUG@iKQp@{;#(eMy2 zVu*-$HcZ6Zv>Ym!qV#02bQy8m{~M6oev!#-hM0ZxF-3VngHHi2neY(kiVF~tE`^9l z;DSWF>cT{%DS;wV;z9+}M*s$tDk;2&qP!3-H8#aDsY zSPSJ7vu$mDg|-}56O870)C9_ zG}o+oyGGFDELLU;yKSyo^udlJjle|Oy>t7{9ou(q-~Qa^ZQ&Dym<|wi3+`Jb6O;~k z5hLaca}m?j0jmLlFCb#3=>TI?UOW%IG)(p21_T^EQKQid2Em}>Z<=!4maaj*tZx5uFr=RJnyJiLdrQM_j_$;X1Q_@&JS?qPeu1`Q%Mi)ORxM3Hgkzk^9Q zxHB;r8k@&+FJJIFKt{9@=KAF23to?}{d!qX`J%8I+GVJ8m$tAtG}rc|#L3DHafLQoT}un3l|x2pg_ zrwMc_=&#HrRxGuPO)yGJ?cIA@9go5(?0pJt-KiOLsZXJI;!1oXc`@$aCEK;fOZf1! zXOa?!hSz(5uGzX{+<&WopTQu)qY9yt;2~KyI=HxTI>8qZtizDWJ0*TL#OWd=Zt{bP zhY!oXq)oNP{onC_Hx&;MAJOm-Jc88_OdpgmUCb;KLXxBG5|tfbUxKw=EuQsV;xR9mg$?> z^MAGjrrAA?er0N-w_EvpguIweN_W*WG-QiMDfX5L+~WHWR>_VctjSZnDV!qCtshn$?M zixQl&A)ZZeO!9-tIeYE+OYhNj)cIfpEEpwTc8eyL?v9y-)7CbEQK%*^VMNT0wFLgr z71cVew9@?I%TX(fM%jzmx0bbCTzD{3>eRyG!BPz>GC+VBOs9!am|`133E^#>2CnKt z(#j!f?3EM z`0!&`FhLDc>~VPv*9CPfjZmPC53xFK<4{cEosf(&7o3hw$R-9Zx0g+K`}J|ttncWY zu>Mpq?(+GlTS=H`e(D1PeJ;lM#*{f)$D&(2jKaDD)8{qNn{A?#3M#VKzZ|PDrY$tx zglxj6!w5yMsUw>XA4Nl+80wFC_kf|2F6(jAV>9p3E7NJyBX0FWgcMx%x#+>&HUET# zSlsGKcuaTM8}6EUi7)Z#;k?u(<$LgRhKQf#LIiidXo&bJA;Qki!tqC0m8=uXd$rZU z0gqK_4Sa`6nJjdR2+oW&D{9ugt8g7n(F+)-q8WJs*C9Yu2$z!LQUWnJ3p0%tCA2&U znYKfSj&gUfh=`gzrD8vv7Aip@^wj0VS)=&J*{f@E)=J>a6QH~UXT5yhdoIkU1*lme zX$wQr=3GcJxek$ZHX+H0>li-sa2+;)T*oz}5pWBH2bj5=iJ^Ksodw^GtEoOYpC@;0 zpWJz#@;Rx*iw5vWznJ0pid;B084v3jSayK4yCwgWwgq}uPu~3*pf^>C!Ey!i~#Kz z=GP8{xhaj%L_xcEJGdL{)!=SGM{o~5-GfiRA)np^PwAd))va-VHrf>PFi!&K(0_1Q zPx5teo={4O+~|WAerT~(q@<}XKfV_Qw`;_<*XVmuvUXMMD`I$RU$NByz0_)prZ~4} zm0Cijmc&a*!5d{2%xZ2dco4ActjKU%Pw;>&L7Qt<`N)iw+0uZh>tV ziB+gH7W8~msqJNAyLYgro?4-Z-trwTu#s^0m8DwSn<&KnUKFZ{?Sbj#FVTZG=Q#CR zm`#qk<%nd{LOEj6!xy$N&Y#P&#~$OF(Vdf12`Lbl@zq4sH)Utj5oL?>C`1K+kJZHD zJQooY%Pmvx3`HFn*ET1l?SfHp9=XSbi*osH_Viz^J9n`tP5zH<0eX=MALz z@9~SSCiDo8+~txI;lXca&rcdMYs$8l9KDUS%SQJqMrXSW>9l<>;d)GWi8EX& z+e4TX!dTG5KtEuFQ(++78O;P5XD`J-$H6BtXz1BQ-$i!xrNeqYb<5d(f2FkztNWg} zbQ?^pgugKwBFQcK#NW(b?X|afJ5-m`Th9RLlROI@gPF5-H)Hfy^AYjY);6p}=}Ex$KGBaBxGRpNy69`GL!eM`^DaC?}#mU^)Qi0lZ|4B7*_`3)nxt;TN}*a2G&#_ zvx~S2puiCMp`OqEpgL5=0I2W2u+jI6MB5SsyiUp)nQP^)lYO?31GTl0d z4WDdxS@4gbaxx=R3c^HW>T^)eAu{ErVpWI=!4V-UW}Z<*;R}$=9dJEEROvI1%s7?M zH+hT6u^B8`0ls$~CL(RN^v6-p z9kh%KvK1|6<*_e~ch)lA(IXHyUk-vXBm9NyPhp!MxDVx)E#@Jkk-&Ctl^eCrTpe}~ zZZ3s>4UU0;h47mv3i7REzRt$paTW3AiIL4SRX9Y5j&;DoaHx@b!EM6wi)f@$ZR17_ zo<6d<4V$S|kwac})(^2Jkx!-giEknWx-bjCKp|MYTWrh}r>m{d$AosRGmC!%Jpbtk zKEZ;a10L%rET29|eJIty!3xKzAPU;WcBO?Gsg5GRTK0G7YR9a6gH#%^} z4&}wYJkYtY=!3e&3S|N`hnVvY!!ZR_+}K+@;(?UHJX)GS04+p{%7xQd@uw z!ZdruPGUdUTidI#!@Mv*a)j4X@V9^-m=1ksTv(}AigmyxW~4d{E&{yaj&@E~&dmbt z%|@GTvtgn^__eRik)j=9YM4CYN|) z>I&beR;kjs=_RrmRu!0EY3VrVB_@pm%nTD{q~(Q!BasN^7^WdKn)yX0naHc93j~r` zmQqev`=U4(PKpzF0t&BK9-TLMuCyAZxh9GyOf~@;eMkU}T$?oAL}i-!W+ow0M8z>P zPGpp(oJfFPpgAXA$)po6x><*5NCN?@jvQ{@iDF^^=;SoaJW&^7P&4&Jc`~LHHRx3z zsFa`$TzVJ$jWAMzF#n8Nb*L#xjQa6Z-8}rA;V-V7bZ zJIC{N*IbQv7xsXy#=8Pn;|+{c#cs4p;*Gg5#Mj)tj?iLfFq19E0GW3r^M2^j5T+{e+~uZq6f1@mo;}E19Y%8cOSr2~{k)z^Tc*^X`d7VVl|8wfYq0{ z{Fw4;%cS}bz^_?pV_ECUg;h`#R!AvSiHN&30Dqdb!3^ZErc7b0OX(zXdgsLed>RV{ ziyb}w4p1{QdaNK!M2{bXatjbWMsP&*n2jq6XMPcqGo#0u`#muT!)TnXW{;nLZlH zcN60Ktq^Aj_j{p`tLA%?V_p>QX((89;r?0xI8(SQ2ou8HgK`TX+z}ih+!8Yb|g%a9DF(=cSRZOa+6Ffs3tb#(W+F&{5 z)#gE~HExdsocspIr|bDm^q<`i@P$Eu?|%~a0xPW6N!&#Yg-X*qP2yf+Z7_qftSM6~ zWKH52WqapEJ)g$xi>~K?2E!~<&npNMdj8u`ZUOW>f+O@ilK`aWzk|Xc>y#RoNt~zv z^t==-CY4+!ajysVVS4^9d?GzhPYa{x^+_DXc}Q6GWQFXn1jd z&rzAoJ#3W(cBTGKS@xKi%qh$!RJuN0)3YTfbJ|x^pL|GmHorPohzdT4)x^~Kdalj| zBg~%$|GX9G3_1VDP{>t-e}?jE8oYlc_zzZCtxE9k7!)~`;18`0W*~<(Ws1x!CCHG| zJ1;81G!|KOCHM$nBvT102op;10VuZsN)W*jN|22kQi5;ibb~N7H5|qQy zq>_sgJZo4e!L#v+lpsA>m7r>8Ja`lqaz@XI%{%209O$l`#Lu51#?bbkzUe(_$nb+%l=7z|Re<{X1=)JLc=DA*( zo0Z1FfO9o6&pc0uRKGe_Lm!h+zUA2Sn+zcNQD?gf*TN#HTVNmltzw7OpvRnN-n=0C z!=|~TQSXd$z)-AuMzVFfJz_g2V_KFy%6w)Xim^tgiNK4QXyTwF`;m?V+YuNF2SJF{ z#B8!H&Mc9z)=o$k<$08W0vEW`#Z97@^4;v~FrFP#!BUs+wYyk16-WK=2c5? zjp5u1J6M)DLvX&%S` zCx{CK$IlYqU~R)XhvF^eJICS-dwYGHW5K}4L&Dy9aekF1?icl_hTT?F^&!aIk7~#o zy3YfU947kwdf;>v_Pf|-yysC3QuvBf8V;cZ64wC7&ktAhP0UVjg>nwF)1F#D9ngS~ zh>0mP9%y1(gyc-`?6`S`_~f-x!SLVM~BlAuCOm=ckC_o$6U(( zHUxz!^vkSE+1;9mnFJ5!!VX`^-X)a86sV@>74rrfBH!vziZ5hmIM3EB*Z@D9Kx^RF zr~z|t#pl_{Ae_Gn<9Nj}-b~zv1GZss(-ps7p;DcptF1$M=r#@eLbXk2a>Hh$8eH%i z7D@+8)mmw~2IqFu{Nb2ADddlktYg7v@K4^8LO4coOUx%2Mtkgtj{flJj9wu^p5C2M zs^+VV^k2zEdcN(1lOB&7f1F5fALD$qGA_w8T9LPKv~q22?1l<5juPp0l0NG1$>}4L z_!myxkt1~>4%YuM!|^}o!m+7ZRs#yYM>y`ePinupO?dthA%?)hH>B9~mkfo!$b~|a zO%a7ZBNRHZDZ_RiHpK?A*mURhrE1%QQ)_O*`Fp1{IBQ5DzUQwH(an~a5{`eC#I1{qCEX`3gmWXGP!jfl;2G4 z+d<_*m66NdfN26DkrlVvOY*&Z!mfu%925Pnd-=pv3kpi_t=XbW2yYh9!Pc?(_(UN* zdP;=wD2O!ZUcM`pg$z0ZHuZ^H&+`UJF6!a1oXTn_wuVK8#(D=#rnBl4bEl_M@Z?S) zceX6EuX?#%mW|$Uw1RXd`y`gToSH=|A=0ulNEt*z?k%#f`2#EpPrK`iq)8{92`Rj$pA}c?BKBkBC}&C-?GD* zj!52sQw-%t_69Wff+Hf_V<<^HWXDjRvV&>EgQ*~`%pJ?BbzYC5Jk02~crldI*O=@d zW7hN-%BfHY3{ThlShrf+u*Oh$OZj3b7unnEBZk7j$V0;3c`=5PCear)hVm;`R2`KV z$_>O^F+NNV7ZF2AMd2dGQ0{{fn;AnW1v zi=k}1R*#{0t%9(p>2OzJ>-5Ma4W8g0!Wwr?{63*7TaYPc4>SaSz@Zd(C@@-NYZ@)4qBZa~YT%ie zia!+m9g=k{_&WZ{>xbhQMZfo7GK@YGQ>h>0aW^%pgC8)`e=is5x%-ck9%Cv`66x(@ zoR3z@^LT!SlO4h99K=hke|CAe}Ypo0ZhejT4ExI#}06I{7O z&QH`uI(Tyo-viZadj)Jg+2I^j5iJZ5PPrusE?>m*bc&j2U!>Zjkjd}KZsiY|D3k>6$7+trkjbA|JDA~B{zE9_8Z!B+ zy@7NOH*X*%WP)FG9vwVn^4(aC*&^RDQjjBL@|^@CvqL8TX@@f%k-Pz?kjc&JOcZd5kjZ^eZhk{12#E-pF#CX<{ar}rPM4k`lhJEM$VAvaMyw?3vovvn|B3uG z7QDxqt(4#RSTOSlVGncQU7{m<^F~JmUK7EA?K=nGQt~N={ZHh=KKB+pf`wRE-kyplg(pN}hHdcw9 z1-LDOEvJVxusf1Dllw)ew_`yZayv7b+=>y0Tc~|Is9a7ma@iZ;Aq_5wk;sZ$?In3g zLs;$*nP8Q;hBU++2?|Qjs@bATNaJ0A4i?gQH$G8FgPs;9q;a|QLer^~Vs%^I-69t{ z$2E**1_*BK&j9>s7~%LB5IWn^utzvPBFjdvKS~jf7)4`Yj?r{F34NONS;`%nWPL&Q zGJn8BVJP@KR&z`SJick|V1`flZ=sNDz~cw@2Gaf0yn&Q}2Y%66cJP45Q?VMe6~g=n zJbs=)WOl&g_zh;yV{oP;k~iQK@OZYp0nO*{hzR$9M-mU&0gs(_Fl~4+m7w850iqUWJ0v^}dJD(o#xE2cK2zb2G+J-gY!CT4~@Tl3_>m%U7z{o?w-gz zHQ@0kE2@r4z~ha?Trpvk94;c@k&41a40t>SBQ`VOp@2&SJU$5J<~QJhkcfZ>vk%CB z|ND^4ohv;99%C}#fu>OU4iRe}Y3u}lgq#Iu+Bh?q@)RF4_8;Nzle`Bm@sC&Jjei&x zXCer~&ea-hZ^&OWI{alWI&lBO<5-9VLH+_B9o$tfa}0vG_B$mK@?8XqMM7Tc771}| zOnC^%QvY5qjByvl6NIL0;iZ^7(2)C24yCx`fYBjayJ+DMt${C51M?OR*|Fp48xC20 zJjtbD{NwBsH^&3ItcRey!+7W`l5nh`SIlgm-kne?>2yZ=({hoXyZQKF!f?nbM0)!e z=cAQzNuJS)yoIBcYiuJNa-hF($cUlK7kXF7E{5cbav|9qU%1!cd_r{5 zE>{`3><#en2^ZW*WW}xal01AO41b7%uu5FRCt}701*MT9&1?p#HL+Zij)d8W%{-RKYwsTbv^hNRuco;Zm_r4hqsi0VM;vRm8d(se@w{97WyJDdP~z-WKq4P z+pMV4yXY9u82na(=Kx4KZY4-X;UaoVzXRmX^p+~%65i6spxpd=OA!*`EoB0SynSDU zWLIzLxu&=D>dOn&I;=RGD}~iYy-=!G3bn>;qb+6-uGytb4NAywV?oiG?UZ}zXzC9_ zkN4KfeMB{_pG;oMvwUZ00e9AC~g_uY5Hh6UK0ByB4Qcf#Q=OFZZ{+GHp9{Q!I+rb$6{7_jN?zwsid*mud)Ycd^J8ia)Tsf}j~TE# z1L9U;VSKvLsKMm{SI-powmP1;q7M8Yg(i&6Qk(X2%(UR*$SKTpWvSLSb2VzEmYys1 za*H8qUW{@6riSn%i2bqPL-;4JyODPseNOLZ9q+aJy1WQj5M5oAwiF`&gdy^?xe&=G znHnNLLx{99HfK?W^E@nyltLCwbKc$BkMjBSa>i!1=;G!WJyEzhPQfQ~bI_C3&7s;E5BA_qSLeo@ z5b|PR?x#rJ2^=pGzsJz&I$*pX*K!^3Suv-Vg@OZ2>w8>6i3X4PZC zxqww?%|llJpCwDs6~G1P;w!{i4`5NCHS+A+yW{?u@NgFV*Vbl4E4_Nyg3EWhw?YOl zI;+3YaCxJShul>cmUb|VJuPc8q)GKpap9~;?r)@lg zosdIf5N4o@O{{EJ{XW+Kj_${leTU>aH!OpZEq<|UR=1mS`L{8 z^6wIy5}j@XR;VO*NijEEL8sx**jS3e&n&5{ypkV&5C9DccrsbR#9!#YSV6!IyeL z@56-H7$oVdKN*UY297HM;j+U6JPuN|num5+%8&%#90#8SG3nVP=VNxPrNjC;YTx-Z z^S`mSVfDT8mJHwP5D81sC;n#k>Ir*`eRy9PAbpZ&m5}R!E}pgOuU*64xeO>M6{PBpTZ95Q+J6m9hM1BiC+PK%%$)%}*WMO&n*glB$-;G= zT8(bBLj>c_x2JG|^{M|pnQrexl zxl-%Mem0_Ep8Uc=8&V^G9_oU@^D8k`0s6;xy`?vGU(6A#+(K}p2j zo(Kt;7o*X#iGr$+-g?5hmf$q74Hd0MAbFyyksxgM4`H@Y!p< zU&8#i+(^VjDCx@AdiA!BSKz4=g)&^rH|-S~(_yJvhudIj1fbJf?DZetGBwhD;PAam z=pPzmF?U!GN9|h?yXWjM3*G+)i5HE=h>w6Q|+=fdSVg4%W$O-vyc2jZ{Lm?35&r+?v4 zHS`MyJh*vuvaktBadlYzuytJgy>36-su50t@c!kQ?}0(-Z^y}oxt zvjv5yp+==bSsV4MS=|INgc_+Gy4rYCYHUI|e7TQFB`A9T38VLCbJ5#meDpegnvo+V zLh*ZG<%#(k^Btrm+^oBF2I{)NVY5Eh|(Lr>iCZZ%sRX41o^>)AKd0cIyisbdl5xHHiBR6b__^4=B*Lke#vCEo zih#3a5-&ibFmstikGd6qlZ8H!3m<$AF-}OZbF?`Qa3%dY4y8EnF)ZidJ*17|J$p$$ z7K8;4^I1k<*QlVF^2&*DqM%Du@K*sHZ1M9g_(V}bdP+nEDHJs5UBegqUabjN3?uK* zxT?S`J5OY0yT#u+l~#FdDYOb`^^VY-l~&(4I7N!#VF=Px>(y`z4fgScjK+jOmuy*K zU*_DDWusRuPWUbjWr>x}C#C0NawYwv6rm(9UMKsNKT4x;68t(=6N}QUHR#L9t0FQh z`DZ5NwOEXiJ{rkoGII0@+>@3&H3Ee^08*)$HVmN*)*7Zc8p^aJOI za>JP|Aw_HheZFZF3l_=ic^C94)9a}KOL#pGK)LzzdLk6U>&cjwygzf0oayzuxd9=q?N9_UC4xl#*F#x#% zIZ@y;ViAHijti3}F6!df z06L~FZpSB57xc6+>f%yUdK7285^QVq9Cu%WJc9LL=eg?*S^oeEWLp-*gKOiDd!iRtsGAN2$0_jAK(3gIlpn8s_1r5Jw~ z`jn{{6<`U)_+}_Ke~J;I5Q>ovH&TrELh?ZKC{Y0@MmdU2nz$&&b50S8@mzc&#Yj&J zqZlvIrJsytp)4|geUIg zRHO?1m+{rqetTrs@@qeZmf)pWO-%clvyypp!d0vgXK1_qP{>u=?Q_hF&aE^QES6Bd z4&clb$_lWAP`(h#&7V+4D1=aETtGtkB1j%cD2oa}C`;zbFO(k!FxYJ6H}Q#tGCeJf zP`(f&BPi1<)%Jlrve>$1MXz)K5qy?j&$u#8OQBy38^xgAs4s$V>yw-@d>)K4BY^gR z$VX+_#agweqbwnQ*O*^f0K`l*h3%h9`11=})%V~rtR^OGPfn%he9tt#W$i$Q?EQNv zJ#?+)cJ_5-D=?+l==ngqnOq#eD^xIAqx??*&k?x=;tL{+kj0czE zqWeuT?~~ke3wk2C-8P;M? z;}aAUNJ{x)BIQomv2>)^cK<4*1b1LHG4twqY;9*BT*J9SO@CKwRq>C?qFKQ{iY|*+ zv!8Fb_A|o@yB`X{qQen=(c{wHq9J%P+jyU~4eJ(9-jcD!b2$z-(HMI%+k4dBULPAg z85l|(+xFkyOUTj8&efV=ND_&C6x~_DU(F7E#oD0^B7KqIa3<0>t!-F|#9PWoq#xMZ z>w`!Pj6R8EIqY&=+a|jqF4qO&k{?VGE`65OGD+V`{mXnLTyJf|NxCR4X-4ko8BgUa>3W2i2IUK z5!~;p#VtMeY7666ZPYRoBAiRW?hZ`>0EW+3G>BUN8-!f$Nc696W#=JQm#V z%*cQOv1j-gnaKh8Z{gs(Y$5(HwuP8sRPG>d!#3ftQK)z`uw%GTsm{!JE!b$u5#OFF z?A-2jXFIck*m;s(sSZa<-pH$F`WLs9dTpYuBs42C5?9d2$f1-UU>x%PTpYr;e7*+@ zopH?yAkq8a(P6vKPybueMc69;O?$7z(H}Bq`*bd5;~S(nvn>Z^`vfstwwgmB#y3bY zzRb2aFe~3ru5t5w88;jI$@g7PcMY_6jW{1%IU;3%5(}a46F z5<8?oZP4aRaaDrC=r4&+{ zWHKIZApIVp$%*k8M)NQp(g7Kdn;Qo6<-J6QZn00Y)XM8i)wb7`=cLJZg{k4w$YNND zfAVhsx{$eftg9f|VfB|6mGB;m=Xe??xsB6c@0>*=a5F7((r}Msr$eKOJi$#1=QMi~9pF2R* zXGVM!bcu-143wMah!27SlT13Nml>`U@$n(qHR7{7F=a}L_y}X%i1?s+DwNKEwMcap zJcj%^7JR{(O>vIR{07Iye1-_{ryP7xD9Pg@$BpwMZ${u*z=!Ppd5`yK_N%CyB^7WU zJW<#|M1|uyCt31DVJD@uyx9)i7{>j(6NU3wzD7Hg8P8|@P^(SC$$qVg!Ugo+n3>@@ zM#$I_t=2np(_RZtE^T?RF0TTfu?9Ti?Y9#c)+%*saKKDpy?kObAB@ybAA*L zf!P@R6&5<<8oX2MZ{e}GWAWxWDEJIZEFw#AVfGB-7kex!_jm9wY>53R*AU}=V?M+% zy!HbcV%atYY7Oo;W}`9Ne8a5VZ>){szh-0Dh;Z~1UXz;DgGCf@?mb+%O6x2V@ik)3 ze!^?3PbZB!jDPY*LwJWVtTP1Vdkd}T2KVqkcF^Cqz-ZoSqLi<3QDJ_*`iCx;xa%73t3!_PZYACr-cbw zTrmd*N)ZeU7)^BkB{OPr$1R@QYw_AtdP4&QFHTLbaXh)%(_tF|cO^LFlmmdg=(k$A z-yT|cg)AGrVX?Q+q~k6TUl`5G$AS!5pG_RqWIyvq8WfI#16WNg(y)wEJM&mU;j2bx zLRwvYZ%piG?PDE!kU0}p&?RQVd!gJs&x8>aF%xEprHR;0h-1)tpt+Ja)?^|XRX&f~ ziV8poN_yoNg1-%5Fd_Ipd?F!8PYWXicOUUeE!->Kr|{d>Q}~$;3?TGQGHV*g*F_b0 zpXD6V&p!80%d*jH7F*z%cuNSnb@A^^z{QhI!S)xjKlufl!bR{EtR^PdmLoiDf~BIL zTA~VjYKd^#6BRR|e9{VKhC}zCppfh4_J459i!wN^+lwlLzXrf&%3uXuLI!^f%FVM3 zMo@$dW;{SL_!E#kkPH?TfDD$bm0t#L+$d!58Tdppn4YXMShX`A{3-yD;Ta+|oA z&LsDW$BQJk(u%Fz)(zvhecG~N{G1qEqLF>YkggtQyP}1^HY@iMGq)UgQC5L?^<%*# zVApw~N&CdN$`Z6se0$#;pS|duUi-ue8QtjY9-G8jHgku#0y8??pR}i1owtXx+|oVI zK^kmh_%5?Lw`mcz3|qn{GFXi9jM@**AvLzwE95arWZMkBCkAAE+rgmwz0a`sLyVDL z+2r7$iPL_`#Ob=zw$po`EPGTr%iQ3-&ZL6CN<7)bLPPc>9SgRt-3kjq9jl315YO(x z1_a=^wsL2oml@n;?Q4b|awimm$%yOr?)$B6SeF^_mhvq#c#plkK9(6UFq9g#ZSTIF zkfV{X|78ZBws*v~%;4h;3T2Us3wyawMZB7v|3zzq8Qj2{%7dKVdC@9O8%m41%;0-= zY%Jh1gJ`2wx~J|reS#>S?Xr!nKuE=Izbg>V?YZZasZ)7F@NEEWropd3OBnpW0_7YG z{vPO4yG;=eVeK<{P1gP^AUV_8kL@>3rc#Le!+{VZ-nTrfOUVaW1Vwccvfk%hXQ+W}v zAgrK8X-o0*3mGCW&V@)Gn$i&Yd_rWloI}4SkBTy!XKNeG%A=xM1LrbI8Bx(2uFt*c zkPZEmSSZ{VIot6D*lYMohRA)n5NUEGuHd+t^|RMr!>gTxp}l%6_--C+RNU2INSVup z6qCCUDFGqHiMts7@^BY6fZWAZi@{+Ri8~=j9B*PI%`!f~1(R=P7<*GLjG1hNdokZg z7z=u_(YAiNW$Y0Q9!5x+CPjyp!;O+0M`~#X&2oP$SC9E7!go_Q?Tlfv6Nb6p;ZTYT zD~88B?1b1?>|`&=dc2+rJnKmMM^;SLXLB7K?cC zV6o`pROE+B>MZdyz(=&6!zu~ji8;FXq-bL$G@w&e9*w1OTC-F>P@46+G!el)Scz=N zUkYeY&74piyxiB`3} zFvKhFyV3ELM!6GKYi&3?lGw7+jaJZ}?RNM6F2DVK{hnxe)~kE1QrKug`C(eD8J6an z@UogUQ-rz?!0QNq?X+s}wl-{(i_?G#{0hwt0bH#k-ROjwMkA~@LQng6&C+OX+bf5# zdbW5NR)@`XA!S`VY*m}Za$|0ehRhy7Bc*sXk5Y<qvT3-zr#RBF`$SCFFx;RQ$; z4qK(Nhn?!)9j&X@o1L)e)ys_v<=KieRXE9@ z3!`-upP-Sp*TOtNVXUnJBwyVs9g?rWqQ~Q?0?w(=0wc0bcQ4(rIa=5Bny@^J&Mqnw z!q#BQbO~U`!4VbwFl@GWZQFL}&>@g&q2H)aHCnUVD&E0ukh8t58=Z_L2oNX%if9hV z1X~ecZ;98OJZJm)fY)`cnR2WdOi|68E}97&4H)KCFPx!f{BW)YO$>*nX&6J5(C=Qe zD%uDfIaic}Q-WV?gP`)d_e2}oN9tkeaM7>M`Zf3u+`GKiK*nFwo-4sHyR8GGx(#SL zk>1&BvDU!TRS2;g%YF$45RjdXHo!3<5K15DG@*%gAd$)$>mz3bfDTI0V5Kat6HyAf!)I*Ec2iBKt}^B91jfvvxFcgfe~Q+e!F`+h%h6y4#(|MGSoIB zUsm9ts8IS!Ok~gF!73QG!4UlTm-#OY?pqFT_u|jwF#K%Ap8_1z9c;v(=RqtsxDbCf zgIN=tjX&Q2Z5DhTf0kfPzl5hwG>k3aP6up{wXw&)Y!6kxhpOH~)$XAx_fU0vsH#0w z&7O{bCKe>P*7z^8uJQ}LDLFIPu z#yRwD*xikhmtXax8DDBvT{J)HRLn~j0Gva9C7VV0hx|K&2bTe=9>O2O@B@hONAafx zka0~H?-XTl>er$E`bC!{v|uHt*J?X=P%p%L8en($l<{-esXxb`$5z14m+{A22|oe; zd>enB#GeON!OuJKhZt=;^4%W%u^|LAI3Z-NKZL+&t>nQ4&o9t`o#1G=6}vKxKZjSt z&+Yhg-&**2JN|rdJ^XwGe;zv?e!h%9G|KQscRDBEiUvGL-w6$U@;lym)jYBb1Roj+ z^;z6Z6f-mB1O@Vn?u+Z_!HIYs_TxtU!MmgB((3@P1r6}p&;aj23-F$_059L6TZ00; zN+H0j7MPI1gH>oHh!qjIRKy7pjsEySEH-h*hA1FVA`WKx5{P6O*hsxult-wfi{Lbn1S!zwXmy)w%DKTXbA3M1YQCM1(?Rq$&cOi3K<# zvUUwKICX6?W^*tJ7W2o>bSGM?n<%7T*Oseb!lQx)oy$_t4i)*BBG*$`K2Yyq(#%aa zYSQYVtS7}1W$t04#$nMa6*|GFA9o6dIEOhoUKjteBpLxLx!mZWCEJZwmm9Sjm>$pv zm?^EijHllZ+WfBW^BiT zw_jldXgRfuslvslWoYwfq0) zeeUD*x!p4(RW_+bKC{NZ>gszr5Ac@>X^t!6t4t8V0;^xB~}+3@R5%dMPn=e#aP zgIH^hdZ$qwbKu9iL|Vx1+<0xv1^J?`zbzgv3x)jE@mi+GbiAfsYk1x1&Wsz6xKY%u z&2%ErAN5^btA?oCx=P&*LwE?+Lk)MHUahZoTJmNFL~VPs7^V}ii`+SU)%CadWBx|} zB7e2NJ-+Fdsbja*qT4$&6Sshx$HMB#iK=&!{B129yPK521V|Yk%K&4t9fBS^4z23U z_#0?}a76vB{5~GJyE8N2taj=kBSCFcdnB>h2ZHUPf=&B7P<{VJptFB5Abkn^zZCvo z4*z$7KK@IH9uqXUKMqn1sS)ClQ0D6L=!vIFVj~h1o>)?)lA_ZkS!jTH94ou6nnSha z%I1Y#p@tPG#CgH>q{sY&#gICnAhjOKYehU%YgE0{iow&mkc)Y|4$QV)BbHW**RRaA21teBEe%=1n?hooop>kq2Yqa zjX9?|YmyLaHpVHpW?E>_D*>R_a04y`%i0if12+<1tt466<&znc6H;+6Dpx- zTs;1likXEMtv67(YrtVvQe|6$H=FTIIjn&M6u@~zKg_z-X;h+) zo9HXVE2ntgaHZ$^byKdbbgo}zxsJvwokUWfQj_vO!`%=x{n%^3ZU`RXiY7CS@c$hE@i%JAP1@=&4G z2n1x+UfMYIYi$o)V2;y^DuN%6xlP7E5sS0$xyYUu&S#}#0zu8uh>D4@;^A~|7mw&2%^-gUH_SQEwRN)7sX2T#nE{OFVVE1?Vn;Kr%2xq`prMNVTZgq)#_Ru!zcyY!+o{)ybMl`-3x3Goj`nkCC4l(`EOFvYs3BFFjYu7@ z@a{1VIEK+H&b8f^ub>&fPVx89ZSt4=!DS$AgJ&3WghyAC?w z3tCO^4j}|Tj!3rZ?!wFh{vIEvPr}apyxU$lP8>+dAB}GuFdyl(22aW#3+_zR7Qkl< z8IYC10z7Eoq1jg2nypDSxntg}t1qR*zC>_uBA0wGh^BQ9F)bn*6zOQZYFu&E}c1N^QuPefex)M(0?6wDsID> z@to#N1WlS(rPT(lKdks34ogeBCP!GkfB`-9Y_v>WABB|&CHDy^tWK?x2+C}CSSFM{4&twl*og7N`WY%F*`{vkwz#dw}s zs0zw61Qc^6eJ{<5;%BY`k73a5Y6fp283d2R9|7yHj?l(`Y6M&SDKhK{~gGl0p zkr8rE)gs?H;Vq!k-HDG6R(Fm&JJIQecrnvF9afrc53haCt$Jr}MR;_ZP0?iD~!kq&< z52K^LcQ*`kT#yPfYF4WxXtQy!LPR1GH)fk8AClfqRtbVwg=0O3gPV$cAj|tXM0PCr z6Z~Uc-g+e%hWLLB@jElY`;`2d;Xjgoq!rpvmb|Q%tEl=fspCtH0k?q2KBMnG{61PW zzWeYq;||=B>cMY#72)@^n((_BQC0AXY$t=iKrkr z@L9l;Iu8z4(t+^j!in$)%#rY0)tT^T(xLDtNvDF(M?ePjD?AMGE&PW27k*E(fjgxj ze?$xN9|{#D?Z|L1z8(zWd!!((nh9FwMVblpOMhmO!NXLSZ&(CnO&0ZbMP9baD0Ibi2_!hPDhDFd)>#7G=py0@^jK(h;1VL%vWXLgI2oE;o zC=n3M7OmHVjR4)H7O|M^QIw0=9+r)?J#$P}7kH0f^tpiNG)Fu@DiIw;(@}LQN{Mn( zFbD^V4(IX`<9Nng1+N7#;&m|AC(KoFAKntuQ@LN)fKfBffxH#R@e9Okaq3|^qtW}C z0`NQW6xt6?C&&?o{RF^x2GhaWGyYd3+|8UdVdB4@_i6c%>W%zCd|gUHXeQcG8G5$f zlml}n<47vV+vEhmVE*jOQ7o z`FQe1HAt0`hnhkaPIJPm@tv;u7tDew9JNZq8z^CtM{h>>^8~y^b7I0d-%In_1}uB< z6fF~JCz~{%k+PEY{57```9R0%duE)nCI4G0jg^J`%$z~Cg|G}VECk;yj)kmWt*4YI zvn8{THIQ#=&cK9o5ew-Z7N%k{eO9m$Is~H17oLLIyVz*jPQgrM!KoA;t3_-ICR_D! zMRnSM=T3cT(?rGQSc12L?TTrZUL`#DXhgJobdy!V!?pmHVDQof%Ru}I`l}gq3wJ2gtz5#vgDSdS=IBP~lD{z;vX-f`-);a~hK;0t;q3A0Z$a=$EuD&yq-l|X4WSKY{A zO>yuui&)I<(6TCG{^>G2^3H9Q`7*s0LSpK%V2uhMoE zx9%sGf#XW)#L9Mc8F=1e-DBlCO!LB?0iFr7^StmYlHrB3&0Po&~Af(}hT47bW4uE^T254_AT5Y-rw-*qt~G{V!+w_QMV?al#ew zycF0m&qL0pKcqj^O4Q@t8V?aO??a5prbQo(B-P%j zg=CYA=fb{3FN_@bq+6@IGj)%J<4gPtk?=7>J{J5Y{wcbqjcJQSq$lhBBGRB)Jko^vU%xt&J0x7yhED*~CEs(W9Iyo#T`&LzsXs4XJ$Z*%e zR)>ss9o*Ul3+=J}!QENdCQZyNOyJ*8vA)@6o4jv(uqh>sz?Qk)X1hzCJukDY<@TSW z7=N}9#=5&iZt{-^#y!d5<4X+$S9R$#l4kl13gOoaL8zN4LiiN|p_Q3Zw2PQ2W{_v5 z`;NFZ`25Gz$USbo|8tVRm9^61GH0MS>sZLrTFo)p zWCZW4!khwi{kD@4eE0wn1#PA0Xfqjc4U{3BjJOuB$Ycb$N=`;l@^sKu!3Si=650r_ zUFO?giqgQ>^OPkbS+LHOPqq&VKQfwLhcw9$)S)RfJn9!@}uSqvPnIbK?a44Z`6^+ z;1u%Hi4^m_y(KxCrjT>l(Wg>C=Cj1ln&r#}l4W4U692I|1BLvw1j3#rPUDcz5`WVy zrU?hV#WNarBB-Lg$`a3$^2@gv9u`^RtZKikEb&jxz$q$Pmha#!@sI8B<1BF|gv-bh zyC7~ZODu&;vBbAQyv1dSQ4-D)(?*1FnFryqEla%pu*wp9ZHM=+$dH{kh&@7yRx!9_ z9b`QgT#SFLnIGEYK8v~w1jQXp2OCECKF-$m*hQMx@!-9|1UtNMAa?kAS{Bz9DhoCO z-HnkbJA5TvTCl^vVapEdoil^x#Q3yPW^f0JMVaDPS~A79*(C<5hYO*`2CBCcsPY>E zg*8?HzSSZYHV5Bxm@=AK+$Wf`#G4g0d>CA$Zopg?}S5RkgPBD?!60n*A|Ddt7&6-z45UYCzP z{xgcyHwz)9n;cS?Un5BMG`VGNGR0Rz5$1Q+ z%uMlK5jTG08Dfe##wm(;TdEj%i<$98v~PqIzD1~2oD`Oj1UF+cXOt9vtx=I2-s~QT zWJ?PB<_xkqJeENQDU5GaTO6DeekhS*z7MzL$_kLeuTKG)PYOS2mNOejmVp&1{JZ81 zij#)+q;MLCd{X$6W-(1T=vf$}itycvvKbv#R~FlEU9H1E;8H zY3PHK!r!vPkCVcg5H2Grd;rAFC55GMDN=YG;w>&IjFNCtm^LDW&YXnDYzW?S>G0@b zP73n|Qe%VjUW;}xSU(JzjRkA*k2RG;d(LM=Uw{C%nh9^i{~j&M|8iV;=S~p6d-e^! zEO-Y+|DrN?U7s90l@q33Q@oS-S2IIRM<)Mu4?RJcB?|c$e@7_Ws&0qKN4}AO+LLAbjY7n(jpp0{mmpL8_-Y*^;(GpWLIh(Xab%b%I*ZdM~;VU_bOwGomTPt#^&yoDiqB1{8i_4q=BW;-@ z#zL0XYL3ZB+tos!fF@!~+VXxskhX$S(sQ&SZC8wN(sm_Y5z>}iEe&ZaS0;po;RG zrbSBkT2g-b7Q@3LrJGglmzC1}oEbPpMN2~;oYMWY9e$kB&4h3nDczTVxVeOX;E{oYJL@2w^DCg~x2D-ILNC6O=Btm)Wa)jbTKK82lKs9Si=6<<5_N$gdZY z3p;B%2?W0%O8k0$)%G<-8D6bh!ux-M=A~bM7;Js->J6kER^p$cB-$@gbxQO8AGoxj zd0%Ns^V;g2!S$YpnozFyPCKsGGBY_|B;C2uLWr>e={XR|^eG!@PH7)i?^ zza>%#UnPTI(bChXwzKey5HCf@$Aaq%A^1_K**P-{r05p0Kr9oqK-LOrR(PU6!goY#pO$)MZ$Lrz3xTZL zC9W5(5|De^rCW#q?gt5Opv#bxwA43H5Elx8s9P%XwxT{LLXrue02fHQzVqmfor^u_UP*z&Y$_ia!1Va6W{j#w_mw`y8ffd7#vqG5= zE+Z?n6~xVDg`{vPR_NzYgBF(+LP?2zzS zr9nRX*`}bj3W3Uckv>ME#7`YAEr_2h%*2nX60sAl3m80#`BCp&!l1d|_Egf5y*J{# zA1kCM>mEKrlrwkFSf@^bqG4|5WH2C4-tp2CPHJ%@6uzGy&+)=|{Kq zI11}o#BD72F#ai6_gIdH)vLcH*o){79@T@gY9>nMY=1@_}?hU!Xwz zX(1r=`hw@Be-7eX)R&_L2KmKwhvC`MBrc))7KQ4+7lKN!M|irGN5p4!WzXVC?!01Gj@yz`QXVmhJ$=ANl3->I97%;Uw8(rX6A)mp@tQf z80Q7olO79(Hx+Pi2-Jy9(@z<>~U}pIUh$mbw-L9xBDAz zL7LVF_Fz~qY&90^u%2tw#O%iBAOh^PE#;UKT1Y~|9Jpw=+czBHKA{OW2gmM|Yt<>} z_eSGR+dZ}afLjl}KF)1N5qUp3_uO_8NZ2^(-Mma#WA@r2(@{LviMVITOrT4moY}rn zg_dKXKh$+UB;?Q5-83whx(9b)GKpdN1vw>4I}yqu?HkpA?5#YZeI0wtwGsEaBCb)9 zoTVrCLnOe@>g4UFIfLwFFf4m`P;Q>dqqs*n}k6r*cz>WNVv8-r(||*N|it(-{{2B4szL;T5+Ta;@9TADB_gmi2=) z{}u7JsuxzuVQsG5YLK|4iT5hGi7&r#$PvQya^5|a69#=d=kIPtqT#K8+KZ)W;-f}^KLtOpwo=J zu#0CN247WWD^iBN!{QRIEDxiTb%jq>|3@PZD)yGOC4I7b@NUQ>>kOtJ!1Dq;Tc-B? z@@%~iS0{CupTlz}s*raMj}$D$v0VxAEI76?LpP%<&)`8RIKM`l7(xqO4Uf@x!ced$ zzgAXTM4Sq6?&{R*8^v0*QmDOe4Z$T;K;q~#;;w? zEa30)arz|e%tK{ZkSDFbe9(-fr-%nn3o6eGH|s5o#tOw-lX`LQv02wwN=5ogmn0HR zxOinvBf1C11fe9*%#OyZ#vPd+x$7zCCL*VXHYBtGg=7z9ibTR|`j;@@DWNiSjD3c> zdBpB7V5(*EKxfq%@JCTn_yR+rvS57A9TyeMgjrZ?~?5S2ne zFg8a4;y3}qT-g|#LoqX(GC%(SrJ_h5A{pqE`mY3+NKRFE@E0Z}!0S29S%-K}a+KMr zTI4$?JUBdN!r6(B4_0@MJ3GmFGI+tiXN%Xqhg=DH+6LOB;)YSUb9}_vd9qgZns7X z6aK6v914GubSn6K1Y|&Kuv5OCkEE{Qi=9sK5 z@QypoW6@5X?aDwt(#424=tYWE&(UTAZ2J~I0k#9L$OIUcEQe zxr`#y4_6p?fk{2t#M(ePU(bT>cau@hDe}1PMR+=#J&EN5R{(c%drt&Slo znQ40`T>Pq6`0m?}^5h~#U_hhqOiy$ZY<|cj)^)fVvUuBk`-B&g@hhGwL z;|I?~bEp^;X@}k;2Fw@am>H5#jrbX(uvkK`{HNOJCZ zGbB&G0?Cy}y5v{)Bl+GzB;U+2r)@?O_srL$aq_XQ#hv|Vad03lIHEaPWW5G05|4E) zyneKB2hn09FAHp9lO>Vyax~T->heF>kNgV*$j97qw4H|sa26v=!KiX@-wiu`Loiu`mCMMh+=KB+_M6|`|mXkGG6+xqBN z3`3;8`xV4X^3h3B_KcUzuZkGD{FnA4|HXsIf3Cz*jHi-nkBKT z^9Xs|PS$v1xwxIbgkE$mw<85haXWts@fMHUK`A)5Lx)_1+xY@K9*EoF2>`bvMsj+Y z*l;_ipxi09a~iJ*w?nR$hTFMO9RZ7})FLwixkrBi@uSTMh|~w*b#`Qn%Q)3#;6a`= z)@3sf@^%q7e#7$fAPW2m7UcX)P^l9fjdR}WF7Ew8wc-qjgeCZ0OeVpA2*RAq1bk9( zH~RC!g!_LoN}MwS|0G1RZ7Dxy85WmOXF;%BPWb~MXRcG0f~B1D6A*9loH9zmoieoo z;*@_A9uMS{c>>^+g>e;k%Kr<*picRxctxBtxmp^hd<{3b7j1QQHmBr zFM!+nk^knx^6P#ouS1_zqRZ8GjDb$MZHE*r)po3ec#GF|pcK6AphivFj@9sZptgf2 z0Bwh;D|(sObm$)eF=*TICcGkT2e~q~9WtL%@M2h46z@z-po-1y&`zW`wkJdLP3P)9 za^t3U-0^ML)SlPS94m{ni0FoH-iyqOup$*fRJGi#&Ka4&(K)QNvnMFk%7dn|1v*{7 zC1Q|0?ha_&?;iJ3uRZP@m2P}_kB#mkq_1t?mcqu5^fz|k30rS@7ZJoN!irmmjaP0P zny^DF`-ZEd?4IH&+I4ccM6jMZ6rNWbi;$wekY#IM5=NcfH!y9Le@?_bJ8VqfIlnDc1{|b}H##Wa7K&wq zV%kG5K?%Nv$s{^V8|Y$jVh2aYkkKjv?Ox})XmebeNoYr8C;9AieU8kXQO+EME`DA{*H#bu;WQ%Fqp#FCR zoc!g=>N2>jqSrS!8_#L0me8wR4+C{rM797TcM44$HT3KDb{MoPA?ag`Bec2 z1h(j-^Y0X90^IL9!(C-6d$>Cx`feCs;eXN7=*D~#_48m2x&2ouU8?{3If!S`f9(yT z+)0h1@J=jsG^7*zd3c=Li7n~hI=eCDAz{B?z1ckBbXsIT_4MtB9o`y}vm#w*u6ELE zIJz$yI2~cd!K=}N$AVW`8!|w^_wk-3{TvYg2ov9CTkrihwdcaA5sIi&f<5ZHrkq!W zUe$@34t&D_J`CYhYqPUn8+Ji5;KNC0-yWN@PBXVX_V~ozJS~C~g^GY}cBx<_y8mKpjh z9dT=+7fx}|#l1x8zfz>WRtPEGpm3|?mkClmx9{1W5Fqp^enP-sB{b1?`^D>zQ=dxr zJ3{K&V}-v@Bw{uJwtEkACLjJd7Y}x+dtnG21-C((BjX7H>pk|(A^{!vb0unOtue@xAZ{yQD2+d&ZqXe(KEA+9lW0g5ydc3&(Ve}yx=^} z6<&x}gexRhDXx&P7lZBtlwYyotjSXHFg%X|c$n9S9xhL;M4Bx!{2ph$sdBW?^4ODl zZN3)mzk1J}!bc2PFfXjlvj{1C7vX63LfN7R&88~Nb5|20?iu80IQXflEXl;kAap(} z;pZ#7H<2f|r)| zOfR#y8THF4v$qhjtQohbjd>W$jOA3U%syn!ua7dLV)R*N!@?=23TueG;>E=7^a6U$ zs7y`){pVr@^xuqm7z>Eyq!rL=Xn(j)Z^0UiIi2m9L zmFhJXn-C4Ifu3sCCnJABOe?d;!6T;=BJYNCw=2D(gx)kMEf_RHed7>!Lh4vqU zPBU${3FUbOJhr7g<-{ECaAZ)P;uRd*;&93nn{Qqz2TVm&=V0P|5Pd9o3I56L;HhSc zBM3SwIy<^^qyS1xd<(ks-YxL`Dav?~{?MWmo=W@9(`F1K_VUM_y@auXkH<~W$K%dE z@}%v}b?R=L4GG4bD`~i97>R&akwC%o;(>zkhG#SyuOmhvG<|~P5<~Nh*2pQ2stvJ6& zf9X};krwaGt!|%4{!waeoMC6jBEluxH3e+QofYMD5fqIAlWo3F4(;}va8KCLW z4vRS^;~07K3cVg`zqTAB?-+^hQYxYi$M|88maa$q2woA6kz6ee$9U^Jw7F$yGodz- z50265Qs-^lFG7B!SU)JzT%*y&< z4WAQn?N(o6=rT&mhzL**U%Q^pr|?o8H*7gbwdgD3JV7 zw0WB3Vi7lfXt^Dpj$6vnZBD*t-(pNW)*ahbLZ9M}O=1yTiOD1!+ZxQ6G9hy5s`5}y zc_);PF5{m?r=}SeRCEAN*zzgZ4-t!z<#0$(7Lu%X~_~v$&4)@xaR_sTfty)u_3neD@WvPQ?}%m^$YVtuh2t_iWlcaNG3lPbXjp{ z_aGCrB-tYKq=-Sb$dvlt^WnvpQZ-+iyC#-^(T!hB!%gIXMo~yKYf{YkbbsESWNy7P zhK3ev5vb$??unVwld~Hik(^;S%(xLPxfO;6Ye??encH?N;^YTRz_l%kj^j(!hA~)+Wb5xR#brilMG|5K6w8=%H5Im2` zB)SNN#y(kTE$bxV&x}B*Zf>zB3FGZX^UmBd@y{TwMfZnqnaD)9-}%GKdTyDZ4nyph z_zMs=w@V{MOLb}f1manAX?nmXH%p)#ygx$?mh@+ygU7l3nZ#y^oq8QprVVkI1ap+M zYi<)%)WzV^3qXjm;1c{(bPn*$wq78$!=%of`qR%bI?~5ta{_Ayr4Hx3%yqo}DfO39 z%&#wmIiu9~Vx%31R)h9e!)5RJv-qGjrB3arhr&RlFJg+)5D!yO?kEHbqt6*AICFCw z0mX_wr})W^d2%D3GIOJ#zRw~9MxAr>gz*MVAjSEp?j5sZ%X7JBfQX`j)8(`wi$ai; z4pbt%A_En2l^Up!P8D08EAuG@A4doJ;zTPU1}da_q(>>q@O%KFj|hxI6zsVhw)1bf zvsvxbJ*U!yli6U_i}yHE&!u%e1_*$4Jc@s8-5eQHJS<|6F~yz%#uQ_QF-13iVg5)W zF@BID#jh(qekhk~japPL+YBGr&Uw`&o&h}KElgHX03NS9{FN*lV?hI)GV#>0(8SVO zZsmkKherrsa(@@=ODY%|7bk&21#&)M*B{5Y zE91?TP8+wqmD%&@cq4y84oseo*N0vd;W2_;IKjDD=|r`9I0>h|qFGkE@!EUv@Z(UP zth{A99-i|W?1X2yA?q3R6xE*1)opv4WHbu=!c?NXEw~*Zs$kkrx z#^=m7n^6M}P*z?ZS{ZK&y-EaYK+30KLU_IlPd0~9yVfdKn)CCd%1i@{fE9#*vlHjx$#B$mD;_G9AYqy*2GVJ`Q*1b01jG|VeZoH-0iS$&4NGjcUd*V$ZiQwC4)|+X%QU)ITBK<`o za=aCF2mvS`AgY5--FQ>oZOnDRZUONs+^_N)-S}L(7N=bGT3{cL4V*FwIIM+gGz)41 zBDHE676;ANPD7Q4Z&G)FdY6ylRW&fGP1VkP3kZ2;cmOPU06RCmcEhb>f?LQFK(`TM z@ykc8(Cbv2<&!$1jRYE>xm4RE5mxCIm4hP4VTr!fbH zNSD^$x%KjRbIWVNj4auLS|NaK#3wT@$c~jGcKj%6g;TqCpE`94yjtWp8R^SW*P6I=&sBiy=JsqQkqthP*<8f4iJDEQ<~1*x zCE57Vd>yhFj@%ijL)FOd9$Fu72aB99i^?g)FNff$yzX1$t>Ho=a!;52+MHj9|6sjq z>Pfw4s3C&3V46y4q1px?#wPk^|K-8nHE=hHKda%h48aQg zc^h!8!4vrNThN&a-iAL9VyxHW&j<161Mt&{hcR7TrH8rDl&Afhr%9sIB*AHt*fdFK znj|t!5}5Az7h@D6$439<{sa-iKj6>#!|_@QfNTVYfCa*lnmB)FPBd*kYrEp1S_n;S z_X+G1j$R1rZI8X;O+9`^z-`rRT6%0lF)V5Eq18M<2d4fh|KXsy3P6hRhah?t0(=C2 zE&$2+H#-takpNbD&G)Al7$Y3~8(Qne!ZZB>RD!A)xTwl3{@k_}eh%Z$_7eNCNhlb<4lK@ z`py2VfX|g!W56SDAoypM00{g_Mu1<@2ypQ@**y^80)qe-AOtwgMJDN}LU86Fwc z$MKVu`nCSlCnGRx-5E;+AH#x?5a-cBaiqHfq{E0O3~+c6U^m~W8V0aZZ8?T0pbsAu zb!Iy8CbiuqjmD-*4f>_n>Oez6pJ3;Thoh~N`hvm)xa)QtqKRh{Uwe8wLd)nz@F(|d-M&}5k zW6pd>mtYIvPK;N!ZNM+;x|`y`s%VhAJYLD#F%hq5Hd;=1qBCj7Lv|Dejmb^~?Z@^m zZ`4C<+p1dA4ns(U=Yf_zL!VaHJ8k(g38)6n6z1u~t0H?EKXu&=?ufhAJ3G0Y^f$VJ%70W(Od)ogy~jo{r6T&ww_&X9A_qhX3cn|1I!;E40VG zfV5+bW_L#civhJjJQPZ(HZPt?RVo{jMB$Ysn^aNSG?qmT5Rd)UX`u1`YT$S(s z&6}N9Y6&T|Bw0#OCeACE2X4gOQ;v?i6*{g4ZZ3%j8m+o>MClH+r{r=ZUIjuOG>9Bl z;uUSX5p?5G+Wt$e9CDF%^JaJB3uyMd*6O+4l@n3dMnTvIykiZBUu_oX1Rnd5W$<6C zdAvQl6D1y6)0(#YsY1w)J7K$3O;&}SV=0jHxx6!!BI3#r5CD}FUz=HZWvf+bw3vt$ zLUL<2-kG2P>NNbXQy>#wHI9mKZDm5LU?-|J z>N~AQD;l*x#qZi}H$w*=9jKvIwZm%LZq@B@*Ns63lFvW?;9)zM4!2rkmw|@HFXJ^o zaK@(ssA(Kk+irX!9T8DlOHe|~f~nk^xC8OIh3LaObiv%BBLn+K+*jmx%}Trm$TZdl z1A%zO>kv(%>B+mJs=#>g$V?O1X`|^|4MjpHIOGt`_;P@9JB4!WbqLVwz<@!*fF$)# z&J+hu?VPVVb^pvn-PK54O4`+kK$@!VpSW*Ao$_W$LibOJF#@DdAV`D#2IhczZ#KaE zDYA1(zhW!IlJ*nEkORGn~9a4zOwDB;_tfTk?wcdP31RPLjs_xI&jaD~a)}95=3=*Q*Hgw7X z(sR2cO9bl4iMkY0JP0O+xd!n$MKlif@Y$WTz6s>R_kkkcQ5_!Q;(R8LUrw&w<^ul# zsPM0Vu^$u^MAw??T_$b{ap4nZ?s9zAXy*-r2X7!>CYIg+6e5DPNeGtTb^<%{1BGSr z(p?-dJn3K`X9OEB^;3f7oq|P1IPAfAWurxWZ>3ic5YxkfvNs*@`DVb-NvG^iGpZi4 zn;l0-B0e$fv~ieG4QK2idS1szPya4pu~$)X>xY3SXIje?GdCu)@wTQv=*_;`*lahO z%~wF7&ZKuL{1K97;OX^77~VPXIVtm;sSMIq719dy7kK;Vi{7RKW_TAR;q~SpMWY}7 zt>$hGWtmR1W#^yI+kuaID`w5DFj1`oMWGwYzP^&WRvZ|A)d9ZJ2rx$!D5%1e>7DUC zpu?EoBaJq7^{9-8swNrfcW zCc|2I?;woLw`9x^cyC9%lXL_zr;{--+cAEq;T*2Pj7&G)z#pr2tLAEoy4Oet?It&h zV^wMl!)*y$xD7@o_+J~HfMJEQje6I8YrK+J-&z;(OU>1wFYG;}HHVH?6iGTv<_2J1 z1ss_!Ob>v6=udhuP1Mz$?Um)swfie~WxRT-(`*vI%zZ1`?AzSUD3*Jd0(oD6_K}!7 zDxWpK6{!#d7rUua=oDS@^tBP;-h!X~dW7c&5*NR-nLLbnYoV6PDao&H+78644 z{R^R__XGSxW{(O?(sWeO?dWHmXQ_U^lF)^ zjmVi%x$?d%VHmn|h&kUlK##WWw4T#yJg-C90mIr+t7f;XNyqXhBk&uYx-}K}GZxBu zSaThmDXVN9A4-~Kt1{{PO{i_pltqWVOsQEm8}nK?RY`}um`NKmhnUN6+At|4a}80; zg7cU#p3xdIKpMiOhP_E)Sh|cjPJbvR4F8u1gZB&k1M_vjf#1R-q5hC0tU^voc<&L& z#Z_^9-(!TfoOF*Fd?j;FqRc%3|CH?MoVhE2xf`H-YKWrPJyg%@C@3liie667_jwM|2~drzJhYI^2j{tL8U5 zGc72(#g5!2jPjwsZW*=2TStGLPQ2s5Z_DV=mZ=6zjgv7s7KW*M`GQ74eBnUqjj-Lc zXIc8tmJp1oEOOXsOuG@*#Pi7%H}#FDKoh0`87O2!GyJLQWFv@NObDBuY5eP9RAUyO zV9;~Wp$&xy$sy7Py9qi0rtFXeVKuA=n32>`1zc-RwbqD8VKps`pa4=pO|1DHm>VW} zUB|9N=euIY(g7*Kk@;|EvSr3iBvwoB(7fmB?>TP6p1Q2APP!Vu(v8 zWDI04XpWU{oL}r~vIZBX600;1V2Uq#jv<(s=2_*gj@B`DK?Nkcuje4~Ow8V_04o!-HZs^D`wlZnvWN-T+HFY!r zBo2rL^I45rC|DCY_6&ng9=WiS7bf`Rnab197V^^cYk~tLfs>rRG-?O6gd{;pE>1Zu zC+!oNpP<+Jgo>oJ6kL{WZE1v6n+Z03fj$T=&MKn_d()rx1vSV^-3|_l6lRFFP$TO7 zUQt8wYOaI08ITE5&+eegZ+j^{s~gc%LC=m7J?pK9Kio=Y7|wlDpc#)VMA8wDD@36k zc^6|E@VG)et`HMSv`>ZT4)1FX%t_-sa{Hfh|EN)aGI}a?&ysm$Dq;$bo5-N*DxQc2mp@Y{ zj=xbyj=xcxhi|0Tq(jHEX}%gbby?}C9|R?}0@S(VWrRP6{V0_PrWw-^8^x(3jvmje zq;moiXD_3i>O1h_Opi%PU@u)bNNObNdk8&wOUoINv;-iF11`qmWn#F8c(5(>^+U@H;?aA>K|Gxd)duqMb=MrRXWC6?=girj)FfqNMJoKOP!-3!!8$H$8sZd^Q*7HOachDrjQ| zmX*L_5gFU`0&Eh-$cN)==7q>o{$K<$;vFhQvIujJcx^%_v&89gIs&C;3Y21#O_UL3 zAg}-{E^r}hB1pj&B6#lv zV#KQ;kbx$y>%9wK$$}Tefx1Qr`(mCDKOwKe3hMiiuUJ$To>ArE-XW?exHOFQ zzJj@hP9nYQQ^3~z`Pqv5diQQkKKN1(SRUqmT$GJpceIvF<`EfIP2wW4w+LFEnxB&@ zulx);#l9k1ofW&PxbJrF%cK~r22&{JeHp7EIKEflcuJ0BFu{<^YSw8Psojy$_N=eN zaUxZ3)Q^pg%89-I01BDr=DDoEvLNq!B(JhEsQ-lte=y=|T+Z>I6bnfzz7m0TA^xfp zXWepBAITvO1(mO@)5PhW7uF_ZijemyW!jKV+vTp+(v}1C88r7E20Dtkx%Xv&jxHPx zlPee|Bjw%$P>u%6y?=s2-dEvA>nz?kA*JAQHjIyZ-@=?}ev%Hk{WgT8?`3eOt|9 ztGqMe^=mRu&#bKyc4lB*+w9h(VIOT{-#6RGTSeKUw{{_uqABBea!xjC8FRksO0q2) zTiz^6LU}$`lQ4^`m;g{aC#|5KE|t2TJ{*g89phIRG0suzFNH!yZQgAK^P*bMBEh1o z^#BmgRqGPNlv>{hlUnB$fLa&grW;@L7>TO&PXZcLtv`fM zM6Hvj1yJixSNbPiZa@|d^*0=PO4;F%3b9|+eoH%Tz zNFaX-1myF_?>Wey2!;AazW;&SHY2Xaf!hTHMtF|6Me;ydjr_^6tTE5{7(3LJ;Q9c7Mc(bdHAqAQUQU z{@zFODz>|S^YheaeC)P^Oq$ei*Ekz z1&ZXFKZ#+={M`xV7Qp-=I^6uxenZUPs~F#4i(`NDmb?Nmf1*3;TqSuFE-34SVyt)AwRJb(pH zAxWGmqaMJrC4nCYw7=T)?Rxs?mAw2*lKF*Vjq{46L5z6k0hJA|yuRrHPp z&QgK6^iH+1V$K;u1CC7@%@H>)J>zQSx+kWPkLo7Z5O}^xt)~(?cId-q6zdtVu<91r zN_K0`2`B~6J(1Dr%#}2_0)#bLY!~dEQFIs*lYXuc>ugWN6qA0AD0`IUOpiXVRe3^( z{KK)HsMEj|?Z~EqDatHq;7wyS32$s`VUbArX(yzMvOLl?fdkTr#PL>&cfEnP85@}6 zoID>2fs*?I59Jxo2k~MvfUhRE{JU^Kh{ph=&j=x3RPC(HMGT6r#fXvK^SuJIB&Kfe zH6y6#`+}RhDG+rRcYH<m2ct9TzX5Q5g#!(BAdz#K$=YCz!hCiK4S(@ zo=OCUN+j=Z07TYq_cR?~l^Y#VExk2Chp(H_G|@piP5I}qFu_b@RT{6(N*w}RpqI$0 z(S<(5(-jQ+i#2}SB#AA~gULS{eP&CT0uy3LsIHIkRj>ykeoXsThTYyU;Pa5xUIuJ z9J0D4#Ldu6XBtkBu)~8+G-<=B61e36Pm~~k@gMEAp)!+qduFigvfG z43?(%U!9OxZxW?-#5+(}Xn`F32#TXUbOF3?Wq8Bnz^{}(P8T&*^yTl*;tSDxEPq#> zQ$AqW71*t5r-Hl~-LZYgj;&h{u#+AVn^ZXr&au@ZLDKvPrTL*!G-s>dTUhAyy?7Wv z0FTh_9-CC-Ys{FmIwi`Qxy~dJ^`Rh}-y6oc~n)@k5d&&NTk7e?574q0_gj9PEQPzK=6zkcx6xNwm0qY+mtT*>@)?2BR zl&WT2@(ds_@+~}t2#q)#88w84eQa`e4FAHJL87Nl z`2p#~fdeNQ!Qs|*(Z{n*3kT2TBJ2Ev?@=0mrxcC#?zI+3@J&MFo~Gr1e)z%s6M{E& z;Q>i8{WnU&|6PiNx|ps368@Z!u#lKiYL^jH%KOhHO z9iY1K0NM^XI35iMoSB6l4V{{-=z4q=tA?{cyh}>0Uqy8JTKvmg=GRex%)LDH>bn-uua z3<`?bUL9hQVBgI^4jSyc1)oT;k31!VeKZs?*A0HVrH2eQ0(SaM`bR=MZM|$z<&qu~ zoJvqd2Q-HO%VaL{%=vmO1zRFucfKw&ua9Yqvhgd9R!}d=)x;{-Q}cMmRa$-qp@WE$ zdzol!`GAb1llM}r=9mn~yxG{moPf+5p-|C)%zb76*;C{UKt@0Yzo>#c_khgD5;f)< zhl2B%>3~dbtl@zaA@gG{UpC{JO-KeHJ<*#HbNQ|rfD(2nNQmMwmoy#nV=liiqiLdp zZXk`qoye**UX8iDp7L+;VlEq2>*Ajf*6f(e8Yl#gXVHVxHydFXV=fF*xtPlZW_W$X zTqqi4SlBx+#$2*A`l7~Mo^B+S>M9oOqH~YA>?XpMki&F$;W3v?5-wuQr5X#MO%mp#wF&F9|keJI1B(qtW^fJIi%;myo6o|PvfyLcyGIzsWpo?W;Q;a25 zTsTPH3sD+j^+aKzl+)73kmpFKewekPNPOiM9_lC@U(vnQ1&OuXO?mOUQoLXjVs~O8 z8f%H+vA`U~vx~-B3b&j{r8W=Yy#vvrahGQmjJp&Cmzc_US1GEosf@Q1QspZ?DeRs? z;I|bh#U>yqFY+yu5`$3y+(iH!kHNSD-WQRrBi`rnPuY1pMp2w6{|u$*@fb|&7?0D* zUhRF0vi_T;SkI;l8S60y^EJYHb025Dl}bsO-ioq?y_H#eEe12ggCKpxU=)kJ&|#S0 zQZoOh6q$9agH!y!B4q9vhN+bg!<424_l_0?mUN|t28pU#jeiQODqUVN7&8FnO$(sV zWH5Q9G%q8s$f-PeUA$mSg;MIgQk2p~4(I95B9!VWa*Gg*c~YV!niEts7;{CU60^!; z9t=XBOE4yhxmlQWX~kg7g#^ACRVJ;JU1k6*7{k;zGFeKixg-n5@Ie5?RA?ndgE4%} z1_i}TuMV+DFeU=4VjP^d(R^7MjXlmbo8h@X56@uBgPYm3ECdc%=Z zti*T!L@7G4X@|cf#K~7@QlLGBq#rF%icJgU@V1i&T>6aeoa0FDPvAaL?SWb26c z2>vNN8>#mKoZS8{rRed%$zv#R@@vZaUzK7#o7!WY2?HnpMp$p|D4cy?2jvTxmFvsJNs2U`&*m&d)zbGl zhmz^6Qe@JF52GMw5Hb}KK1$It!iRjw6TZcZf;^Q{>WWg7(lsR5<^JUh{n zzM>#ECn_ zRG}A*g79G#6cm%OI>aJTkkmv0*gG%ANU}8g zqQ*#WGm`44#7Le`ge#$p(%pr}NHR&dh%u4}pvUIMNF;Kp7|A_QZhm7VhzXC8Q2&5T z;lCY{+5Ae+7|9M9?Z`g*3q7RlO(@C^mb^eoMq%L`dGAL_gkyFJ3#6P}K4$zyLhtvm z78D7YJhyDfMDtJ=ByRFq%7@RC;scwHcn}NGxXCBsvA_h^vkJydinco=c=B~biUv=f zSuA)`5LhCD^375dV^bDiC1lE1c2dYag}DE#Kq)rSK>3hwjg$zA0^nl=!0`wQL{NT@ zY#s4_hkwdW_%e#(Ecc_7qQ@gBkD&<4hINGX>+nxu*0X6r#(IpP3_DGv|_l$CGgFtGHIpkG6P`Y7N)q7$x>R)C0V$I zJN^(gp_LR3x9|}f6cqEiI>aL3mcIaU&~VFN;u8tCkSAlfMZ&3gSK=ww=OyAF;?f_S z{f6Bl>HB_?CHOTw{2q})SN-7KH-@Wz&Q4&(VeGHMtuPz_^YZiZE>ZIe-5KX)*Q*}! zJ_=MV>;{nQeLgHokn4R;g(miUz0cXbuJ^&qapJRjTJ;VXSGuM@Ug^L3ri#cE~60)2o2?02fd0~(fJ(HH9k+$ki#pps*U<}$j~Q#_?EYG{ z2>Yts#KXSwcCF&?dXqM;H!7y=Dg3oi2q;86zg z{isXE1LpQAp{0Uaf&$U*vDpnUvaX=|FyTl-8z>N27w5sO%+~iC%sQv%JvQL&$UAI) z2PQ2ykR=gI1+pH2as>ieMnx*`uR$!(IlT=AL)6HUpvn)?-ohLlgcN!PvHn=~3BW5H zv##sFtu=74y@du8Z?0+7$E~aJ;+mTS5hCJG_@0dn_EIL5BN8IxDv zY{L`rCui~Zz^Ex-*qNEJgV_U2HxI{G%oQmaZJ39;pridX(JSqS_kP3}l^M{~V?=xC6*0_D#f6vThL~0Z?u#Iu4~9UaRt-+^pL8sL z67QUVOC(4q#LygHzh2pzdBA^o72H_$^TBmOn){h=xuSiW_o|4sglw8!5-Hp99ASfa9FXnLS;Sv8wXOY`w@ZoTy)eihgd$Q4NM6;N} z7jNUU>)^a2Ney5MvSx=-c-H8Ub=ILq-SOd(*7Vfb>%c1C;CF(WgK1#|m)yXGHY5XZ z5$7&3J!;H2eg~_7a*f)-n$wDC_4YKVqcFPBb#7<}PyloI^*YTNI4A->-~o`)mjBUp z_?@(-HRaN(oaZy)baD%#6gYwx5EPdgeroQ*B0iGTORvtA@_QA+t z@#({ztXCBW{mAWqPW#y-x4(7Nf;ZCF_>11(cq;F6{GIkY{x&K49;Qg=$9oAC+857J zXkU;kw2y#Liut}b9iy_sdGtA;HPuU;a7cMoH}Q0#pLhb(QT&zbDgG{X6;Da(E1r%z zi$8-Rqk0Pyh5$LLy7*_VzW6&;VeqCf_-@MJ*Og+h?oAAWR(dsIa3ODk%0d}$0@dD; z0sxpd(J7FXc{Ws;mc``&0iY?@cz2<-ep>XQg8TuOdDn&8pRnWNi!XZOg`W@gT_jY!d?OE4ufQh~swPjVP&J89&h@gY%Uq|~hC8ZIY)Fqnue40K zaJwN0FrHO;VmF|SvQko;!iHyO)mMR;x@vR;B8PBEW3-jcDlK4$a5L~FUrU%b@;y$J zjr+WB8Kbc6*`0O27DfSHN(p1G@+?Sf-#aObu-tBEe-z4-=!#L%uJWNnNfYk^tR@jU zT&MAuu~&u(SFz6&TO~lKr4Pp=6>+JsWhJX+L5;j0})vq^A>3;ejM7AI~6YxOX_X!Bq%q3Q}KuepNgkKOQr*d zAvt#{-aX?*-HLlAY_>Za{wskF#fI<-z=nvvp!1}N4e0_Jbh*aZC7fp%vt(_t4C8gsrd-1)5lb1yi=o{78Ail{8%EmQh+(`0lILU? zc?Dn?MK9KQQp7NR3DBV9sDH#KVi?KO!WhQORqZFjuVu_)?p-A?&`~GjWWZz&M)CA) zbQf<-^%Eu~{fw!8D9Xk!SbQ5On420}ot#NjZvS$=>elZ!qG{!=pCpU-?^sR3`st&R zd7Fe=cY?{8$+32WP^hT28z`6;C%3Xluvkj@w?Ok;r7RIkDdq1$x%pGdhy_>5R0@bv z{yqvs-a6AFlUuw3P|8BM$}8ok0WYaiz80T|QYKFeqm(b6fyfBzw0a{vSVk8c_fe^p z&OrsAt=7XMqA;yxM{YIptGJ?T5$3jR;6uM-ky}OC#ahOxk}Rcu*Xm#Cyq2D*E8Dif zzr3=QZTEbvCZTLkmO~ANpYuJ^c!jY6IlA|yP^hTxy^Z8mMjC5X@uZebI`A1Ehhsfa zXTaO~!T&9#@z?cKolLi zEBF^tg0$FzeGL6>2e0(n4$fE7cjGNR_JoU^uC~)zA{)P?zp(?axNsz26cAfwYIYO$ z<=6q3u)RzBCRazdCYSrX&FzI;&kHSoPnGuGONeogsCcWMauMZ|sH2P%H$(af} z#jcIXY@BiF4>}9)H#Rqih3_uM!jBqZ7+J_5=`2K(#oM=nyy$KEtQlS(ETm}k$-@43 z_kYja5Yz7dZxI%i-QEAMMu0ieKtYuuPVc-p@0ew-^xgde~j zUVFi_0F1EJBLd>w6|CHY%sftH6(C$-7J?rjkV)WvS1oSsd4PaKeIRhA8uhL>3gVVK zLYLU3M(8I%xdJ2f-b9o~2_Pzbm`)5K`}5h7u3=LlqW$AQ|6uevFL#0R1O3 z^b71vjekcsp;JK1%qgr zp`=02->z)40UOzAOvh`P+mqXV(+)xk;XVp4~P6dWK@r>`_t%{Xc^ zwyTN?XuhL@S=k{4idgTZVyzvl+{e|FLqZuy{eVXh1P&aH5k{b#XulZG?7^$Ar{@^# zyUYM|HHl1?(rPZrHlK2T4nkd2*QWN7CY3nbfXcmhC@3a8b%;e` zWWNV;&=}bt@QK98$WtmtMrQ5jx&c)l{>2AO3mJdlFy;_{xI+v~cx4r0w%A1Sbl`W| z;jW9ek20%Ojb_^p``B#EG$Qy0ACnI6F&vpV^<>k;fO+rINuumgj7)GwrIvfh_=C(s z*L-Ar5oVHvYqp4nm5)qFqIlT{O76arPcRARb-uzw0 z=I2bEJsAp>2w3bj!Z1#xGD!MFYOjFBbItJjm`J5)=sH+c-fF<&B0?O6LxVzv?)FN0E4p-c6gkfY6gQT%&*+tuVoA6O@&uh%E`d|-5p-=W~q#lt} z?

^upr67o28}c?Yqy|z8rqNqa43JY=mLt7lWknt7i=G(`I;m@Qk9-C(j0j0ZuV% zfPCU?VsAQwzHMww4uif?jzK>&!Z0$3LDCqs9DEJlCi1Me=utDYKG;K%=#xDQ9sS!h zY;t6B7`P4!l`vXo7-1M0$RKG9qyaxRF_snm8#BY}gM}20K3Uj*^l!JhA*SfxQwfX8 zM*nU!0?d&H3aSiodgsOHUzSU_sL{W`Obz9@iusQId7ELJnRP&Ti?B6+V9vZJ9^cC( z;bO-3wt_azjqge9Qt`bFP;Q>%dx#2;@6jO!iSKP<`X2OIiTEBL8x@c5i4s$OVD*ac ziOd<{!@T&OD3bv|m^km9DAOa}y@f^ln27K3wvm;cfc`xUePCMpkkZ7-v-iVVFdE_` zL%4a5@X>Kc&uZ(9LyZuQyA8*!i?-ve-qkF@+B0fh^aMR+&o=pW&z|ui#joaoc9581 z;7oTQrpM-0My)53)V3c+W30oS_z-dHcs;Go%%l_Gky(KQ>pkl*)9ACA zm{AOW+MOnx)eAn;_^}$0{2x@LKUzwp*}U`nvC!#z@mkEkfk%+19)XN~?*MT+E=V?P zqdKMqiFx?P+4b1y_i|pt`v=;czfh_>vk(UB&KT+aEa}eqo{|#jr9D62DODo9ioW;& z)fZZ@?G_$*9V&itEVpgSN|4M*FCCsIPIBe&_<%Mm(EC$L&>xo~sIDI|!1x2&-X8if zE*_1wO0^sUYztAf^o;(4lIhV>WYXns4Up;AgiM9xj#9LY+#w%O?r5ObT#|*(xT^#a zFe7UWZsDbav(`Jal#zK9SHFd0Lpz*)=oZ`c%P_fn)l~0j|^wDjh$2 zQqN5-*?{H{L_0OR#!7m(a)Es`Y<8qTPSF8K2H_wB6M1>FTDPhUr{z9bjT+V0U6@U^H{3@>mUkYf@^#d=%C!z$& zlTisuI2CU>u9M!9P)cHtEjOg;jkd`mseOoiAFOe=tp#gQ47+X5P2k0W?5{?SJri;e zOef{#UZUp~eJ;)`kgR^hyPL7Q_$J%eiV|d#ZKdyBqfUHouT8du7~S}s9y@GlHhp`o zL^D3ppK2ww7T#M+bBk3S45hj4s@cD~oAoNjIu_vw3O6Fr4k@V^%loo$^=7W^Sm_Na=PzEyU;&Js?QWU@|$ ze-bUprh>^vODcE|V>JmIJz7R4=%7tm!LUXt%|6OSCVbHX)0F&GZ`#j{P0NYV{0It_ zh}b=9gkg->F-UsEuGcb?m8Y8APOp!M9YsT$P}9EHM+k8gCM|T(^$cTkb67Y6g-Wn+ z%m~BCLIz1^VXrNhmzv@A!9t2gpDgS@=z62MA*P_~b%aG_gRYZCfH~4YL6sp+@4Psu z$Wnfb8g#wgOpOH$y2hK0_BeCDJ7zwv*p)7&*hHS zBx1Fe;>oTKsYu261%gPtm)=KM82mnwOw*fQO~5m zeT-~?`~qe5=Ss1f&8L3?3!T0fZ%_R+Job(^#-Ce^`0^r9n)~hl-?T^e{ePwc@r_af z!6FAtATVO}RU#1iwgfc>+jK)^C*PuDR<`Lzk+@G%iPIuhH}D8jiHH@^ePmUE9;kvV zDW^Nm9JVArN(uSzr3k6_ER5v;lD4x~q-sy`7+sGRbJ?E98l`j%K8{$HRrsgyMU1+1 zAyHO9dDF=digZ!>m60w4fYQaxwCFut10jnfuvUs>N)t;l-qxX0@Y> z6NWjTSfCVBR+Nrq#0jY{#mQWfg*mwk0~7UB;}s2aa<3B#irF3=Vv#VX3*?|P{0)2} zGyLQ!HN#J4BiRl?38&)y6*}kVBt{@&hMy>)^z?o*Kc9Kmr_zmc`#9xonA_j*Ouyb~ zI#$hxd(T@@$W>h`T}Yz$e4qd@@Cg1Xs<6n3mmN`pOvZ1Y!({x3VKTlOKR$m7J`uo` zD8>J+{KR{;iWeJJt3~GB*`bxHNZbwdh&M1?1p^#DmBl|v-WVAT`~b=il0|!wJ=2DlH55!0>OKgsL+rH^G~sPsl>|hC#pKFnqP-p-FUDQO^sbL7B=Ad zH2q`M2?9T;!shdO(+Pmih-xkB#vA-jq}MV)YU#$C6JHWlc%MhHUQg?lIPlV^=^qk> zc02B%ZS>*}4Ec2x)-5SAn~KotfpyNRsZKvICZtq|ES zliH!vsr%IHXxTEn}XiNxQ zz>Z)UF;u#aWvfm#;DmrK^wwc~0wAFiU>@Kw)>a3WuL`q;VKF9f=-52k7fFqk62oW{XKz7O5J;Y3pej2q1~ z0mNWrPeLE6M{alT>Uc9q04MhDhk*P*A*0xZ{YBB<@;TGO`=R-GBU(X86_I@|<@ zisH_Axzjq-1bck zanHme1dp}u748^8!rkpoyMyscN&smD2EZ*wzgk=~J1tBbKV3W?XoO&5yLaLMXwBoH zd7It#s5^7G9d*a$X|ybhc4 z9Q--B3Vz!7bH^b3yb6ErSpz@s#2+Gbcx^g4jNaIIJgvS^RqQCka0R9Y})5J(ANvJ=_>Zb_RO5ij1s*qv{36Rb&ulY&4Ft z3N^aXU!%!5t#FG;Zr)tJOa+>Y0T&X9Bl^t zRO?Jk%eqB%6Y-lYTHs>>I&d^x2Yi2Ud82VYSf6t}VsKBUnS8X8MMw5D$Ti|Mp^$M%I3NZ)E6_Cn zlK#<^*H7%3cp7?ZI0oE1Acl?J<2FETV6Z(zWyJf@dX5qK0uh!K04qL0_~o47gzsxa fGV!0*G+MQ0rw;D0KaH-_P#rhIF)y1MqVfL+Hhz+% literal 61843 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

C}LSL_fKKG((d(;SpUJ^<_@Cb+&lh*0>M_W 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 WFy^oqFg}gc@D*h~lwqD>=05;~A|iVL diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.wnba.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.wnba.doctree index 3e7805a63abaf2c60a26f4611f89ab74f1522512..4dd97046cc33b3030bf457c891cf787ca747270f 100755 GIT binary patch literal 83926 zcmeHw36va1m7uPc)U8{#+cs_4W%*FGt!@d~U@VWovW>8j>GjA43oKL0uFCGLRCQ%h znI)-VZM=ZNhJ@iL?g57T^1uwYXE??zvyWll%))wr$1=boW(Q^&SPaZ?4Y2?Jh{%Y@ zBPz4{kTlPFPt}=`as2rI$NxY6kK>+!*DhbObP4_oH~1~5-aI~T)oM+zX8YZ6b4;r@eDLl__dF{ZjS%G!bZu|E5vF4;zYFX7I*0kNla!_o= zZT4}5r$_F4y96&Zct*IgWdVdi*Vz;fR-!IB%fpqtClld{M!jiwCpwc>IAjGuyFS?o z00Y?L<@K76y<1gnSiTR5@I278X4uo}TBjvnCIQyAJ%xEX;i|xz#!p>mgEQi+bZi4{><` z@ymGCYun>fZK!D+CEh-KqQQwUttFUHX6OvaWk{O0qBM3?4I`^&@Tr^9Lh#b)PP#~TD9`10PzV9vq~J9t9X8az35#>*Mr&U3YRv!mdbdD zmrnv-K1wt^laPT;w>r&g(6M6HSGaVyXd9mNR=;FwwUuo3i@eq0aA_%4)koE;oKFeS z3#JrWVQ9y}z;D^j8c5g5aWI$^q;mN@%rfgBeow78+fo4|)t;((GgiIX4VSfM!FGd$ zV73KgbASeLm$C#PsqCQ3S_%h2?{Lc@yfmNi!J$68o7Fghl=v>-3BFBxF1ODmQ~pLA>di5sxv{%jqz-vugMR3 zw{I|Z+es($wNR)t>7E0BB2F{V^nkGq_hR@Q=XpU?-qNVr(h4*lME}@}-lxOn_8u2+ zuQ&Y|82!+1wXks*$#k48yX-R24}8>nF>CIH32PlF3d2y2^_9%d5{-#hy}_3l8_Zw@ z)>LlH^v?J}kYU^al4_gTeT>I_Rg#SKvKpNefoTqg+_xj#aXbQ>GnWmxM8=QS?PFE&+jPSX;;~{itBxk9 zd(D1Oa|*S%qr%joAD;&Az*!dkXwwx?u28yO>pE`W25qqB`68nX@unJ=JM6$UJl2f{OcYXAAP?{-uE9 z=b@4$oyVB8>NNw^2PXV2=)@XCyKuT~wH%d)ccdH5Fk2ie-2x4l0>>_)Em)eIE!At| zViXrH7xA|He+ZlI_wWxnnsT^v-0x!UEvM+L1Zm-ZoA(FEF&J(Dja_fn(Wb4$?0m-8 zND(lBwM>ugVEikr#I`*=`f2hM zrKz?zQ<7x$tB#HCweslrP+U=$%9EbgfI4rQDzRBH7jq?aX5y5%gf31tBgRbHl-kE! zUc&-EmdrK8bPc*cVUDA9XMnmRIEcSVzF4}9OsU@&;)~xBU$~FpA8_&k9exFmM1vuT zUInQP{q70KC4_VMr>7N$1p-eqs3oD#WQ5*~e+mi!LFg4g=nYUlF@aIEB1-OctSQD5 zipWknjNM-(IO*raN$xKdRylaXiX}@9kV6RuW~(OMM7vAqF@nK|vJzZ@Br}ojM_!r<(eFQ_e| zFQSoJ-ETFlS)M+$%?DL0iyX7-(@uajiF`7}NqiG2(1mFL1`1iw4R5M4S#JjpCisoc zH2!rlGBS%#FeN%-vyMWPsmIdhxIE8XOR@6_y7e#TPxt5KD{!1sHEzQ$a-}2VhcRn=G{r z$RJF!SL`JAgT487gN3;;KXQcKmhiWM9+(b&XIxmT*D6iGC1#{LG%f8s4-Qb&+0bR{Kbl!Zpn{rX}6q zDY7M8Ed+~@@G?OP&s|KS?JOibbuD=g2+!>#Jl*y1N2tLR&3Nz{5OB9|rE$|sWYerFFu&5$anMJY)Cw>&Oq7W%FB}|+L@>uN4WUuYFEYtQ zUL{>1kj%1_a;n-F#kp{joWK)Mc*XLlyuov&)fky;qIkk&6QI$D1klK}Nz+YKrkHPf z5+X%Z95drY#>kWt3D65P=fo?SbmB!d>o5&zAYfII!_7NUObh^(oSK;@>Ou@Erk*HI z#+0N6y{ZG15|n{U?}EP(MoJLopH{01HRXy^FDjnaPu394h%!Hu=_fkKnux|w^bP47 znT7&DOyK6BC?A=KB9Cq+id1eY!mD)7yVo=GZ*L*M zc3yGy<3m;TZP|IgNCuXf?~4_O%2F{VX`FkbI&v@N;v+G4 z{zypCK>IvUINMcPc3$PYT4NQlCIc39xu1>7hOa%Diykwv4eK6pS=XB)q@HWY$^@Dr zhmN?fM4e9yGL@b8IPXnG9IW3`5a)gwt05>Kh@d<>gGo4#$Yn6=w2Vw2$>@06*Woah zs(0%9#!h9#%D)GNOx|+AGO&!t{SM_-R`T?}kl^=5SdGgse#4-Us1hp&_*fFJI&#)6 zHw~2xaiAKd<=wn9nV9ZIu?j?>ortJ`9+Q;@s|+p?$RPSX5hq zF&@}<^mTNFb5OGxHMiuf_4MH|de zkTZq7gfSuP4??*G5%vg>5cZ5WNZ3CMlIJAsMFk-2BbL+EvUxtmg#BLu7);ndgij>w z>1hFk{nOM@${ZTB=BMAO)06s`I;qX}EAStm*$El3?P zK)D(CQbVYz!>|((t)`fJ>i5PTWN3%qK%uHUWxc}{v?Q~-KE5-vKW^633zz(P## zAIB%s`}DK`dS9F5QS^w!Ri6&iekOUc(SCJ4tfZ5EUs+w5i*m8kpIJ#WWT<2}J1LB)}A%3D@#yJz>z!Xb8>Lq?U3m#4q zJ8(ulonhEVI0ELM!wVS+%o26mY}m_dAj1H5WK`MU=q5EM(o9Y|ZJQ z6$pXv(Q3=wH9=fO~E7+TaF>deY;js*|#mZv*^oTxkLs4xYzB`Vx$tR`l*jppW#gh97CS(N9IHVmAwM$9L=8NKTryxZ8p3@hfj zPzdDT--w8z;qVYQG&k_o_-OzRoF3xx29gJe&=*y3E3S;eXq0heB)5aFz$`J(&pXTj zDyksozHiouy1_fVu;*}1J21q3YXZ-A2y!mHBi4;in7fgVXWl>(QFI=Q&M99oH&7f; z)P{;C_hZmR+E(}^9$=jvn|&?4Glqw+o53{ULE2W?hqG|jjAd0iue!C40xU32q;GYh zPx$y(bLZ1f_;`e%)K9B0oba)HrO9+MPPBMS#ZLIxWNxpI6FwLiMM&5?FHXYJ1it$Y zWo1QZJQ9L9Iyn@LW%MDs(}*f{o|#jW8Qab^bhiUYIlKY<6pM5e_Pc`TvK+@$pz?%wTroWe>9C#JBsK)D=K*c_TbuG>La#O#%s5Hx!|0Lhsy-Lk&de*9+WVCnXW z>-Ls#*8(0ZL4WZCfzmXrS)DH7(WIroEA863bC(dSMTbA`QTXNtMOp0?GTw#$h7?YnmF+BG_Qm>=m7 z+pH>@5foY-B}m*KXShFF2={z}eG?0vzaMw{x8V`G-ea?Bcw?^x_IW!@U2;3H+DjOe z?)?Z2)A0LxtftS|O3X}nSs@(pwe&9`#OU#CRjNu>b`^BKC`T#2mY!igTiaj*{NMzo z0goEc+48@#vrPCTpj5_FQSdMmA5JWRu})`o*ri&1incNYC7yAdq(G_e(?K+_6RH8n z#|9p6#UYiDS+lJ$d;P~rWG{(~vjA&l)McQcy z-(fiZRv{eg!)q;|;2VVFo(kr$eiXv|BSLg_;Tw`{`V)r29~VNQ&ZcVsg+C+|=3-NZ z?ILW7G|FPrT?ec>oOQo@=oxr8=9AlPuTAG!9cHrdFzOCCTpkq&9F&E24f8~nbveF@ zS0hLey-Q53Uq!NfE&l0Kmam77tcLPAteMM-_6IilZHSeTHb(LjK(Zy)6V2oaR|^aZ zl&oYJm6ein_R?4-W)|Rv3$_f7h~e;zH1b3!Pyd_(xtW=CZpGlqW2k*Is9dNra+w>@ zO&}z)sa1qCCYuHIsvxZkq?9W3s52R>2UkDlUj zKNftL>mI_J6cZVg1nef9^p5*@_UF7s$CIc~<5rzj7h|G?(eAx_VIwE-k zN%w9_q~+V@29#h!PDJF7v?TG69cg*Q45kSWy2>;%cPy*Yc{S4VDn`G>i?nQ5t+RiO zS<@peYoHLAqIu6!-(qaT7-``x6^pc7Zf>uSNDBj_2nl=V#Yjtx-yCke^I*?XaJiW3-W{>-yk;h;yL~d4-?&nj@EF@$e;~mKpRe83E zZWM{C=+@|hL|9(UX!6QJG~o`g`>+s;u!QiK!zXcDfe1_P&J!bQt_Sy>2pNmE9LOJS z$=hPgf${D_xZ@6tw-ds$g_~r3S3&bzbClw42S$->?W06qlm_mn1|E&PI0NpNkgOx_ z7x7O~Zym=dddI)OF#2fZrFn`+=j5n%zsX4djY6d7PC`z4jJ$k}NN*nFe6%tyDKc7- zw{Wy_ZLURLW<=8C1@#j9lghcnF4z zZzQthR&z-nf)TR;h@`Me@`hl(7{459ekn?3_UGO2 zBs6QSy>>k@^Au?*@2b+Ylbr&k-ULift6pH8&I63IT4XRu#&_>c3@@A zkj?=ydYSN`D?%f4$FeG&R|6{_Wb|9Sz{;q(^XY+=OQ2AJz{*v|HjIH4-cqr^%9G9Q z^$}QMU=$%?@4OgTNt5V{8d#|rQFT%RD~E}>VuLU_Ttr|c6@`l$Sa}-&~aafTs7i$b2FFp zC!aDlAK~p)ya#z=D7W_!LlFpGkl@J!j0ztpL26Zb~{<$v*#FA#8DH!(S4LvPG6;Z&yL=r*f3yE&@h_Y@LD)a2J8n zz&ojdN5dr$F8N0!>xg>-|K#>T>SF<2+TUgveKcHh8ih;#m685ug-FjGdVDZpxa229 zdh;0Pqm^+2pA#&wU+~&!Jon z6pN1J>AG;)EJW4P61te7X=@=g>CA_*j|&J*xtNb(vF-%=s2vfR* zMz{DM5T;Ir*vB8ndeT?y<5{sv%q+mo5o~!bv5z?9W@gg46=NS0)V>*1E-D$h%nj&< z4H8*$tGOhPeF!TaLKsY-=Z$@cX%iHT9Ikqcd14>00Ccd}#~OGIanT|-_K(5HhW^)6Iw=*Xq@<&FJc*u^7 zTw(^(ga=(s8kswmRq4DM8To5Qzr~A;TyO4tdSv8DP^ds;~qj3|6 zoBR&RI^zC2{weC_xlI0@K0{ib7vqYJ;qIjpnT3{`;1G9 zj8^0=9Iae~YjKk!ea1~R)xOZ1OvV_JFE4~-UF)Fxej6dVr`Ovm7Dnj<6T;`S2rWrd z>WK`Ae^3aCI;UcglkJnMvnX46-=Xz8O@`t&Cje26&K#3vMK`>J7sq-ogNXNLRqD0f zCAh8!ZtJm*?b&BFd|SC!xit2~(hc=$K&D06k(ctEZw6SHBE7OjV}PPiKa6-aJql&I zjOY7N*^|jYAa|WNiIcsa_tWPDmu1Gyz5=sxZWtU(tY`Bp%CCQ*R?z6n;S@vl<)>pLT~s*m9a|bjUqiH z<#R=vu8h@`Z7}p3X$rR($6@MCJ39%rF<4AgkcwK|sVA<;Q)5J{saoX%n88S(Bsga| zd=gwk&pO8(G~+ED*4wE4+y{W`TL1T%81H!{pg#G4Q9kQSyM%j(>pH)$I_&%9voW{X-p?6 zCfG-vGafYePl+<+loPBG?b4ck&_>#QQy(Xa#H4}JA|K)+u9e#Re&bq~=D4&5%pZAS z&A)?M%ZzDB&=N7N6HqQkOsj`5m6z2Z9B8@T4f7_ZYAF!&kEnIQT?a9Up7E?n%{BmQ zajbMpr_rFb!l*~__L_QaymSLzUNhD9W=eZ+xv69~Yc06QCh#FUMKN~ZQTUUy_n#4y(H_Sx1Nx$XOL<;r&oJ{kV zM!`RE8srH_?q^rGaKjPTNYc$M%{tuUP~xfEOI0{sf6^{_lXw>dT<$=_fnsW6v3YrP zJoToB9#^Pbgrpd8FDyh6?vgtf3$eK47I@4Ncf4Ufx+w<2{8Sd{A@@p#id}_J!98ON zDs~Vm%q-14V+?C%mgb%@rGdvVT&vNBgO`GF5d|Dj`R=AkYu#&t7%X{H#7VS|vUIE- zIHe;t+#WJk+KTkJskU`{X)9eQf)B^)%~so+v?l9~dN7MAVhJxkJ2twZwr(GSdb=p-Mzxn~(YXWzeP=*H8L63Y@t=)!;hcI7IpRv6T z)(8!>>eVAvyBT16rLER9u)ZJM=GeEk+E4?gd9@l9gM=D%+M|pp>TkaVKU43TQyxCE zmbRvj5qbFu_g~2b$sPw7d)!ipJ@k<>2qW!gM!l3+Wb*PcEe3pW1Zcb{q+MvrE*@cP zi-9j8kB;YW?8H5P%tqdcd)~6W1aCC@#EU)xMXDHu;+>5|e3RPMbs3X7b7az5CX=23 zCXBp0A3rTQMbPU#JSiX*G(;fHI2s>LU_M{$5#d=jQ z5kqs0&lbW8pAY>7p~8$neL!G<{gXLLaVBKAF2aOJA<2a1l6+8zX$VZwnfaJECMss} z(cC^!Fi%YMUjQ8}Ci)|MqL?T>C1Ro!`kL##M|&N+(SqBKh<#{y;jOpDf>2i&eaYjg zEyOmP$uM5;VARE_6_&x_QwfekJPNM;3r0y@EqLH*5CY#-C<{?qF9&!8DAr#pCrgxQTK`mq$FzGGqIXjKx&;vYEEex zd|JgnGjNr#la@XlM)`EWFE;|7;WXX`g-i?P)PToDlvfFOtU!*9L#reXhwMd%^ZI_^ zd{SXJvt_2}6FRrkDCRDjd-rVURi=Aaf|hXa{uU(ce7biL4&mNqj7yHK-$OH*?p^1C zySioPLfFrB5&Tyi=;)c_H|mKBK(j>SL8nU|&2kq&$27~`_(Yn8o)$*4T%*bm7<$EY z%2<9e26fLtn_L(NliQfvOI%zmk}7#U&_st>W$mAob@8_q7?BA6KBx=c*VRSX*v z8JMY>NY~}sd!o)1R~!-%?z^#?nBv$Nr)^Sq@Embz94%4c=63Y1!~SU_>>0Y_Q&1?c z?)VtxRdh$B#0ap$%I@#`0r<;>0oH|;D&`nR?unNelczG z_(IwwYGe*-WoIVfaT&Cz6P7X%tzRv3aa1;Z!Aa9H3TzI-2{d5s-Y)S-uY8Qc0T}Ha{*YHUP+~?B&N@~ z?&=55*}`z@VmV7)rt#Y1smrUOSDETkf|gL17eKlBRF?>cP?v1Tk-EGPlINr@MFpTP zqmiuBC6BuNB!I!@T%W=xQkV3!FzWI;O$J7hl48ozaD@g;i8Obf493hsRbG$*^y1FH zzQP%%A6MSrMrFf$PLgJfXq}jb4X2}5XvM70x=#GxQ4fpjM2Q{uJ6KIjC+ahpd2{3a zn-Ss+9r#lylvfA-IA>mTkENkt@kI3rfHYH7OVARc`ioF*K1DUcAw)If1QOL>g5)`g zYEc1*>WIOLi|U=5g{XckK9Q)Vr-c#KS5w3VWn8W9A1NlGd+hqrYMz5&zF4cMENxdS za4LaUaec43NZzrh0!jLvx9p3`F6I(P4R;Blye{^cdB$R?x(I$|)Q#dISfao^h}FbI z@L8#(o$pzRXY4?RL_PwA@=D}s%B##u*0vN*WeQIk`zaq`{jF9)C%{Ykf&Ybt;nyWs zmhqoP(Z$m!cR;T)HHri+p;7iix%t#62#3%pjIl|hTo1`}(kP+=&?wP((dm-M_j1ogW# zhh5H>_1@(yaMKM(dv0}(@;chCT?ugb;(n(ZqUr)m2cjZ!9Z}V4z^)Rj4Z3ar(!TN5 z39nC9nb#T;P{Bs%$7Ql)#9qp7XbF1;a+%~ZZHaEAfX$W-?wRnKuov0`>RON(+{BTmGKg+MW1*Y4&Bj>b33hsWz;18^=97`|ydYV!4g_q8!~7GrU}RqcKN| zH&&d{zqz#}DtiiXsJ_2?bCPxfS!thi+?)~huecp1apP{rYGQWS)qOJ%XAn7iUZYgS zKT6YQCHpA)EME0~jvD)!VXj>Qg;(sRx2^+6;CMxR7FD^+$U$h9$^O%N{mK_}sBjNQ#3;U5(v;oFUE z7)i)m(n*NgOZ0CAz36@Vu(`cHNXWqGlZ5?mS^uKBBc?6uUmy|{-Ln2QV}lvoz?v$8 zoZflSZJDO7^eyWny^NNWo$SUkIv@Sqj16VAT+R*d##Mia znFm)0VqbhJiu+wSd3la~@xo@7o8!*{h%+bi628P_{x>jp$}ySGgGjkU9)S@vdS)Wg zjQ+QLo|b2ays~Ru*!i$&v*CG1N}U$%lfQl9y1lSDn9X14sx7NDT|a6!OS)k+4>d*n z>~1>~489Tf68ux}XwFl}kS1zu<$cJrQ~M2Ur#3@uv{BoK-PnOws@YSpF}qZ&Pfgiv z*t5#Dh9kuEWYqDYUeB(pRC@PgFJrDMhHAi=sp3bXg z>KHfg+CGsuqBkp(x}~+pw(mWRgx3@zA>T4|1r|Dg{{XPSRq&W&+xM$ZIh+ZVe+oh- z$JqoU(#?g4#J44JBCP---9$vnmg69#T)r)d(bInI=bo~J{u!5a^PTAmJ6y@wLEV`i z_H%z=#Nd+W1II44Z8#Ib4`9GCeo8;^K)L%SVXEgI%tbib_5-n=X!|w8)rZ*-WSdsuxR-R#%SU6C zm|1{tzQmU4Fq$(^ZGv|abtjyEf{Jupj^Qx%GvF`|xtW=CZpB@e@1^$5pmKT1$YpLo z=T;=L`}qMrQS67F z60slh=+AW+?qti=W7LC@bFEAz3s3COzx}^i7x>{7vukrN%yIczU)%p#uK(D~)X! z{eHY9-S5}SzkZ#$y*~VY3=CcVs)Aefub)rIQ7E)AEtO#DqJ&?}9d$skZzj6iQS zwqYa?Z%HH2a!_WXPxP$!=mB$UeUOI%(IocByByBg&l;H;KNb)D)>M_aO2rTsoASWpM`1 zyb7n#6o;sBEAK;|XvYZ_?I7!a-h&;AH7n}gMWKzhJ>6+oZSLURUb=!(Tb>_`u`=6B zS5lHUHN{iLhsc-`^)x#(lXe@A4{F=60IdejBact8w_}vX>NMbRRIpUiy~jP`CQZf+ zjY7=8-F!7HbpHO8z)TK2=7^ho^FJ{5g`xN#x{p$dJfAVnU4KJl*tw9F^|HGCqUI6WpeP$N4OMV};pw z{llni__C9+V<7{H2?zGsnHiZEJM+7as^3N3O`Eco9g{0tqRRa(Rugk&FXM#ITwfrZ zp*UMjtE=w~V*M>FTXMExswLq|OttnsSL#$N6VYe|@Nl81 z07PR%v*M!h@c2VG@r_NSQW-{iisn^Btcyob1bRFAV?Q8KWUX+I==+>~xi7 zx<0cLl|A{Krf>4v5Tl2{P9j}L#(hzT(ve}(-x3+_OR$=l{=T3GN3dC)62DyNV9Q&L zea&#MzX=L~w#>W5>j7gM#$XF?saUY(FU;-r5o}>#NL_2%;`KU0jzYrzgDnr6J7Nm9 z{0|0&T#k_Iw#4rm8_eJa)>IMX^v;XsYuZp+)L_f+&DdDLU`u#G52cm5@8mZ`^K4&3 zY~M*Le*2BYlymGmVd^y;Xv32KTGxFkfSVbHk-#P91}!L;V{XtBg>uIULLw#xOls4_ zpbg1j$5f3HfT67>+_Yfx$b6Y2rR4592LKsycjBL-`$kRy z;}1X|LOr4SVfD{mSpN@GDO~92$9^usvz<L&}Q_Aw}mdM9K>ZDY>|d z;jajHVFSorT*qh}c8$2MYAFA$QH1aE9PYz<4@26!3L#DBB#e3d2_dcLUW{FR_DtJQ z;eHZD@^xKbm2p-J$Ah5F4mU3{E6YvvH0uW%>BM{<+bV%i3EwBVWB67H z*-qKD(3re5W;H9mPZ}FZ$7j5R^hoLsMx)mg-GBzK=Z2?wwGM1pt$J`HE8M>=FYr%ZgO41PKaWa~gL3B_9F*so9F*N~#Nd^T1-v9s;o&}9k(CNwj#R1S zazyf@s8rV2bS|*Q1`cjS1Y6AE#V1K0Be4Ocvhwh}RE}#|)g#ul-KALyE>%zDBR&JO zRpi?S7ypdj*~Li+9>lhsQJrYG%(S=;wvW+O!ChxNmR%aIuXftFjk?0qC&INNr2+e_ zyA$DR-wp!2QMem!f?LBnLA~L_VN=AMwQjib8CC-}i*{er<+qh?mjJM@nS?&#S7>ek;A+oy!_8Bk7c{-VRzB`u z8m{y0Y5=Q{D#u~b!%P=a*5j3CtxDCKnW2F*5st7FyJ1sG1@C08R2!D>1IW-KFAycH zZQCuc9aI|jQM=LIzbqVbtVZ-Y5N@)M18c#hq(R3QnFhjvnmyT>hWgeYv)WC-736@M zlV>1hFlbv<8#~p#FI*2dK6iqO-K=^w$kh!8JHgc06UO{HJfEk3tk~_g*RH^RyjsI< z1D+AoYS0Zgz_rbKEdx|bH{25Y605>}3NrS3R{$ZRf!l65K9SgWxL&OS`Dml1Em0Vn?90XCbfLKQ}ZgvXON{f z+Q(r{8(aXYlq4>x29=|Azs^{610JZ0hXg=JIFzfzfN-{lYoRkf3;`>EYrdhT zst%hbyD(gb@d+C7?Kb8C3S(_GAo-fMbu4-X7TtuS3OJ`Z4UEV(Cp>NAws3vRZo#?( zI?|v@2wRIOlNP{^gCi_CLD2Gd@7Qtd*f9`nf#WsDz4r8untgN!#4RqMd+K_#%-(>7>oV1-T45HL#sauOH;)t279lv+I@da5 z1cbBCnRW)ll?(w=2@Ei&myCu_N4iZMA{q|VeNeL9`*0flQtaNA@OgcxdotqbG~zZa z-m%+$F1=3*wW1ayO8|7~J~q8_jydvh;?K*VH{$N@7$PV`E?hjPUt9nS*NUNmm*5z1u_Jtrc?)*u>G*SG75udD=iWj1 zc`5$9Z4Laq3xA0D@J3cTf7XZqJfJog0_y!D0I!@Vbj7LYxVTal9Bn7CFW<)>yz!AP zt90?&E*G!$a`8qI7jGP&XRb*j!Lg|n_&2b&f(7APT> zf`*yM$26Oo!t#N>106pz=`|uVfU=$$ON??4YBdguR;kctt$whv+W{K&XT|H{UzUVJ zU^Z604w`n|a81=~G(ZUy638c=lib`OB*(*j?-`GgT=}kjI2j3c}JZvvdglKdhD5zkPRD;qafMFpvX)d*B1lDX4LdZFJ9gYnIXE*F7;cRk3 z63GAm_ui_y^|)QtJu{jGoulb`+{gdA|9|gyANAnCyMFb5ZJ>X_sMj*B=J{#8UT->e z!|Mb?b*I)|G@5?riO$4Bo%=hLVA$2qc~0A{8J%DQq^OxzqwX5b&fT40Cnoo;hBwXg zZ}p6tZ#m5{yE$MEnp^MgRLtREz_xzk)AH8AL$UiP~qNSu%!cqe#hJ$3|B*z%+0|d=VUh6(y*FFXSO}32P3-g zyVhLW2MHj@o2|Ns+-l#|`fG1(1)1~&u)REAt}P5lt~&t_fsrfZICOwYUEkEYS@+>BS$P_UBiR0%qMd@7v1IS?G^&ZcN> zMUsZi43su*zAeA;RDx~5@>C1_Hc}MqKmy4D)4dUQOu_K^#m0;WaRL$!K^D(&mkjb( z4?`^vvRc-I;evk!VGonAlak&cDN_hF@l=-IGb;HFq5PQehOol4+{|~IPh#u-*+@v{ zQxpV%$UQ_P1WpjYZ@3LGskmM2NgEp@Sr}}Xvq&y9LDDeVc{M2+=tYoW3z>xXvNS%C zFO6Vhv!lmN2HE~Nu>Da|$VJ2}R^4hhYkpf#^jU(9Goo&|l571Rs#;s=T0h5Y9Sb&U ziK0FlFUtH);opMcusstM)LvUgvksoDdLDWkF$r^DI2LTdJc;tZ8ZSRT&O;Z^n6EpF zy4CCin_A1z^MMEcatm7505!o5CGmB4!SFl0s$dxWIuF0WWnH}nHq_-AR@y9T<9U$F zUxPM2AF{piu2ATAB{=yP1t(ulDH=tNMZX}54yCjioTOZ|y>;V;4T{oZ{h_#=%zuc4 zWPVE$57iNuj``08c!w?*c6Y%_?oJT+ay5au>>F~I;cgM{4sMvjGr8rOhe*7Rbg8;o}gR2d3X=8+nzeM~L47@q|dtE{`L!tEaf zdI|{9oAL*%^%~j{bq?7*87PW+&LOGz>o`kaR&wt5W$=| z8^(|wIp7bLa5Uz(%)wx2zTIe0TruB^-sUalezeGw8$q)7VRNTAz)Z8|H2wIHR}8Ok z%sPx=ZNb%BW=yof^exaq)$W+R;kedn=*!yNGW*4WthV4_KNkmvn(t^w4j(=uns(W2 zp*`DgAu-xNfj=~=EHHMn@5R*n*F!f5#crSD#DnCAgV8z|QPaY>TTOH|nBk)EAxuk& zs+Mmo#`SROwdeK4R>PPXIYXm``n+~DyE8BKwM57HsH-;@j0&n~^2p&MMT47McF@vKbM@`0{r`iC&}xczJJqw=um zkXWOL%liEVa{UskfT<ShfS*uX)#L7hH#5RJx>^O>z{HI9 zM$f3D*C4GMTHTr_;rkj-I8FXUvpmf=4b9M@n{If%rZ4GML!WCHJl!-mWBbW?odZ;- zuwH%_neLw<;p6t7;!k18%ci>(^8Yd9Z_nAk9IH9YT<=^h)&+m7S3xAQZ!nR4y_Cpg z3q+BR~UOU6A>kRrs0KU;F>P_;H(Q1ktd3q=~qC>jR{Wd4?{Z$qtK9$pb*I*lW0^ZxbAWL zmP8?HPsM{V)(l;kv0!hOdP0$Bzs{gsZA`MY?u7iusD3r6+*+Bs)D_5K9hDqet2*b~ z(fLN*->`20*q}ONn=-QD3LCzpvQ!ve1EU>)v2flkF>KdB~iCEto;)UT>=bZB%qSNI;`0KLV*zgM&gG5UBwJP2;dr=Lf+@&NoJVd+Icbl%f0k~C~r)Np?e^qe0wE9N`QcS;5$ za2!C6)YxLxw{noSQ^=FEjGHN=D2~DA4{iWCF>?@Yq}s-2S4vf=o*l435ZZGT%$WVRG}1!*y4VB15R zRqa1MKT;3^ErxB|X(8mp+v92)RYFLFPnB6GlE(Bed>(+@_9sbs`F#eF7I5y&25(;< z?_DYJVrsx?B`WjCjr=ug!NpiCs9a^;-7Wumphq=4;KJkhcDDTPiph^fVtEoH$y7f8 zw*4W{c8Foy7F3M`?Ke}eD@I-tw6@;``510(FG6Cw34f%xV7u_7$eb8L)wYjiAT8MK z0-T!155i2JgvaO@0Z>_jWFl7ZPKpcwUUG{BN*k^w`dKuP7tyd=ve3xvd(gnfZKtrE zq4-!we7$uUNSF=I2naLC5T=CB{kQ;o%OJ~gPo+D`EDuj3IGG3c=s~2DN109@DWwxW zK)oL`F?{(UxGVtD-ChDyF3da`qBIV*pF+wQoP4S{oLscV#Ax>EQnKTt*>{t`@*_iJ z`gTmrPZY?-$Dm9j`C$qx;A7BO1#h7WWKeG%Ld%~p=-6#<^RLP{CB0q zEc;VLE?*=ucLn@v<;i16dZkiS3(UjvMJGQo)y+k=7_^Q#hts7-)JWvsa$j5$fU+L&Z( zMT~iZ>Q|G>y(?3fx&k@AqmmuO5S|6-JeBh+sdjW*WHjp*);?Z5cRORMRVt{MrnP*E@jx zR?uU>n%sLZZZce_jwsD~c6=E+xf$toSQxBD1B) zD@enP8CdZ*)fFU>H$Jo2_c~}hErfimc*B6K`-G-cLP*5VY^<1BT_S0WbAkUNR{TCv zUV?&*w1^dFHTX5fimy-$E=G1i6Ymo%)_|T8SaGKO))Xu5fVOk7;)uKyR(vnyTWPEq zX@R~QVZ|(d5WM*l@R*OpyJE!`M_924j}l0n`#ubU_M6b8#_ih+TUG>-t(*P;$?Zou z2Ss4vJ9>bHg@9KD4{k6m*rl|<@ryak#PHyGxGXT3d_!?OxB%A~K=A!Y5(9!y76yX5 zSCv43A1Ea)KGwXCM3nFEBlEUnf<0d#7at)q9pncetbmUYV-?h>f_0Ao{{o79-2Sil zQx^2#+#=febBx>X3k3K#n8-d_N@N_Ph>7gOBr>(#atmbAEn|VG6SP1ccO(S(bdLzI zXwDk0CSLL7#9v}k{bDJp$_^7@#Q#ZB?F#swDhK!$VO4j2al8q1H z+^>+B3K<`hXc^-}J>(hR+QGS-2FbH+z@HvH8zO!GR?MK*0M6Z=;G`!w_vMK~)F!|~ zEY{o)oSURvZA`MYBAmOE>Q|G>T_sbOx&k>^qmmKqplzuq2m=~fW7!8ZlV2yz0W2}%=h*Mk+7m*?*~&-%m;fvt0ppAioAkC zVDF!(D=3c#7F-yd7D7JQ`*k&qDj~?RPHEPOq%qFpVDBTOynI^`X%XzrYVd0c_6~2A z8%;jh1x>t9uy+9HDFOCo%5P1<-eu5sF4!B9mjZjMkZ+~IUZh2Uy)1qZXsN?vKIrZW z_U?EsO#etS8@4@r{_s-bEr)+by?1Cfza?h&n+F}o!#zksjB(eXrl@=Hc_zd^C?!PMQV}=)ED5oYr80S!u~gJjo~5oGZu|`< zsjruklx%JYDLzk<>S}IlfE)i)f|H(bQIQh|4|e-7NZKljYF(TuCWMie9e%+jjzQk!j0)F zA2*IFshA5vB>~}1VJDFNK|Bx6{hs3I0UF{Y%2b~8@yz0^a9x~QiNh*zHsYXmI+7gE z=uVun9zT-&$^>OmEvKt+RJEvrcV6>TZ-}Q3y=Rc0K`tK7bpgp1?mgit?)I@T2Qj(} z813yu4a-F!%zGit2K;R7WRe|{9SE5u zp2{M4BxEC71gaBAA`#dRV=;+|hKqBQ%yk=`L$W=dkiU)2rryfm3U>0UX#LZPlJdI+1<}_Ov>xnNw!W;B zw(U=nXcM$#FM?)5iXyTIu_9~=tQ!|WKS9Nsk@4WyVwm$tSSNknxwZTRwb)vid510kkwy?T~gF&lFACrmB zM-2wZe~L=^om}yjVcFtj-Qel?^Lo4CSC{b2JDCVL47%OIFXvRfMcwu9X*>Ao8r)!I zZ^BXqZDn-WJuS{{Wce6*ZevRrFEUQ=W-nzqT%nXhe1Sv3_QEQOpI2gk5mc3N2D`06 z-6duwGP&<{X5tkE&fJEOAD*{uKZ;Csl|-baW+fIOUx8VPE>0ynss!o4t}`B0 z!U7FJ=?*;R*iY9Pi8(1ULJw(5yA{ssrp103U+_R}t3IvacT2Q+*ICq#-+DrWFCetw zh&tbcX!t+PxmhtbIN&d)dFG8DGKEvJ5Sa7`Wx+0IU5`{r4x;%*?`cHQ!ij_S|yd|Nh;A3Z`1!C@4uxT>(c?!DgO6g8UsU&hJX*-e0pC)K* z^-g2HheCGPsQf~9{@7*$&R>xUrIYLoXx^d1?-9`v&`k>t&(-+TNv#G)n$8)TGlvK8 z!il=n9B5WVtQM_I^mN|y&mgJec2G(q9DsTlGqIVY$KbNS%+XCN!KP>kD^pk)hwPtc zqIhp9QE(h6CW@y?6zXQjaUdo&wMlavC|1EEOxAI1{mxgwQAQ!Wj=^_E!@qDCD7ZoV z+!i@h`ZB>e%Qv;N1{~Qur5(V>C+Y_#wF7jDGhUptnl0Cv)90*)4xQ7;qPtq#Xx-P`EZyt92`vv8dL^d^2Kome%cJv z`PQQ0w6R2}pk>w0*5I2ASf6%4UjWnh{M$|AHu%_+PDDHPI%R_gbzs+_gy`z;xD~Sy zcg=Z+D4?$i9q9cC>Q6EA_+%;bkXyi?PbhZrH`YMX7TPE2OY18YFAscYp2-y zTF6WIqw~cpd*Q+Nu$K42gYTWx;Er0KxRG0+cq&?3EzhIBdGJxy@?b_ z7bl*hn-kZes}m2HyAyZ9<%xSn2;9YtWus#ATf#$ z$M&g=MP>l+0CBLLBX{Bz%>dF>at4r1_w;+h{Pm{M0Msv>z$rK7qMn|bPLwTg5{XS$ zb)R&(EK!u0cA3-w=c)SV{RxUst3J(lbCJU#l3u}Tdvv&DGw)3$oZ9?V0pKa_M8cpcS5ED zc&CZb{?Aa_7_`5YTPf>A#R!(3j|E{mS=hhc3&Foyn&5mdC*~~>$IjB8y=F-6PXbrD zNNz-03duDg-%296NQXdjnGq9)Wy9lKBscdwZUo_Y5D^7H5DD9ctd}A|taWl4K)4 z4SY8)E5FRtqrEWo)_#~85#i|sd;uE=8Bsqt^$N@>mRhF#=X;_2nZ76&K{|u-& z_DH7ux&B^(Ht(aUIQ12$1)e+=!J`~ z_QM6a80`9CZ!D>ajF;Rgg*jyUzuOD_|JoP*LY|rQr(Xj7(nFd4i?^#fi8=9iFJ$UD z{)Se~y30uoWWJ8I1)fXh;!rPKT-_HJLZ&%fWWA2Hg-kA9*9#Xn_QS=f@JoDTl2k_a zd-#NFGM}uDYA=l3)fXc|-g%5i(nh_aUh%a-BBR*N5g(*OAUZS~;u8XZi^_OUSVbRb&!ss;uk94VbIPWzozhC&y-x5R3-hk!T53slsp;JviPvm$H}O< zOu_}fB!Z@6s-*}~i6f;vS#ne$&-;Q$7U^ULEF}Xu9@&6QMLn`kX=4O$Zzb7@fm<0b zVQ4Jnkm-M@7y9onO~359@;d)n8eO}H@WaktYwYX*2p{Ql1Cr^Z z9sThw`pPo{a4Wsy%z&#B6h$?hu8J)jiGu_3vusFZ37cJzpDUxHar-&0catx_Qm%QU zBLqGd=Aa`4UJ88odW67L-H#9uvgrg9UC$8+)0)zj_=w=(ZM_aPMA0Qq6bQ3K3l?j7 z1GcZ}F2uHz8+(>pJGe1jWYOM`pbAbIcvY@RhUOJJAt0hXN4U&$sm>cXnL##NpL}%I zX~G%-2ZXci(G8tC(1J}Px;I^OTw{9P1*~}*_X6IGS3J^2hY?&}fEzJ@E0i{DQnX6! zJE7HQcO_KE5_~(%yN(P{UOTrZtr{V$tXHyZcI;I5G*v4ABH7s6v6w_R>!o@2!Sx@F zeYeF@@|V#7c#ypmEQwp0jmwp6_+D4HR}3Cp)jew?wiUP8BxSl9Skr|!in`Rx5Zj` zT1o7sU}uou$_(A9WGF{Tw-X(Ol{BxcL#ZTQQ#mF1>iT+65+kFGlDelwP$sLj$~*s+ zrvwvRl|Da1nMYRt6G~CVTTa1q0+e;fj}HhAbPq6@hi?35GQRwN6`!B5XJBVlo``#0 zlzMf6;{`;IAUb1U6e!LeszfG`8mjz1h;s#oDn$et9W;Q%i17-G5Hw!-XC6~CCo3wp zv`sCx`Cs&6VDD?gan5S(79BTm$LtNqVUIQIozW+UbZx;}GMbtkI98yf(4OtzM_Yzd z4)Le(wjH+XWF7S*NE@HwJb*URyI3pIHrC)Nw&9%#Hr(&-!KQZK(cruj*x#vZhC|T4XrZgIItzJIWeQXkr(Y6e%o#0 z2M!I7R2=G?<;mO`JV3;Xki_pI75)`d;a`+eA>R}HCz$DylLKG^e+HKzx{N)+k{!Qe z-N{S{W*t%%De0R`N&mZ)lKAc}uB0uXq!&p^`F+f6NNL#H=8` zuRrMJ^J$@7N2czZhUOY@qJ-x|J7Z>)eO$zlI7L9HHhK_AblWI-u3`Ks?73vK#|@+d zkiUT0m-xn6QPu@>t5I|$rhQCIFE1q~+3-+I7n7I@86J~p8N#}hT&nzgwta%eojYwV}s1sAj2f(YGabE75Av#O!ccte76G*12I63V_M>=3fHqyF0BwRP``rW^ zLk2~^e$|mdQBh*rG=g2bBZF^_;3rX9g`yV+23d2d`pYeYKWSKtmVfkBhYznepzwX5 z=KP?jUKI15FziHXNXLqO-v7WR`wU$x;p;^%=+n9iO70;g^(kDDfy>Q#m~9d3Aj~ z;8BbW*?Y#_u2`dqTaG_P!ij-~yOOOxr6-R4l7A4|lAg&r`?iw59G(3WkyTh{L%URN zK&dlcQ#qZDsq5=OXN-&755q|4e{E)2m_(46JqDdwDSA3 z2?#e+glh}oJ`3I|7s8F`OF_6#LcSG;aFG}R;j-9GpyAKLV?OLmuQ$(!a7DKx3&IWG z;aB6kLbze-3@|u+mr+vKwC(qxt&ZDI7dBZOU#t%ZS1?Fx5rBs$IS=4MyF$1xFbJ1= zM=Ort#-hC_`v~E+uCdT==q`t^CbetmY0L5aDVAqayOtg~^Yi@4^auqTQBJeHIA^$c z9Hwi)qJlaMjUBi|TzC?pXFO_qMgvctjg55ygFnKI;lrhjfrA+D$4r-;90Wyt04@ul zVt@HPEPWA3zo&VOyvXk|!}&}p!{NvaHyi}0f13;^KT4tR;m8ZKvHT7JCgsRW%rbw4 zS*C;$-X-wjk>Z~+6RsyV7n2E4v#w*}Ks-^Ou#qf$`gtbMuapv~+!7HN{Y%Dgw`rLY zAaW_r0T*12q9eQeM<%AfFC`|~?(mq#Z<3e_*&UN;8M{M0pxv>Kf;uNh{6r)J;Eow@ zQN&MlNFZYfF~~I*LHu5}TOfWf$16ho=xTKkzt=B90IWhhgV|Dc_8kj`J-~i5-B&nF z=hW5*_V==!A}i^A5{1_7(IS(vpitVdEf73)4d|;^=Fu)KKdx z$hYD{EhI(^wU}^es5OalhM8LESEh$rxq`-C0G|vI1%PV|bynUr-V0=~$&mZ-id-XI zDP3b!PsRQlSoRZ4B>Yrtb-xI(=_UTjOwj`TaJ92of3IBUkL%qP>--NUXp5>jUG?F_ z3{O)Iny0)Js zlo$3#qz3o*>T{{w607=I`e?d})79U4{-cTVipFlL99Hrl$~8J>L$PK2oL*@Se};D13h^wA}2Nq*|i?i7@YlHzj#x{%AtrLOQAE zvM|07GL$V0)l^#~4Es4Ork?v$L1#&pkU&?}kX50w%;M}rPxrb|w42JWwh%L2IUiQ+g|5rzQ- zTEP+{YrKI8KFKGg`M6O1c{9SKA$A}To3!&swQwVIY+tri+-@a@5I(|7`85{z_U_GMaJORt^P7x0_2m%{<4SKi%8r6W6ouD?ik5YkQD-N6_P>om>HOzWh> zO{yLZtbV2M#C6O`%)2qzS!=tv`J>98&j#DY6Z*vNY%t^*zK@@i>%cdEom$(s8XoKr zCgZGkg26Z8=W@MH_$}Vi*B8*nz4Ew=8YFS@MNdwyH=}Oa~2n= z;mii(?1|AZ=t&hn$XBg3bk75lp+=q|e6Za$=rg|!W65ZAPHut&Wp(%@FuxrLb{prx zwBU$bse_~%Zcj^!v#4bsqkU=U=+UDCzmonrF4S*i8qNN?9ZcN z?_}jhG>F{$)1waYjUlh*L{r&vz1k{W#H+43y>Y&DdHhaBm1 z4P=Bb28Wt%y`fQbo|C6B;Uc4Rp%{O^WA~wI)+zJiD$T&1~iPN)j{Rk;2YfG zEtu$TY*k=6%>^(-wmRz#yAB3BTSf~e7HA_@jRdwGpUmk%J2s9$Gkw41%^W&(?%X+W zY`*CO+Kh)2%b+}dH7n8>rQvRmZ%1wMAhsQ z)%dWd%bT_gf1au_{lx}UG3@Je(1z;1**Q5B><5cntcHy=2fMuGrmvr` zn%08Zfd62Q`ij^fwB1D zC&3V4p@RoI)q_3K(=C&0H8uRRJ#;+<{kgA-vPkRG=pKG^$r9#8d19qoN#q2@dc<5WRWMe9(T^CJ@!r_~+g& z@aF;iQyqjqb^JpTK86Cv<8JI7K)E;*Ai*>xD8#qbE8lRUP6?hNwODe53^0~${5&~* zSKh|Y+t8PJY&`bV#$!=!JXMbl1hnxqBpXjlVy1h5AMIpu)du5!4Bqg|}^nzu9 zI|IuI2iXW0k$bSQw{2seSIHhGD8$~{^w?prBSs1BxpuH49)!q)?x1+T1 diff --git a/Sphinx-docs/_build/markdown/index.md b/Sphinx-docs/_build/markdown/index.md index a3115d9..6538b9c 100755 --- a/Sphinx-docs/_build/markdown/index.md +++ b/Sphinx-docs/_build/markdown/index.md @@ -22,6 +22,9 @@ contain the root `toctree` directive. --> * Submodules + * sportsdataverse.cfb.cfb_game_rosters module + + * sportsdataverse.cfb.cfb_loaders module @@ -103,6 +106,9 @@ contain the root `toctree` directive. --> * Submodules + * sportsdataverse.nba.nba_game_rosters module + + * sportsdataverse.nba.nba_loaders module @@ -127,6 +133,9 @@ contain the root `toctree` directive. --> * sportsdataverse.nfl.model_vars module + * sportsdataverse.nfl.nfl_game_rosters module + + * sportsdataverse.nfl.nfl_games module @@ -154,6 +163,9 @@ contain the root `toctree` directive. --> * sportsdataverse.nhl.nhl_api module + * sportsdataverse.nhl.nhl_game_rosters module + + * sportsdataverse.nhl.nhl_loaders module @@ -175,6 +187,9 @@ contain the root `toctree` directive. --> * Submodules + * sportsdataverse.wbb.wbb_game_rosters module + + * sportsdataverse.wbb.wbb_loaders module @@ -196,6 +211,9 @@ contain the root `toctree` directive. --> * Submodules + * sportsdataverse.wnba.wnba_game_rosters module + + * sportsdataverse.wnba.wnba_loaders module @@ -217,6 +235,9 @@ contain the root `toctree` directive. --> * sportsdataverse.config module + * sportsdataverse.decorators module + + * sportsdataverse.dl_utils module diff --git a/Sphinx-docs/_build/markdown/modules.md b/Sphinx-docs/_build/markdown/modules.md index 6e9ac73..addbbe0 100755 --- a/Sphinx-docs/_build/markdown/modules.md +++ b/Sphinx-docs/_build/markdown/modules.md @@ -13,6 +13,9 @@ * Submodules + * sportsdataverse.cfb.cfb_game_rosters module + + * sportsdataverse.cfb.cfb_loaders module @@ -94,6 +97,9 @@ * Submodules + * sportsdataverse.nba.nba_game_rosters module + + * sportsdataverse.nba.nba_loaders module @@ -118,6 +124,9 @@ * sportsdataverse.nfl.model_vars module + * sportsdataverse.nfl.nfl_game_rosters module + + * sportsdataverse.nfl.nfl_games module @@ -145,6 +154,9 @@ * sportsdataverse.nhl.nhl_api module + * sportsdataverse.nhl.nhl_game_rosters module + + * sportsdataverse.nhl.nhl_loaders module @@ -166,6 +178,9 @@ * Submodules + * sportsdataverse.wbb.wbb_game_rosters module + + * sportsdataverse.wbb.wbb_loaders module @@ -187,6 +202,9 @@ * Submodules + * sportsdataverse.wnba.wnba_game_rosters module + + * sportsdataverse.wnba.wnba_loaders module @@ -208,6 +226,9 @@ * sportsdataverse.config module + * sportsdataverse.decorators module + + * sportsdataverse.dl_utils module diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.cfb.md b/Sphinx-docs/_build/markdown/sportsdataverse.cfb.md index d65fbd8..d02a889 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.cfb.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.cfb.md @@ -2,10 +2,54 @@ ## Submodules +## sportsdataverse.cfb.cfb_game_rosters module + + +### sportsdataverse.cfb.cfb_game_rosters.espn_cfb_game_rosters(game_id: int, raw=False, return_as_pandas=True, \*\*kwargs) +espn_cfb_game_rosters() - Pull the game by id. + +Args: + + game_id (int): Unique game_id, can be obtained from espn_cfb_schedule(). + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Data frame of game roster data with columns: + ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, + ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, + ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, + ‘age’, ‘date_of_birth’, ‘slug’, ‘jersey’, ‘linked’, ‘active’, + ‘alternate_ids_sdr’, ‘birth_place_city’, ‘birth_place_state’, + ‘birth_place_country’, ‘headshot_href’, ‘headshot_alt’, + ‘experience_years’, ‘experience_display_value’, + ‘experience_abbreviation’, ‘status_id’, ‘status_name’, ‘status_type’, + ‘status_abbreviation’, ‘hand_type’, ‘hand_abbreviation’, + ‘hand_display_value’, ‘draft_display_text’, ‘draft_round’, ‘draft_year’, + ‘draft_selection’, ‘player_id’, ‘starter’, ‘valid’, ‘did_not_play’, + ‘display_name’, ‘ejected’, ‘athlete_href’, ‘position_href’, + ‘statistics_href’, ‘team_id’, ‘team_guid’, ‘team_uid’, ‘team_slug’, + ‘team_location’, ‘team_name’, ‘team_nickname’, ‘team_abbreviation’, + ‘team_display_name’, ‘team_short_display_name’, ‘team_color’, + ‘team_alternate_color’, ‘is_active’, ‘is_all_star’, + ‘team_alternate_ids_sdr’, ‘logo_href’, ‘logo_dark_href’, ‘game_id’ + +Example: + + cfb_df = sportsdataverse.cfb.espn_cfb_game_rosters(game_id=401256137) + + +### sportsdataverse.cfb.cfb_game_rosters.helper_cfb_athlete_items(teams_rosters, \*\*kwargs) + +### sportsdataverse.cfb.cfb_game_rosters.helper_cfb_game_items(summary) + +### sportsdataverse.cfb.cfb_game_rosters.helper_cfb_roster_items(items, summary_url, \*\*kwargs) + +### sportsdataverse.cfb.cfb_game_rosters.helper_cfb_team_items(items, \*\*kwargs) ## sportsdataverse.cfb.cfb_loaders module -### sportsdataverse.cfb.cfb_loaders.get_cfb_teams() +### sportsdataverse.cfb.cfb_loaders.get_cfb_teams(return_as_pandas=True) Load college football team ID information and logos Example: @@ -14,12 +58,14 @@ Example: Args: + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Returns: pd.DataFrame: Pandas dataframe containing teams available for the requested seasons. -### sportsdataverse.cfb.cfb_loaders.load_cfb_pbp(seasons: List[int]) +### sportsdataverse.cfb.cfb_loaders.load_cfb_pbp(seasons: List[int], return_as_pandas=True) Load college football play by play data going back to 2003 Example: @@ -29,6 +75,7 @@ Example: Args: seasons (list): Used to define different seasons. 2003 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -39,7 +86,7 @@ Raises: ValueError: If season is less than 2003. -### sportsdataverse.cfb.cfb_loaders.load_cfb_rosters(seasons: List[int]) +### sportsdataverse.cfb.cfb_loaders.load_cfb_rosters(seasons: List[int], return_as_pandas=True) Load roster data Example: @@ -49,6 +96,7 @@ Example: Args: seasons (list): Used to define different seasons. 2014 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -59,7 +107,7 @@ Raises: ValueError: If season is less than 2014. -### sportsdataverse.cfb.cfb_loaders.load_cfb_schedule(seasons: List[int]) +### sportsdataverse.cfb.cfb_loaders.load_cfb_schedule(seasons: List[int], return_as_pandas=True) Load college football schedule data Example: @@ -69,6 +117,7 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -79,7 +128,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.cfb.cfb_loaders.load_cfb_team_info(seasons: List[int]) +### sportsdataverse.cfb.cfb_loaders.load_cfb_team_info(seasons: List[int], return_as_pandas=True) Load college football team info Example: @@ -89,6 +138,7 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -101,24 +151,25 @@ Raises: ## sportsdataverse.cfb.cfb_pbp module -### class sportsdataverse.cfb.cfb_pbp.CFBPlayProcess(gameId=0, raw=False, path_to_json='/') +### class sportsdataverse.cfb.cfb_pbp.CFBPlayProcess(gameId=0, raw=False, path_to_json='/', return_keys=None, \*\*kwargs) Bases: `object` -#### \__init__(gameId=0, raw=False, path_to_json='/') +#### \__init__(gameId=0, raw=False, path_to_json='/', return_keys=None, \*\*kwargs) Initialize self. See help(type(self)) for accurate signature. #### cfb_pbp_disk() -#### create_box_score() +#### create_box_score(play_df) -#### espn_cfb_pbp() +#### espn_cfb_pbp(\*\*kwargs) espn_cfb_pbp() - Pull the game by id. Data from API endpoints: college-football/playbyplay, college-football/summary Args: game_id (int): Unique game_id, can be obtained from cfb_schedule(). + raw (bool): If True, returns the raw json from the API endpoint. If False, returns a cleaned dictionary of datasets. Returns: @@ -143,13 +194,15 @@ Example: #### raw( = False) +#### return_keys( = None) + #### run_cleaning_pipeline() #### run_processing_pipeline() ## sportsdataverse.cfb.cfb_schedule module -### sportsdataverse.cfb.cfb_schedule.espn_cfb_calendar(season=None, groups=None, ondays=None) +### sportsdataverse.cfb.cfb_schedule.espn_cfb_calendar(season=None, groups=None, ondays=None, return_as_pandas=True, \*\*kwargs) espn_cfb_calendar - look up the men’s college football calendar for a given season Args: @@ -157,6 +210,7 @@ Args: season (int): Used to define different seasons. 2002 is the earliest available season. groups (int): Used to define different divisions. 80 is FBS, 81 is FCS. ondays (boolean): Used to return dates for calendar ondays + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -167,7 +221,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.cfb.cfb_schedule.espn_cfb_schedule(dates=None, week=None, season_type=None, groups=None, limit=500) +### sportsdataverse.cfb.cfb_schedule.espn_cfb_schedule(dates=None, week=None, season_type=None, groups=None, limit=500, return_as_pandas=True, \*\*kwargs) espn_cfb_schedule - look up the college football schedule for a given season Args: @@ -177,6 +231,7 @@ Args: groups (int): Used to define different divisions. 80 is FBS, 81 is FCS. season_type (int): 2 for regular season, 3 for post-season, 4 for off-season. limit (int): number of records to return, default: 500. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -187,17 +242,22 @@ Returns: ## sportsdataverse.cfb.cfb_teams module -### sportsdataverse.cfb.cfb_teams.espn_cfb_teams(groups=None) +### sportsdataverse.cfb.cfb_teams.espn_cfb_teams(groups=None, return_as_pandas=True, \*\*kwargs) espn_cfb_teams - look up the college football teams Args: groups (int): Used to define different divisions. 80 is FBS, 81 is FCS. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. +Example: + + cfb_df = sportsdataverse.cfb.espn_cfb_teams() + ## sportsdataverse.cfb.model_vars module ## Module contents diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.mbb.md b/Sphinx-docs/_build/markdown/sportsdataverse.mbb.md index 5eb2481..d4ee416 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.mbb.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.mbb.md @@ -5,12 +5,13 @@ ## sportsdataverse.mbb.mbb_game_rosters module -### sportsdataverse.mbb.mbb_game_rosters.espn_mbb_game_rosters(game_id: int, raw=False) +### sportsdataverse.mbb.mbb_game_rosters.espn_mbb_game_rosters(game_id: int, raw=False, return_as_pandas=True, \*\*kwargs) espn_mbb_game_rosters() - Pull the game by id. Args: game_id (int): Unique game_id, can be obtained from mbb_schedule(). + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -38,17 +39,17 @@ Example: mbb_df = sportsdataverse.mbb.espn_mbb_game_rosters(game_id=401265031) -### sportsdataverse.mbb.mbb_game_rosters.helper_mbb_athlete_items(teams_rosters) +### sportsdataverse.mbb.mbb_game_rosters.helper_mbb_athlete_items(teams_rosters, \*\*kwargs) ### sportsdataverse.mbb.mbb_game_rosters.helper_mbb_game_items(summary) -### sportsdataverse.mbb.mbb_game_rosters.helper_mbb_roster_items(items, summary_url) +### sportsdataverse.mbb.mbb_game_rosters.helper_mbb_roster_items(items, summary_url, \*\*kwargs) -### sportsdataverse.mbb.mbb_game_rosters.helper_mbb_team_items(items) +### sportsdataverse.mbb.mbb_game_rosters.helper_mbb_team_items(items, \*\*kwargs) ## sportsdataverse.mbb.mbb_loaders module -### sportsdataverse.mbb.mbb_loaders.load_mbb_pbp(seasons: List[int]) +### sportsdataverse.mbb.mbb_loaders.load_mbb_pbp(seasons: List[int], return_as_pandas=True) Load men’s college basketball play by play data going back to 2002 Example: @@ -58,6 +59,7 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -69,7 +71,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.mbb.mbb_loaders.load_mbb_player_boxscore(seasons: List[int]) +### sportsdataverse.mbb.mbb_loaders.load_mbb_player_boxscore(seasons: List[int], return_as_pandas=True) Load men’s college basketball player boxscore data Example: @@ -79,6 +81,7 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -90,7 +93,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.mbb.mbb_loaders.load_mbb_schedule(seasons: List[int]) +### sportsdataverse.mbb.mbb_loaders.load_mbb_schedule(seasons: List[int], return_as_pandas=True) Load men’s college basketball schedule data Example: @@ -100,6 +103,7 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -111,7 +115,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.mbb.mbb_loaders.load_mbb_team_boxscore(seasons: List[int]) +### sportsdataverse.mbb.mbb_loaders.load_mbb_team_boxscore(seasons: List[int], return_as_pandas=True) Load men’s college basketball team boxscore data Example: @@ -121,6 +125,7 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -134,12 +139,13 @@ Raises: ## sportsdataverse.mbb.mbb_pbp module -### sportsdataverse.mbb.mbb_pbp.espn_mbb_pbp(game_id: int, raw=False) +### sportsdataverse.mbb.mbb_pbp.espn_mbb_pbp(game_id: int, raw=False, \*\*kwargs) espn_mbb_pbp() - Pull the game by id. Data from API endpoints: mens-college-basketball/playbyplay, mens-college-basketball/summary Args: game_id (int): Unique game_id, can be obtained from mbb_schedule(). + raw (bool): If True, returns the raw json from the API endpoint. If False, returns a cleaned dictionary of datasets. Returns: @@ -152,9 +158,11 @@ Example: mbb_df = sportsdataverse.mbb.espn_mbb_pbp(game_id=401265031) +### sportsdataverse.mbb.mbb_pbp.helper_mbb_game_data(pbp_txt, init) + ### sportsdataverse.mbb.mbb_pbp.helper_mbb_pbp(game_id, pbp_txt) -### sportsdataverse.mbb.mbb_pbp.helper_mbb_pbp_features(game_id, pbp_txt, gameSpread, homeFavorite, gameSpreadAvailable, homeTeamId, awayTeamId, homeTeamMascot, awayTeamMascot, homeTeamName, awayTeamName, homeTeamAbbrev, awayTeamAbbrev, homeTeamNameAlt, awayTeamNameAlt) +### sportsdataverse.mbb.mbb_pbp.helper_mbb_pbp_features(game_id, pbp_txt, init) ### sportsdataverse.mbb.mbb_pbp.helper_mbb_pickcenter(pbp_txt) @@ -162,13 +170,14 @@ Example: ## sportsdataverse.mbb.mbb_schedule module -### sportsdataverse.mbb.mbb_schedule.espn_mbb_calendar(season=None, ondays=None) +### sportsdataverse.mbb.mbb_schedule.espn_mbb_calendar(season=None, ondays=None, return_as_pandas=True, \*\*kwargs) espn_mbb_calendar - look up the men’s college basketball calendar for a given season Args: season (int): Used to define different seasons. 2002 is the earliest available season. ondays (boolean): Used to return dates for calendar ondays + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -180,7 +189,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.mbb.mbb_schedule.espn_mbb_schedule(dates=None, groups=50, season_type=None, limit=500) +### sportsdataverse.mbb.mbb_schedule.espn_mbb_schedule(dates=None, groups=50, season_type=None, limit=500, return_as_pandas=True, \*\*kwargs) espn_mbb_schedule - look up the men’s college basketball scheduler for a given season Args: @@ -189,6 +198,7 @@ Args: groups (int): Used to define different divisions. 50 is Division I, 51 is Division II/Division III. season_type (int): 2 for regular season, 3 for post-season, 4 for off-season. limit (int): number of records to return, default: 500. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -199,15 +209,20 @@ Returns: ## sportsdataverse.mbb.mbb_teams module -### sportsdataverse.mbb.mbb_teams.espn_mbb_teams(groups=None) +### sportsdataverse.mbb.mbb_teams.espn_mbb_teams(groups=None, return_as_pandas=True, \*\*kwargs) espn_mbb_teams - look up the men’s college basketball teams Args: groups (int): Used to define different divisions. 50 is Division I, 51 is Division II/Division III. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing teams for the requested league. +Example: + + mbb_df = sportsdataverse.mbb.espn_mbb_teams() + ## Module contents diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.md b/Sphinx-docs/_build/markdown/sportsdataverse.md index d2b9351..acbfe75 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.md @@ -9,6 +9,9 @@ * Submodules + * sportsdataverse.cfb.cfb_game_rosters module + + * sportsdataverse.cfb.cfb_loaders module @@ -90,6 +93,9 @@ * Submodules + * sportsdataverse.nba.nba_game_rosters module + + * sportsdataverse.nba.nba_loaders module @@ -114,6 +120,9 @@ * sportsdataverse.nfl.model_vars module + * sportsdataverse.nfl.nfl_game_rosters module + + * sportsdataverse.nfl.nfl_games module @@ -141,6 +150,9 @@ * sportsdataverse.nhl.nhl_api module + * sportsdataverse.nhl.nhl_game_rosters module + + * sportsdataverse.nhl.nhl_loaders module @@ -162,6 +174,9 @@ * Submodules + * sportsdataverse.wbb.wbb_game_rosters module + + * sportsdataverse.wbb.wbb_loaders module @@ -183,6 +198,9 @@ * Submodules + * sportsdataverse.wnba.wnba_game_rosters module + + * sportsdataverse.wnba.wnba_loaders module @@ -202,6 +220,24 @@ ## sportsdataverse.config module +## sportsdataverse.decorators module + + +### sportsdataverse.decorators.record_mem_usage(func) + +### sportsdataverse.decorators.record_time_usage(func) + +### sportsdataverse.decorators.timer(number=10) +Decorator that times the function it wraps over repeated executions + +Args: + + number: int, The number of repeated executions of the function being wrapped + +Returns: + + func + ## sportsdataverse.dl_utils module @@ -270,7 +306,7 @@ although there are some cases where that does not hold: -### sportsdataverse.dl_utils.download(url, params={}, num_retries=15) +### sportsdataverse.dl_utils.download(url, params=None, headers=None, proxy=None, timeout=30, num_retries=15, logger=None) ### sportsdataverse.dl_utils.flatten_json_iterative(dictionary, sep='.', ind_start=0) Flattening a nested json file diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.mlb.md b/Sphinx-docs/_build/markdown/sportsdataverse.mlb.md index 4249be9..986d8cc 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.mlb.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.mlb.md @@ -43,7 +43,7 @@ Args: ‘L’ - League Championship ‘W’ - World Series -Returns: +Returns: A pandas dataframe containing MLB scheduled games. @@ -380,8 +380,8 @@ Returns: ### sportsdataverse.mlb.retrosheet.retrosheet_game_logs_team() -Retrives the team-level stats for MLB games in a season, or range of seasons. -THIS DOES NOT GET PLAYER STATS! +Retrives the team-level stats for MLB games in a season, or range of seasons. +THIS DOES NOT GET PLAYER STATS! Use retrosplits_game_logs_player() for player-level game stats. Args: @@ -400,7 +400,7 @@ Args: The full list of supported keywards for game_type are as follows. Case does not matter (you can set game_type to “rEgUlAr”, and the function call will still work): - > + > > * “regular”: Regular season games. diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.nba.md b/Sphinx-docs/_build/markdown/sportsdataverse.nba.md index 3835d84..2ca6190 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.nba.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.nba.md @@ -2,10 +2,54 @@ ## Submodules +## sportsdataverse.nba.nba_game_rosters module + + +### sportsdataverse.nba.nba_game_rosters.espn_nba_game_rosters(game_id: int, raw=False, return_as_pandas=True, \*\*kwargs) +espn_nba_game_rosters() - Pull the game by id. + +Args: + + game_id (int): Unique game_id, can be obtained from espn_nba_schedule(). + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Data frame of game roster data with columns: + ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, + ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, + ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, + ‘age’, ‘date_of_birth’, ‘slug’, ‘jersey’, ‘linked’, ‘active’, + ‘alternate_ids_sdr’, ‘birth_place_city’, ‘birth_place_state’, + ‘birth_place_country’, ‘headshot_href’, ‘headshot_alt’, + ‘experience_years’, ‘experience_display_value’, + ‘experience_abbreviation’, ‘status_id’, ‘status_name’, ‘status_type’, + ‘status_abbreviation’, ‘hand_type’, ‘hand_abbreviation’, + ‘hand_display_value’, ‘draft_display_text’, ‘draft_round’, ‘draft_year’, + ‘draft_selection’, ‘player_id’, ‘starter’, ‘valid’, ‘did_not_play’, + ‘display_name’, ‘ejected’, ‘athlete_href’, ‘position_href’, + ‘statistics_href’, ‘team_id’, ‘team_guid’, ‘team_uid’, ‘team_slug’, + ‘team_location’, ‘team_name’, ‘team_abbreviation’, + ‘team_display_name’, ‘team_short_display_name’, ‘team_color’, + ‘team_alternate_color’, ‘is_active’, ‘is_all_star’, + ‘logo_href’, ‘logo_dark_href’, ‘game_id’ + +Example: + + nba_df = sportsdataverse.nba.espn_nba_game_rosters(game_id=401307514) + + +### sportsdataverse.nba.nba_game_rosters.helper_nba_athlete_items(teams_rosters, \*\*kwargs) + +### sportsdataverse.nba.nba_game_rosters.helper_nba_game_items(summary) + +### sportsdataverse.nba.nba_game_rosters.helper_nba_roster_items(items, summary_url, \*\*kwargs) + +### sportsdataverse.nba.nba_game_rosters.helper_nba_team_items(items, \*\*kwargs) ## sportsdataverse.nba.nba_loaders module -### sportsdataverse.nba.nba_loaders.load_nba_pbp(seasons: List[int]) +### sportsdataverse.nba.nba_loaders.load_nba_pbp(seasons: List[int], return_as_pandas=True) Load NBA play by play data going back to 2002 Example: @@ -15,6 +59,7 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -26,7 +71,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.nba.nba_loaders.load_nba_player_boxscore(seasons: List[int]) +### sportsdataverse.nba.nba_loaders.load_nba_player_boxscore(seasons: List[int], return_as_pandas=True) Load NBA player boxscore data Example: @@ -36,6 +81,7 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -47,7 +93,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.nba.nba_loaders.load_nba_schedule(seasons: List[int]) +### sportsdataverse.nba.nba_loaders.load_nba_schedule(seasons: List[int], return_as_pandas=True) Load NBA schedule data Example: @@ -57,6 +103,7 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -68,7 +115,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.nba.nba_loaders.load_nba_team_boxscore(seasons: List[int]) +### sportsdataverse.nba.nba_loaders.load_nba_team_boxscore(seasons: List[int], return_as_pandas=True) Load NBA team boxscore data Example: @@ -78,6 +125,7 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -91,12 +139,13 @@ Raises: ## sportsdataverse.nba.nba_pbp module -### sportsdataverse.nba.nba_pbp.espn_nba_pbp(game_id: int, raw=False) +### sportsdataverse.nba.nba_pbp.espn_nba_pbp(game_id: int, raw=False, \*\*kwargs) espn_nba_pbp() - Pull the game by id - Data from API endpoints - nba/playbyplay, nba/summary Args: game_id (int): Unique game_id, can be obtained from nba_schedule(). + raw (bool): If True, returns the raw json from the API endpoint. If False, returns a cleaned dictionary of datasets. Returns: @@ -110,9 +159,11 @@ Example: nba_df = sportsdataverse.nba.espn_nba_pbp(game_id=401307514) +### sportsdataverse.nba.nba_pbp.helper_nba_game_data(pbp_txt, init) + ### sportsdataverse.nba.nba_pbp.helper_nba_pbp(game_id, pbp_txt) -### sportsdataverse.nba.nba_pbp.helper_nba_pbp_features(game_id, pbp_txt, homeTeamId, awayTeamId, homeTeamMascot, awayTeamMascot, homeTeamName, awayTeamName, homeTeamAbbrev, awayTeamAbbrev, homeTeamNameAlt, awayTeamNameAlt, gameSpread, homeFavorite, gameSpreadAvailable) +### sportsdataverse.nba.nba_pbp.helper_nba_pbp_features(game_id, pbp_txt, init) ### sportsdataverse.nba.nba_pbp.helper_nba_pickcenter(pbp_txt) @@ -120,12 +171,14 @@ Example: ## sportsdataverse.nba.nba_schedule module -### sportsdataverse.nba.nba_schedule.espn_nba_calendar(season=None, ondays=None) +### sportsdataverse.nba.nba_schedule.espn_nba_calendar(season=None, ondays=None, return_as_pandas=True, \*\*kwargs) espn_nba_calendar - look up the NBA calendar for a given season from ESPN Args: season (int): Used to define different seasons. 2002 is the earliest available season. + ondays (boolean): Used to return dates for calendar ondays + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -137,7 +190,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.nba.nba_schedule.espn_nba_schedule(dates=None, season_type=None, limit=500) +### sportsdataverse.nba.nba_schedule.espn_nba_schedule(dates=None, season_type=None, limit=500, return_as_pandas=True, \*\*kwargs) espn_nba_schedule - look up the NBA schedule for a given date from ESPN Args: @@ -145,6 +198,7 @@ Args: dates (int): Used to define different seasons. 2002 is the earliest available season. season_type (int): season type, 1 for pre-season, 2 for regular season, 3 for post-season, 4 for all-star, 5 for off-season limit (int): number of records to return, default: 500. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -158,11 +212,19 @@ Returns: ## sportsdataverse.nba.nba_teams module -### sportsdataverse.nba.nba_teams.espn_nba_teams() +### sportsdataverse.nba.nba_teams.espn_nba_teams(return_as_pandas=True, \*\*kwargs) espn_nba_teams - look up NBA teams +Args: + + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Returns: pd.DataFrame: Pandas dataframe containing teams for the requested league. +Example: + + nba_df = sportsdataverse.nba.espn_nba_teams() + ## Module contents diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.nfl.md b/Sphinx-docs/_build/markdown/sportsdataverse.nfl.md index 67ffe57..c9a7e5a 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.nfl.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.nfl.md @@ -4,6 +4,50 @@ ## sportsdataverse.nfl.model_vars module +## sportsdataverse.nfl.nfl_game_rosters module + + +### sportsdataverse.nfl.nfl_game_rosters.espn_nfl_game_rosters(game_id: int, raw=False, return_as_pandas=True, \*\*kwargs) +espn_nfl_game_rosters() - Pull the game by id. + +Args: + + game_id (int): Unique game_id, can be obtained from espn_nfl_schedule(). + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Data frame of game roster data with columns: + ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, + ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, + ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, + ‘age’, ‘date_of_birth’, ‘slug’, ‘jersey’, ‘linked’, ‘active’, + ‘alternate_ids_sdr’, ‘birth_place_city’, ‘birth_place_state’, + ‘birth_place_country’, ‘headshot_href’, ‘headshot_alt’, + ‘experience_years’, ‘experience_display_value’, + ‘experience_abbreviation’, ‘status_id’, ‘status_name’, ‘status_type’, + ‘status_abbreviation’, ‘hand_type’, ‘hand_abbreviation’, + ‘hand_display_value’, ‘draft_display_text’, ‘draft_round’, ‘draft_year’, + ‘draft_selection’, ‘player_id’, ‘starter’, ‘valid’, ‘did_not_play’, + ‘display_name’, ‘ejected’, ‘athlete_href’, ‘position_href’, + ‘statistics_href’, ‘team_id’, ‘team_guid’, ‘team_uid’, ‘team_slug’, + ‘team_location’, ‘team_name’, ‘team_nickname’, ‘team_abbreviation’, + ‘team_display_name’, ‘team_short_display_name’, ‘team_color’, + ‘team_alternate_color’, ‘is_active’, ‘is_all_star’, + ‘team_alternate_ids_sdr’, ‘logo_href’, ‘logo_dark_href’, ‘game_id’ + +Example: + + nfl_df = sportsdataverse.nfl.espn_nfl_game_rosters(game_id=401220403) + + +### sportsdataverse.nfl.nfl_game_rosters.helper_nfl_athlete_items(teams_rosters, \*\*kwargs) + +### sportsdataverse.nfl.nfl_game_rosters.helper_nfl_game_items(summary) + +### sportsdataverse.nfl.nfl_game_rosters.helper_nfl_roster_items(items, summary_url, \*\*kwargs) + +### sportsdataverse.nfl.nfl_game_rosters.helper_nfl_team_items(items, \*\*kwargs) ## sportsdataverse.nfl.nfl_games module @@ -55,7 +99,7 @@ Example: ## sportsdataverse.nfl.nfl_loaders module -### sportsdataverse.nfl.nfl_loaders.load_nfl_combine() +### sportsdataverse.nfl.nfl_loaders.load_nfl_combine(return_as_pandas=True) Load NFL Combine information Example: @@ -69,7 +113,7 @@ Returns: pd.DataFrame: Pandas dataframe containing NFL combine data available. -### sportsdataverse.nfl.nfl_loaders.load_nfl_contracts() +### sportsdataverse.nfl.nfl_loaders.load_nfl_contracts(return_as_pandas=True) Load NFL Historical contracts information Example: @@ -83,7 +127,7 @@ Returns: pd.DataFrame: Pandas dataframe containing historical contracts available. -### sportsdataverse.nfl.nfl_loaders.load_nfl_depth_charts(seasons: List[int]) +### sportsdataverse.nfl.nfl_loaders.load_nfl_depth_charts(seasons: List[int], return_as_pandas=True) Load NFL Depth Chart data for selected seasons Example: @@ -99,7 +143,7 @@ Returns: pd.DataFrame: Pandas dataframe containing depth chart data available for the requested seasons. -### sportsdataverse.nfl.nfl_loaders.load_nfl_draft_picks() +### sportsdataverse.nfl.nfl_loaders.load_nfl_draft_picks(return_as_pandas=True) Load NFL Draft picks information Example: @@ -113,7 +157,7 @@ Returns: pd.DataFrame: Pandas dataframe containing NFL Draft picks data available. -### sportsdataverse.nfl.nfl_loaders.load_nfl_injuries(seasons: List[int]) +### sportsdataverse.nfl.nfl_loaders.load_nfl_injuries(seasons: List[int], return_as_pandas=True) Load NFL injuries data for selected seasons Example: @@ -129,7 +173,7 @@ Returns: pd.DataFrame: Pandas dataframe containing injuries data available for the requested seasons. -### sportsdataverse.nfl.nfl_loaders.load_nfl_ngs_passing() +### sportsdataverse.nfl.nfl_loaders.load_nfl_ngs_passing(return_as_pandas=True) Load NFL NextGen Stats Passing data going back to 2016 Example: @@ -141,7 +185,7 @@ Returns: pd.DataFrame: Pandas dataframe containing the NextGen Stats Passing data available. -### sportsdataverse.nfl.nfl_loaders.load_nfl_ngs_receiving() +### sportsdataverse.nfl.nfl_loaders.load_nfl_ngs_receiving(return_as_pandas=True) Load NFL NextGen Stats Receiving data going back to 2016 Example: @@ -153,7 +197,7 @@ Returns: pd.DataFrame: Pandas dataframe containing the NextGen Stats Receiving data available. -### sportsdataverse.nfl.nfl_loaders.load_nfl_ngs_rushing() +### sportsdataverse.nfl.nfl_loaders.load_nfl_ngs_rushing(return_as_pandas=True) Load NFL NextGen Stats Rushing data going back to 2016 Example: @@ -165,7 +209,7 @@ Returns: pd.DataFrame: Pandas dataframe containing the NextGen Stats Rushing data available. -### sportsdataverse.nfl.nfl_loaders.load_nfl_officials() +### sportsdataverse.nfl.nfl_loaders.load_nfl_officials(return_as_pandas=True) Load NFL Officials information Example: @@ -199,7 +243,7 @@ Raises: ValueError: If season is less than 1999. -### sportsdataverse.nfl.nfl_loaders.load_nfl_pbp_participation(seasons: List[int]) +### sportsdataverse.nfl.nfl_loaders.load_nfl_pbp_participation(seasons: List[int], return_as_pandas=True) Load NFL play-by-play participation data for selected seasons Example: @@ -215,7 +259,7 @@ Returns: pd.DataFrame: Pandas dataframe containing play-by-play participation data available for the requested seasons. -### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_def() +### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_def(return_as_pandas=True) Load NFL Pro-Football Reference Advanced Defensive data going back to 2018 Example: @@ -229,7 +273,7 @@ Returns: advanced defensive stats data available. -### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_pass() +### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_pass(return_as_pandas=True) Load NFL Pro-Football Reference Advanced Passing data going back to 2018 Example: @@ -243,7 +287,7 @@ Returns: advanced passing stats data available. -### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_rec() +### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_rec(return_as_pandas=True) Load NFL Pro-Football Reference Advanced Receiving data going back to 2018 Example: @@ -257,7 +301,7 @@ Returns: advanced receiving stats data available. -### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_rush() +### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_rush(return_as_pandas=True) Load NFL Pro-Football Reference Advanced Rushing data going back to 2018 Example: @@ -271,7 +315,7 @@ Returns: advanced rushing stats data available. -### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_weekly_def(seasons: List[int]) +### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_weekly_def(seasons: List[int], return_as_pandas=True) Load NFL Pro-Football Reference Weekly Advanced Defensive data going back to 2018 Example: @@ -289,7 +333,7 @@ Returns: advanced defensive stats data available for the requested seasons. -### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_weekly_pass(seasons: List[int]) +### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_weekly_pass(seasons: List[int], return_as_pandas=True) Load NFL Pro-Football Reference Weekly Advanced Passing data going back to 2018 Example: @@ -307,7 +351,7 @@ Returns: advanced passing stats data available for the requested seasons. -### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_weekly_rec(seasons: List[int]) +### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_weekly_rec(seasons: List[int], return_as_pandas=True) Load NFL Pro-Football Reference Weekly Advanced Receiving data going back to 2018 Example: @@ -325,7 +369,7 @@ Returns: advanced receiving stats data available for the requested seasons. -### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_weekly_rush(seasons: List[int]) +### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_weekly_rush(seasons: List[int], return_as_pandas=True) Load NFL Pro-Football Reference Weekly Advanced Rushing data going back to 2018 Example: @@ -343,7 +387,7 @@ Returns: advanced rushing stats data available for the requested seasons. -### sportsdataverse.nfl.nfl_loaders.load_nfl_player_stats(kicking=False) +### sportsdataverse.nfl.nfl_loaders.load_nfl_player_stats(kicking=False, return_as_pandas=True) Load NFL player stats data Example: @@ -359,7 +403,7 @@ Returns: pd.DataFrame: Pandas dataframe containing player stats. -### sportsdataverse.nfl.nfl_loaders.load_nfl_players() +### sportsdataverse.nfl.nfl_loaders.load_nfl_players(return_as_pandas=True) Load NFL Player ID information Example: @@ -373,7 +417,7 @@ Returns: pd.DataFrame: Pandas dataframe containing players available. -### sportsdataverse.nfl.nfl_loaders.load_nfl_rosters(seasons: List[int]) +### sportsdataverse.nfl.nfl_loaders.load_nfl_rosters(seasons: List[int], return_as_pandas=True) Load NFL roster data for all seasons Example: @@ -389,7 +433,7 @@ Returns: pd.DataFrame: Pandas dataframe containing rosters available for the requested seasons. -### sportsdataverse.nfl.nfl_loaders.load_nfl_schedule(seasons: List[int]) +### sportsdataverse.nfl.nfl_loaders.load_nfl_schedule(seasons: List[int], return_as_pandas=True) Load NFL schedule data Example: @@ -409,7 +453,7 @@ Raises: ValueError: If season is less than 1999. -### sportsdataverse.nfl.nfl_loaders.load_nfl_snap_counts(seasons: List[int]) +### sportsdataverse.nfl.nfl_loaders.load_nfl_snap_counts(seasons: List[int], return_as_pandas=True) Load NFL snap counts data for selected seasons Example: @@ -425,7 +469,7 @@ Returns: pd.DataFrame: Pandas dataframe containing snap counts available for the requested seasons. -### sportsdataverse.nfl.nfl_loaders.load_nfl_teams() +### sportsdataverse.nfl.nfl_loaders.load_nfl_teams(return_as_pandas=True) Load NFL team ID information and logos Example: @@ -439,7 +483,7 @@ Returns: pd.DataFrame: Pandas dataframe containing teams available. -### sportsdataverse.nfl.nfl_loaders.load_nfl_weekly_rosters(seasons: List[int]) +### sportsdataverse.nfl.nfl_loaders.load_nfl_weekly_rosters(seasons: List[int], return_as_pandas=True) Load NFL weekly roster data for selected seasons Example: @@ -484,7 +528,7 @@ Returns: Example: - nfl_df = sportsdataverse.nfl.NFLPlayProcess(gameId=401256137).espn_nfl_pbp() + nfl_df = sportsdataverse.nfl.NFLPlayProcess(gameId=401220403).espn_nfl_pbp() #### gameId( = 0) @@ -505,13 +549,14 @@ Example: ## sportsdataverse.nfl.nfl_schedule module -### sportsdataverse.nfl.nfl_schedule.espn_nfl_calendar(season=None, ondays=None) +### sportsdataverse.nfl.nfl_schedule.espn_nfl_calendar(season=None, ondays=None, return_as_pandas=True, \*\*kwargs) espn_nfl_calendar - look up the NFL calendar for a given season Args: season (int): Used to define different seasons. 2002 is the earliest available season. ondays (boolean): Used to return dates for calendar ondays + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -522,7 +567,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.nfl.nfl_schedule.espn_nfl_schedule(dates=None, week=None, season_type=None, limit=500) +### sportsdataverse.nfl.nfl_schedule.espn_nfl_schedule(dates=None, week=None, season_type=None, groups=None, limit=500, return_as_pandas=True, \*\*kwargs) espn_nfl_schedule - look up the NFL schedule for a given season Args: @@ -531,6 +576,7 @@ Args: week (int): Week of the schedule. season_type (int): 2 for regular season, 3 for post-season, 4 for off-season. limit (int): number of records to return, default: 500. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -543,11 +589,19 @@ Returns: ## sportsdataverse.nfl.nfl_teams module -### sportsdataverse.nfl.nfl_teams.espn_nfl_teams() +### sportsdataverse.nfl.nfl_teams.espn_nfl_teams(return_as_pandas=True, \*\*kwargs) espn_nfl_teams - look up NFL teams +Args: + + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Returns: pd.DataFrame: Pandas dataframe containing teams for the requested league. +Example: + + nfl_df = sportsdataverse.nfl.espn_nfl_teams() + ## Module contents diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.nhl.md b/Sphinx-docs/_build/markdown/sportsdataverse.nhl.md index efcdddb..2b111d5 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.nhl.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.nhl.md @@ -5,7 +5,7 @@ ## sportsdataverse.nhl.nhl_api module -### sportsdataverse.nhl.nhl_api.nhl_api_pbp(game_id: int) +### sportsdataverse.nhl.nhl_api.nhl_api_pbp(game_id: int, \*\*kwargs) nhl_api_pbp() - Pull the game by id. Data from API endpoints - nhl/playbyplay, nhl/summary Args: @@ -24,7 +24,7 @@ Example: nhl_df = sportsdataverse.nhl.nhl_api_pbp(game_id=2021020079) -### sportsdataverse.nhl.nhl_api.nhl_api_schedule(start_date: str, end_date: str) +### sportsdataverse.nhl.nhl_api.nhl_api_schedule(start_date: str, end_date: str, return_as_pandas=True) nhl_api_schedule() - Pull the game by id. Data from API endpoints - nhl/schedule Args: @@ -33,16 +33,60 @@ Args: Returns: - Dict: + pd.DataFrame: Pandas dataframe containing the schedule for the requested seasons. Example: nhl_sched_df = sportsdataverse.nhl.nhl_api_schedule(start_date=2021-10-23, end_date=2021-10-28) +## sportsdataverse.nhl.nhl_game_rosters module + + +### sportsdataverse.nhl.nhl_game_rosters.espn_nhl_game_rosters(game_id: int, raw=False, return_as_pandas=True, \*\*kwargs) +espn_nhl_game_rosters() - Pull the game by id. + +Args: + + game_id (int): Unique game_id, can be obtained from espn_nhl_schedule(). + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Data frame of game roster data with columns: + ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, + ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, + ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, + ‘age’, ‘date_of_birth’, ‘slug’, ‘jersey’, ‘linked’, ‘active’, + ‘alternate_ids_sdr’, ‘birth_place_city’, ‘birth_place_state’, + ‘birth_place_country’, ‘headshot_href’, ‘headshot_alt’, + ‘experience_years’, ‘experience_display_value’, + ‘experience_abbreviation’, ‘status_id’, ‘status_name’, ‘status_type’, + ‘status_abbreviation’, ‘hand_type’, ‘hand_abbreviation’, + ‘hand_display_value’, ‘draft_display_text’, ‘draft_round’, ‘draft_year’, + ‘draft_selection’, ‘player_id’, ‘starter’, ‘valid’, ‘did_not_play’, + ‘display_name’, ‘ejected’, ‘athlete_href’, ‘position_href’, + ‘statistics_href’, ‘team_id’, ‘team_guid’, ‘team_uid’, ‘team_slug’, + ‘team_location’, ‘team_name’, ‘team_abbreviation’, + ‘team_display_name’, ‘team_short_display_name’, ‘team_color’, + ‘team_alternate_color’, ‘is_active’, ‘is_all_star’, + ‘logo_href’, ‘logo_dark_href’, ‘game_id’ + +Example: + + nhl_df = sportsdataverse.nhl.espn_nhl_game_rosters(game_id=401247153) + + +### sportsdataverse.nhl.nhl_game_rosters.helper_nhl_athlete_items(teams_rosters, \*\*kwargs) + +### sportsdataverse.nhl.nhl_game_rosters.helper_nhl_game_items(summary) + +### sportsdataverse.nhl.nhl_game_rosters.helper_nhl_roster_items(items, summary_url, \*\*kwargs) + +### sportsdataverse.nhl.nhl_game_rosters.helper_nhl_team_items(items, \*\*kwargs) ## sportsdataverse.nhl.nhl_loaders module -### sportsdataverse.nhl.nhl_loaders.load_nhl_pbp(seasons: List[int]) +### sportsdataverse.nhl.nhl_loaders.load_nhl_pbp(seasons: List[int], return_as_pandas=True) Load NHL play by play data going back to 2011 Example: @@ -52,6 +96,7 @@ Example: Args: seasons (list): Used to define different seasons. 2011 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -62,7 +107,7 @@ Raises: ValueError: If season is less than 2011. -### sportsdataverse.nhl.nhl_loaders.load_nhl_player_boxscore(seasons: List[int]) +### sportsdataverse.nhl.nhl_loaders.load_nhl_player_boxscore(seasons: List[int], return_as_pandas=True) Load NHL player boxscore data Example: @@ -72,6 +117,7 @@ Example: Args: seasons (list): Used to define different seasons. 2011 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -83,7 +129,7 @@ Raises: ValueError: If season is less than 2011. -### sportsdataverse.nhl.nhl_loaders.load_nhl_schedule(seasons: List[int]) +### sportsdataverse.nhl.nhl_loaders.load_nhl_schedule(seasons: List[int], return_as_pandas=True) Load NHL schedule data Example: @@ -93,6 +139,7 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -103,7 +150,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.nhl.nhl_loaders.load_nhl_team_boxscore(seasons: List[int]) +### sportsdataverse.nhl.nhl_loaders.load_nhl_team_boxscore(seasons: List[int], return_as_pandas=True) Load NHL team boxscore data Example: @@ -113,6 +160,7 @@ Example: Args: seasons (list): Used to define different seasons. 2011 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -124,7 +172,7 @@ Raises: ValueError: If season is less than 2011. -### sportsdataverse.nhl.nhl_loaders.nhl_teams() +### sportsdataverse.nhl.nhl_loaders.nhl_teams(return_as_pandas=True) Load NHL team ID information and logos Example: @@ -133,6 +181,8 @@ Example: Args: + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Returns: pd.DataFrame: Pandas dataframe containing teams available for the requested seasons. @@ -140,7 +190,7 @@ Returns: ## sportsdataverse.nhl.nhl_pbp module -### sportsdataverse.nhl.nhl_pbp.espn_nhl_pbp(game_id: int, raw=False) +### sportsdataverse.nhl.nhl_pbp.espn_nhl_pbp(game_id: int, raw=False, \*\*kwargs) espn_nhl_pbp() - Pull the game by id. Data from API endpoints - nhl/playbyplay, nhl/summary Args: @@ -159,9 +209,11 @@ Example: nhl_df = sportsdataverse.nhl.espn_nhl_pbp(game_id=401247153) +### sportsdataverse.nhl.nhl_pbp.helper_nhl_game_data(pbp_txt, init) + ### sportsdataverse.nhl.nhl_pbp.helper_nhl_pbp(game_id, pbp_txt) -### sportsdataverse.nhl.nhl_pbp.helper_nhl_pbp_features(game_id, pbp_txt, homeTeamId, awayTeamId, homeTeamMascot, awayTeamMascot, homeTeamName, awayTeamName, homeTeamAbbrev, awayTeamAbbrev, homeTeamNameAlt, awayTeamNameAlt, gameSpread, homeFavorite, gameSpreadAvailable) +### sportsdataverse.nhl.nhl_pbp.helper_nhl_pbp_features(game_id, pbp_txt, init) ### sportsdataverse.nhl.nhl_pbp.helper_nhl_pickcenter(pbp_txt) @@ -169,23 +221,26 @@ Example: ## sportsdataverse.nhl.nhl_schedule module -### sportsdataverse.nhl.nhl_schedule.espn_nhl_calendar(season=None) +### sportsdataverse.nhl.nhl_schedule.espn_nhl_calendar(season=None, ondays=None, return_as_pandas=True, \*\*kwargs) espn_nhl_calendar - look up the NHL calendar for a given season Args: season (int): Used to define different seasons. 2002 is the earliest available season. + ondays (boolean): Used to return dates for calendar ondays + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing calendar dates for the requested season. + pd.DataFrame: Pandas dataframe containing + calendar dates for the requested season. Raises: ValueError: If season is less than 2002. -### sportsdataverse.nhl.nhl_schedule.espn_nhl_schedule(dates=None, season_type=None, limit=500) +### sportsdataverse.nhl.nhl_schedule.espn_nhl_schedule(dates=None, season_type=None, limit=500, return_as_pandas=True, \*\*kwargs) espn_nhl_schedule - look up the NHL schedule for a given date Args: @@ -193,6 +248,7 @@ Args: dates (int): Used to define different seasons. 2002 is the earliest available season. season_type (int): season type, 1 for pre-season, 2 for regular season, 3 for post-season, 4 for all-star, 5 for off-season limit (int): number of records to return, default: 500. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -206,11 +262,19 @@ Returns: ## sportsdataverse.nhl.nhl_teams module -### sportsdataverse.nhl.nhl_teams.espn_nhl_teams() +### sportsdataverse.nhl.nhl_teams.espn_nhl_teams(return_as_pandas=True, \*\*kwargs) espn_nhl_teams - look up NHL teams +Args: + + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Returns: pd.DataFrame: Pandas dataframe containing teams for the requested league. +Example: + + nhl_df = sportsdataverse.nhl.espn_nhl_teams() + ## Module contents diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.wbb.md b/Sphinx-docs/_build/markdown/sportsdataverse.wbb.md index 57e86b0..90aab91 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.wbb.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.wbb.md @@ -2,10 +2,54 @@ ## Submodules +## sportsdataverse.wbb.wbb_game_rosters module + + +### sportsdataverse.wbb.wbb_game_rosters.espn_wbb_game_rosters(game_id: int, raw=False, return_as_pandas=True, \*\*kwargs) +espn_wbb_game_rosters() - Pull the game by id. + +Args: + + game_id (int): Unique game_id, can be obtained from wbb_schedule(). + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Data frame of game roster data with columns: + ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, + ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, + ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, + ‘age’, ‘date_of_birth’, ‘slug’, ‘jersey’, ‘linked’, ‘active’, + ‘alternate_ids_sdr’, ‘birth_place_city’, ‘birth_place_state’, + ‘birth_place_country’, ‘headshot_href’, ‘headshot_alt’, + ‘experience_years’, ‘experience_display_value’, + ‘experience_abbreviation’, ‘status_id’, ‘status_name’, ‘status_type’, + ‘status_abbreviation’, ‘hand_type’, ‘hand_abbreviation’, + ‘hand_display_value’, ‘draft_display_text’, ‘draft_round’, ‘draft_year’, + ‘draft_selection’, ‘player_id’, ‘starter’, ‘valid’, ‘did_not_play’, + ‘display_name’, ‘ejected’, ‘athlete_href’, ‘position_href’, + ‘statistics_href’, ‘team_id’, ‘team_guid’, ‘team_uid’, ‘team_slug’, + ‘team_location’, ‘team_name’, ‘team_nickname’, ‘team_abbreviation’, + ‘team_display_name’, ‘team_short_display_name’, ‘team_color’, + ‘team_alternate_color’, ‘is_active’, ‘is_all_star’, + ‘team_alternate_ids_sdr’, ‘logo_href’, ‘logo_dark_href’, ‘game_id’ + +Example: + + wbb_df = sportsdataverse.wbb.espn_wbb_game_rosters(game_id=401266534) + + +### sportsdataverse.wbb.wbb_game_rosters.helper_wbb_athlete_items(teams_rosters, \*\*kwargs) + +### sportsdataverse.wbb.wbb_game_rosters.helper_wbb_game_items(summary) + +### sportsdataverse.wbb.wbb_game_rosters.helper_wbb_roster_items(items, summary_url, \*\*kwargs) + +### sportsdataverse.wbb.wbb_game_rosters.helper_wbb_team_items(items, \*\*kwargs) ## sportsdataverse.wbb.wbb_loaders module -### sportsdataverse.wbb.wbb_loaders.load_wbb_pbp(seasons: List[int]) +### sportsdataverse.wbb.wbb_loaders.load_wbb_pbp(seasons: List[int], return_as_pandas=True) Load women’s college basketball play by play data going back to 2002 Example: @@ -15,6 +59,7 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -26,7 +71,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.wbb.wbb_loaders.load_wbb_player_boxscore(seasons: List[int]) +### sportsdataverse.wbb.wbb_loaders.load_wbb_player_boxscore(seasons: List[int], return_as_pandas=True) Load women’s college basketball player boxscore data Example: @@ -36,6 +81,7 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -47,7 +93,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.wbb.wbb_loaders.load_wbb_schedule(seasons: List[int]) +### sportsdataverse.wbb.wbb_loaders.load_wbb_schedule(seasons: List[int], return_as_pandas=True) Load women’s college basketball schedule data Example: @@ -57,6 +103,7 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -68,7 +115,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.wbb.wbb_loaders.load_wbb_team_boxscore(seasons: List[int]) +### sportsdataverse.wbb.wbb_loaders.load_wbb_team_boxscore(seasons: List[int], return_as_pandas=True) Load women’s college basketball team boxscore data Example: @@ -78,6 +125,7 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -91,13 +139,15 @@ Raises: ## sportsdataverse.wbb.wbb_pbp module -### sportsdataverse.wbb.wbb_pbp.espn_wbb_pbp(game_id: int, raw=False) +### sportsdataverse.wbb.wbb_pbp.espn_wbb_pbp(game_id: int, raw=False, \*\*kwargs) espn_wbb_pbp() - Pull the game by id. Data from API endpoints - womens-college-basketball/playbyplay, womens-college-basketball/summary Args: game_id (int): Unique game_id, can be obtained from wbb_schedule(). +raw (bool): If True, returns the raw json from the API endpoint. If False, returns a cleaned dictionary of datasets. + Returns: Dict: Dictionary of game data with keys - “gameId”, “plays”, “winprobability”, “boxscore”, “header”, “broadcasts”, @@ -110,23 +160,26 @@ Example: wbb_df = sportsdataverse.wb.espn_wbb_pbp(game_id=401266534) +### sportsdataverse.wbb.wbb_pbp.helper_wbb_game_data(pbp_txt, init) + ### sportsdataverse.wbb.wbb_pbp.helper_wbb_pbp(game_id, pbp_txt) -### sportsdataverse.wbb.wbb_pbp.helper_wbb_pbp_features(game_id, pbp_txt, homeTeamId, awayTeamId, homeTeamMascot, awayTeamMascot, homeTeamName, awayTeamName, homeTeamAbbrev, awayTeamAbbrev, homeTeamNameAlt, awayTeamNameAlt, gameSpread, homeFavorite, gameSpreadAvailable) +### sportsdataverse.wbb.wbb_pbp.helper_wbb_pbp_features(game_id, pbp_txt, init) ### sportsdataverse.wbb.wbb_pbp.helper_wbb_pickcenter(pbp_txt) -### sportsdataverse.wbb.wbb_pbp.wbb_pbp_disk(game_id, path_to_json) +### sportsdataverse.wbb.wbb_pbp.mbb_pbp_disk(game_id, path_to_json) ## sportsdataverse.wbb.wbb_schedule module -### sportsdataverse.wbb.wbb_schedule.espn_wbb_calendar(season=None, ondays=None) +### sportsdataverse.wbb.wbb_schedule.espn_wbb_calendar(season=None, ondays=None, return_as_pandas=True, \*\*kwargs) espn_wbb_calendar - look up the women’s college basketball calendar for a given season Args: season (int): Used to define different seasons. 2002 is the earliest available season. ondays (boolean): Used to return dates for calendar ondays + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -138,7 +191,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.wbb.wbb_schedule.espn_wbb_schedule(dates=None, groups=50, season_type=None, limit=500) +### sportsdataverse.wbb.wbb_schedule.espn_wbb_schedule(dates=None, groups=50, season_type=None, limit=500, return_as_pandas=True, \*\*kwargs) espn_wbb_schedule - look up the women’s college basketball schedule for a given season Args: @@ -147,6 +200,7 @@ Args: groups (int): Used to define different divisions. 50 is Division I, 51 is Division II/Division III. season_type (int): 2 for regular season, 3 for post-season, 4 for off-season. limit (int): number of records to return, default: 500. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -157,15 +211,20 @@ Returns: ## sportsdataverse.wbb.wbb_teams module -### sportsdataverse.wbb.wbb_teams.espn_wbb_teams(groups=None) +### sportsdataverse.wbb.wbb_teams.espn_wbb_teams(groups=None, return_as_pandas=True, \*\*kwargs) espn_wbb_teams - look up the women’s college basketball teams Args: groups (int): Used to define different divisions. 50 is Division I, 51 is Division II/Division III. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing teams for the requested league. +Example: + + wbb_df = sportsdataverse.wbb.espn_wbb_teams() + ## Module contents diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.wnba.md b/Sphinx-docs/_build/markdown/sportsdataverse.wnba.md index 71c9796..0998bf3 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.wnba.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.wnba.md @@ -2,10 +2,54 @@ ## Submodules +## sportsdataverse.wnba.wnba_game_rosters module + + +### sportsdataverse.wnba.wnba_game_rosters.espn_wnba_game_rosters(game_id: int, raw=False, return_as_pandas=True, \*\*kwargs) +espn_wnba_game_rosters() - Pull the game by id. + +Args: + + game_id (int): Unique game_id, can be obtained from espn_wnba_schedule(). + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Data frame of game roster data with columns: + ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, + ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, + ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, + ‘age’, ‘date_of_birth’, ‘slug’, ‘jersey’, ‘linked’, ‘active’, + ‘alternate_ids_sdr’, ‘birth_place_city’, ‘birth_place_state’, + ‘birth_place_country’, ‘headshot_href’, ‘headshot_alt’, + ‘experience_years’, ‘experience_display_value’, + ‘experience_abbreviation’, ‘status_id’, ‘status_name’, ‘status_type’, + ‘status_abbreviation’, ‘hand_type’, ‘hand_abbreviation’, + ‘hand_display_value’, ‘draft_display_text’, ‘draft_round’, ‘draft_year’, + ‘draft_selection’, ‘player_id’, ‘starter’, ‘valid’, ‘did_not_play’, + ‘display_name’, ‘ejected’, ‘athlete_href’, ‘position_href’, + ‘statistics_href’, ‘team_id’, ‘team_guid’, ‘team_uid’, ‘team_slug’, + ‘team_location’, ‘team_name’, ‘team_abbreviation’, + ‘team_display_name’, ‘team_short_display_name’, ‘team_color’, + ‘team_alternate_color’, ‘is_active’, ‘is_all_star’, + ‘logo_href’, ‘logo_dark_href’, ‘game_id’ + +Example: + + wnba_df = sportsdataverse.wnba.espn_wnba_game_rosters(game_id=401370395) + + +### sportsdataverse.wnba.wnba_game_rosters.helper_wnba_athlete_items(teams_rosters, \*\*kwargs) + +### sportsdataverse.wnba.wnba_game_rosters.helper_wnba_game_items(summary) + +### sportsdataverse.wnba.wnba_game_rosters.helper_wnba_roster_items(items, summary_url, \*\*kwargs) + +### sportsdataverse.wnba.wnba_game_rosters.helper_wnba_team_items(items, \*\*kwargs) ## sportsdataverse.wnba.wnba_loaders module -### sportsdataverse.wnba.wnba_loaders.load_wnba_pbp(seasons: List[int]) +### sportsdataverse.wnba.wnba_loaders.load_wnba_pbp(seasons: List[int], return_as_pandas=True) Load WNBA play by play data going back to 2002 Example: @@ -15,6 +59,7 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -26,7 +71,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.wnba.wnba_loaders.load_wnba_player_boxscore(seasons: List[int]) +### sportsdataverse.wnba.wnba_loaders.load_wnba_player_boxscore(seasons: List[int], return_as_pandas=True) Load WNBA player boxscore data Example: @@ -36,6 +81,7 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -47,7 +93,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.wnba.wnba_loaders.load_wnba_schedule(seasons: List[int]) +### sportsdataverse.wnba.wnba_loaders.load_wnba_schedule(seasons: List[int], return_as_pandas=True) Load WNBA schedule data Example: @@ -57,6 +103,7 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -68,7 +115,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.wnba.wnba_loaders.load_wnba_team_boxscore(seasons: List[int]) +### sportsdataverse.wnba.wnba_loaders.load_wnba_team_boxscore(seasons: List[int], return_as_pandas=True) Load WNBA team boxscore data Example: @@ -78,6 +125,7 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -91,7 +139,7 @@ Raises: ## sportsdataverse.wnba.wnba_pbp module -### sportsdataverse.wnba.wnba_pbp.espn_wnba_pbp(game_id: int, raw=False) +### sportsdataverse.wnba.wnba_pbp.espn_wnba_pbp(game_id: int, raw=False, \*\*kwargs) espn_wnba_pbp() - Pull the game by id. Data from API endpoints - wnba/playbyplay, wnba/summary Args: @@ -110,9 +158,11 @@ Example: wnba_df = sportsdataverse.wnba.espn_wnba_pbp(game_id=401370395) +### sportsdataverse.wnba.wnba_pbp.helper_wnba_game_data(pbp_txt, init) + ### sportsdataverse.wnba.wnba_pbp.helper_wnba_pbp(game_id, pbp_txt) -### sportsdataverse.wnba.wnba_pbp.helper_wnba_pbp_features(game_id, pbp_txt, gameSpread, homeFavorite, gameSpreadAvailable, homeTeamId, awayTeamId, homeTeamMascot, awayTeamMascot, homeTeamName, awayTeamName, homeTeamAbbrev, awayTeamAbbrev, homeTeamNameAlt, awayTeamNameAlt) +### sportsdataverse.wnba.wnba_pbp.helper_wnba_pbp_features(game_id, pbp_txt, init) ### sportsdataverse.wnba.wnba_pbp.helper_wnba_pickcenter(pbp_txt) @@ -120,7 +170,7 @@ Example: ## sportsdataverse.wnba.wnba_schedule module -### sportsdataverse.wnba.wnba_schedule.espn_wnba_calendar(season=None, ondays=None) +### sportsdataverse.wnba.wnba_schedule.espn_wnba_calendar(season=None, ondays=None, return_as_pandas=True, \*\*kwargs) espn_wnba_calendar - look up the WNBA calendar for a given season Args: @@ -137,7 +187,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.wnba.wnba_schedule.espn_wnba_schedule(dates=None, season_type=None, limit=500) +### sportsdataverse.wnba.wnba_schedule.espn_wnba_schedule(dates=None, season_type=None, limit=500, return_as_pandas=True, \*\*kwargs) espn_wnba_schedule - look up the WNBA schedule for a given season Args: @@ -155,11 +205,19 @@ Returns: ## sportsdataverse.wnba.wnba_teams module -### sportsdataverse.wnba.wnba_teams.espn_wnba_teams() +### sportsdataverse.wnba.wnba_teams.espn_wnba_teams(return_as_pandas=True, \*\*kwargs) espn_wnba_teams - look up WNBA teams +Args: + + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Returns: pd.DataFrame: Pandas dataframe containing teams for the requested league. +Example: + + wnba_df = sportsdataverse.wnba.espn_wnba_teams() + ## Module contents diff --git a/Sphinx-docs/sportsdataverse.cfb.rst b/Sphinx-docs/sportsdataverse.cfb.rst index e0574c0..8002e60 100755 --- a/Sphinx-docs/sportsdataverse.cfb.rst +++ b/Sphinx-docs/sportsdataverse.cfb.rst @@ -4,6 +4,14 @@ sportsdataverse.cfb package Submodules ---------- +sportsdataverse.cfb.cfb\_game\_rosters module +--------------------------------------------- + +.. automodule:: sportsdataverse.cfb.cfb_game_rosters + :members: + :undoc-members: + :show-inheritance: + sportsdataverse.cfb.cfb\_loaders module --------------------------------------- diff --git a/Sphinx-docs/sportsdataverse.nba.rst b/Sphinx-docs/sportsdataverse.nba.rst index 1cafcca..b136a28 100755 --- a/Sphinx-docs/sportsdataverse.nba.rst +++ b/Sphinx-docs/sportsdataverse.nba.rst @@ -4,6 +4,14 @@ sportsdataverse.nba package Submodules ---------- +sportsdataverse.nba.nba\_game\_rosters module +--------------------------------------------- + +.. automodule:: sportsdataverse.nba.nba_game_rosters + :members: + :undoc-members: + :show-inheritance: + sportsdataverse.nba.nba\_loaders module --------------------------------------- diff --git a/Sphinx-docs/sportsdataverse.nfl.rst b/Sphinx-docs/sportsdataverse.nfl.rst index 7170ee9..8d92b48 100755 --- a/Sphinx-docs/sportsdataverse.nfl.rst +++ b/Sphinx-docs/sportsdataverse.nfl.rst @@ -12,6 +12,14 @@ sportsdataverse.nfl.model\_vars module :undoc-members: :show-inheritance: +sportsdataverse.nfl.nfl\_game\_rosters module +--------------------------------------------- + +.. automodule:: sportsdataverse.nfl.nfl_game_rosters + :members: + :undoc-members: + :show-inheritance: + sportsdataverse.nfl.nfl\_games module ------------------------------------- diff --git a/Sphinx-docs/sportsdataverse.nhl.rst b/Sphinx-docs/sportsdataverse.nhl.rst index 895bfca..b2d9382 100755 --- a/Sphinx-docs/sportsdataverse.nhl.rst +++ b/Sphinx-docs/sportsdataverse.nhl.rst @@ -12,6 +12,14 @@ sportsdataverse.nhl.nhl\_api module :undoc-members: :show-inheritance: +sportsdataverse.nhl.nhl\_game\_rosters module +--------------------------------------------- + +.. automodule:: sportsdataverse.nhl.nhl_game_rosters + :members: + :undoc-members: + :show-inheritance: + sportsdataverse.nhl.nhl\_loaders module --------------------------------------- diff --git a/Sphinx-docs/sportsdataverse.rst b/Sphinx-docs/sportsdataverse.rst index f401761..36efdfc 100755 --- a/Sphinx-docs/sportsdataverse.rst +++ b/Sphinx-docs/sportsdataverse.rst @@ -27,6 +27,14 @@ sportsdataverse.config module :undoc-members: :show-inheritance: +sportsdataverse.decorators module +--------------------------------- + +.. automodule:: sportsdataverse.decorators + :members: + :undoc-members: + :show-inheritance: + sportsdataverse.dl\_utils module -------------------------------- diff --git a/Sphinx-docs/sportsdataverse.wbb.rst b/Sphinx-docs/sportsdataverse.wbb.rst index 9806747..46f1425 100755 --- a/Sphinx-docs/sportsdataverse.wbb.rst +++ b/Sphinx-docs/sportsdataverse.wbb.rst @@ -4,6 +4,14 @@ sportsdataverse.wbb package Submodules ---------- +sportsdataverse.wbb.wbb\_game\_rosters module +--------------------------------------------- + +.. automodule:: sportsdataverse.wbb.wbb_game_rosters + :members: + :undoc-members: + :show-inheritance: + sportsdataverse.wbb.wbb\_loaders module --------------------------------------- diff --git a/Sphinx-docs/sportsdataverse.wnba.rst b/Sphinx-docs/sportsdataverse.wnba.rst index adce412..cb2f885 100755 --- a/Sphinx-docs/sportsdataverse.wnba.rst +++ b/Sphinx-docs/sportsdataverse.wnba.rst @@ -4,6 +4,14 @@ sportsdataverse.wnba package Submodules ---------- +sportsdataverse.wnba.wnba\_game\_rosters module +----------------------------------------------- + +.. automodule:: sportsdataverse.wnba.wnba_game_rosters + :members: + :undoc-members: + :show-inheritance: + sportsdataverse.wnba.wnba\_loaders module ----------------------------------------- diff --git a/docs/docs/cfb/index.md b/docs/docs/cfb/index.md index d65fbd8..d02a889 100755 --- a/docs/docs/cfb/index.md +++ b/docs/docs/cfb/index.md @@ -2,10 +2,54 @@ ## Submodules +## sportsdataverse.cfb.cfb_game_rosters module + + +### sportsdataverse.cfb.cfb_game_rosters.espn_cfb_game_rosters(game_id: int, raw=False, return_as_pandas=True, \*\*kwargs) +espn_cfb_game_rosters() - Pull the game by id. + +Args: + + game_id (int): Unique game_id, can be obtained from espn_cfb_schedule(). + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Data frame of game roster data with columns: + ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, + ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, + ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, + ‘age’, ‘date_of_birth’, ‘slug’, ‘jersey’, ‘linked’, ‘active’, + ‘alternate_ids_sdr’, ‘birth_place_city’, ‘birth_place_state’, + ‘birth_place_country’, ‘headshot_href’, ‘headshot_alt’, + ‘experience_years’, ‘experience_display_value’, + ‘experience_abbreviation’, ‘status_id’, ‘status_name’, ‘status_type’, + ‘status_abbreviation’, ‘hand_type’, ‘hand_abbreviation’, + ‘hand_display_value’, ‘draft_display_text’, ‘draft_round’, ‘draft_year’, + ‘draft_selection’, ‘player_id’, ‘starter’, ‘valid’, ‘did_not_play’, + ‘display_name’, ‘ejected’, ‘athlete_href’, ‘position_href’, + ‘statistics_href’, ‘team_id’, ‘team_guid’, ‘team_uid’, ‘team_slug’, + ‘team_location’, ‘team_name’, ‘team_nickname’, ‘team_abbreviation’, + ‘team_display_name’, ‘team_short_display_name’, ‘team_color’, + ‘team_alternate_color’, ‘is_active’, ‘is_all_star’, + ‘team_alternate_ids_sdr’, ‘logo_href’, ‘logo_dark_href’, ‘game_id’ + +Example: + + cfb_df = sportsdataverse.cfb.espn_cfb_game_rosters(game_id=401256137) + + +### sportsdataverse.cfb.cfb_game_rosters.helper_cfb_athlete_items(teams_rosters, \*\*kwargs) + +### sportsdataverse.cfb.cfb_game_rosters.helper_cfb_game_items(summary) + +### sportsdataverse.cfb.cfb_game_rosters.helper_cfb_roster_items(items, summary_url, \*\*kwargs) + +### sportsdataverse.cfb.cfb_game_rosters.helper_cfb_team_items(items, \*\*kwargs) ## sportsdataverse.cfb.cfb_loaders module -### sportsdataverse.cfb.cfb_loaders.get_cfb_teams() +### sportsdataverse.cfb.cfb_loaders.get_cfb_teams(return_as_pandas=True) Load college football team ID information and logos Example: @@ -14,12 +58,14 @@ Example: Args: + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Returns: pd.DataFrame: Pandas dataframe containing teams available for the requested seasons. -### sportsdataverse.cfb.cfb_loaders.load_cfb_pbp(seasons: List[int]) +### sportsdataverse.cfb.cfb_loaders.load_cfb_pbp(seasons: List[int], return_as_pandas=True) Load college football play by play data going back to 2003 Example: @@ -29,6 +75,7 @@ Example: Args: seasons (list): Used to define different seasons. 2003 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -39,7 +86,7 @@ Raises: ValueError: If season is less than 2003. -### sportsdataverse.cfb.cfb_loaders.load_cfb_rosters(seasons: List[int]) +### sportsdataverse.cfb.cfb_loaders.load_cfb_rosters(seasons: List[int], return_as_pandas=True) Load roster data Example: @@ -49,6 +96,7 @@ Example: Args: seasons (list): Used to define different seasons. 2014 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -59,7 +107,7 @@ Raises: ValueError: If season is less than 2014. -### sportsdataverse.cfb.cfb_loaders.load_cfb_schedule(seasons: List[int]) +### sportsdataverse.cfb.cfb_loaders.load_cfb_schedule(seasons: List[int], return_as_pandas=True) Load college football schedule data Example: @@ -69,6 +117,7 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -79,7 +128,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.cfb.cfb_loaders.load_cfb_team_info(seasons: List[int]) +### sportsdataverse.cfb.cfb_loaders.load_cfb_team_info(seasons: List[int], return_as_pandas=True) Load college football team info Example: @@ -89,6 +138,7 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -101,24 +151,25 @@ Raises: ## sportsdataverse.cfb.cfb_pbp module -### class sportsdataverse.cfb.cfb_pbp.CFBPlayProcess(gameId=0, raw=False, path_to_json='/') +### class sportsdataverse.cfb.cfb_pbp.CFBPlayProcess(gameId=0, raw=False, path_to_json='/', return_keys=None, \*\*kwargs) Bases: `object` -#### \__init__(gameId=0, raw=False, path_to_json='/') +#### \__init__(gameId=0, raw=False, path_to_json='/', return_keys=None, \*\*kwargs) Initialize self. See help(type(self)) for accurate signature. #### cfb_pbp_disk() -#### create_box_score() +#### create_box_score(play_df) -#### espn_cfb_pbp() +#### espn_cfb_pbp(\*\*kwargs) espn_cfb_pbp() - Pull the game by id. Data from API endpoints: college-football/playbyplay, college-football/summary Args: game_id (int): Unique game_id, can be obtained from cfb_schedule(). + raw (bool): If True, returns the raw json from the API endpoint. If False, returns a cleaned dictionary of datasets. Returns: @@ -143,13 +194,15 @@ Example: #### raw( = False) +#### return_keys( = None) + #### run_cleaning_pipeline() #### run_processing_pipeline() ## sportsdataverse.cfb.cfb_schedule module -### sportsdataverse.cfb.cfb_schedule.espn_cfb_calendar(season=None, groups=None, ondays=None) +### sportsdataverse.cfb.cfb_schedule.espn_cfb_calendar(season=None, groups=None, ondays=None, return_as_pandas=True, \*\*kwargs) espn_cfb_calendar - look up the men’s college football calendar for a given season Args: @@ -157,6 +210,7 @@ Args: season (int): Used to define different seasons. 2002 is the earliest available season. groups (int): Used to define different divisions. 80 is FBS, 81 is FCS. ondays (boolean): Used to return dates for calendar ondays + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -167,7 +221,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.cfb.cfb_schedule.espn_cfb_schedule(dates=None, week=None, season_type=None, groups=None, limit=500) +### sportsdataverse.cfb.cfb_schedule.espn_cfb_schedule(dates=None, week=None, season_type=None, groups=None, limit=500, return_as_pandas=True, \*\*kwargs) espn_cfb_schedule - look up the college football schedule for a given season Args: @@ -177,6 +231,7 @@ Args: groups (int): Used to define different divisions. 80 is FBS, 81 is FCS. season_type (int): 2 for regular season, 3 for post-season, 4 for off-season. limit (int): number of records to return, default: 500. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -187,17 +242,22 @@ Returns: ## sportsdataverse.cfb.cfb_teams module -### sportsdataverse.cfb.cfb_teams.espn_cfb_teams(groups=None) +### sportsdataverse.cfb.cfb_teams.espn_cfb_teams(groups=None, return_as_pandas=True, \*\*kwargs) espn_cfb_teams - look up the college football teams Args: groups (int): Used to define different divisions. 80 is FBS, 81 is FCS. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. +Example: + + cfb_df = sportsdataverse.cfb.espn_cfb_teams() + ## sportsdataverse.cfb.model_vars module ## Module contents diff --git a/docs/docs/mbb/index.md b/docs/docs/mbb/index.md index 5eb2481..d4ee416 100755 --- a/docs/docs/mbb/index.md +++ b/docs/docs/mbb/index.md @@ -5,12 +5,13 @@ ## sportsdataverse.mbb.mbb_game_rosters module -### sportsdataverse.mbb.mbb_game_rosters.espn_mbb_game_rosters(game_id: int, raw=False) +### sportsdataverse.mbb.mbb_game_rosters.espn_mbb_game_rosters(game_id: int, raw=False, return_as_pandas=True, \*\*kwargs) espn_mbb_game_rosters() - Pull the game by id. Args: game_id (int): Unique game_id, can be obtained from mbb_schedule(). + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -38,17 +39,17 @@ Example: mbb_df = sportsdataverse.mbb.espn_mbb_game_rosters(game_id=401265031) -### sportsdataverse.mbb.mbb_game_rosters.helper_mbb_athlete_items(teams_rosters) +### sportsdataverse.mbb.mbb_game_rosters.helper_mbb_athlete_items(teams_rosters, \*\*kwargs) ### sportsdataverse.mbb.mbb_game_rosters.helper_mbb_game_items(summary) -### sportsdataverse.mbb.mbb_game_rosters.helper_mbb_roster_items(items, summary_url) +### sportsdataverse.mbb.mbb_game_rosters.helper_mbb_roster_items(items, summary_url, \*\*kwargs) -### sportsdataverse.mbb.mbb_game_rosters.helper_mbb_team_items(items) +### sportsdataverse.mbb.mbb_game_rosters.helper_mbb_team_items(items, \*\*kwargs) ## sportsdataverse.mbb.mbb_loaders module -### sportsdataverse.mbb.mbb_loaders.load_mbb_pbp(seasons: List[int]) +### sportsdataverse.mbb.mbb_loaders.load_mbb_pbp(seasons: List[int], return_as_pandas=True) Load men’s college basketball play by play data going back to 2002 Example: @@ -58,6 +59,7 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -69,7 +71,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.mbb.mbb_loaders.load_mbb_player_boxscore(seasons: List[int]) +### sportsdataverse.mbb.mbb_loaders.load_mbb_player_boxscore(seasons: List[int], return_as_pandas=True) Load men’s college basketball player boxscore data Example: @@ -79,6 +81,7 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -90,7 +93,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.mbb.mbb_loaders.load_mbb_schedule(seasons: List[int]) +### sportsdataverse.mbb.mbb_loaders.load_mbb_schedule(seasons: List[int], return_as_pandas=True) Load men’s college basketball schedule data Example: @@ -100,6 +103,7 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -111,7 +115,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.mbb.mbb_loaders.load_mbb_team_boxscore(seasons: List[int]) +### sportsdataverse.mbb.mbb_loaders.load_mbb_team_boxscore(seasons: List[int], return_as_pandas=True) Load men’s college basketball team boxscore data Example: @@ -121,6 +125,7 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -134,12 +139,13 @@ Raises: ## sportsdataverse.mbb.mbb_pbp module -### sportsdataverse.mbb.mbb_pbp.espn_mbb_pbp(game_id: int, raw=False) +### sportsdataverse.mbb.mbb_pbp.espn_mbb_pbp(game_id: int, raw=False, \*\*kwargs) espn_mbb_pbp() - Pull the game by id. Data from API endpoints: mens-college-basketball/playbyplay, mens-college-basketball/summary Args: game_id (int): Unique game_id, can be obtained from mbb_schedule(). + raw (bool): If True, returns the raw json from the API endpoint. If False, returns a cleaned dictionary of datasets. Returns: @@ -152,9 +158,11 @@ Example: mbb_df = sportsdataverse.mbb.espn_mbb_pbp(game_id=401265031) +### sportsdataverse.mbb.mbb_pbp.helper_mbb_game_data(pbp_txt, init) + ### sportsdataverse.mbb.mbb_pbp.helper_mbb_pbp(game_id, pbp_txt) -### sportsdataverse.mbb.mbb_pbp.helper_mbb_pbp_features(game_id, pbp_txt, gameSpread, homeFavorite, gameSpreadAvailable, homeTeamId, awayTeamId, homeTeamMascot, awayTeamMascot, homeTeamName, awayTeamName, homeTeamAbbrev, awayTeamAbbrev, homeTeamNameAlt, awayTeamNameAlt) +### sportsdataverse.mbb.mbb_pbp.helper_mbb_pbp_features(game_id, pbp_txt, init) ### sportsdataverse.mbb.mbb_pbp.helper_mbb_pickcenter(pbp_txt) @@ -162,13 +170,14 @@ Example: ## sportsdataverse.mbb.mbb_schedule module -### sportsdataverse.mbb.mbb_schedule.espn_mbb_calendar(season=None, ondays=None) +### sportsdataverse.mbb.mbb_schedule.espn_mbb_calendar(season=None, ondays=None, return_as_pandas=True, \*\*kwargs) espn_mbb_calendar - look up the men’s college basketball calendar for a given season Args: season (int): Used to define different seasons. 2002 is the earliest available season. ondays (boolean): Used to return dates for calendar ondays + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -180,7 +189,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.mbb.mbb_schedule.espn_mbb_schedule(dates=None, groups=50, season_type=None, limit=500) +### sportsdataverse.mbb.mbb_schedule.espn_mbb_schedule(dates=None, groups=50, season_type=None, limit=500, return_as_pandas=True, \*\*kwargs) espn_mbb_schedule - look up the men’s college basketball scheduler for a given season Args: @@ -189,6 +198,7 @@ Args: groups (int): Used to define different divisions. 50 is Division I, 51 is Division II/Division III. season_type (int): 2 for regular season, 3 for post-season, 4 for off-season. limit (int): number of records to return, default: 500. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -199,15 +209,20 @@ Returns: ## sportsdataverse.mbb.mbb_teams module -### sportsdataverse.mbb.mbb_teams.espn_mbb_teams(groups=None) +### sportsdataverse.mbb.mbb_teams.espn_mbb_teams(groups=None, return_as_pandas=True, \*\*kwargs) espn_mbb_teams - look up the men’s college basketball teams Args: groups (int): Used to define different divisions. 50 is Division I, 51 is Division II/Division III. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing teams for the requested league. +Example: + + mbb_df = sportsdataverse.mbb.espn_mbb_teams() + ## Module contents diff --git a/docs/docs/mlb/index.md b/docs/docs/mlb/index.md index 4249be9..986d8cc 100755 --- a/docs/docs/mlb/index.md +++ b/docs/docs/mlb/index.md @@ -43,7 +43,7 @@ Args: ‘L’ - League Championship ‘W’ - World Series -Returns: +Returns: A pandas dataframe containing MLB scheduled games. @@ -380,8 +380,8 @@ Returns: ### sportsdataverse.mlb.retrosheet.retrosheet_game_logs_team() -Retrives the team-level stats for MLB games in a season, or range of seasons. -THIS DOES NOT GET PLAYER STATS! +Retrives the team-level stats for MLB games in a season, or range of seasons. +THIS DOES NOT GET PLAYER STATS! Use retrosplits_game_logs_player() for player-level game stats. Args: @@ -400,7 +400,7 @@ Args: The full list of supported keywards for game_type are as follows. Case does not matter (you can set game_type to “rEgUlAr”, and the function call will still work): - > + > > * “regular”: Regular season games. diff --git a/docs/docs/nba/index.md b/docs/docs/nba/index.md index 3835d84..2ca6190 100755 --- a/docs/docs/nba/index.md +++ b/docs/docs/nba/index.md @@ -2,10 +2,54 @@ ## Submodules +## sportsdataverse.nba.nba_game_rosters module + + +### sportsdataverse.nba.nba_game_rosters.espn_nba_game_rosters(game_id: int, raw=False, return_as_pandas=True, \*\*kwargs) +espn_nba_game_rosters() - Pull the game by id. + +Args: + + game_id (int): Unique game_id, can be obtained from espn_nba_schedule(). + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Data frame of game roster data with columns: + ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, + ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, + ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, + ‘age’, ‘date_of_birth’, ‘slug’, ‘jersey’, ‘linked’, ‘active’, + ‘alternate_ids_sdr’, ‘birth_place_city’, ‘birth_place_state’, + ‘birth_place_country’, ‘headshot_href’, ‘headshot_alt’, + ‘experience_years’, ‘experience_display_value’, + ‘experience_abbreviation’, ‘status_id’, ‘status_name’, ‘status_type’, + ‘status_abbreviation’, ‘hand_type’, ‘hand_abbreviation’, + ‘hand_display_value’, ‘draft_display_text’, ‘draft_round’, ‘draft_year’, + ‘draft_selection’, ‘player_id’, ‘starter’, ‘valid’, ‘did_not_play’, + ‘display_name’, ‘ejected’, ‘athlete_href’, ‘position_href’, + ‘statistics_href’, ‘team_id’, ‘team_guid’, ‘team_uid’, ‘team_slug’, + ‘team_location’, ‘team_name’, ‘team_abbreviation’, + ‘team_display_name’, ‘team_short_display_name’, ‘team_color’, + ‘team_alternate_color’, ‘is_active’, ‘is_all_star’, + ‘logo_href’, ‘logo_dark_href’, ‘game_id’ + +Example: + + nba_df = sportsdataverse.nba.espn_nba_game_rosters(game_id=401307514) + + +### sportsdataverse.nba.nba_game_rosters.helper_nba_athlete_items(teams_rosters, \*\*kwargs) + +### sportsdataverse.nba.nba_game_rosters.helper_nba_game_items(summary) + +### sportsdataverse.nba.nba_game_rosters.helper_nba_roster_items(items, summary_url, \*\*kwargs) + +### sportsdataverse.nba.nba_game_rosters.helper_nba_team_items(items, \*\*kwargs) ## sportsdataverse.nba.nba_loaders module -### sportsdataverse.nba.nba_loaders.load_nba_pbp(seasons: List[int]) +### sportsdataverse.nba.nba_loaders.load_nba_pbp(seasons: List[int], return_as_pandas=True) Load NBA play by play data going back to 2002 Example: @@ -15,6 +59,7 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -26,7 +71,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.nba.nba_loaders.load_nba_player_boxscore(seasons: List[int]) +### sportsdataverse.nba.nba_loaders.load_nba_player_boxscore(seasons: List[int], return_as_pandas=True) Load NBA player boxscore data Example: @@ -36,6 +81,7 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -47,7 +93,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.nba.nba_loaders.load_nba_schedule(seasons: List[int]) +### sportsdataverse.nba.nba_loaders.load_nba_schedule(seasons: List[int], return_as_pandas=True) Load NBA schedule data Example: @@ -57,6 +103,7 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -68,7 +115,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.nba.nba_loaders.load_nba_team_boxscore(seasons: List[int]) +### sportsdataverse.nba.nba_loaders.load_nba_team_boxscore(seasons: List[int], return_as_pandas=True) Load NBA team boxscore data Example: @@ -78,6 +125,7 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -91,12 +139,13 @@ Raises: ## sportsdataverse.nba.nba_pbp module -### sportsdataverse.nba.nba_pbp.espn_nba_pbp(game_id: int, raw=False) +### sportsdataverse.nba.nba_pbp.espn_nba_pbp(game_id: int, raw=False, \*\*kwargs) espn_nba_pbp() - Pull the game by id - Data from API endpoints - nba/playbyplay, nba/summary Args: game_id (int): Unique game_id, can be obtained from nba_schedule(). + raw (bool): If True, returns the raw json from the API endpoint. If False, returns a cleaned dictionary of datasets. Returns: @@ -110,9 +159,11 @@ Example: nba_df = sportsdataverse.nba.espn_nba_pbp(game_id=401307514) +### sportsdataverse.nba.nba_pbp.helper_nba_game_data(pbp_txt, init) + ### sportsdataverse.nba.nba_pbp.helper_nba_pbp(game_id, pbp_txt) -### sportsdataverse.nba.nba_pbp.helper_nba_pbp_features(game_id, pbp_txt, homeTeamId, awayTeamId, homeTeamMascot, awayTeamMascot, homeTeamName, awayTeamName, homeTeamAbbrev, awayTeamAbbrev, homeTeamNameAlt, awayTeamNameAlt, gameSpread, homeFavorite, gameSpreadAvailable) +### sportsdataverse.nba.nba_pbp.helper_nba_pbp_features(game_id, pbp_txt, init) ### sportsdataverse.nba.nba_pbp.helper_nba_pickcenter(pbp_txt) @@ -120,12 +171,14 @@ Example: ## sportsdataverse.nba.nba_schedule module -### sportsdataverse.nba.nba_schedule.espn_nba_calendar(season=None, ondays=None) +### sportsdataverse.nba.nba_schedule.espn_nba_calendar(season=None, ondays=None, return_as_pandas=True, \*\*kwargs) espn_nba_calendar - look up the NBA calendar for a given season from ESPN Args: season (int): Used to define different seasons. 2002 is the earliest available season. + ondays (boolean): Used to return dates for calendar ondays + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -137,7 +190,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.nba.nba_schedule.espn_nba_schedule(dates=None, season_type=None, limit=500) +### sportsdataverse.nba.nba_schedule.espn_nba_schedule(dates=None, season_type=None, limit=500, return_as_pandas=True, \*\*kwargs) espn_nba_schedule - look up the NBA schedule for a given date from ESPN Args: @@ -145,6 +198,7 @@ Args: dates (int): Used to define different seasons. 2002 is the earliest available season. season_type (int): season type, 1 for pre-season, 2 for regular season, 3 for post-season, 4 for all-star, 5 for off-season limit (int): number of records to return, default: 500. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -158,11 +212,19 @@ Returns: ## sportsdataverse.nba.nba_teams module -### sportsdataverse.nba.nba_teams.espn_nba_teams() +### sportsdataverse.nba.nba_teams.espn_nba_teams(return_as_pandas=True, \*\*kwargs) espn_nba_teams - look up NBA teams +Args: + + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Returns: pd.DataFrame: Pandas dataframe containing teams for the requested league. +Example: + + nba_df = sportsdataverse.nba.espn_nba_teams() + ## Module contents diff --git a/docs/docs/nfl/index.md b/docs/docs/nfl/index.md index 67ffe57..c9a7e5a 100755 --- a/docs/docs/nfl/index.md +++ b/docs/docs/nfl/index.md @@ -4,6 +4,50 @@ ## sportsdataverse.nfl.model_vars module +## sportsdataverse.nfl.nfl_game_rosters module + + +### sportsdataverse.nfl.nfl_game_rosters.espn_nfl_game_rosters(game_id: int, raw=False, return_as_pandas=True, \*\*kwargs) +espn_nfl_game_rosters() - Pull the game by id. + +Args: + + game_id (int): Unique game_id, can be obtained from espn_nfl_schedule(). + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Data frame of game roster data with columns: + ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, + ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, + ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, + ‘age’, ‘date_of_birth’, ‘slug’, ‘jersey’, ‘linked’, ‘active’, + ‘alternate_ids_sdr’, ‘birth_place_city’, ‘birth_place_state’, + ‘birth_place_country’, ‘headshot_href’, ‘headshot_alt’, + ‘experience_years’, ‘experience_display_value’, + ‘experience_abbreviation’, ‘status_id’, ‘status_name’, ‘status_type’, + ‘status_abbreviation’, ‘hand_type’, ‘hand_abbreviation’, + ‘hand_display_value’, ‘draft_display_text’, ‘draft_round’, ‘draft_year’, + ‘draft_selection’, ‘player_id’, ‘starter’, ‘valid’, ‘did_not_play’, + ‘display_name’, ‘ejected’, ‘athlete_href’, ‘position_href’, + ‘statistics_href’, ‘team_id’, ‘team_guid’, ‘team_uid’, ‘team_slug’, + ‘team_location’, ‘team_name’, ‘team_nickname’, ‘team_abbreviation’, + ‘team_display_name’, ‘team_short_display_name’, ‘team_color’, + ‘team_alternate_color’, ‘is_active’, ‘is_all_star’, + ‘team_alternate_ids_sdr’, ‘logo_href’, ‘logo_dark_href’, ‘game_id’ + +Example: + + nfl_df = sportsdataverse.nfl.espn_nfl_game_rosters(game_id=401220403) + + +### sportsdataverse.nfl.nfl_game_rosters.helper_nfl_athlete_items(teams_rosters, \*\*kwargs) + +### sportsdataverse.nfl.nfl_game_rosters.helper_nfl_game_items(summary) + +### sportsdataverse.nfl.nfl_game_rosters.helper_nfl_roster_items(items, summary_url, \*\*kwargs) + +### sportsdataverse.nfl.nfl_game_rosters.helper_nfl_team_items(items, \*\*kwargs) ## sportsdataverse.nfl.nfl_games module @@ -55,7 +99,7 @@ Example: ## sportsdataverse.nfl.nfl_loaders module -### sportsdataverse.nfl.nfl_loaders.load_nfl_combine() +### sportsdataverse.nfl.nfl_loaders.load_nfl_combine(return_as_pandas=True) Load NFL Combine information Example: @@ -69,7 +113,7 @@ Returns: pd.DataFrame: Pandas dataframe containing NFL combine data available. -### sportsdataverse.nfl.nfl_loaders.load_nfl_contracts() +### sportsdataverse.nfl.nfl_loaders.load_nfl_contracts(return_as_pandas=True) Load NFL Historical contracts information Example: @@ -83,7 +127,7 @@ Returns: pd.DataFrame: Pandas dataframe containing historical contracts available. -### sportsdataverse.nfl.nfl_loaders.load_nfl_depth_charts(seasons: List[int]) +### sportsdataverse.nfl.nfl_loaders.load_nfl_depth_charts(seasons: List[int], return_as_pandas=True) Load NFL Depth Chart data for selected seasons Example: @@ -99,7 +143,7 @@ Returns: pd.DataFrame: Pandas dataframe containing depth chart data available for the requested seasons. -### sportsdataverse.nfl.nfl_loaders.load_nfl_draft_picks() +### sportsdataverse.nfl.nfl_loaders.load_nfl_draft_picks(return_as_pandas=True) Load NFL Draft picks information Example: @@ -113,7 +157,7 @@ Returns: pd.DataFrame: Pandas dataframe containing NFL Draft picks data available. -### sportsdataverse.nfl.nfl_loaders.load_nfl_injuries(seasons: List[int]) +### sportsdataverse.nfl.nfl_loaders.load_nfl_injuries(seasons: List[int], return_as_pandas=True) Load NFL injuries data for selected seasons Example: @@ -129,7 +173,7 @@ Returns: pd.DataFrame: Pandas dataframe containing injuries data available for the requested seasons. -### sportsdataverse.nfl.nfl_loaders.load_nfl_ngs_passing() +### sportsdataverse.nfl.nfl_loaders.load_nfl_ngs_passing(return_as_pandas=True) Load NFL NextGen Stats Passing data going back to 2016 Example: @@ -141,7 +185,7 @@ Returns: pd.DataFrame: Pandas dataframe containing the NextGen Stats Passing data available. -### sportsdataverse.nfl.nfl_loaders.load_nfl_ngs_receiving() +### sportsdataverse.nfl.nfl_loaders.load_nfl_ngs_receiving(return_as_pandas=True) Load NFL NextGen Stats Receiving data going back to 2016 Example: @@ -153,7 +197,7 @@ Returns: pd.DataFrame: Pandas dataframe containing the NextGen Stats Receiving data available. -### sportsdataverse.nfl.nfl_loaders.load_nfl_ngs_rushing() +### sportsdataverse.nfl.nfl_loaders.load_nfl_ngs_rushing(return_as_pandas=True) Load NFL NextGen Stats Rushing data going back to 2016 Example: @@ -165,7 +209,7 @@ Returns: pd.DataFrame: Pandas dataframe containing the NextGen Stats Rushing data available. -### sportsdataverse.nfl.nfl_loaders.load_nfl_officials() +### sportsdataverse.nfl.nfl_loaders.load_nfl_officials(return_as_pandas=True) Load NFL Officials information Example: @@ -199,7 +243,7 @@ Raises: ValueError: If season is less than 1999. -### sportsdataverse.nfl.nfl_loaders.load_nfl_pbp_participation(seasons: List[int]) +### sportsdataverse.nfl.nfl_loaders.load_nfl_pbp_participation(seasons: List[int], return_as_pandas=True) Load NFL play-by-play participation data for selected seasons Example: @@ -215,7 +259,7 @@ Returns: pd.DataFrame: Pandas dataframe containing play-by-play participation data available for the requested seasons. -### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_def() +### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_def(return_as_pandas=True) Load NFL Pro-Football Reference Advanced Defensive data going back to 2018 Example: @@ -229,7 +273,7 @@ Returns: advanced defensive stats data available. -### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_pass() +### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_pass(return_as_pandas=True) Load NFL Pro-Football Reference Advanced Passing data going back to 2018 Example: @@ -243,7 +287,7 @@ Returns: advanced passing stats data available. -### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_rec() +### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_rec(return_as_pandas=True) Load NFL Pro-Football Reference Advanced Receiving data going back to 2018 Example: @@ -257,7 +301,7 @@ Returns: advanced receiving stats data available. -### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_rush() +### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_rush(return_as_pandas=True) Load NFL Pro-Football Reference Advanced Rushing data going back to 2018 Example: @@ -271,7 +315,7 @@ Returns: advanced rushing stats data available. -### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_weekly_def(seasons: List[int]) +### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_weekly_def(seasons: List[int], return_as_pandas=True) Load NFL Pro-Football Reference Weekly Advanced Defensive data going back to 2018 Example: @@ -289,7 +333,7 @@ Returns: advanced defensive stats data available for the requested seasons. -### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_weekly_pass(seasons: List[int]) +### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_weekly_pass(seasons: List[int], return_as_pandas=True) Load NFL Pro-Football Reference Weekly Advanced Passing data going back to 2018 Example: @@ -307,7 +351,7 @@ Returns: advanced passing stats data available for the requested seasons. -### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_weekly_rec(seasons: List[int]) +### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_weekly_rec(seasons: List[int], return_as_pandas=True) Load NFL Pro-Football Reference Weekly Advanced Receiving data going back to 2018 Example: @@ -325,7 +369,7 @@ Returns: advanced receiving stats data available for the requested seasons. -### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_weekly_rush(seasons: List[int]) +### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_weekly_rush(seasons: List[int], return_as_pandas=True) Load NFL Pro-Football Reference Weekly Advanced Rushing data going back to 2018 Example: @@ -343,7 +387,7 @@ Returns: advanced rushing stats data available for the requested seasons. -### sportsdataverse.nfl.nfl_loaders.load_nfl_player_stats(kicking=False) +### sportsdataverse.nfl.nfl_loaders.load_nfl_player_stats(kicking=False, return_as_pandas=True) Load NFL player stats data Example: @@ -359,7 +403,7 @@ Returns: pd.DataFrame: Pandas dataframe containing player stats. -### sportsdataverse.nfl.nfl_loaders.load_nfl_players() +### sportsdataverse.nfl.nfl_loaders.load_nfl_players(return_as_pandas=True) Load NFL Player ID information Example: @@ -373,7 +417,7 @@ Returns: pd.DataFrame: Pandas dataframe containing players available. -### sportsdataverse.nfl.nfl_loaders.load_nfl_rosters(seasons: List[int]) +### sportsdataverse.nfl.nfl_loaders.load_nfl_rosters(seasons: List[int], return_as_pandas=True) Load NFL roster data for all seasons Example: @@ -389,7 +433,7 @@ Returns: pd.DataFrame: Pandas dataframe containing rosters available for the requested seasons. -### sportsdataverse.nfl.nfl_loaders.load_nfl_schedule(seasons: List[int]) +### sportsdataverse.nfl.nfl_loaders.load_nfl_schedule(seasons: List[int], return_as_pandas=True) Load NFL schedule data Example: @@ -409,7 +453,7 @@ Raises: ValueError: If season is less than 1999. -### sportsdataverse.nfl.nfl_loaders.load_nfl_snap_counts(seasons: List[int]) +### sportsdataverse.nfl.nfl_loaders.load_nfl_snap_counts(seasons: List[int], return_as_pandas=True) Load NFL snap counts data for selected seasons Example: @@ -425,7 +469,7 @@ Returns: pd.DataFrame: Pandas dataframe containing snap counts available for the requested seasons. -### sportsdataverse.nfl.nfl_loaders.load_nfl_teams() +### sportsdataverse.nfl.nfl_loaders.load_nfl_teams(return_as_pandas=True) Load NFL team ID information and logos Example: @@ -439,7 +483,7 @@ Returns: pd.DataFrame: Pandas dataframe containing teams available. -### sportsdataverse.nfl.nfl_loaders.load_nfl_weekly_rosters(seasons: List[int]) +### sportsdataverse.nfl.nfl_loaders.load_nfl_weekly_rosters(seasons: List[int], return_as_pandas=True) Load NFL weekly roster data for selected seasons Example: @@ -484,7 +528,7 @@ Returns: Example: - nfl_df = sportsdataverse.nfl.NFLPlayProcess(gameId=401256137).espn_nfl_pbp() + nfl_df = sportsdataverse.nfl.NFLPlayProcess(gameId=401220403).espn_nfl_pbp() #### gameId( = 0) @@ -505,13 +549,14 @@ Example: ## sportsdataverse.nfl.nfl_schedule module -### sportsdataverse.nfl.nfl_schedule.espn_nfl_calendar(season=None, ondays=None) +### sportsdataverse.nfl.nfl_schedule.espn_nfl_calendar(season=None, ondays=None, return_as_pandas=True, \*\*kwargs) espn_nfl_calendar - look up the NFL calendar for a given season Args: season (int): Used to define different seasons. 2002 is the earliest available season. ondays (boolean): Used to return dates for calendar ondays + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -522,7 +567,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.nfl.nfl_schedule.espn_nfl_schedule(dates=None, week=None, season_type=None, limit=500) +### sportsdataverse.nfl.nfl_schedule.espn_nfl_schedule(dates=None, week=None, season_type=None, groups=None, limit=500, return_as_pandas=True, \*\*kwargs) espn_nfl_schedule - look up the NFL schedule for a given season Args: @@ -531,6 +576,7 @@ Args: week (int): Week of the schedule. season_type (int): 2 for regular season, 3 for post-season, 4 for off-season. limit (int): number of records to return, default: 500. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -543,11 +589,19 @@ Returns: ## sportsdataverse.nfl.nfl_teams module -### sportsdataverse.nfl.nfl_teams.espn_nfl_teams() +### sportsdataverse.nfl.nfl_teams.espn_nfl_teams(return_as_pandas=True, \*\*kwargs) espn_nfl_teams - look up NFL teams +Args: + + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Returns: pd.DataFrame: Pandas dataframe containing teams for the requested league. +Example: + + nfl_df = sportsdataverse.nfl.espn_nfl_teams() + ## Module contents diff --git a/docs/docs/nhl/index.md b/docs/docs/nhl/index.md index efcdddb..2b111d5 100755 --- a/docs/docs/nhl/index.md +++ b/docs/docs/nhl/index.md @@ -5,7 +5,7 @@ ## sportsdataverse.nhl.nhl_api module -### sportsdataverse.nhl.nhl_api.nhl_api_pbp(game_id: int) +### sportsdataverse.nhl.nhl_api.nhl_api_pbp(game_id: int, \*\*kwargs) nhl_api_pbp() - Pull the game by id. Data from API endpoints - nhl/playbyplay, nhl/summary Args: @@ -24,7 +24,7 @@ Example: nhl_df = sportsdataverse.nhl.nhl_api_pbp(game_id=2021020079) -### sportsdataverse.nhl.nhl_api.nhl_api_schedule(start_date: str, end_date: str) +### sportsdataverse.nhl.nhl_api.nhl_api_schedule(start_date: str, end_date: str, return_as_pandas=True) nhl_api_schedule() - Pull the game by id. Data from API endpoints - nhl/schedule Args: @@ -33,16 +33,60 @@ Args: Returns: - Dict: + pd.DataFrame: Pandas dataframe containing the schedule for the requested seasons. Example: nhl_sched_df = sportsdataverse.nhl.nhl_api_schedule(start_date=2021-10-23, end_date=2021-10-28) +## sportsdataverse.nhl.nhl_game_rosters module + + +### sportsdataverse.nhl.nhl_game_rosters.espn_nhl_game_rosters(game_id: int, raw=False, return_as_pandas=True, \*\*kwargs) +espn_nhl_game_rosters() - Pull the game by id. + +Args: + + game_id (int): Unique game_id, can be obtained from espn_nhl_schedule(). + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Data frame of game roster data with columns: + ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, + ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, + ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, + ‘age’, ‘date_of_birth’, ‘slug’, ‘jersey’, ‘linked’, ‘active’, + ‘alternate_ids_sdr’, ‘birth_place_city’, ‘birth_place_state’, + ‘birth_place_country’, ‘headshot_href’, ‘headshot_alt’, + ‘experience_years’, ‘experience_display_value’, + ‘experience_abbreviation’, ‘status_id’, ‘status_name’, ‘status_type’, + ‘status_abbreviation’, ‘hand_type’, ‘hand_abbreviation’, + ‘hand_display_value’, ‘draft_display_text’, ‘draft_round’, ‘draft_year’, + ‘draft_selection’, ‘player_id’, ‘starter’, ‘valid’, ‘did_not_play’, + ‘display_name’, ‘ejected’, ‘athlete_href’, ‘position_href’, + ‘statistics_href’, ‘team_id’, ‘team_guid’, ‘team_uid’, ‘team_slug’, + ‘team_location’, ‘team_name’, ‘team_abbreviation’, + ‘team_display_name’, ‘team_short_display_name’, ‘team_color’, + ‘team_alternate_color’, ‘is_active’, ‘is_all_star’, + ‘logo_href’, ‘logo_dark_href’, ‘game_id’ + +Example: + + nhl_df = sportsdataverse.nhl.espn_nhl_game_rosters(game_id=401247153) + + +### sportsdataverse.nhl.nhl_game_rosters.helper_nhl_athlete_items(teams_rosters, \*\*kwargs) + +### sportsdataverse.nhl.nhl_game_rosters.helper_nhl_game_items(summary) + +### sportsdataverse.nhl.nhl_game_rosters.helper_nhl_roster_items(items, summary_url, \*\*kwargs) + +### sportsdataverse.nhl.nhl_game_rosters.helper_nhl_team_items(items, \*\*kwargs) ## sportsdataverse.nhl.nhl_loaders module -### sportsdataverse.nhl.nhl_loaders.load_nhl_pbp(seasons: List[int]) +### sportsdataverse.nhl.nhl_loaders.load_nhl_pbp(seasons: List[int], return_as_pandas=True) Load NHL play by play data going back to 2011 Example: @@ -52,6 +96,7 @@ Example: Args: seasons (list): Used to define different seasons. 2011 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -62,7 +107,7 @@ Raises: ValueError: If season is less than 2011. -### sportsdataverse.nhl.nhl_loaders.load_nhl_player_boxscore(seasons: List[int]) +### sportsdataverse.nhl.nhl_loaders.load_nhl_player_boxscore(seasons: List[int], return_as_pandas=True) Load NHL player boxscore data Example: @@ -72,6 +117,7 @@ Example: Args: seasons (list): Used to define different seasons. 2011 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -83,7 +129,7 @@ Raises: ValueError: If season is less than 2011. -### sportsdataverse.nhl.nhl_loaders.load_nhl_schedule(seasons: List[int]) +### sportsdataverse.nhl.nhl_loaders.load_nhl_schedule(seasons: List[int], return_as_pandas=True) Load NHL schedule data Example: @@ -93,6 +139,7 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -103,7 +150,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.nhl.nhl_loaders.load_nhl_team_boxscore(seasons: List[int]) +### sportsdataverse.nhl.nhl_loaders.load_nhl_team_boxscore(seasons: List[int], return_as_pandas=True) Load NHL team boxscore data Example: @@ -113,6 +160,7 @@ Example: Args: seasons (list): Used to define different seasons. 2011 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -124,7 +172,7 @@ Raises: ValueError: If season is less than 2011. -### sportsdataverse.nhl.nhl_loaders.nhl_teams() +### sportsdataverse.nhl.nhl_loaders.nhl_teams(return_as_pandas=True) Load NHL team ID information and logos Example: @@ -133,6 +181,8 @@ Example: Args: + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Returns: pd.DataFrame: Pandas dataframe containing teams available for the requested seasons. @@ -140,7 +190,7 @@ Returns: ## sportsdataverse.nhl.nhl_pbp module -### sportsdataverse.nhl.nhl_pbp.espn_nhl_pbp(game_id: int, raw=False) +### sportsdataverse.nhl.nhl_pbp.espn_nhl_pbp(game_id: int, raw=False, \*\*kwargs) espn_nhl_pbp() - Pull the game by id. Data from API endpoints - nhl/playbyplay, nhl/summary Args: @@ -159,9 +209,11 @@ Example: nhl_df = sportsdataverse.nhl.espn_nhl_pbp(game_id=401247153) +### sportsdataverse.nhl.nhl_pbp.helper_nhl_game_data(pbp_txt, init) + ### sportsdataverse.nhl.nhl_pbp.helper_nhl_pbp(game_id, pbp_txt) -### sportsdataverse.nhl.nhl_pbp.helper_nhl_pbp_features(game_id, pbp_txt, homeTeamId, awayTeamId, homeTeamMascot, awayTeamMascot, homeTeamName, awayTeamName, homeTeamAbbrev, awayTeamAbbrev, homeTeamNameAlt, awayTeamNameAlt, gameSpread, homeFavorite, gameSpreadAvailable) +### sportsdataverse.nhl.nhl_pbp.helper_nhl_pbp_features(game_id, pbp_txt, init) ### sportsdataverse.nhl.nhl_pbp.helper_nhl_pickcenter(pbp_txt) @@ -169,23 +221,26 @@ Example: ## sportsdataverse.nhl.nhl_schedule module -### sportsdataverse.nhl.nhl_schedule.espn_nhl_calendar(season=None) +### sportsdataverse.nhl.nhl_schedule.espn_nhl_calendar(season=None, ondays=None, return_as_pandas=True, \*\*kwargs) espn_nhl_calendar - look up the NHL calendar for a given season Args: season (int): Used to define different seasons. 2002 is the earliest available season. + ondays (boolean): Used to return dates for calendar ondays + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing calendar dates for the requested season. + pd.DataFrame: Pandas dataframe containing + calendar dates for the requested season. Raises: ValueError: If season is less than 2002. -### sportsdataverse.nhl.nhl_schedule.espn_nhl_schedule(dates=None, season_type=None, limit=500) +### sportsdataverse.nhl.nhl_schedule.espn_nhl_schedule(dates=None, season_type=None, limit=500, return_as_pandas=True, \*\*kwargs) espn_nhl_schedule - look up the NHL schedule for a given date Args: @@ -193,6 +248,7 @@ Args: dates (int): Used to define different seasons. 2002 is the earliest available season. season_type (int): season type, 1 for pre-season, 2 for regular season, 3 for post-season, 4 for all-star, 5 for off-season limit (int): number of records to return, default: 500. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -206,11 +262,19 @@ Returns: ## sportsdataverse.nhl.nhl_teams module -### sportsdataverse.nhl.nhl_teams.espn_nhl_teams() +### sportsdataverse.nhl.nhl_teams.espn_nhl_teams(return_as_pandas=True, \*\*kwargs) espn_nhl_teams - look up NHL teams +Args: + + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Returns: pd.DataFrame: Pandas dataframe containing teams for the requested league. +Example: + + nhl_df = sportsdataverse.nhl.espn_nhl_teams() + ## Module contents diff --git a/docs/docs/wbb/index.md b/docs/docs/wbb/index.md index 57e86b0..90aab91 100755 --- a/docs/docs/wbb/index.md +++ b/docs/docs/wbb/index.md @@ -2,10 +2,54 @@ ## Submodules +## sportsdataverse.wbb.wbb_game_rosters module + + +### sportsdataverse.wbb.wbb_game_rosters.espn_wbb_game_rosters(game_id: int, raw=False, return_as_pandas=True, \*\*kwargs) +espn_wbb_game_rosters() - Pull the game by id. + +Args: + + game_id (int): Unique game_id, can be obtained from wbb_schedule(). + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Data frame of game roster data with columns: + ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, + ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, + ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, + ‘age’, ‘date_of_birth’, ‘slug’, ‘jersey’, ‘linked’, ‘active’, + ‘alternate_ids_sdr’, ‘birth_place_city’, ‘birth_place_state’, + ‘birth_place_country’, ‘headshot_href’, ‘headshot_alt’, + ‘experience_years’, ‘experience_display_value’, + ‘experience_abbreviation’, ‘status_id’, ‘status_name’, ‘status_type’, + ‘status_abbreviation’, ‘hand_type’, ‘hand_abbreviation’, + ‘hand_display_value’, ‘draft_display_text’, ‘draft_round’, ‘draft_year’, + ‘draft_selection’, ‘player_id’, ‘starter’, ‘valid’, ‘did_not_play’, + ‘display_name’, ‘ejected’, ‘athlete_href’, ‘position_href’, + ‘statistics_href’, ‘team_id’, ‘team_guid’, ‘team_uid’, ‘team_slug’, + ‘team_location’, ‘team_name’, ‘team_nickname’, ‘team_abbreviation’, + ‘team_display_name’, ‘team_short_display_name’, ‘team_color’, + ‘team_alternate_color’, ‘is_active’, ‘is_all_star’, + ‘team_alternate_ids_sdr’, ‘logo_href’, ‘logo_dark_href’, ‘game_id’ + +Example: + + wbb_df = sportsdataverse.wbb.espn_wbb_game_rosters(game_id=401266534) + + +### sportsdataverse.wbb.wbb_game_rosters.helper_wbb_athlete_items(teams_rosters, \*\*kwargs) + +### sportsdataverse.wbb.wbb_game_rosters.helper_wbb_game_items(summary) + +### sportsdataverse.wbb.wbb_game_rosters.helper_wbb_roster_items(items, summary_url, \*\*kwargs) + +### sportsdataverse.wbb.wbb_game_rosters.helper_wbb_team_items(items, \*\*kwargs) ## sportsdataverse.wbb.wbb_loaders module -### sportsdataverse.wbb.wbb_loaders.load_wbb_pbp(seasons: List[int]) +### sportsdataverse.wbb.wbb_loaders.load_wbb_pbp(seasons: List[int], return_as_pandas=True) Load women’s college basketball play by play data going back to 2002 Example: @@ -15,6 +59,7 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -26,7 +71,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.wbb.wbb_loaders.load_wbb_player_boxscore(seasons: List[int]) +### sportsdataverse.wbb.wbb_loaders.load_wbb_player_boxscore(seasons: List[int], return_as_pandas=True) Load women’s college basketball player boxscore data Example: @@ -36,6 +81,7 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -47,7 +93,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.wbb.wbb_loaders.load_wbb_schedule(seasons: List[int]) +### sportsdataverse.wbb.wbb_loaders.load_wbb_schedule(seasons: List[int], return_as_pandas=True) Load women’s college basketball schedule data Example: @@ -57,6 +103,7 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -68,7 +115,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.wbb.wbb_loaders.load_wbb_team_boxscore(seasons: List[int]) +### sportsdataverse.wbb.wbb_loaders.load_wbb_team_boxscore(seasons: List[int], return_as_pandas=True) Load women’s college basketball team boxscore data Example: @@ -78,6 +125,7 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -91,13 +139,15 @@ Raises: ## sportsdataverse.wbb.wbb_pbp module -### sportsdataverse.wbb.wbb_pbp.espn_wbb_pbp(game_id: int, raw=False) +### sportsdataverse.wbb.wbb_pbp.espn_wbb_pbp(game_id: int, raw=False, \*\*kwargs) espn_wbb_pbp() - Pull the game by id. Data from API endpoints - womens-college-basketball/playbyplay, womens-college-basketball/summary Args: game_id (int): Unique game_id, can be obtained from wbb_schedule(). +raw (bool): If True, returns the raw json from the API endpoint. If False, returns a cleaned dictionary of datasets. + Returns: Dict: Dictionary of game data with keys - “gameId”, “plays”, “winprobability”, “boxscore”, “header”, “broadcasts”, @@ -110,23 +160,26 @@ Example: wbb_df = sportsdataverse.wb.espn_wbb_pbp(game_id=401266534) +### sportsdataverse.wbb.wbb_pbp.helper_wbb_game_data(pbp_txt, init) + ### sportsdataverse.wbb.wbb_pbp.helper_wbb_pbp(game_id, pbp_txt) -### sportsdataverse.wbb.wbb_pbp.helper_wbb_pbp_features(game_id, pbp_txt, homeTeamId, awayTeamId, homeTeamMascot, awayTeamMascot, homeTeamName, awayTeamName, homeTeamAbbrev, awayTeamAbbrev, homeTeamNameAlt, awayTeamNameAlt, gameSpread, homeFavorite, gameSpreadAvailable) +### sportsdataverse.wbb.wbb_pbp.helper_wbb_pbp_features(game_id, pbp_txt, init) ### sportsdataverse.wbb.wbb_pbp.helper_wbb_pickcenter(pbp_txt) -### sportsdataverse.wbb.wbb_pbp.wbb_pbp_disk(game_id, path_to_json) +### sportsdataverse.wbb.wbb_pbp.mbb_pbp_disk(game_id, path_to_json) ## sportsdataverse.wbb.wbb_schedule module -### sportsdataverse.wbb.wbb_schedule.espn_wbb_calendar(season=None, ondays=None) +### sportsdataverse.wbb.wbb_schedule.espn_wbb_calendar(season=None, ondays=None, return_as_pandas=True, \*\*kwargs) espn_wbb_calendar - look up the women’s college basketball calendar for a given season Args: season (int): Used to define different seasons. 2002 is the earliest available season. ondays (boolean): Used to return dates for calendar ondays + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -138,7 +191,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.wbb.wbb_schedule.espn_wbb_schedule(dates=None, groups=50, season_type=None, limit=500) +### sportsdataverse.wbb.wbb_schedule.espn_wbb_schedule(dates=None, groups=50, season_type=None, limit=500, return_as_pandas=True, \*\*kwargs) espn_wbb_schedule - look up the women’s college basketball schedule for a given season Args: @@ -147,6 +200,7 @@ Args: groups (int): Used to define different divisions. 50 is Division I, 51 is Division II/Division III. season_type (int): 2 for regular season, 3 for post-season, 4 for off-season. limit (int): number of records to return, default: 500. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -157,15 +211,20 @@ Returns: ## sportsdataverse.wbb.wbb_teams module -### sportsdataverse.wbb.wbb_teams.espn_wbb_teams(groups=None) +### sportsdataverse.wbb.wbb_teams.espn_wbb_teams(groups=None, return_as_pandas=True, \*\*kwargs) espn_wbb_teams - look up the women’s college basketball teams Args: groups (int): Used to define different divisions. 50 is Division I, 51 is Division II/Division III. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing teams for the requested league. +Example: + + wbb_df = sportsdataverse.wbb.espn_wbb_teams() + ## Module contents diff --git a/docs/docs/wnba/index.md b/docs/docs/wnba/index.md index 71c9796..0998bf3 100755 --- a/docs/docs/wnba/index.md +++ b/docs/docs/wnba/index.md @@ -2,10 +2,54 @@ ## Submodules +## sportsdataverse.wnba.wnba_game_rosters module + + +### sportsdataverse.wnba.wnba_game_rosters.espn_wnba_game_rosters(game_id: int, raw=False, return_as_pandas=True, \*\*kwargs) +espn_wnba_game_rosters() - Pull the game by id. + +Args: + + game_id (int): Unique game_id, can be obtained from espn_wnba_schedule(). + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Data frame of game roster data with columns: + ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, + ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, + ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, + ‘age’, ‘date_of_birth’, ‘slug’, ‘jersey’, ‘linked’, ‘active’, + ‘alternate_ids_sdr’, ‘birth_place_city’, ‘birth_place_state’, + ‘birth_place_country’, ‘headshot_href’, ‘headshot_alt’, + ‘experience_years’, ‘experience_display_value’, + ‘experience_abbreviation’, ‘status_id’, ‘status_name’, ‘status_type’, + ‘status_abbreviation’, ‘hand_type’, ‘hand_abbreviation’, + ‘hand_display_value’, ‘draft_display_text’, ‘draft_round’, ‘draft_year’, + ‘draft_selection’, ‘player_id’, ‘starter’, ‘valid’, ‘did_not_play’, + ‘display_name’, ‘ejected’, ‘athlete_href’, ‘position_href’, + ‘statistics_href’, ‘team_id’, ‘team_guid’, ‘team_uid’, ‘team_slug’, + ‘team_location’, ‘team_name’, ‘team_abbreviation’, + ‘team_display_name’, ‘team_short_display_name’, ‘team_color’, + ‘team_alternate_color’, ‘is_active’, ‘is_all_star’, + ‘logo_href’, ‘logo_dark_href’, ‘game_id’ + +Example: + + wnba_df = sportsdataverse.wnba.espn_wnba_game_rosters(game_id=401370395) + + +### sportsdataverse.wnba.wnba_game_rosters.helper_wnba_athlete_items(teams_rosters, \*\*kwargs) + +### sportsdataverse.wnba.wnba_game_rosters.helper_wnba_game_items(summary) + +### sportsdataverse.wnba.wnba_game_rosters.helper_wnba_roster_items(items, summary_url, \*\*kwargs) + +### sportsdataverse.wnba.wnba_game_rosters.helper_wnba_team_items(items, \*\*kwargs) ## sportsdataverse.wnba.wnba_loaders module -### sportsdataverse.wnba.wnba_loaders.load_wnba_pbp(seasons: List[int]) +### sportsdataverse.wnba.wnba_loaders.load_wnba_pbp(seasons: List[int], return_as_pandas=True) Load WNBA play by play data going back to 2002 Example: @@ -15,6 +59,7 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -26,7 +71,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.wnba.wnba_loaders.load_wnba_player_boxscore(seasons: List[int]) +### sportsdataverse.wnba.wnba_loaders.load_wnba_player_boxscore(seasons: List[int], return_as_pandas=True) Load WNBA player boxscore data Example: @@ -36,6 +81,7 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -47,7 +93,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.wnba.wnba_loaders.load_wnba_schedule(seasons: List[int]) +### sportsdataverse.wnba.wnba_loaders.load_wnba_schedule(seasons: List[int], return_as_pandas=True) Load WNBA schedule data Example: @@ -57,6 +103,7 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -68,7 +115,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.wnba.wnba_loaders.load_wnba_team_boxscore(seasons: List[int]) +### sportsdataverse.wnba.wnba_loaders.load_wnba_team_boxscore(seasons: List[int], return_as_pandas=True) Load WNBA team boxscore data Example: @@ -78,6 +125,7 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: @@ -91,7 +139,7 @@ Raises: ## sportsdataverse.wnba.wnba_pbp module -### sportsdataverse.wnba.wnba_pbp.espn_wnba_pbp(game_id: int, raw=False) +### sportsdataverse.wnba.wnba_pbp.espn_wnba_pbp(game_id: int, raw=False, \*\*kwargs) espn_wnba_pbp() - Pull the game by id. Data from API endpoints - wnba/playbyplay, wnba/summary Args: @@ -110,9 +158,11 @@ Example: wnba_df = sportsdataverse.wnba.espn_wnba_pbp(game_id=401370395) +### sportsdataverse.wnba.wnba_pbp.helper_wnba_game_data(pbp_txt, init) + ### sportsdataverse.wnba.wnba_pbp.helper_wnba_pbp(game_id, pbp_txt) -### sportsdataverse.wnba.wnba_pbp.helper_wnba_pbp_features(game_id, pbp_txt, gameSpread, homeFavorite, gameSpreadAvailable, homeTeamId, awayTeamId, homeTeamMascot, awayTeamMascot, homeTeamName, awayTeamName, homeTeamAbbrev, awayTeamAbbrev, homeTeamNameAlt, awayTeamNameAlt) +### sportsdataverse.wnba.wnba_pbp.helper_wnba_pbp_features(game_id, pbp_txt, init) ### sportsdataverse.wnba.wnba_pbp.helper_wnba_pickcenter(pbp_txt) @@ -120,7 +170,7 @@ Example: ## sportsdataverse.wnba.wnba_schedule module -### sportsdataverse.wnba.wnba_schedule.espn_wnba_calendar(season=None, ondays=None) +### sportsdataverse.wnba.wnba_schedule.espn_wnba_calendar(season=None, ondays=None, return_as_pandas=True, \*\*kwargs) espn_wnba_calendar - look up the WNBA calendar for a given season Args: @@ -137,7 +187,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.wnba.wnba_schedule.espn_wnba_schedule(dates=None, season_type=None, limit=500) +### sportsdataverse.wnba.wnba_schedule.espn_wnba_schedule(dates=None, season_type=None, limit=500, return_as_pandas=True, \*\*kwargs) espn_wnba_schedule - look up the WNBA schedule for a given season Args: @@ -155,11 +205,19 @@ Returns: ## sportsdataverse.wnba.wnba_teams module -### sportsdataverse.wnba.wnba_teams.espn_wnba_teams() +### sportsdataverse.wnba.wnba_teams.espn_wnba_teams(return_as_pandas=True, \*\*kwargs) espn_wnba_teams - look up WNBA teams +Args: + + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Returns: pd.DataFrame: Pandas dataframe containing teams for the requested league. +Example: + + wnba_df = sportsdataverse.wnba.espn_wnba_teams() + ## Module contents diff --git a/docs/src/pages/CHANGELOG.md b/docs/src/pages/CHANGELOG.md index 9be5a92..96a1831 100755 --- a/docs/src/pages/CHANGELOG.md +++ b/docs/src/pages/CHANGELOG.md @@ -1,4 +1,12 @@ - +## 0.0.36-7 Release: July 9, 2023 +- Switched most under the hood dataframe operations to use the python `polars` library and many functions now have a parameter `return_as_pandas` which defaults to `True` but can be set to `False` to return a polars dataframe instead of a pandas dataframe. This is set this way for backward compatibility but will be changed to default to `False` in a future release. +- Added `**kwargs` which pass arguments to the `dl_utils.download()` function, including `headers`, `proxy`, `timeout` (default 30s), `num_retries` (default = 15), `logger` (default = None) +- Function `espn_cfb_game_rosters()` added. +- Function `espn_nba_game_rosters()` added. +- Function `espn_nfl_game_rosters()` added. +- Function `espn_nhl_game_rosters()` added. +- Function `espn_wbb_game_rosters()` added. +- Function `espn_wnba_game_rosters()` added. ## 0.0.34-35 Release: May 7-9, 2023 - Reconfigured some imports diff --git a/sportsdataverse/decorators.py b/sportsdataverse/decorators.py index 17f74c2..15acb2e 100644 --- a/sportsdataverse/decorators.py +++ b/sportsdataverse/decorators.py @@ -7,13 +7,11 @@ def timer(number=10): """Decorator that times the function it wraps over repeated executions - Parameters - ---------- - number : int - The number of repeated executions of the function being wrapped - Returns - ------- - func + Args: + number: int, The number of repeated executions of the function being wrapped + + Returns: + func """ def actual_wrapper(func): @functools.wraps(func) From 3eaf954eb7b098d012e93181d72389a4c6f38d0a Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Sat, 15 Jul 2023 00:47:22 -0400 Subject: [PATCH 32/79] Update tests.yml --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bcb7e96..47746af 100755 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,9 +8,9 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: '3.9' - name: Install dependencies From 5519b4988e0f5d0ab789cf4c00e6001d734bc4a8 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Sat, 15 Jul 2023 00:48:07 -0400 Subject: [PATCH 33/79] Update python-publish.yml --- .github/workflows/python-publish.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 8dc0c9c..fb3e7d5 100755 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -14,11 +14,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: - python-version: '3.x' + python-version: '3.9' - name: Install dependencies run: | python -m pip install --upgrade pip From 8e6290dc0c9da89dfc7fdb1d2100d92d1cf763e7 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Sat, 15 Jul 2023 02:15:00 -0400 Subject: [PATCH 34/79] tests --- .github/workflows/tests.yml | 1 + tests/__init__.py | 2 - tests/cfb/__init__.py | 2 +- tests/cfb/test_cfb_schedule.py | 46 +++++++++++++++++++++++ tests/cfb/test_pbp.py | 67 ++++++++++++++++++++++++++++++++-- tests/test_dl_utils.py | 44 ++++++++++++++++++++++ 6 files changed, 155 insertions(+), 7 deletions(-) delete mode 100755 tests/__init__.py create mode 100644 tests/cfb/test_cfb_schedule.py create mode 100644 tests/test_dl_utils.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 47746af..bb0bfea 100755 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,3 +22,4 @@ jobs: pip install .[all] pytest ./tests/cfb pytest ./tests/mbb + pytest ./tests/test_dl_utils.py diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100755 index a416b78..0000000 --- a/tests/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from tests.cfb import * -from tests.mbb import * diff --git a/tests/cfb/__init__.py b/tests/cfb/__init__.py index e4f21b6..1ee2999 100755 --- a/tests/cfb/__init__.py +++ b/tests/cfb/__init__.py @@ -1 +1 @@ -from .test_pbp import * \ No newline at end of file +"tests " \ No newline at end of file diff --git a/tests/cfb/test_cfb_schedule.py b/tests/cfb/test_cfb_schedule.py new file mode 100644 index 0000000..a9f0163 --- /dev/null +++ b/tests/cfb/test_cfb_schedule.py @@ -0,0 +1,46 @@ +from sportsdataverse.cfb.cfb_schedule import espn_cfb_calendar, espn_cfb_schedule, most_recent_cfb_season +import pandas as pd +import polars as pl +import pytest + +@pytest.fixture() +def calendar_data(): + yield espn_cfb_calendar(season=most_recent_cfb_season(), return_as_pandas=False) + +def calendar_data_check(calendar_data): + assert isinstance(calendar_data, pl.DataFrame) + assert len(calendar_data) > 0 + +@pytest.fixture() +def calendar_ondays_data(): + yield espn_cfb_calendar(season=2021, ondays=True, return_as_pandas=False) + + +def calendar_ondays_data_check(calendar_ondays_data): + assert isinstance(calendar_ondays_data, pl.DataFrame) + assert len(calendar_ondays_data) > 0 + +@pytest.fixture() +def schedule_data(): + yield espn_cfb_schedule(return_as_pandas=False) + +def schedule_data_check(schedule_data): + assert isinstance(schedule_data, pl.DataFrame) + assert len(schedule_data) > 0 + +@pytest.fixture() +def schedule_data2(): + yield espn_cfb_schedule(dates=20220901, return_as_pandas=False) + +def schedule_data_check2(schedule_data2): + assert isinstance(schedule_data, pl.DataFrame) + assert len(schedule_data) > 0 + +@pytest.fixture() +def week_1_schedule(): + yield espn_cfb_schedule(dates = 2022, week = 1, return_as_pandas=False) + + +def week_1_schedule_check(week_1_schedule): + assert isinstance(week_1_schedule, pl.DataFrame) + assert len(week_1_schedule) > 0 \ No newline at end of file diff --git a/tests/cfb/test_pbp.py b/tests/cfb/test_pbp.py index bc31915..1b15157 100755 --- a/tests/cfb/test_pbp.py +++ b/tests/cfb/test_pbp.py @@ -1,4 +1,6 @@ from sportsdataverse.cfb.cfb_pbp import CFBPlayProcess +from sportsdataverse.cfb.cfb_schedule import espn_cfb_calendar, espn_cfb_schedule, most_recent_cfb_season + import pandas as pd import polars as pl import pytest @@ -12,8 +14,9 @@ def generated_data(): @pytest.fixture() def box_score(generated_data): - box = generated_data.create_box_score(pl.DataFrame(generated_data.plays_json, infer_schema_length=400)) - yield box + yield generated_data.create_box_score( + pl.DataFrame(generated_data.plays_json, infer_schema_length=400) + ) def test_basic_pbp(generated_data): assert generated_data.json != None @@ -25,7 +28,19 @@ def test_basic_pbp(generated_data): def test_adv_box_score(box_score): assert box_score != None - assert len(set(box_score.keys()).difference({"win_pct","pass","team","situational","receiver","rush","receiver","defensive","turnover","drives"})) == 0 + assert not set(box_score.keys()).difference( + { + "win_pct", + "pass", + "team", + "situational", + "rush", + "receiver", + "defensive", + "turnover", + "drives", + } + ) def test_havoc_rate(box_score): defense_home = box_score["defensive"][0] @@ -171,4 +186,48 @@ def test_expected_turnovers(iu_play_base_box): print(f"home team: {home_team} vs def {def_away_team} - fum: {home_fum}, int: {home_off_int}, pd: {home_pd} -> xTO: {home_exp_xTO}") print(f"away off {away_team} vs def {def_home_team} - fum: {away_fum}, int: {away_off_int}, pd: {away_pd} -> xTO: {away_exp_xTO}") assert round(away_exp_xTO, 4) == round(away_actual_xTO, 4) - assert round(home_exp_xTO, 4) == round(home_actual_xTO, 4) \ No newline at end of file + assert round(home_exp_xTO, 4) == round(home_actual_xTO, 4) + + + +@pytest.fixture() +def calendar_data(): + yield espn_cfb_calendar(season=most_recent_cfb_season(), return_as_pandas=False) + +def calendar_data_check(calendar_data): + assert isinstance(calendar_data, pl.DataFrame) + assert len(calendar_data) > 0 + +@pytest.fixture() +def calendar_ondays_data(): + yield espn_cfb_calendar(season=2021, ondays=True, return_as_pandas=False) + + +def calendar_ondays_data_check(calendar_ondays_data): + assert isinstance(calendar_ondays_data, pl.DataFrame) + assert len(calendar_ondays_data) > 0 + +@pytest.fixture() +def schedule_data(): + yield espn_cfb_schedule(return_as_pandas=False) + +def schedule_data_check(schedule_data): + assert isinstance(schedule_data, pl.DataFrame) + assert len(schedule_data) > 0 + +@pytest.fixture() +def schedule_data2(): + yield espn_cfb_schedule(dates=20220901, return_as_pandas=False) + +def schedule_data_check2(schedule_data2): + assert isinstance(schedule_data, pl.DataFrame) + assert len(schedule_data) > 0 + +@pytest.fixture() +def week_1_schedule(): + yield espn_cfb_schedule(dates = 2022, week = 1, return_as_pandas=False) + + +def week_1_schedule_check(week_1_schedule): + assert isinstance(week_1_schedule, pl.DataFrame) + assert len(week_1_schedule) > 0 \ No newline at end of file diff --git a/tests/test_dl_utils.py b/tests/test_dl_utils.py new file mode 100644 index 0000000..167a78a --- /dev/null +++ b/tests/test_dl_utils.py @@ -0,0 +1,44 @@ +import pytest +import requests +from sportsdataverse.dl_utils import download + +class TestDownload: + # Tests that the function can download a valid URL with default parameters + def test_download_valid_url_default_params(self): + url = 'https://www.google.com' + response = download(url) + assert response.status_code == 200 + + # Tests that the function can download a valid URL with custom parameters + def test_download_valid_url_custom_params(self): + url = 'https://jsonplaceholder.typicode.com/posts' + params = {'userId': 1} + response = download(url, params=params) + assert response.status_code == 200 + + # Tests that the function can download a valid URL with custom headers + def test_download_valid_url_custom_headers(self): + url = 'https://jsonplaceholder.typicode.com/posts' + headers = {'User-Agent': 'Mozilla/5.0'} + response = download(url, headers=headers) + assert response.status_code == 200 + + # Tests that the function can download a valid URL with a proxy + def test_download_valid_url_with_proxy(self): + url = 'https://jsonplaceholder.typicode.com/posts' + proxy = {'https': 'https://localhost:8080'} + response = download(url, proxy=proxy) + assert response.status_code == 200 + + # Tests that the function can download a valid URL with a very short timeout + def test_download_valid_url_with_short_timeout(self): + url = 'https://jsonplaceholder.typicode.com/posts' + timeout = 0.001 + with pytest.raises(requests.exceptions.Timeout): + download(url, timeout=timeout) + + # Tests that the function handles an invalid URL + def test_download_invalid_url(self): + url = 'https://thisisnotavalidurl.com' + with pytest.raises(requests.exceptions.RequestException): + download(url) \ No newline at end of file From 22fd209b07e520049a3e8cf22b05597d0e3d7ece Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Sat, 15 Jul 2023 02:17:32 -0400 Subject: [PATCH 35/79] Update tests.yml --- .github/workflows/tests.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bb0bfea..47746af 100755 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,4 +22,3 @@ jobs: pip install .[all] pytest ./tests/cfb pytest ./tests/mbb - pytest ./tests/test_dl_utils.py From 6ee26ba022498803fe9b3f23b1bd42e06ea3b853 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Wed, 26 Jul 2023 09:25:05 -0400 Subject: [PATCH 36/79] pl.Int64 changes per comment --- sportsdataverse/cfb/cfb_game_rosters.py | 2 +- sportsdataverse/cfb/cfb_schedule.py | 7 ++++++- sportsdataverse/mbb/mbb_game_rosters.py | 2 +- sportsdataverse/mbb/mbb_schedule.py | 2 +- sportsdataverse/nba/nba_game_rosters.py | 2 +- sportsdataverse/nba/nba_schedule.py | 2 +- sportsdataverse/nfl/nfl_game_rosters.py | 2 +- sportsdataverse/nfl/nfl_schedule.py | 2 +- sportsdataverse/nhl/nhl_game_rosters.py | 2 +- sportsdataverse/nhl/nhl_schedule.py | 2 +- sportsdataverse/wbb/wbb_game_rosters.py | 2 +- sportsdataverse/wbb/wbb_schedule.py | 2 +- sportsdataverse/wnba/wnba_game_rosters.py | 2 +- sportsdataverse/wnba/wnba_schedule.py | 2 +- 14 files changed, 19 insertions(+), 14 deletions(-) diff --git a/sportsdataverse/cfb/cfb_game_rosters.py b/sportsdataverse/cfb/cfb_game_rosters.py index 8be545f..7e47755 100644 --- a/sportsdataverse/cfb/cfb_game_rosters.py +++ b/sportsdataverse/cfb/cfb_game_rosters.py @@ -50,7 +50,7 @@ def espn_cfb_game_rosters(game_id: int, raw = False, return_as_pandas = True, ** athletes = helper_cfb_athlete_items(teams_rosters = team_rosters, **kwargs) rosters = athletes.join(teams_rosters, how = 'left', left_on = 'athlete_id', right_on = 'player_id') rosters = rosters.with_columns( - game_id = pl.lit(game_id).cast(pl.Int64) + game_id = pl.lit(game_id).cast(pl.Int32) ) rosters.columns = [underscore(c) for c in rosters.columns] return rosters.to_pandas() if return_as_pandas else rosters diff --git a/sportsdataverse/cfb/cfb_schedule.py b/sportsdataverse/cfb/cfb_schedule.py index 43ff601..14492ad 100755 --- a/sportsdataverse/cfb/cfb_schedule.py +++ b/sportsdataverse/cfb/cfb_schedule.py @@ -5,6 +5,7 @@ import datetime from sportsdataverse.dl_utils import download, underscore + def espn_cfb_schedule(dates=None, week=None, season_type=None, groups=None, limit=500, return_as_pandas = True, **kwargs) -> pd.DataFrame: @@ -60,7 +61,7 @@ def espn_cfb_schedule(dates=None, week=None, season_type=None, groups=None, limi event.get('competitions')[0].pop('competitors', None) x = pl.from_pandas(pd.json_normalize(event.get('competitions')[0], sep = '_')) x = x.with_columns( - game_id = (pl.col('id').cast(pl.Int64)), + game_id = (pl.col('id').cast(pl.Int32)), season = (event.get('season').get('year')), season_type = (event.get('season').get('type')), week = (event.get('week', {}).get('number')), @@ -78,6 +79,7 @@ def espn_cfb_schedule(dates=None, week=None, season_type=None, groups=None, limi return ev.to_pandas() if return_as_pandas else ev + # TODO Rename this here and in `espn_cfb_schedule` def _extract_home_away(event, arg1, arg2): event['competitions'][0][arg2] = ( @@ -117,6 +119,7 @@ def _extract_home_away(event, arg1, arg2): ) return event + def espn_cfb_calendar(season = None, groups = None, ondays = None, return_as_pandas = True, **kwargs) -> pd.DataFrame: """espn_cfb_calendar - look up the men's college football calendar for a given season @@ -164,6 +167,7 @@ def espn_cfb_calendar(season = None, groups = None, ondays = None, def __ondays_cfb_calendar(season, **kwargs): + url = f"https://sports.core.api.espn.com/v2/sports/football/leagues/college-football/seasons/{season}/types/2/calendar/ondays" resp = download(url=url, **kwargs) if resp is not None: @@ -177,6 +181,7 @@ def __ondays_cfb_calendar(season, **kwargs): return result + def most_recent_cfb_season(): date = datetime.datetime.now() if date.month >= 8 and date.day >= 15: diff --git a/sportsdataverse/mbb/mbb_game_rosters.py b/sportsdataverse/mbb/mbb_game_rosters.py index e085a70..c80c5cd 100755 --- a/sportsdataverse/mbb/mbb_game_rosters.py +++ b/sportsdataverse/mbb/mbb_game_rosters.py @@ -50,7 +50,7 @@ def espn_mbb_game_rosters(game_id: int, raw = False, return_as_pandas = True, ** athletes = helper_mbb_athlete_items(teams_rosters = team_rosters, **kwargs) rosters = athletes.join(teams_rosters, how = 'left', left_on = 'athlete_id', right_on = 'player_id') rosters = rosters.with_columns( - game_id = pl.lit(game_id).cast(pl.Int64) + game_id = pl.lit(game_id).cast(pl.Int32) ) rosters.columns = [underscore(c) for c in rosters.columns] return rosters.to_pandas() if return_as_pandas else rosters diff --git a/sportsdataverse/mbb/mbb_schedule.py b/sportsdataverse/mbb/mbb_schedule.py index 874b9dc..921817b 100755 --- a/sportsdataverse/mbb/mbb_schedule.py +++ b/sportsdataverse/mbb/mbb_schedule.py @@ -56,7 +56,7 @@ def espn_mbb_schedule(dates=None, groups=50, season_type=None, limit=500, event.get('competitions')[0].pop('competitors', None) x = pl.from_pandas(pd.json_normalize(event.get('competitions')[0], sep = '_')) x = x.with_columns( - game_id = (pl.col('id').cast(pl.Int64)), + game_id = (pl.col('id').cast(pl.Int32)), season = (event.get('season').get('year')), season_type = (event.get('season').get('type')), home_linescores = pl.when(pl.col('status_type_description') == 'Postponed').then(None).otherwise(pl.col('home_linescores')), diff --git a/sportsdataverse/nba/nba_game_rosters.py b/sportsdataverse/nba/nba_game_rosters.py index 9d41631..c9fc0b5 100644 --- a/sportsdataverse/nba/nba_game_rosters.py +++ b/sportsdataverse/nba/nba_game_rosters.py @@ -50,7 +50,7 @@ def espn_nba_game_rosters(game_id: int, raw = False, return_as_pandas = True, ** athletes = helper_nba_athlete_items(teams_rosters = team_rosters, **kwargs) rosters = athletes.join(teams_rosters, how = 'left', left_on = 'athlete_id', right_on = 'player_id') rosters = rosters.with_columns( - game_id = pl.lit(game_id).cast(pl.Int64) + game_id = pl.lit(game_id).cast(pl.Int32) ) rosters.columns = [underscore(c) for c in rosters.columns] return rosters.to_pandas() if return_as_pandas else rosters diff --git a/sportsdataverse/nba/nba_schedule.py b/sportsdataverse/nba/nba_schedule.py index f0d204a..94d7fd6 100755 --- a/sportsdataverse/nba/nba_schedule.py +++ b/sportsdataverse/nba/nba_schedule.py @@ -55,7 +55,7 @@ def espn_nba_schedule(dates=None, season_type=None, limit=500, x = pl.from_pandas(pd.json_normalize(event.get('competitions')[0], sep = '_')) x = x.with_columns( - game_id = (pl.col('id').cast(pl.Int64)), + game_id = (pl.col('id').cast(pl.Int32)), season = (event.get('season').get('year')), season_type = (event.get('season').get('type')), home_linescores = pl.when(pl.col('status_type_description') == 'Postponed') diff --git a/sportsdataverse/nfl/nfl_game_rosters.py b/sportsdataverse/nfl/nfl_game_rosters.py index 70f550a..566107c 100644 --- a/sportsdataverse/nfl/nfl_game_rosters.py +++ b/sportsdataverse/nfl/nfl_game_rosters.py @@ -50,7 +50,7 @@ def espn_nfl_game_rosters(game_id: int, raw = False, return_as_pandas = True, ** athletes = helper_nfl_athlete_items(teams_rosters = team_rosters, **kwargs) rosters = athletes.join(teams_rosters, how = 'left', left_on = 'athlete_id', right_on = 'player_id') rosters = rosters.with_columns( - game_id = pl.lit(game_id).cast(pl.Int64) + game_id = pl.lit(game_id).cast(pl.Int32) ) rosters.columns = [underscore(c) for c in rosters.columns] return rosters.to_pandas() if return_as_pandas else rosters diff --git a/sportsdataverse/nfl/nfl_schedule.py b/sportsdataverse/nfl/nfl_schedule.py index 990effe..7d6c90b 100755 --- a/sportsdataverse/nfl/nfl_schedule.py +++ b/sportsdataverse/nfl/nfl_schedule.py @@ -59,7 +59,7 @@ def espn_nfl_schedule(dates=None, week=None, season_type=None, groups=None, limi event.get('competitions')[0].pop('competitors', None) x = pl.from_pandas(pd.json_normalize(event.get('competitions')[0], sep = '_')) x = x.with_columns( - game_id = (pl.col('id').cast(pl.Int64)), + game_id = (pl.col('id').cast(pl.Int32)), season = (event.get('season').get('year')), season_type = (event.get('season').get('type')), week = (event.get('week', {}).get('number')), diff --git a/sportsdataverse/nhl/nhl_game_rosters.py b/sportsdataverse/nhl/nhl_game_rosters.py index ecdf434..3353207 100644 --- a/sportsdataverse/nhl/nhl_game_rosters.py +++ b/sportsdataverse/nhl/nhl_game_rosters.py @@ -50,7 +50,7 @@ def espn_nhl_game_rosters(game_id: int, raw = False, return_as_pandas = True, ** athletes = helper_nhl_athlete_items(teams_rosters = team_rosters, **kwargs) rosters = athletes.join(teams_rosters, how = 'left', left_on = 'athlete_id', right_on = 'player_id') rosters = rosters.with_columns( - game_id = pl.lit(game_id).cast(pl.Int64) + game_id = pl.lit(game_id).cast(pl.Int32) ) rosters.columns = [underscore(c) for c in rosters.columns] return rosters.to_pandas() if return_as_pandas else rosters diff --git a/sportsdataverse/nhl/nhl_schedule.py b/sportsdataverse/nhl/nhl_schedule.py index 42fa0c5..2fe5408 100755 --- a/sportsdataverse/nhl/nhl_schedule.py +++ b/sportsdataverse/nhl/nhl_schedule.py @@ -55,7 +55,7 @@ def espn_nhl_schedule(dates=None, season_type=None, limit=500, x = pl.from_pandas(pd.json_normalize(event.get('competitions')[0], sep = '_')) x = x.with_columns( - game_id = (pl.col('id').cast(pl.Int64)), + game_id = (pl.col('id').cast(pl.Int32)), season = (event.get('season').get('year')), season_type = (event.get('season').get('type')), home_linescores = pl.when(pl.col('status_type_description') == 'Postponed') diff --git a/sportsdataverse/wbb/wbb_game_rosters.py b/sportsdataverse/wbb/wbb_game_rosters.py index 8b027ae..95cd6a0 100644 --- a/sportsdataverse/wbb/wbb_game_rosters.py +++ b/sportsdataverse/wbb/wbb_game_rosters.py @@ -50,7 +50,7 @@ def espn_wbb_game_rosters(game_id: int, raw = False, return_as_pandas = True, ** athletes = helper_wbb_athlete_items(teams_rosters = team_rosters, **kwargs) rosters = athletes.join(teams_rosters, how = 'left', left_on = 'athlete_id', right_on = 'player_id') rosters = rosters.with_columns( - game_id = pl.lit(game_id).cast(pl.Int64) + game_id = pl.lit(game_id).cast(pl.Int32) ) rosters.columns = [underscore(c) for c in rosters.columns] return rosters.to_pandas() if return_as_pandas else rosters diff --git a/sportsdataverse/wbb/wbb_schedule.py b/sportsdataverse/wbb/wbb_schedule.py index 9be6fc1..5b8654b 100755 --- a/sportsdataverse/wbb/wbb_schedule.py +++ b/sportsdataverse/wbb/wbb_schedule.py @@ -55,7 +55,7 @@ def espn_wbb_schedule(dates=None, groups=50, season_type=None, limit=500, event.get('competitions')[0].pop('competitors', None) x = pl.from_pandas(pd.json_normalize(event.get('competitions')[0], sep = '_')) x = x.with_columns( - game_id = (pl.col('id').cast(pl.Int64)), + game_id = (pl.col('id').cast(pl.Int32)), season = (event.get('season').get('year')), season_type = (event.get('season').get('type')), home_linescores = pl.when(pl.col('status_type_description') == 'Postponed').then(None).otherwise(pl.col('home_linescores')), diff --git a/sportsdataverse/wnba/wnba_game_rosters.py b/sportsdataverse/wnba/wnba_game_rosters.py index d58f8a4..99bacb5 100644 --- a/sportsdataverse/wnba/wnba_game_rosters.py +++ b/sportsdataverse/wnba/wnba_game_rosters.py @@ -50,7 +50,7 @@ def espn_wnba_game_rosters(game_id: int, raw = False, return_as_pandas = True, * athletes = helper_wnba_athlete_items(teams_rosters = team_rosters, **kwargs) rosters = athletes.join(teams_rosters, how = 'left', left_on = 'athlete_id', right_on = 'player_id') rosters = rosters.with_columns( - game_id = pl.lit(game_id).cast(pl.Int64) + game_id = pl.lit(game_id).cast(pl.Int32) ) rosters.columns = [underscore(c) for c in rosters.columns] return rosters.to_pandas() if return_as_pandas else rosters diff --git a/sportsdataverse/wnba/wnba_schedule.py b/sportsdataverse/wnba/wnba_schedule.py index e89e6f7..6118fdf 100755 --- a/sportsdataverse/wnba/wnba_schedule.py +++ b/sportsdataverse/wnba/wnba_schedule.py @@ -53,7 +53,7 @@ def espn_wnba_schedule(dates=None, season_type=None, limit = 500, x = pl.from_pandas(pd.json_normalize(event.get('competitions')[0], sep = '_')) x = x.with_columns( - game_id = (pl.col('id').cast(pl.Int64)), + game_id = (pl.col('id').cast(pl.Int32)), season = (event.get('season').get('year')), season_type = (event.get('season').get('type')), home_linescores = pl.when(pl.col('status_type_description') == 'Postponed') From 4729354d8aa7295866d229d77b6e68d9394f6648 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Wed, 26 Jul 2023 19:10:36 -0400 Subject: [PATCH 37/79] black/flake/isort formatting, minor documentation details --- .../_build/doctrees/environment.pickle | Bin 937165 -> 925711 bytes .../doctrees/sportsdataverse.mlb.doctree | Bin 166977 -> 167270 bytes .../doctrees/sportsdataverse.nba.doctree | Bin 85789 -> 85789 bytes .../doctrees/sportsdataverse.nfl.doctree | Bin 205651 -> 206449 bytes .../doctrees/sportsdataverse.wbb.doctree | Bin 86629 -> 86055 bytes Sphinx-docs/_build/doctrees/tests.cfb.doctree | Bin 19628 -> 3131 bytes Sphinx-docs/_build/doctrees/tests.doctree | Bin 3512 -> 11817 bytes Sphinx-docs/_build/doctrees/tests.mbb.doctree | Bin 6646 -> 3131 bytes .../_build/markdown/sportsdataverse.mlb.md | 2 +- .../_build/markdown/sportsdataverse.nba.md | 3 +- .../_build/markdown/sportsdataverse.nfl.md | 3 +- .../_build/markdown/sportsdataverse.wbb.md | 10 +- Sphinx-docs/_build/markdown/tests.cfb.md | 24 - Sphinx-docs/_build/markdown/tests.mbb.md | 4 - Sphinx-docs/_build/markdown/tests.md | 15 + docs/docs/mlb/index.md | 2 +- docs/docs/nba/index.md | 3 +- docs/docs/nfl/index.md | 3 +- docs/docs/wbb/index.md | 10 +- sportsdataverse/cfb/cfb_game_rosters.py | 203 ++++--- sportsdataverse/cfb/cfb_loaders.py | 61 ++- sportsdataverse/cfb/cfb_schedule.py | 186 ++++--- sportsdataverse/cfb/cfb_teams.py | 19 +- sportsdataverse/cfb/model_vars.py | 70 +-- sportsdataverse/mbb/mbb_game_rosters.py | 196 ++++--- sportsdataverse/mbb/mbb_loaders.py | 37 +- sportsdataverse/mbb/mbb_pbp.py | 400 +++++++------- sportsdataverse/mbb/mbb_schedule.py | 177 +++--- sportsdataverse/mbb/mbb_teams.py | 19 +- sportsdataverse/mlb/mlb_loaders.py | 83 ++- sportsdataverse/nba/nba_game_rosters.py | 203 ++++--- sportsdataverse/nba/nba_loaders.py | 38 +- sportsdataverse/nba/nba_pbp.py | 500 ++++++++--------- sportsdataverse/nba/nba_schedule.py | 170 +++--- sportsdataverse/nba/nba_teams.py | 17 +- sportsdataverse/nfl/model_vars.py | 70 +-- sportsdataverse/nfl/nfl_game_rosters.py | 207 ++++--- sportsdataverse/nfl/nfl_games.py | 195 ++++--- sportsdataverse/nfl/nfl_loaders.py | 291 ++++++---- sportsdataverse/nfl/nfl_schedule.py | 189 ++++--- sportsdataverse/nfl/nfl_teams.py | 19 +- sportsdataverse/nhl/nhl_api.py | 36 +- sportsdataverse/nhl/nhl_game_rosters.py | 213 ++++---- sportsdataverse/nhl/nhl_loaders.py | 51 +- sportsdataverse/nhl/nhl_pbp.py | 325 ++++++----- sportsdataverse/nhl/nhl_schedule.py | 166 +++--- sportsdataverse/nhl/nhl_teams.py | 19 +- sportsdataverse/wbb/wbb_game_rosters.py | 195 +++---- sportsdataverse/wbb/wbb_loaders.py | 38 +- sportsdataverse/wbb/wbb_pbp.py | 503 +++++++++--------- sportsdataverse/wbb/wbb_schedule.py | 171 +++--- sportsdataverse/wbb/wbb_teams.py | 19 +- sportsdataverse/wnba/wnba_game_rosters.py | 206 ++++--- sportsdataverse/wnba/wnba_loaders.py | 38 +- sportsdataverse/wnba/wnba_pbp.py | 498 ++++++++--------- sportsdataverse/wnba/wnba_schedule.py | 168 +++--- sportsdataverse/wnba/wnba_teams.py | 20 +- 57 files changed, 3065 insertions(+), 3030 deletions(-) diff --git a/Sphinx-docs/_build/doctrees/environment.pickle b/Sphinx-docs/_build/doctrees/environment.pickle index 29c904b11a964ce48cf63ecf42d1d1e15bdf1125..89a32264b7074b7c24fb8d1354e12fb1d9972bb6 100755 GIT binary patch delta 121623 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ⅆexn4d#+Jk8u$W{muS>N$Sh|3`1WB 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 diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.mlb.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.mlb.doctree index 99ac763069820c1d8c7d4336dc40c00959fd5f66..d4e1c2f25e043f1d330c80903755bac113424e2f 100755 GIT binary patch literal 167270 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 delta 102 zcmbO`k9F=m)`l&NX5x$r)4e1a_e@q;YBF72g3+0geR{S8<2^=&?LgrQMu_y@=?&71 y>wr@FGK?P>xwb3FGHNigab-wntrVNQakA$0aydpLFh5mna^PCs?RVrD9Ss2R)*Wd8 diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.nfl.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.nfl.doctree index 68555defcebd5b2884a001f377218231068f8f03..360ca3d9741edf8b55a383b7e83c24e583c57f30 100755 GIT binary patch delta 20004 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 diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.wbb.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.wbb.doctree index d316849b643557c502179df6d59defa35fde3db6..4c6fa8e3a52755464c72d7e68244fe4d88bde3ec 100755 GIT binary patch delta 4884 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 diff --git a/Sphinx-docs/_build/doctrees/tests.doctree b/Sphinx-docs/_build/doctrees/tests.doctree index d2c99142098d1bd9da906709ee67d5e655dc7ff4..e3de039cf0252a678888ec1b34a82d169f4d35b8 100755 GIT binary patch literal 11817 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 Axc~qF delta 189 zcmZ1(vqPG-fpzLy-i@p$S>&8ttYX|VOFT-GbX^il5@U)}%5+mw%fNI&<>ZZQs;pBo zUi0gRRYw&dAS@$zU!`+I&qXm=OS8 C;zKV0 diff --git a/Sphinx-docs/_build/doctrees/tests.mbb.doctree b/Sphinx-docs/_build/doctrees/tests.mbb.doctree index 499e9bfce12fad8e787db0370208a9c8beec9fe5..dbc0874a42c5835b9752290f276256d553db20dd 100755 GIT binary patch delta 215 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 ZmK@J=V;f-U4+&!zZ7M7&2J2>E@4vh0ydnSq diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.mlb.md b/Sphinx-docs/_build/markdown/sportsdataverse.mlb.md index 986d8cc..1daccd6 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.mlb.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.mlb.md @@ -5,7 +5,7 @@ ## sportsdataverse.mlb.mlb_loaders module -### sportsdataverse.mlb.mlb_loaders.mlbam_copyright_info(saveFile=False, returnFile=False) +### sportsdataverse.mlb.mlb_loaders.mlbam_copyright_info(saveFile=False, returnFile=False, \*\*kwargs) Displays the copyright info for the MLBAM API. Args: diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.nba.md b/Sphinx-docs/_build/markdown/sportsdataverse.nba.md index 2ca6190..f1af879 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.nba.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.nba.md @@ -196,7 +196,8 @@ espn_nba_schedule - look up the NBA schedule for a given date from ESPN Args: dates (int): Used to define different seasons. 2002 is the earliest available season. - season_type (int): season type, 1 for pre-season, 2 for regular season, 3 for post-season, 4 for all-star, 5 for off-season + season_type (int): season type, 1 for pre-season, 2 for regular season, 3 for post-season, + 4 for all-star, 5 for off-season limit (int): number of records to return, default: 500. return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.nfl.md b/Sphinx-docs/_build/markdown/sportsdataverse.nfl.md index c9a7e5a..b2421a8 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.nfl.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.nfl.md @@ -223,7 +223,7 @@ Returns: pd.DataFrame: Pandas dataframe containing officials available. -### sportsdataverse.nfl.nfl_loaders.load_nfl_pbp(seasons: List[int]) +### sportsdataverse.nfl.nfl_loaders.load_nfl_pbp(seasons: List[int], return_as_pandas=True) Load NFL play by play data going back to 1999 Example: @@ -233,6 +233,7 @@ Example: Args: seasons (list): Used to define different seasons. 1999 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.wbb.md b/Sphinx-docs/_build/markdown/sportsdataverse.wbb.md index 90aab91..fdbb500 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.wbb.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.wbb.md @@ -140,7 +140,8 @@ Raises: ### sportsdataverse.wbb.wbb_pbp.espn_wbb_pbp(game_id: int, raw=False, \*\*kwargs) -espn_wbb_pbp() - Pull the game by id. Data from API endpoints - womens-college-basketball/playbyplay, womens-college-basketball/summary +espn_wbb_pbp() - Pull the game by id. Data from API endpoints - womens-college-basketball/playbyplay, +womens-college-basketball/summary Args: @@ -150,10 +151,9 @@ raw (bool): If True, returns the raw json from the API endpoint. If False, retur Returns: - Dict: Dictionary of game data with keys - “gameId”, “plays”, “winprobability”, “boxscore”, “header”, “broadcasts”, - - “videos”, “playByPlaySource”, “standings”, “leaders”, “timeouts”, “pickcenter”, “againstTheSpread”, “odds”, “predictor”, - “espnWP”, “gameInfo”, “season” + Dict: Dictionary of game data with keys - “gameId”, “plays”, “winprobability”, “boxscore”, “header”, + “broadcasts”, “videos”, “playByPlaySource”, “standings”, “leaders”, “timeouts”, “pickcenter”, + “againstTheSpread”, “odds”, “predictor”,”espnWP”, “gameInfo”, “season” Example: diff --git a/Sphinx-docs/_build/markdown/tests.cfb.md b/Sphinx-docs/_build/markdown/tests.cfb.md index b117b8e..4082897 100755 --- a/Sphinx-docs/_build/markdown/tests.cfb.md +++ b/Sphinx-docs/_build/markdown/tests.cfb.md @@ -4,28 +4,4 @@ ## tests.cfb.test_pbp module - -### tests.cfb.test_pbp.box_score(generated_data) - -### tests.cfb.test_pbp.dupe_fsu_play_base() - -### tests.cfb.test_pbp.dupe_iu_play_base(iu_play_base) - -### tests.cfb.test_pbp.generated_data() - -### tests.cfb.test_pbp.iu_play_base() - -### tests.cfb.test_pbp.iu_play_base_box(iu_play_base) - -### tests.cfb.test_pbp.test_adv_box_score(box_score) - -### tests.cfb.test_pbp.test_basic_pbp(generated_data) - -### tests.cfb.test_pbp.test_expected_turnovers(iu_play_base_box) - -### tests.cfb.test_pbp.test_fsu_play_dedupe(dupe_fsu_play_base) - -### tests.cfb.test_pbp.test_havoc_rate(box_score) - -### tests.cfb.test_pbp.test_iu_play_dedupe(dupe_iu_play_base) ## Module contents diff --git a/Sphinx-docs/_build/markdown/tests.mbb.md b/Sphinx-docs/_build/markdown/tests.mbb.md index 0e77f6e..3c51523 100755 --- a/Sphinx-docs/_build/markdown/tests.mbb.md +++ b/Sphinx-docs/_build/markdown/tests.mbb.md @@ -4,8 +4,4 @@ ## tests.mbb.test_pbp module - -### tests.mbb.test_pbp.generated_data() - -### tests.mbb.test_pbp.test_basic_pbp(generated_data) ## Module contents diff --git a/Sphinx-docs/_build/markdown/tests.md b/Sphinx-docs/_build/markdown/tests.md index 4b1b429..9017c83 100755 --- a/Sphinx-docs/_build/markdown/tests.md +++ b/Sphinx-docs/_build/markdown/tests.md @@ -28,3 +28,18 @@ ## Module contents + + +### class tests.SpecTestSuite() +Bases: `object` + + +#### assert_case(text, html) + +#### classmethod ignore_case(n) + +#### classmethod load_spec(spec_name) + +#### test_mixed_tab_space_in_list_item() + +### tests.parse_examples(text) diff --git a/docs/docs/mlb/index.md b/docs/docs/mlb/index.md index 986d8cc..1daccd6 100755 --- a/docs/docs/mlb/index.md +++ b/docs/docs/mlb/index.md @@ -5,7 +5,7 @@ ## sportsdataverse.mlb.mlb_loaders module -### sportsdataverse.mlb.mlb_loaders.mlbam_copyright_info(saveFile=False, returnFile=False) +### sportsdataverse.mlb.mlb_loaders.mlbam_copyright_info(saveFile=False, returnFile=False, \*\*kwargs) Displays the copyright info for the MLBAM API. Args: diff --git a/docs/docs/nba/index.md b/docs/docs/nba/index.md index 2ca6190..f1af879 100755 --- a/docs/docs/nba/index.md +++ b/docs/docs/nba/index.md @@ -196,7 +196,8 @@ espn_nba_schedule - look up the NBA schedule for a given date from ESPN Args: dates (int): Used to define different seasons. 2002 is the earliest available season. - season_type (int): season type, 1 for pre-season, 2 for regular season, 3 for post-season, 4 for all-star, 5 for off-season + season_type (int): season type, 1 for pre-season, 2 for regular season, 3 for post-season, + 4 for all-star, 5 for off-season limit (int): number of records to return, default: 500. return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. diff --git a/docs/docs/nfl/index.md b/docs/docs/nfl/index.md index c9a7e5a..b2421a8 100755 --- a/docs/docs/nfl/index.md +++ b/docs/docs/nfl/index.md @@ -223,7 +223,7 @@ Returns: pd.DataFrame: Pandas dataframe containing officials available. -### sportsdataverse.nfl.nfl_loaders.load_nfl_pbp(seasons: List[int]) +### sportsdataverse.nfl.nfl_loaders.load_nfl_pbp(seasons: List[int], return_as_pandas=True) Load NFL play by play data going back to 1999 Example: @@ -233,6 +233,7 @@ Example: Args: seasons (list): Used to define different seasons. 1999 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: diff --git a/docs/docs/wbb/index.md b/docs/docs/wbb/index.md index 90aab91..fdbb500 100755 --- a/docs/docs/wbb/index.md +++ b/docs/docs/wbb/index.md @@ -140,7 +140,8 @@ Raises: ### sportsdataverse.wbb.wbb_pbp.espn_wbb_pbp(game_id: int, raw=False, \*\*kwargs) -espn_wbb_pbp() - Pull the game by id. Data from API endpoints - womens-college-basketball/playbyplay, womens-college-basketball/summary +espn_wbb_pbp() - Pull the game by id. Data from API endpoints - womens-college-basketball/playbyplay, +womens-college-basketball/summary Args: @@ -150,10 +151,9 @@ raw (bool): If True, returns the raw json from the API endpoint. If False, retur Returns: - Dict: Dictionary of game data with keys - “gameId”, “plays”, “winprobability”, “boxscore”, “header”, “broadcasts”, - - “videos”, “playByPlaySource”, “standings”, “leaders”, “timeouts”, “pickcenter”, “againstTheSpread”, “odds”, “predictor”, - “espnWP”, “gameInfo”, “season” + Dict: Dictionary of game data with keys - “gameId”, “plays”, “winprobability”, “boxscore”, “header”, + “broadcasts”, “videos”, “playByPlaySource”, “standings”, “leaders”, “timeouts”, “pickcenter”, + “againstTheSpread”, “odds”, “predictor”,”espnWP”, “gameInfo”, “season” Example: diff --git a/sportsdataverse/cfb/cfb_game_rosters.py b/sportsdataverse/cfb/cfb_game_rosters.py index 7e47755..951511b 100644 --- a/sportsdataverse/cfb/cfb_game_rosters.py +++ b/sportsdataverse/cfb/cfb_game_rosters.py @@ -1,13 +1,9 @@ import pandas as pd import polars as pl -import numpy as np -import os -import json -import re -from typing import List, Callable, Iterator, Union, Optional, Dict from sportsdataverse.dl_utils import download, underscore -def espn_cfb_game_rosters(game_id: int, raw = False, return_as_pandas = True, **kwargs) -> pd.DataFrame: + +def espn_cfb_game_rosters(game_id: int, raw=False, return_as_pandas=True, **kwargs) -> pd.DataFrame: """espn_cfb_game_rosters() - Pull the game by id. Args: @@ -36,162 +32,147 @@ def espn_cfb_game_rosters(game_id: int, raw = False, return_as_pandas = True, ** Example: `cfb_df = sportsdataverse.cfb.espn_cfb_game_rosters(game_id=401256137)` """ - # play by play - pbp_txt = {} # summary endpoint for pickcenter array - summary_url = "https://sports.core.api.espn.com/v2/sports/football/leagues/college-football/events/{x}/competitions/{x}/competitors".format(x=game_id) + summary_url = "https://sports.core.api.espn.com/v2/sports/football/leagues/college-football/events/{x}/competitions/{x}/competitors".format( + x=game_id + ) summary_resp = download(summary_url, **kwargs) summary = summary_resp.json() items = helper_cfb_game_items(summary) - team_rosters = helper_cfb_roster_items(items = items, summary_url = summary_url, **kwargs) - team_rosters = team_rosters.join(items[['team_id', 'order', 'home_away', 'winner']], how = 'left', on = 'team_id') - teams_df = helper_cfb_team_items(items = items, **kwargs) - teams_rosters = team_rosters.join(teams_df, how = 'left',on = 'team_id') - athletes = helper_cfb_athlete_items(teams_rosters = team_rosters, **kwargs) - rosters = athletes.join(teams_rosters, how = 'left', left_on = 'athlete_id', right_on = 'player_id') - rosters = rosters.with_columns( - game_id = pl.lit(game_id).cast(pl.Int32) - ) + team_rosters = helper_cfb_roster_items(items=items, summary_url=summary_url, **kwargs) + team_rosters = team_rosters.join(items[["team_id", "order", "home_away", "winner"]], how="left", on="team_id") + teams_df = helper_cfb_team_items(items=items, **kwargs) + teams_rosters = team_rosters.join(teams_df, how="left", on="team_id") + athletes = helper_cfb_athlete_items(teams_rosters=team_rosters, **kwargs) + rosters = athletes.join(teams_rosters, how="left", left_on="athlete_id", right_on="player_id") + rosters = rosters.with_columns(game_id=pl.lit(game_id).cast(pl.Int32)) rosters.columns = [underscore(c) for c in rosters.columns] return rosters.to_pandas() if return_as_pandas else rosters + def helper_cfb_game_items(summary): - items = pl.from_pandas(pd.json_normalize(summary, record_path = "items", sep = '_')) + items = pl.from_pandas(pd.json_normalize(summary, record_path="items", sep="_")) items.columns = [col.replace("$ref", "href") for col in items.columns] items.columns = [underscore(c) for c in items.columns] - items = items.rename({ - "id": "team_id", - "uid": "team_uid", - "statistics_href": "team_statistics_href" - } - ) - items = items.with_columns( - team_id = pl.col("team_id").cast(pl.Int32) - ) + items = items.rename({"id": "team_id", "uid": "team_uid", "statistics_href": "team_statistics_href"}) + items = items.with_columns(team_id=pl.col("team_id").cast(pl.Int32)) return items + def helper_cfb_team_items(items, **kwargs): pop_cols = [ - '$ref', - 'record', - 'athletes', - 'venue', - 'groups', - 'ranks', - 'statistics', - 'leaders', - 'links', - 'notes', - 'againstTheSpreadRecords', - 'franchise', - 'events', - 'college', - 'awards', - 'coaches', - 'recruiting', - 'injuries' + "$ref", + "record", + "athletes", + "venue", + "groups", + "ranks", + "statistics", + "leaders", + "links", + "notes", + "againstTheSpreadRecords", + "franchise", + "events", + "college", + "awards", + "coaches", + "recruiting", + "injuries", ] teams_df = pl.DataFrame() - for x in items['team_href']: + for x in items["team_href"]: team = download(x, **kwargs).json() for k in pop_cols: team.pop(k, None) - team_row = pl.from_pandas(pd.json_normalize(team, sep = '_')) - teams_df = pl.concat([teams_df, team_row], how = 'vertical') + team_row = pl.from_pandas(pd.json_normalize(team, sep="_")) + teams_df = pl.concat([teams_df, team_row], how="vertical") print(teams_df.columns) teams_df.columns = [ - 'team_id', - 'team_guid', - 'team_uid', - 'team_slug', - 'team_location', - 'team_name', - 'team_nickname', - 'team_abbreviation', - 'team_display_name', - 'team_short_display_name', - 'team_color', - 'team_alternate_color', - 'is_active', - 'is_all_star', - 'logos', - 'team_alternate_ids_sdr' + "team_id", + "team_guid", + "team_uid", + "team_slug", + "team_location", + "team_name", + "team_nickname", + "team_abbreviation", + "team_display_name", + "team_short_display_name", + "team_color", + "team_alternate_color", + "is_active", + "is_all_star", + "logos", + "team_alternate_ids_sdr", ] - teams_df = teams_df.with_columns( - logo_href = pl.lit(""), - logo_dark_href = pl.lit("") - ) + teams_df = teams_df.with_columns(logo_href=pl.lit(""), logo_dark_href=pl.lit("")) - for row in range(len(teams_df['logos'])): - team = teams_df['logos'][row] - teams_df[row, 'logo_href'] = team[0]['href'] - teams_df[row, 'logo_dark_href'] = team[1]['href'] + for row in range(len(teams_df["logos"])): + team = teams_df["logos"][row] + teams_df[row, "logo_href"] = team[0]["href"] + teams_df[row, "logo_dark_href"] = team[1]["href"] - teams_df = teams_df.drop(['logos']) - teams_df = teams_df.with_columns( - team_id = pl.col("team_id").cast(pl.Int32) - ) + teams_df = teams_df.drop(["logos"]) + teams_df = teams_df.with_columns(team_id=pl.col("team_id").cast(pl.Int32)) return teams_df + def helper_cfb_roster_items(items, summary_url, **kwargs): - team_ids = list(items['team_id']) + team_ids = list(items["team_id"]) game_rosters = pl.DataFrame() for tm in team_ids: - team_roster_url = "{x}/{t}/roster".format(x = summary_url, t = tm) + team_roster_url = "{x}/{t}/roster".format(x=summary_url, t=tm) team_roster_resp = download(team_roster_url, **kwargs) - team_roster = pl.from_pandas(pd.json_normalize(team_roster_resp.json().get('entries',[]), sep = '_')) + team_roster = pl.from_pandas(pd.json_normalize(team_roster_resp.json().get("entries", []), sep="_")) team_roster.columns = [col.replace("$ref", "href") for col in team_roster.columns] team_roster.columns = [underscore(c) for c in team_roster.columns] - team_roster= team_roster.with_columns( - team_id = pl.lit(tm).cast(pl.Int32) - ) - game_rosters = pl.concat([game_rosters, team_roster], how = 'vertical') + team_roster = team_roster.with_columns(team_id=pl.lit(tm).cast(pl.Int32)) + game_rosters = pl.concat([game_rosters, team_roster], how="vertical") game_rosters = game_rosters.drop(["period", "for_player_id", "active"]) game_rosters = game_rosters.with_columns( - player_id = pl.col('player_id').cast(pl.Int64), - team_id = pl.col('team_id').cast(pl.Int32) + player_id=pl.col("player_id").cast(pl.Int64), team_id=pl.col("team_id").cast(pl.Int32) ) return game_rosters + def helper_cfb_athlete_items(teams_rosters, **kwargs): - athlete_hrefs = list(teams_rosters['athlete_href']) + athlete_hrefs = list(teams_rosters["athlete_href"]) game_athletes = pl.DataFrame() pop_cols = [ - 'links', - 'injuries', - 'teams', - 'team', - 'college', - 'proAthlete', - 'statistics', - 'notes', - 'eventLog', + "links", + "injuries", + "teams", + "team", + "college", + "proAthlete", + "statistics", + "notes", + "eventLog", "$ref", - "position" + "position", ] for athlete_href in athlete_hrefs: - athlete_res = download(athlete_href, **kwargs) athlete_resp = athlete_res.json() for k in pop_cols: athlete_resp.pop(k, None) - athlete = pl.from_pandas(pd.json_normalize(athlete_resp, sep='_')) + athlete = pl.from_pandas(pd.json_normalize(athlete_resp, sep="_")) athlete.columns = [col.replace("$ref", "href") for col in athlete.columns] athlete.columns = [underscore(c) for c in athlete.columns] - game_athletes = pl.concat([game_athletes, athlete], how = 'diagonal') - + game_athletes = pl.concat([game_athletes, athlete], how="diagonal") - game_athletes = game_athletes.rename({ - "id": "athlete_id", - "uid": "athlete_uid", - "guid": "athlete_guid", - "type": "athlete_type", - "display_name": "athlete_display_name" - }) - game_athletes = game_athletes.with_columns( - athlete_id = pl.col("athlete_id").cast(pl.Int64) + game_athletes = game_athletes.rename( + { + "id": "athlete_id", + "uid": "athlete_uid", + "guid": "athlete_guid", + "type": "athlete_type", + "display_name": "athlete_display_name", + } ) - return game_athletes \ No newline at end of file + game_athletes = game_athletes.with_columns(athlete_id=pl.col("athlete_id").cast(pl.Int64)) + return game_athletes diff --git a/sportsdataverse/cfb/cfb_loaders.py b/sportsdataverse/cfb/cfb_loaders.py index db25781..c47c67a 100755 --- a/sportsdataverse/cfb/cfb_loaders.py +++ b/sportsdataverse/cfb/cfb_loaders.py @@ -1,13 +1,20 @@ +from typing import List + import pandas as pd import polars as pl -import json from tqdm import tqdm -from typing import List, Callable, Iterator, Union, Optional -from sportsdataverse.config import CFB_BASE_URL, CFB_ROSTER_URL, CFB_TEAM_LOGO_URL, CFB_TEAM_SCHEDULE_URL, CFB_TEAM_INFO_URL + +from sportsdataverse.config import ( + CFB_BASE_URL, + CFB_ROSTER_URL, + CFB_TEAM_INFO_URL, + CFB_TEAM_LOGO_URL, + CFB_TEAM_SCHEDULE_URL, +) from sportsdataverse.errors import SeasonNotFoundError -from sportsdataverse.dl_utils import download -def load_cfb_pbp(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: + +def load_cfb_pbp(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: """Load college football play by play data going back to 2003 Example: @@ -30,10 +37,11 @@ def load_cfb_pbp(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: if int(i) < 2003: raise SeasonNotFoundError("season cannot be less than 2003") i_data = pl.read_parquet(CFB_BASE_URL.format(season=i), use_pyarrow=True, columns=None) - data = pl.concat([data, i_data], how = 'vertical') - return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data + data = pl.concat([data, i_data], how="vertical") + return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_cfb_schedule(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: + +def load_cfb_schedule(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: """Load college football schedule data Example: @@ -55,13 +63,13 @@ def load_cfb_schedule(seasons: List[int], return_as_pandas = True) -> pd.DataFra for i in tqdm(seasons): if int(i) < 2002: raise SeasonNotFoundError("season cannot be less than 2002") - i_data = pl.read_parquet(CFB_TEAM_SCHEDULE_URL.format(season = i), use_pyarrow=True, columns=None) - #data = data.append(i_data) - data = pl.concat([data, i_data], how = 'vertical') - return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data + i_data = pl.read_parquet(CFB_TEAM_SCHEDULE_URL.format(season=i), use_pyarrow=True, columns=None) + # data = data.append(i_data) + data = pl.concat([data, i_data], how="vertical") + return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_cfb_rosters(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: +def load_cfb_rosters(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: """Load roster data Example: @@ -83,11 +91,12 @@ def load_cfb_rosters(seasons: List[int], return_as_pandas = True) -> pd.DataFram for i in tqdm(seasons): if int(i) < 2004: raise SeasonNotFoundError("season cannot be less than 2004") - i_data = pl.read_parquet(CFB_ROSTER_URL.format(season = i), use_pyarrow=True, columns=None) - data = pl.concat([data, i_data], how = 'vertical') - return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data + i_data = pl.read_parquet(CFB_ROSTER_URL.format(season=i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how="vertical") + return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data + -def load_cfb_team_info(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: +def load_cfb_team_info(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: """Load college football team info Example: @@ -110,13 +119,14 @@ def load_cfb_team_info(seasons: List[int], return_as_pandas = True) -> pd.DataFr if int(i) < 2002: raise SeasonNotFoundError("season cannot be less than 2002") try: - i_data = pl.read_parquet(CFB_TEAM_INFO_URL.format(season = i), use_pyarrow=True, columns=None) + i_data = pl.read_parquet(CFB_TEAM_INFO_URL.format(season=i), use_pyarrow=True, columns=None) except Exception: - print(f'We don\'t seem to have data for the {i} season.') - data = pl.concat([data, i_data], how = 'vertical') - return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data + print(f"We don't seem to have data for the {i} season.") + data = pl.concat([data, i_data], how="vertical") + return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data + -def get_cfb_teams(return_as_pandas = True) -> pd.DataFrame: +def get_cfb_teams(return_as_pandas=True) -> pd.DataFrame: """Load college football team ID information and logos Example: @@ -129,5 +139,8 @@ def get_cfb_teams(return_as_pandas = True) -> pd.DataFrame: pd.DataFrame: Pandas dataframe containing teams available for the requested seasons. """ - return pl.read_parquet(CFB_TEAM_LOGO_URL, use_pyarrow=True, columns=None).to_pandas(use_pyarrow_extension_array = True) \ - if return_as_pandas else pl.read_parquet(CFB_TEAM_LOGO_URL, use_pyarrow=True, columns=None) + return ( + pl.read_parquet(CFB_TEAM_LOGO_URL, use_pyarrow=True, columns=None).to_pandas(use_pyarrow_extension_array=True) + if return_as_pandas + else pl.read_parquet(CFB_TEAM_LOGO_URL, use_pyarrow=True, columns=None) + ) diff --git a/sportsdataverse/cfb/cfb_schedule.py b/sportsdataverse/cfb/cfb_schedule.py index 14492ad..ffc3414 100755 --- a/sportsdataverse/cfb/cfb_schedule.py +++ b/sportsdataverse/cfb/cfb_schedule.py @@ -1,14 +1,14 @@ +import datetime + import pandas as pd import polars as pl -import json -import time -import datetime + from sportsdataverse.dl_utils import download, underscore -def espn_cfb_schedule(dates=None, week=None, season_type=None, groups=None, limit=500, - return_as_pandas = True, - **kwargs) -> pd.DataFrame: +def espn_cfb_schedule( + dates=None, week=None, season_type=None, groups=None, limit=500, return_as_pandas=True, **kwargs +) -> pd.DataFrame: """espn_cfb_schedule - look up the college football schedule for a given season Args: @@ -24,11 +24,11 @@ def espn_cfb_schedule(dates=None, week=None, season_type=None, groups=None, limi """ params = { - 'week': week, - 'dates': dates, - 'seasonType': season_type, - 'groups': groups if groups is not None else '80', - 'limit': limit + "week": week, + "dates": dates, + "seasonType": season_type, + "groups": groups if groups is not None else "80", + "limit": limit, } url = "http://site.api.espn.com/apis/site/v2/sports/football/college-football/scoreboard" @@ -36,44 +36,60 @@ def espn_cfb_schedule(dates=None, week=None, season_type=None, groups=None, limi ev = pl.DataFrame() events_txt = resp.json() - events = events_txt.get('events') + events = events_txt.get("events") if len(events) == 0: return pd.DataFrame() if return_as_pandas else pl.DataFrame() for event in events: - event.get('competitions')[0].get('competitors')[0].get('team').pop('links',None) - event.get('competitions')[0].get('competitors')[1].get('team').pop('links',None) - if event.get('competitions')[0].get('competitors')[0].get('homeAway') == 'home': - event = _extract_home_away(event, 0, 'home') - event = _extract_home_away(event, 1, 'away') + event.get("competitions")[0].get("competitors")[0].get("team").pop("links", None) + event.get("competitions")[0].get("competitors")[1].get("team").pop("links", None) + if event.get("competitions")[0].get("competitors")[0].get("homeAway") == "home": + event = _extract_home_away(event, 0, "home") + event = _extract_home_away(event, 1, "away") else: - event = _extract_home_away(event, 0, 'away') - event = _extract_home_away(event, 1, 'home') - del_keys = ['geoBroadcasts', 'headlines', 'series', 'situation', 'tickets', 'odds'] + event = _extract_home_away(event, 0, "away") + event = _extract_home_away(event, 1, "home") + del_keys = ["geoBroadcasts", "headlines", "series", "situation", "tickets", "odds"] for k in del_keys: - event.get('competitions')[0].pop(k, None) - event.get('competitions')[0]['notes_type'] = event.get('competitions')[0]['notes'][0].get("type") if len(event.get('competitions')[0]['notes']) > 0 else '' - event.get('competitions')[0]['notes_headline'] = event.get('competitions')[0]['notes'][0].get("headline").replace('"','') if len(event.get('competitions')[0]['notes']) > 0 else '' - event.get('competitions')[0]['broadcast_market'] = event.get('competitions')[0].get('broadcasts', [])[0].get('market', "") if len(event.get('competitions')[0].get('broadcasts')) > 0 else "" - event.get('competitions')[0]['broadcast_name'] = event.get('competitions')[0].get('broadcasts', [])[0].get('names', [])[0] if len(event.get('competitions')[0].get('broadcasts')) > 0 else "" - event.get('competitions')[0].pop('broadcasts', None) - event.get('competitions')[0].pop('notes', None) - event.get('competitions')[0].pop('competitors', None) - x = pl.from_pandas(pd.json_normalize(event.get('competitions')[0], sep = '_')) + event.get("competitions")[0].pop(k, None) + event.get("competitions")[0]["notes_type"] = ( + event.get("competitions")[0]["notes"][0].get("type") + if len(event.get("competitions")[0]["notes"]) > 0 + else "" + ) + event.get("competitions")[0]["notes_headline"] = ( + event.get("competitions")[0]["notes"][0].get("headline").replace('"', "") + if len(event.get("competitions")[0]["notes"]) > 0 + else "" + ) + event.get("competitions")[0]["broadcast_market"] = ( + event.get("competitions")[0].get("broadcasts", [])[0].get("market", "") + if len(event.get("competitions")[0].get("broadcasts")) > 0 + else "" + ) + event.get("competitions")[0]["broadcast_name"] = ( + event.get("competitions")[0].get("broadcasts", [])[0].get("names", [])[0] + if len(event.get("competitions")[0].get("broadcasts")) > 0 + else "" + ) + event.get("competitions")[0].pop("broadcasts", None) + event.get("competitions")[0].pop("notes", None) + event.get("competitions")[0].pop("competitors", None) + x = pl.from_pandas(pd.json_normalize(event.get("competitions")[0], sep="_")) x = x.with_columns( - game_id = (pl.col('id').cast(pl.Int32)), - season = (event.get('season').get('year')), - season_type = (event.get('season').get('type')), - week = (event.get('week', {}).get('number')), - home_linescores = pl.when(pl.col('status_type_description') == 'Postponed') - .then(None) - .otherwise(pl.col('home_linescores')), - away_linescores = pl.when(pl.col('status_type_description') == 'Postponed') - .then(None) - .otherwise(pl.col('away_linescores')), + game_id=(pl.col("id").cast(pl.Int32)), + season=(event.get("season").get("year")), + season_type=(event.get("season").get("type")), + week=(event.get("week", {}).get("number")), + home_linescores=pl.when(pl.col("status_type_description") == "Postponed") + .then(None) + .otherwise(pl.col("home_linescores")), + away_linescores=pl.when(pl.col("status_type_description") == "Postponed") + .then(None) + .otherwise(pl.col("away_linescores")), ) x = x[[s.name for s in x if s.null_count() != x.height]] - ev = pl.concat([ev, x], how = 'diagonal') + ev = pl.concat([ev, x], how="diagonal") ev.columns = [underscore(c) for c in ev.columns] @@ -82,46 +98,30 @@ def espn_cfb_schedule(dates=None, week=None, season_type=None, groups=None, limi # TODO Rename this here and in `espn_cfb_schedule` def _extract_home_away(event, arg1, arg2): - event['competitions'][0][arg2] = ( - event.get('competitions')[0].get('competitors')[arg1].get('team') - ) - event['competitions'][0][arg2]['score'] = ( - event.get('competitions')[0].get('competitors')[arg1].get('score') - ) - event['competitions'][0][arg2]['winner'] = ( - event.get('competitions')[0].get('competitors')[arg1].get('winner') + event["competitions"][0][arg2] = event.get("competitions")[0].get("competitors")[arg1].get("team") + event["competitions"][0][arg2]["score"] = event.get("competitions")[0].get("competitors")[arg1].get("score") + event["competitions"][0][arg2]["winner"] = event.get("competitions")[0].get("competitors")[arg1].get("winner") + # add winner back to main competitors if does not exist + event["competitions"][0]["competitors"][arg1]["winner"] = ( + event.get("competitions")[0].get("competitors")[arg1].get("winner", False) ) - ## add winner back to main competitors if does not exist - event['competitions'][0]['competitors'][arg1]['winner'] = ( - event.get('competitions')[0].get('competitors')[arg1].get('winner', False) + event["competitions"][0][arg2]["currentRank"] = ( + event.get("competitions")[0].get("competitors")[arg1].get("curatedRank", {}).get("current", 99) ) - event['competitions'][0][arg2]['currentRank'] = ( - event.get('competitions')[0] - .get('competitors')[arg1] - .get('curatedRank', {}) - .get('current', 99) + event["competitions"][0][arg2]["linescores"] = ( + event.get("competitions")[0].get("competitors")[arg1].get("linescores", [{"value": "N/A"}]) ) - event['competitions'][0][arg2]['linescores'] = ( - event.get('competitions')[0] - .get('competitors')[arg1] - .get('linescores', [{'value': 'N/A'}]) + # add linescores back to main competitors if does not exist + event["competitions"][0]["competitors"][arg1]["linescores"] = ( + event.get("competitions")[0].get("competitors")[arg1].get("linescores", []) ) - ## add linescores back to main competitors if does not exist - event['competitions'][0]['competitors'][arg1]['linescores'] = ( - event.get('competitions')[0] - .get('competitors')[arg1] - .get('linescores', []) - ) - event['competitions'][0][arg2]['records'] = ( - event.get('competitions')[0] - .get('competitors')[arg1] - .get('records', []) + event["competitions"][0][arg2]["records"] = ( + event.get("competitions")[0].get("competitors")[arg1].get("records", []) ) return event -def espn_cfb_calendar(season = None, groups = None, ondays = None, - return_as_pandas = True, **kwargs) -> pd.DataFrame: +def espn_cfb_calendar(season=None, groups=None, ondays=None, return_as_pandas=True, **kwargs) -> pd.DataFrame: """espn_cfb_calendar - look up the men's college football calendar for a given season Args: @@ -140,43 +140,39 @@ def espn_cfb_calendar(season = None, groups = None, ondays = None, full_schedule = __ondays_cfb_calendar(season, **kwargs) else: url = "http://site.api.espn.com/apis/site/v2/sports/football/college-football/scoreboard" - params = { - 'dates': season, - 'groups': groups if groups is not None else '80' - } - resp = download(url = url, params = params, **kwargs) + params = {"dates": season, "groups": groups if groups is not None else "80"} + resp = download(url=url, params=params, **kwargs) txt = resp.json() - txt = txt.get('leagues')[0].get('calendar') + txt = txt.get("leagues")[0].get("calendar") full_schedule = pl.DataFrame() for i in range(len(txt)): - if txt[i].get('entries', None) is not None: - reg = pd.json_normalize(data = txt[i], - record_path = 'entries', - meta=["label","value","startDate","endDate"], - meta_prefix='season_type_', - record_prefix='week_', - errors="ignore", - sep='_') - full_schedule = pl.concat([full_schedule, pl.from_pandas(reg)], how = 'vertical') - full_schedule = full_schedule.with_columns( - season = season - ) + if txt[i].get("entries", None) is not None: + reg = pd.json_normalize( + data=txt[i], + record_path="entries", + meta=["label", "value", "startDate", "endDate"], + meta_prefix="season_type_", + record_prefix="week_", + errors="ignore", + sep="_", + ) + full_schedule = pl.concat([full_schedule, pl.from_pandas(reg)], how="vertical") + full_schedule = full_schedule.with_columns(season=season) full_schedule.columns = [underscore(c) for c in full_schedule.columns] full_schedule = full_schedule.rename({"week_value": "week", "season_type_value": "season_type"}) return full_schedule.to_pandas() if return_as_pandas else full_schedule def __ondays_cfb_calendar(season, **kwargs): - url = f"https://sports.core.api.espn.com/v2/sports/football/leagues/college-football/seasons/{season}/types/2/calendar/ondays" resp = download(url=url, **kwargs) if resp is not None: - txt = resp.json().get('eventDate').get('dates') - result = pl.DataFrame(txt, schema=['dates']) - result = result.with_columns(dateURL = pl.col('dates').str.slice(0, 10)) + txt = resp.json().get("eventDate").get("dates") + result = pl.DataFrame(txt, schema=["dates"]) + result = result.with_columns(dateURL=pl.col("dates").str.slice(0, 10)) result = result.with_columns( url="http://site.api.espn.com/apis/site/v2/sports/football/college-football/scoreboard?dates=" - + pl.col('dateURL') + + pl.col("dateURL") ) return result diff --git a/sportsdataverse/cfb/cfb_teams.py b/sportsdataverse/cfb/cfb_teams.py index 322fde6..3191807 100755 --- a/sportsdataverse/cfb/cfb_teams.py +++ b/sportsdataverse/cfb/cfb_teams.py @@ -1,9 +1,9 @@ import pandas as pd import polars as pl -import json from sportsdataverse.dl_utils import download, underscore -def espn_cfb_teams(groups=None, return_as_pandas = True, **kwargs) -> pd.DataFrame: + +def espn_cfb_teams(groups=None, return_as_pandas=True, **kwargs) -> pd.DataFrame: """espn_cfb_teams - look up the college football teams Args: @@ -18,19 +18,16 @@ def espn_cfb_teams(groups=None, return_as_pandas = True, **kwargs) -> pd.DataFra """ url = "http://site.api.espn.com/apis/site/v2/sports/football/college-football/teams" - params = { - "groups": groups if groups is not None else "80", - "limit": 1000 - } - resp = download(url=url, params = params, **kwargs) + params = {"groups": groups if groups is not None else "80", "limit": 1000} + resp = download(url=url, params=params, **kwargs) if resp is not None: events_txt = resp.json() - teams = events_txt.get('sports')[0].get('leagues')[0].get('teams') - del_keys = ['record', 'links'] + teams = events_txt.get("sports")[0].get("leagues")[0].get("teams") + del_keys = ["record", "links"] for team in teams: for k in del_keys: - team.get('team').pop(k, None) - teams = pd.json_normalize(teams, sep='_') + team.get("team").pop(k, None) + teams = pd.json_normalize(teams, sep="_") teams.columns = [underscore(c) for c in teams.columns.tolist()] return teams if return_as_pandas else pl.from_pandas(teams) diff --git a/sportsdataverse/cfb/model_vars.py b/sportsdataverse/cfb/model_vars.py index 198f6a3..d415c4d 100755 --- a/sportsdataverse/cfb/model_vars.py +++ b/sportsdataverse/cfb/model_vars.py @@ -1,12 +1,4 @@ -ep_class_to_score_mapping = { - 0: 7, - 1: -7, - 2: 3, - 3: -3, - 4: 2, - 5: -2, - 6: 0 -} +ep_class_to_score_mapping = {0: 7, 1: -7, 2: 3, 3: -3, 4: 2, 5: -2, 6: 0} wp_start_touchback_columns = [ "start.pos_team_receives_2H_kickoff", @@ -21,7 +13,7 @@ "start.is_home", "start.posTeamTimeouts", "start.defPosTeamTimeouts", - "period" + "period", ] wp_start_columns = [ "start.pos_team_receives_2H_kickoff", @@ -36,7 +28,7 @@ "start.is_home", "start.posTeamTimeouts", "start.defPosTeamTimeouts", - "period" + "period", ] wp_end_columns = [ "end.pos_team_receives_2H_kickoff", @@ -51,7 +43,7 @@ "end.is_home", "end.posTeamTimeouts", "end.defPosTeamTimeouts", - "period" + "period", ] ep_start_touchback_columns = [ @@ -62,7 +54,7 @@ "down_2", "down_3", "down_4", - "pos_score_diff_start" + "pos_score_diff_start", ] ep_start_columns = [ "start.TimeSecsRem", @@ -72,7 +64,7 @@ "down_2", "down_3", "down_4", - "pos_score_diff_start" + "pos_score_diff_start", ] ep_end_columns = [ "end.TimeSecsRem", @@ -82,7 +74,7 @@ "down_2_end", "down_3_end", "down_4_end", - "pos_score_diff_end" + "pos_score_diff_end", ] ep_final_names = [ @@ -93,7 +85,7 @@ "down_2", "down_3", "down_4", - "pos_score_diff_start" + "pos_score_diff_start", ] wp_final_names = [ "pos_team_receives_2H_kickoff", @@ -108,10 +100,10 @@ "is_home", "pos_team_timeouts_rem_before", "def_pos_team_timeouts_rem_before", - "period" + "period", ] - #-------Play type vectors------------- +# -------Play type vectors------------- scores_vec = [ "Blocked Punt Touchdown", "Blocked Punt (Safety)", @@ -138,7 +130,7 @@ "Rushing Touchdown", "Field Goal Good", "Pass Reception Touchdown", - "Fumble Recovery (Own) Touchdown" + "Fumble Recovery (Own) Touchdown", ] defense_score_vec = [ "Blocked Punt Touchdown", @@ -147,13 +139,13 @@ "Punt Return Touchdown", "Fumble Recovery (Opponent) Touchdown", "Fumble Return Touchdown", - "Kickoff Touchdown", #<--- Kickoff Team recovers the return team fumble and scores + "Kickoff Touchdown", # <--- Kickoff Team recovers the return team fumble and scores "Defensive 2pt Conversion", "Safety", "Sack Touchdown", "Interception Return Touchdown", "Pass Interception Return Touchdown", - "Uncategorized Touchdown" + "Uncategorized Touchdown", ] turnover_vec = [ "Blocked Field Goal", @@ -177,7 +169,7 @@ "Punt Touchdown", "Punt Return Touchdown", "Sack Touchdown", - "Uncategorized Touchdown" + "Uncategorized Touchdown", ] normalplay = [ "Rush", @@ -186,23 +178,19 @@ "Pass Incompletion", "Pass Completion", "Sack", - "Fumble Recovery (Own)" -] -penalty = [ - 'Penalty', - 'Penalty (Kickoff)', - 'Penalty (Safety)' + "Fumble Recovery (Own)", ] +penalty = ["Penalty", "Penalty (Kickoff)", "Penalty (Safety)"] offense_score_vec = [ "Passing Touchdown", "Rushing Touchdown", "Field Goal Good", "Pass Reception Touchdown", "Fumble Recovery (Own) Touchdown", - "Punt Touchdown", #<--- Punting Team recovers the return team fumble and scores + "Punt Touchdown", # <--- Punting Team recovers the return team fumble and scores "Punt Team Fumble Recovery Touchdown", "Kickoff Return Touchdown", - "Kickoff Team Fumble Recovery Touchdown" + "Kickoff Team Fumble Recovery Touchdown", ] punt_vec = [ "Blocked Punt", @@ -214,7 +202,7 @@ "Punt Touchdown", "Punt Team Fumble Recovery", "Punt Team Fumble Recovery Touchdown", - "Punt Return Touchdown" + "Punt Return Touchdown", ] kickoff_vec = [ "Kickoff", @@ -224,7 +212,7 @@ "Kickoff Team Fumble Recovery", "Kickoff Team Fumble Recovery Touchdown", "Kickoff (Safety)", - "Penalty (Kickoff)" + "Penalty (Kickoff)", ] int_vec = [ "Interception", @@ -232,7 +220,7 @@ "Interception Return Touchdown", "Pass Interception", "Pass Interception Return", - "Pass Interception Return Touchdown" + "Pass Interception Return Touchdown", ] end_change_vec = [ "Blocked Field Goal", @@ -258,18 +246,8 @@ "Interception Return Touchdown", "Pass Interception Return", "Pass Interception Return Touchdown", - "Uncategorized Touchdown" -] -kickoff_turnovers = [ - "Kickoff Team Fumble Recovery", - "Kickoff Team Fumble Recovery Touchdown" + "Uncategorized Touchdown", ] +kickoff_turnovers = ["Kickoff Team Fumble Recovery", "Kickoff Team Fumble Recovery Touchdown"] -qbr_vars = [ - "qbr_epa", - "sack_epa", - "pass_epa", - "rush_epa", - "pen_epa", - "spread" -] \ No newline at end of file +qbr_vars = ["qbr_epa", "sack_epa", "pass_epa", "rush_epa", "pen_epa", "spread"] diff --git a/sportsdataverse/mbb/mbb_game_rosters.py b/sportsdataverse/mbb/mbb_game_rosters.py index c80c5cd..6242c4f 100755 --- a/sportsdataverse/mbb/mbb_game_rosters.py +++ b/sportsdataverse/mbb/mbb_game_rosters.py @@ -1,13 +1,10 @@ import pandas as pd import polars as pl -import numpy as np -import os -import json -import re -from typing import List, Callable, Iterator, Union, Optional, Dict + from sportsdataverse.dl_utils import download, underscore -def espn_mbb_game_rosters(game_id: int, raw = False, return_as_pandas = True, **kwargs) -> pd.DataFrame: + +def espn_mbb_game_rosters(game_id: int, raw=False, return_as_pandas=True, **kwargs) -> pd.DataFrame: """espn_mbb_game_rosters() - Pull the game by id. Args: @@ -36,158 +33,143 @@ def espn_mbb_game_rosters(game_id: int, raw = False, return_as_pandas = True, ** Example: `mbb_df = sportsdataverse.mbb.espn_mbb_game_rosters(game_id=401265031)` """ - # play by play - pbp_txt = {} # summary endpoint for pickcenter array - summary_url = "https://sports.core.api.espn.com/v2/sports/basketball/leagues/mens-college-basketball/events/{x}/competitions/{x}/competitors".format(x=game_id) + summary_url = "https://sports.core.api.espn.com/v2/sports/basketball/leagues/mens-college-basketball/events/{x}/competitions/{x}/competitors".format( + x=game_id + ) summary_resp = download(summary_url, **kwargs) summary = summary_resp.json() items = helper_mbb_game_items(summary) - team_rosters = helper_mbb_roster_items(items = items, summary_url = summary_url, **kwargs) - team_rosters = team_rosters.join(items[['team_id', 'order', 'home_away', 'winner']], how = 'left', on = 'team_id') - teams_df = helper_mbb_team_items(items = items, **kwargs) - teams_rosters = team_rosters.join(teams_df, how = 'left',on = 'team_id') - athletes = helper_mbb_athlete_items(teams_rosters = team_rosters, **kwargs) - rosters = athletes.join(teams_rosters, how = 'left', left_on = 'athlete_id', right_on = 'player_id') - rosters = rosters.with_columns( - game_id = pl.lit(game_id).cast(pl.Int32) - ) + team_rosters = helper_mbb_roster_items(items=items, summary_url=summary_url, **kwargs) + team_rosters = team_rosters.join(items[["team_id", "order", "home_away", "winner"]], how="left", on="team_id") + teams_df = helper_mbb_team_items(items=items, **kwargs) + teams_rosters = team_rosters.join(teams_df, how="left", on="team_id") + athletes = helper_mbb_athlete_items(teams_rosters=team_rosters, **kwargs) + rosters = athletes.join(teams_rosters, how="left", left_on="athlete_id", right_on="player_id") + rosters = rosters.with_columns(game_id=pl.lit(game_id).cast(pl.Int32)) rosters.columns = [underscore(c) for c in rosters.columns] return rosters.to_pandas() if return_as_pandas else rosters + def helper_mbb_game_items(summary): - items = pl.from_pandas(pd.json_normalize(summary, record_path = "items", sep = '_')) + items = pl.from_pandas(pd.json_normalize(summary, record_path="items", sep="_")) items.columns = [col.replace("$ref", "href") for col in items.columns] items.columns = [underscore(c) for c in items.columns] - items = items.rename({ - "id": "team_id", - "uid": "team_uid", - "statistics_href": "team_statistics_href" - } - ) - items = items.with_columns( - team_id = pl.col("team_id").cast(pl.Int32) - ) + items = items.rename({"id": "team_id", "uid": "team_uid", "statistics_href": "team_statistics_href"}) + items = items.with_columns(team_id=pl.col("team_id").cast(pl.Int32)) return items + def helper_mbb_team_items(items, **kwargs): pop_cols = [ - '$ref', - 'record', - 'athletes', - 'venue', - 'groups', - 'ranks', - 'statistics', - 'leaders', - 'links', - 'notes', - 'againstTheSpreadRecords', - 'franchise', - 'events', - 'college' + "$ref", + "record", + "athletes", + "venue", + "groups", + "ranks", + "statistics", + "leaders", + "links", + "notes", + "againstTheSpreadRecords", + "franchise", + "events", + "college", ] teams_df = pl.DataFrame() - for x in items['team_href']: + for x in items["team_href"]: team = download(x, **kwargs).json() for k in pop_cols: team.pop(k, None) - team_row = pl.from_pandas(pd.json_normalize(team, sep = '_')) - teams_df = pl.concat([teams_df, team_row], how = 'vertical') + team_row = pl.from_pandas(pd.json_normalize(team, sep="_")) + teams_df = pl.concat([teams_df, team_row], how="vertical") teams_df.columns = [ - 'team_id', - 'team_guid', - 'team_uid', - 'team_slug', - 'team_location', - 'team_name', - 'team_nickname', - 'team_abbreviation', - 'team_display_name', - 'team_short_display_name', - 'team_color', - 'team_alternate_color', - 'is_active', - 'is_all_star', - 'logos', - 'team_alternate_ids_sdr' + "team_id", + "team_guid", + "team_uid", + "team_slug", + "team_location", + "team_name", + "team_nickname", + "team_abbreviation", + "team_display_name", + "team_short_display_name", + "team_color", + "team_alternate_color", + "is_active", + "is_all_star", + "logos", + "team_alternate_ids_sdr", ] - teams_df = teams_df.with_columns( - logo_href = pl.lit(""), - logo_dark_href = pl.lit("") - ) + teams_df = teams_df.with_columns(logo_href=pl.lit(""), logo_dark_href=pl.lit("")) - for row in range(len(teams_df['logos'])): - team = teams_df['logos'][row] - teams_df[row, 'logo_href'] = team[0]['href'] - teams_df[row, 'logo_dark_href'] = team[1]['href'] + for row in range(len(teams_df["logos"])): + team = teams_df["logos"][row] + teams_df[row, "logo_href"] = team[0]["href"] + teams_df[row, "logo_dark_href"] = team[1]["href"] - teams_df = teams_df.drop(['logos']) - teams_df = teams_df.with_columns( - team_id = pl.col("team_id").cast(pl.Int32) - ) + teams_df = teams_df.drop(["logos"]) + teams_df = teams_df.with_columns(team_id=pl.col("team_id").cast(pl.Int32)) return teams_df + def helper_mbb_roster_items(items, summary_url, **kwargs): - team_ids = list(items['team_id']) + team_ids = list(items["team_id"]) game_rosters = pl.DataFrame() for tm in team_ids: - team_roster_url = "{x}/{t}/roster".format(x = summary_url, t = tm) + team_roster_url = "{x}/{t}/roster".format(x=summary_url, t=tm) team_roster_resp = download(team_roster_url, **kwargs) - team_roster = pl.from_pandas(pd.json_normalize(team_roster_resp.json().get('entries',[]), sep = '_')) + team_roster = pl.from_pandas(pd.json_normalize(team_roster_resp.json().get("entries", []), sep="_")) team_roster.columns = [col.replace("$ref", "href") for col in team_roster.columns] team_roster.columns = [underscore(c) for c in team_roster.columns] - team_roster= team_roster.with_columns( - team_id = pl.lit(tm).cast(pl.Int32) - ) - game_rosters = pl.concat([game_rosters, team_roster], how = 'vertical') + team_roster = team_roster.with_columns(team_id=pl.lit(tm).cast(pl.Int32)) + game_rosters = pl.concat([game_rosters, team_roster], how="vertical") game_rosters = game_rosters.drop(["period", "for_player_id", "active"]) game_rosters = game_rosters.with_columns( - player_id = pl.col('player_id').cast(pl.Int64), - team_id = pl.col('team_id').cast(pl.Int32) + player_id=pl.col("player_id").cast(pl.Int64), team_id=pl.col("team_id").cast(pl.Int32) ) return game_rosters + def helper_mbb_athlete_items(teams_rosters, **kwargs): - athlete_hrefs = list(teams_rosters['athlete_href']) + athlete_hrefs = list(teams_rosters["athlete_href"]) game_athletes = pl.DataFrame() pop_cols = [ - 'links', - 'injuries', - 'teams', - 'team', - 'college', - 'proAthlete', - 'statistics', - 'notes', - 'eventLog', + "links", + "injuries", + "teams", + "team", + "college", + "proAthlete", + "statistics", + "notes", + "eventLog", "$ref", - "position" + "position", ] for athlete_href in athlete_hrefs: - athlete_res = download(athlete_href, **kwargs) athlete_resp = athlete_res.json() for k in pop_cols: athlete_resp.pop(k, None) - athlete = pl.from_pandas(pd.json_normalize(athlete_resp, sep='_')) + athlete = pl.from_pandas(pd.json_normalize(athlete_resp, sep="_")) athlete.columns = [col.replace("$ref", "href") for col in athlete.columns] athlete.columns = [underscore(c) for c in athlete.columns] - game_athletes = pl.concat([game_athletes, athlete], how = 'diagonal') + game_athletes = pl.concat([game_athletes, athlete], how="diagonal") - - game_athletes = game_athletes.rename({ - "id": "athlete_id", - "uid": "athlete_uid", - "guid": "athlete_guid", - "type": "athlete_type", - "display_name": "athlete_display_name" - }) - game_athletes = game_athletes.with_columns( - athlete_id = pl.col("athlete_id").cast(pl.Int64) + game_athletes = game_athletes.rename( + { + "id": "athlete_id", + "uid": "athlete_uid", + "guid": "athlete_guid", + "type": "athlete_type", + "display_name": "athlete_display_name", + } ) - return game_athletes \ No newline at end of file + game_athletes = game_athletes.with_columns(athlete_id=pl.col("athlete_id").cast(pl.Int64)) + return game_athletes diff --git a/sportsdataverse/mbb/mbb_loaders.py b/sportsdataverse/mbb/mbb_loaders.py index 9fd043c..86c8cdf 100755 --- a/sportsdataverse/mbb/mbb_loaders.py +++ b/sportsdataverse/mbb/mbb_loaders.py @@ -1,13 +1,12 @@ import pandas as pd import polars as pl -import json from tqdm import tqdm -from typing import List, Callable, Iterator, Union, Optional +from typing import List from sportsdataverse.config import MBB_BASE_URL, MBB_TEAM_BOX_URL, MBB_PLAYER_BOX_URL, MBB_TEAM_SCHEDULE_URL from sportsdataverse.errors import SeasonNotFoundError -from sportsdataverse.dl_utils import download -def load_mbb_pbp(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: + +def load_mbb_pbp(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: """Load men's college basketball play by play data going back to 2002 Example: @@ -31,10 +30,11 @@ def load_mbb_pbp(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: if int(i) < 2002: raise SeasonNotFoundError("season cannot be less than 2002") i_data = pl.read_parquet(MBB_BASE_URL.format(season=i), use_pyarrow=True, columns=None) - data = pl.concat([data, i_data], how = 'vertical') - return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data + data = pl.concat([data, i_data], how="vertical") + return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data + -def load_mbb_team_boxscore(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: +def load_mbb_team_boxscore(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: """Load men's college basketball team boxscore data Example: @@ -57,12 +57,12 @@ def load_mbb_team_boxscore(seasons: List[int], return_as_pandas = True) -> pd.Da for i in tqdm(seasons): if int(i) < 2002: raise SeasonNotFoundError("season cannot be less than 2002") - i_data = pl.read_parquet(MBB_TEAM_BOX_URL.format(season = i), use_pyarrow=True, columns=None) - data = pl.concat([data, i_data], how = 'vertical') - return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data + i_data = pl.read_parquet(MBB_TEAM_BOX_URL.format(season=i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how="vertical") + return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_mbb_player_boxscore(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: +def load_mbb_player_boxscore(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: """Load men's college basketball player boxscore data Example: @@ -85,11 +85,12 @@ def load_mbb_player_boxscore(seasons: List[int], return_as_pandas = True) -> pd. for i in tqdm(seasons): if int(i) < 2002: raise SeasonNotFoundError("season cannot be less than 2002") - i_data = pl.read_parquet(MBB_PLAYER_BOX_URL.format(season = i), use_pyarrow=True, columns=None) - data = pl.concat([data, i_data], how = 'vertical') - return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data + i_data = pl.read_parquet(MBB_PLAYER_BOX_URL.format(season=i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how="vertical") + return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data + -def load_mbb_schedule(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: +def load_mbb_schedule(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: """Load men's college basketball schedule data Example: @@ -112,6 +113,6 @@ def load_mbb_schedule(seasons: List[int], return_as_pandas = True) -> pd.DataFra for i in tqdm(seasons): if int(i) < 2002: raise SeasonNotFoundError("season cannot be less than 2002") - i_data = pl.read_parquet(MBB_TEAM_SCHEDULE_URL.format(season = i), use_pyarrow=True, columns=None) - data = pl.concat([data, i_data], how = 'vertical') - return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data + i_data = pl.read_parquet(MBB_TEAM_SCHEDULE_URL.format(season=i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how="vertical") + return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data diff --git a/sportsdataverse/mbb/mbb_pbp.py b/sportsdataverse/mbb/mbb_pbp.py index 6a3f4db..4b7f81e 100755 --- a/sportsdataverse/mbb/mbb_pbp.py +++ b/sportsdataverse/mbb/mbb_pbp.py @@ -4,10 +4,11 @@ import os import json import re -from typing import List, Callable, Iterator, Union, Optional, Dict -from sportsdataverse.dl_utils import download, flatten_json_iterative, key_check +from typing import Dict +from sportsdataverse.dl_utils import download, flatten_json_iterative -def espn_mbb_pbp(game_id: int, raw = False, **kwargs) -> Dict: + +def espn_mbb_pbp(game_id: int, raw=False, **kwargs) -> Dict: """espn_mbb_pbp() - Pull the game by id. Data from API endpoints: `mens-college-basketball/playbyplay`, `mens-college-basketball/summary` Args: @@ -24,25 +25,47 @@ def espn_mbb_pbp(game_id: int, raw = False, **kwargs) -> Dict: """ # play by play - pbp_txt = {'timeouts': {}} + pbp_txt = {"timeouts": {}} # summary endpoint for pickcenter array - summary_url = f"http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/summary?event={game_id}" + summary_url = ( + f"http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/summary?event={game_id}" + ) summary_resp = download(summary_url, **kwargs) summary = summary_resp.json() incoming_keys_expected = [ - 'boxscore', 'format', 'gameInfo', 'leaders', 'broadcasts', - 'predictor', 'pickcenter', 'againstTheSpread', 'odds', 'winprobability', - 'header', 'plays', 'article', 'videos', 'standings', - 'teamInfo', 'espnWP', 'season', 'timeouts' + "boxscore", + "format", + "gameInfo", + "leaders", + "broadcasts", + "predictor", + "pickcenter", + "againstTheSpread", + "odds", + "winprobability", + "header", + "plays", + "article", + "videos", + "standings", + "teamInfo", + "espnWP", + "season", + "timeouts", ] dict_keys_expected = [ - 'plays', 'videos', 'broadcasts', 'pickcenter', 'againstTheSpread', - 'odds', 'winprobability', 'teamInfo', 'espnWP', 'leaders' - ] - array_keys_expected = [ - 'boxscore','format', 'gameInfo', - 'predictor', 'article', 'header', 'season', 'standings' + "plays", + "videos", + "broadcasts", + "pickcenter", + "againstTheSpread", + "odds", + "winprobability", + "teamInfo", + "espnWP", + "leaders", ] + array_keys_expected = ["boxscore", "format", "gameInfo", "predictor", "article", "header", "season", "standings"] if raw == True: # reorder keys in raw format, appending empty keys which are defined later to the end pbp_json = {} @@ -59,92 +82,95 @@ def espn_mbb_pbp(game_id: int, raw = False, **kwargs) -> Dict: else: pbp_txt[k] = {} if k in dict_keys_expected else [] - for k in ['news','shop']: - pbp_txt.pop(f'{k}', None) + for k in ["news", "shop"]: + pbp_txt.pop(f"{k}", None) return helper_mbb_pbp(game_id, pbp_txt) + def mbb_pbp_disk(game_id, path_to_json): with open(os.path.join(path_to_json, f"{game_id}.json")) as json_file: pbp_txt = json.load(json_file) return pbp_txt + def helper_mbb_pbp(game_id, pbp_txt): init = helper_mbb_pickcenter(pbp_txt) pbp_txt, init = helper_mbb_game_data(pbp_txt, init) - if "plays" in pbp_txt.keys() and pbp_txt.get('header').get('competitions')[0].get('playByPlaySource') != 'none': + if "plays" in pbp_txt.keys() and pbp_txt.get("header").get("competitions")[0].get("playByPlaySource") != "none": pbp_txt = helper_mbb_pbp_features(game_id, pbp_txt, init) else: - pbp_txt['plays'] = pl.DataFrame() - pbp_txt['timeouts'] = { - homeTeamId: {"1": [], "2": []}, - awayTeamId: {"1": [], "2": []}, + pbp_txt["plays"] = pl.DataFrame() + pbp_txt["timeouts"] = { + init["homeTeamId"]: {"1": [], "2": []}, + init["awayTeamId"]: {"1": [], "2": []}, } # pbp_txt['plays'] = pbp_txt['plays'].replace({np.nan: None}) return { "gameId": int(game_id), - "plays": pbp_txt['plays'].to_dicts(), - "winprobability": np.array(pbp_txt['winprobability']).tolist(), - "boxscore": pbp_txt['boxscore'], - "header": pbp_txt['header'], - "format": pbp_txt['format'], - "broadcasts": np.array(pbp_txt['broadcasts']).tolist(), - "videos": np.array(pbp_txt['videos']).tolist(), - "playByPlaySource": pbp_txt['playByPlaySource'], - "standings": pbp_txt['standings'], - "article": pbp_txt['article'], - "leaders": np.array(pbp_txt['leaders']).tolist(), - "timeouts": pbp_txt['timeouts'], - "pickcenter": np.array(pbp_txt['pickcenter']).tolist(), - "againstTheSpread": np.array(pbp_txt['againstTheSpread']).tolist(), - "odds": np.array(pbp_txt['odds']).tolist(), - "predictor": pbp_txt['predictor'], - "espnWP": np.array(pbp_txt['espnWP']).tolist(), - "gameInfo": pbp_txt['gameInfo'], - "teamInfo": np.array(pbp_txt['teamInfo']).tolist(), - "season": np.array(pbp_txt['season']).tolist(), + "plays": pbp_txt["plays"].to_dicts(), + "winprobability": np.array(pbp_txt["winprobability"]).tolist(), + "boxscore": pbp_txt["boxscore"], + "header": pbp_txt["header"], + "format": pbp_txt["format"], + "broadcasts": np.array(pbp_txt["broadcasts"]).tolist(), + "videos": np.array(pbp_txt["videos"]).tolist(), + "playByPlaySource": pbp_txt["playByPlaySource"], + "standings": pbp_txt["standings"], + "article": pbp_txt["article"], + "leaders": np.array(pbp_txt["leaders"]).tolist(), + "timeouts": pbp_txt["timeouts"], + "pickcenter": np.array(pbp_txt["pickcenter"]).tolist(), + "againstTheSpread": np.array(pbp_txt["againstTheSpread"]).tolist(), + "odds": np.array(pbp_txt["odds"]).tolist(), + "predictor": pbp_txt["predictor"], + "espnWP": np.array(pbp_txt["espnWP"]).tolist(), + "gameInfo": pbp_txt["gameInfo"], + "teamInfo": np.array(pbp_txt["teamInfo"]).tolist(), + "season": np.array(pbp_txt["season"]).tolist(), } + def helper_mbb_game_data(pbp_txt, init): - pbp_txt['timeouts'] = {} - pbp_txt['teamInfo'] = pbp_txt['header']['competitions'][0] - pbp_txt['season'] = pbp_txt['header']['season'] - pbp_txt['playByPlaySource'] = pbp_txt['header']['competitions'][0]['playByPlaySource'] - pbp_txt['boxscoreSource'] = pbp_txt['header']['competitions'][0]['boxscoreSource'] - pbp_txt['gameSpreadAvailable'] = init["gameSpreadAvailable"] - pbp_txt['gameSpread'] = init["gameSpread"] + pbp_txt["timeouts"] = {} + pbp_txt["teamInfo"] = pbp_txt["header"]["competitions"][0] + pbp_txt["season"] = pbp_txt["header"]["season"] + pbp_txt["playByPlaySource"] = pbp_txt["header"]["competitions"][0]["playByPlaySource"] + pbp_txt["boxscoreSource"] = pbp_txt["header"]["competitions"][0]["boxscoreSource"] + pbp_txt["gameSpreadAvailable"] = init["gameSpreadAvailable"] + pbp_txt["gameSpread"] = init["gameSpread"] pbp_txt["homeFavorite"] = init["homeFavorite"] pbp_txt["homeTeamSpread"] = np.where( init["homeFavorite"] == True, abs(init["gameSpread"]), -1 * abs(init["gameSpread"]) ) pbp_txt["overUnder"] = init["overUnder"] # Home and Away identification variables - if pbp_txt['header']['competitions'][0]['competitors'][0]['homeAway']=='home': - pbp_txt['header']['competitions'][0]['home'] = pbp_txt['header']['competitions'][0]['competitors'][0]['team'] - homeTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['id']) - homeTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['name']) - homeTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['location']) - homeTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['abbreviation']) + if pbp_txt["header"]["competitions"][0]["competitors"][0]["homeAway"] == "home": + pbp_txt["header"]["competitions"][0]["home"] = pbp_txt["header"]["competitions"][0]["competitors"][0]["team"] + homeTeamId = int(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["id"]) + homeTeamMascot = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["name"]) + homeTeamName = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["location"]) + homeTeamAbbrev = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["abbreviation"]) homeTeamNameAlt = re.sub("Stat(.+)", "St", homeTeamName) - pbp_txt['header']['competitions'][0]['away'] = pbp_txt['header']['competitions'][0]['competitors'][1]['team'] - awayTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['id']) - awayTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['name']) - awayTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['location']) - awayTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['abbreviation']) + pbp_txt["header"]["competitions"][0]["away"] = pbp_txt["header"]["competitions"][0]["competitors"][1]["team"] + awayTeamId = int(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["id"]) + awayTeamMascot = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["name"]) + awayTeamName = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["location"]) + awayTeamAbbrev = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["abbreviation"]) awayTeamNameAlt = re.sub("Stat(.+)", "St", awayTeamName) else: - pbp_txt['header']['competitions'][0]['away'] = pbp_txt['header']['competitions'][0]['competitors'][0]['team'] - awayTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['id']) - awayTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['name']) - awayTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['location']) - awayTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['abbreviation']) + pbp_txt["header"]["competitions"][0]["away"] = pbp_txt["header"]["competitions"][0]["competitors"][0]["team"] + awayTeamId = int(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["id"]) + awayTeamMascot = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["name"]) + awayTeamName = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["location"]) + awayTeamAbbrev = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["abbreviation"]) awayTeamNameAlt = re.sub("Stat(.+)", "St", awayTeamName) - pbp_txt['header']['competitions'][0]['home'] = pbp_txt['header']['competitions'][0]['competitors'][1]['team'] - homeTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['id']) - homeTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['name']) - homeTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['location']) - homeTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['abbreviation']) + pbp_txt["header"]["competitions"][0]["home"] = pbp_txt["header"]["competitions"][0]["competitors"][1]["team"] + homeTeamId = int(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["id"]) + homeTeamMascot = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["name"]) + homeTeamName = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["location"]) + homeTeamAbbrev = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["abbreviation"]) homeTeamNameAlt = re.sub("Stat(.+)", "St", homeTeamName) init["homeTeamId"] = homeTeamId init["homeTeamMascot"] = homeTeamMascot @@ -158,18 +184,19 @@ def helper_mbb_game_data(pbp_txt, init): init["awayTeamNameAlt"] = awayTeamNameAlt return pbp_txt, init + def helper_mbb_pickcenter(pbp_txt): # Spread definition - if len(pbp_txt.get("pickcenter",[])) > 1: - homeFavorite = pbp_txt.get("pickcenter",{})[0].get("homeTeamOdds",{}).get("favorite","") - if "spread" in pbp_txt.get("pickcenter",{})[1].keys(): - gameSpread = pbp_txt.get("pickcenter",{})[1].get("spread","") - overUnder = pbp_txt.get("pickcenter",{})[1].get("overUnder","") + if len(pbp_txt.get("pickcenter", [])) > 1: + homeFavorite = pbp_txt.get("pickcenter", {})[0].get("homeTeamOdds", {}).get("favorite", "") + if "spread" in pbp_txt.get("pickcenter", {})[1].keys(): + gameSpread = pbp_txt.get("pickcenter", {})[1].get("spread", "") + overUnder = pbp_txt.get("pickcenter", {})[1].get("overUnder", "") else: - gameSpread = pbp_txt.get("pickcenter",{})[0].get("spread","") - overUnder = pbp_txt.get("pickcenter",{})[0].get("overUnder","") + gameSpread = pbp_txt.get("pickcenter", {})[0].get("spread", "") + overUnder = pbp_txt.get("pickcenter", {})[0].get("overUnder", "") gameSpreadAvailable = True - # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") + # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") else: gameSpread = 2.5 overUnder = 140.5 @@ -180,91 +207,103 @@ def helper_mbb_pickcenter(pbp_txt): "gameSpread": gameSpread, "overUnder": overUnder, "homeFavorite": homeFavorite, - "gameSpreadAvailable": gameSpreadAvailable + "gameSpreadAvailable": gameSpreadAvailable, } + def helper_mbb_pbp_features(game_id, pbp_txt, init): - pbp_txt['plays_mod'] = [] - for play in pbp_txt['plays']: + pbp_txt["plays_mod"] = [] + for play in pbp_txt["plays"]: p = flatten_json_iterative(play) - pbp_txt['plays_mod'].append(p) - pbp_txt['plays'] = pl.from_pandas(pd.json_normalize(pbp_txt,'plays_mod')) - pbp_txt['plays'] = pbp_txt['plays'].with_columns( - game_id = pl.lit(game_id).cast(pl.Int32), - id = (pl.col('id').cast(pl.Int64)), - season = pl.lit(pbp_txt['header']['season']['year']), - seasonType = pl.lit(pbp_txt['header']['season']['type']), - homeTeamId = pl.lit(init["homeTeamId"]), - homeTeamName = pl.lit(init["homeTeamName"]), - homeTeamMascot = pl.lit(init["homeTeamMascot"]), - homeTeamAbbrev = pl.lit(init["homeTeamAbbrev"]), - homeTeamNameAlt = pl.lit(init["homeTeamNameAlt"]), - awayTeamId = pl.lit(init["awayTeamId"]), - awayTeamName = pl.lit(init["awayTeamName"]), - awayTeamMascot = pl.lit(init["awayTeamMascot"]), - awayTeamAbbrev = pl.lit(init["awayTeamAbbrev"]), - awayTeamNameAlt = pl.lit(init["awayTeamNameAlt"]), - gameSpread = pl.lit(init["gameSpread"]).abs(), - homeFavorite = pl.lit(init["homeFavorite"]), - gameSpreadAvailable = pl.lit(init["gameSpreadAvailable"]), - ).with_columns( - homeTeamSpread = pl.when(pl.col('homeFavorite') == True) - .then(pl.col('gameSpread')) - .otherwise(-1*pl.col('gameSpread')), - ).with_columns( - pl.col("period.number").cast(pl.Int32).alias("period.number"), - pl.col("period.number").cast(pl.Int32).alias("half"), - pl.col("clock.displayValue").alias("time"), - pl.col("clock.displayValue").str.split(':').list.to_struct(n_field_strategy="max_width").alias("clock.mm") - ).with_columns( - pl.col("clock.mm").struct.rename_fields(["clock.minutes", "clock.seconds"]) - ).unnest( - "clock.mm" - ).with_columns( - pl.col("clock.minutes").cast(pl.Int32), - pl.col("clock.seconds").cast(pl.Int32), - pl.when((pl.col("type.text") == "ShortTimeOut") - .and_( - pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamAbbrev"]).lower()) - .or_( - pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamAbbrev"]).lower()), - pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamName"]).lower()), - pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamMascot"]).lower()), - pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamNameAlt"]).lower()) + pbp_txt["plays_mod"].append(p) + pbp_txt["plays"] = pl.from_pandas(pd.json_normalize(pbp_txt, "plays_mod")) + pbp_txt["plays"] = ( + pbp_txt["plays"] + .with_columns( + game_id=pl.lit(game_id).cast(pl.Int32), + id=(pl.col("id").cast(pl.Int64)), + season=pl.lit(pbp_txt["header"]["season"]["year"]), + seasonType=pl.lit(pbp_txt["header"]["season"]["type"]), + homeTeamId=pl.lit(init["homeTeamId"]), + homeTeamName=pl.lit(init["homeTeamName"]), + homeTeamMascot=pl.lit(init["homeTeamMascot"]), + homeTeamAbbrev=pl.lit(init["homeTeamAbbrev"]), + homeTeamNameAlt=pl.lit(init["homeTeamNameAlt"]), + awayTeamId=pl.lit(init["awayTeamId"]), + awayTeamName=pl.lit(init["awayTeamName"]), + awayTeamMascot=pl.lit(init["awayTeamMascot"]), + awayTeamAbbrev=pl.lit(init["awayTeamAbbrev"]), + awayTeamNameAlt=pl.lit(init["awayTeamNameAlt"]), + gameSpread=pl.lit(init["gameSpread"]).abs(), + homeFavorite=pl.lit(init["homeFavorite"]), + gameSpreadAvailable=pl.lit(init["gameSpreadAvailable"]), + ) + .with_columns( + homeTeamSpread=pl.when(pl.col("homeFavorite") == True) + .then(pl.col("gameSpread")) + .otherwise(-1 * pl.col("gameSpread")), + ) + .with_columns( + pl.col("period.number").cast(pl.Int32).alias("period.number"), + pl.col("period.number").cast(pl.Int32).alias("half"), + pl.col("clock.displayValue").alias("time"), + pl.col("clock.displayValue").str.split(":").list.to_struct(n_field_strategy="max_width").alias("clock.mm"), + ) + .with_columns(pl.col("clock.mm").struct.rename_fields(["clock.minutes", "clock.seconds"])) + .unnest("clock.mm") + .with_columns( + pl.col("clock.minutes").cast(pl.Int32), + pl.col("clock.seconds").cast(pl.Int32), + pl.when( + (pl.col("type.text") == "ShortTimeOut").and_( + pl.col("text") + .str.to_lowercase() + .str.contains(str(init["homeTeamAbbrev"]).lower()) + .or_( + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamAbbrev"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamName"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamMascot"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamNameAlt"]).lower()), + ) ) - )) - .then(True) - .otherwise(False) - .alias("homeTimeoutCalled"), - pl.when((pl.col("type.text") == "ShortTimeOut") - .and_( - pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamAbbrev"]).lower()) - .or_( - pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamAbbrev"]).lower()), - pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamName"]).lower()), - pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamMascot"]).lower()), - pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamNameAlt"]).lower()) + ) + .then(True) + .otherwise(False) + .alias("homeTimeoutCalled"), + pl.when( + (pl.col("type.text") == "ShortTimeOut").and_( + pl.col("text") + .str.to_lowercase() + .str.contains(str(init["awayTeamAbbrev"]).lower()) + .or_( + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamAbbrev"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamName"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamMascot"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamNameAlt"]).lower()), + ) ) - )) - .then(True) - .otherwise(False) - .alias("awayTimeoutCalled"), - ).with_columns( - lag_period = pl.col("period.number").shift(1), - lead_period = pl.col("period.number").shift(-1), - lag_half = pl.col("half").shift(1), - lead_half = pl.col("half").shift(-1), - - ).with_columns( - - (60 * pl.col("clock.minutes") + pl.col("clock.seconds")).alias("start.period_seconds_remaining"), - pl.when(pl.col("period.number") == 1) + ) + .then(True) + .otherwise(False) + .alias("awayTimeoutCalled"), + ) + .with_columns( + lag_period=pl.col("period.number").shift(1), + lead_period=pl.col("period.number").shift(-1), + lag_half=pl.col("half").shift(1), + lead_half=pl.col("half").shift(-1), + ) + .with_columns( + (60 * pl.col("clock.minutes") + pl.col("clock.seconds")).alias("start.period_seconds_remaining"), + pl.when(pl.col("period.number") == 1) .then(1200 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) .otherwise(60 * pl.col("clock.minutes") + pl.col("clock.seconds")) .alias("start.game_seconds_remaining"), - ).with_columns( - pl.col("start.period_seconds_remaining").shift(-1).alias("end.period_seconds_remaining"), - pl.col("start.game_seconds_remaining").shift(-1).alias("end.game_seconds_remaining"), + ) + .with_columns( + pl.col("start.period_seconds_remaining").shift(-1).alias("end.period_seconds_remaining"), + pl.col("start.game_seconds_remaining").shift(-1).alias("end.game_seconds_remaining"), + ) ) pbp_txt["timeouts"] = { init["homeTeamId"]: {"1": [], "2": []}, @@ -272,65 +311,46 @@ def helper_mbb_pbp_features(game_id, pbp_txt, init): } pbp_txt["timeouts"][init["homeTeamId"]]["1"] = ( pbp_txt["plays"] - .filter( - (pl.col("homeTimeoutCalled") == True) - .and_(pl.col("period.number") <= 1) - ) + .filter((pl.col("homeTimeoutCalled") == True).and_(pl.col("period.number") <= 1)) .get_column("id") .to_list() ) pbp_txt["timeouts"][init["homeTeamId"]]["2"] = ( pbp_txt["plays"] - .filter( - (pl.col("homeTimeoutCalled") == True) - .and_(pl.col("period.number") > 1) - ) + .filter((pl.col("homeTimeoutCalled") == True).and_(pl.col("period.number") > 1)) .get_column("id") .to_list() ) pbp_txt["timeouts"][init["awayTeamId"]]["1"] = ( pbp_txt["plays"] - .filter( - (pl.col("awayTimeoutCalled") == True) - .and_(pl.col("period.number") <= 1) - ) + .filter((pl.col("awayTimeoutCalled") == True).and_(pl.col("period.number") <= 1)) .get_column("id") .to_list() ) pbp_txt["timeouts"][init["awayTeamId"]]["2"] = ( pbp_txt["plays"] - .filter( - (pl.col("awayTimeoutCalled") == True) - .and_(pl.col("period.number") > 1) - ) + .filter((pl.col("awayTimeoutCalled") == True).and_(pl.col("period.number") > 1)) .get_column("id") .to_list() ) - pbp_txt["plays"] = pbp_txt["plays"].with_row_count("game_play_number", 1) pbp_txt["plays"] = pbp_txt["plays"].with_columns( - pl.when((pl.col("game_play_number") == 1) - .or_((pl.col("lag_period") == 1) - .and_(pl.col("period.number") == 2))) - .then(1200) - .when((pl.col("lag_period") == 2) - .and_(pl.col("period.number") == 3)) - .then(300) - .otherwise(pl.col("end.period_seconds_remaining")) - .alias("end.period_seconds_remaining"), + pl.when((pl.col("game_play_number") == 1).or_((pl.col("lag_period") == 1).and_(pl.col("period.number") == 2))) + .then(1200) + .when((pl.col("lag_period") == 2).and_(pl.col("period.number") == 3)) + .then(300) + .otherwise(pl.col("end.period_seconds_remaining")) + .alias("end.period_seconds_remaining"), pl.when((pl.col("game_play_number") == 1)) - .then(2400) - .when((pl.col("lag_period") == 1) - .and_(pl.col("period.number") == 2)) - .then(1200) - .when((pl.col("lag_period") == (pl.col("period.number") - 1)) - .and_(pl.col("period.number") >= 3)) - .then(300) - .otherwise(pl.col("end.game_seconds_remaining")) - .alias("end.game_seconds_remaining"), + .then(2400) + .when((pl.col("lag_period") == 1).and_(pl.col("period.number") == 2)) + .then(1200) + .when((pl.col("lag_period") == (pl.col("period.number") - 1)).and_(pl.col("period.number") >= 3)) + .then(300) + .otherwise(pl.col("end.game_seconds_remaining")) + .alias("end.game_seconds_remaining"), ) return pbp_txt - diff --git a/sportsdataverse/mbb/mbb_schedule.py b/sportsdataverse/mbb/mbb_schedule.py index 921817b..4b2b8f5 100755 --- a/sportsdataverse/mbb/mbb_schedule.py +++ b/sportsdataverse/mbb/mbb_schedule.py @@ -1,14 +1,15 @@ +import datetime + import pandas as pd import polars as pl -import numpy as np -import json -import datetime -from typing import List, Callable, Iterator, Union, Optional -from sportsdataverse.errors import SeasonNotFoundError + from sportsdataverse.dl_utils import download, underscore +from sportsdataverse.errors import SeasonNotFoundError + -def espn_mbb_schedule(dates=None, groups=50, season_type=None, limit=500, - return_as_pandas=True, **kwargs) -> pd.DataFrame: +def espn_mbb_schedule( + dates=None, groups=50, season_type=None, limit=500, return_as_pandas=True, **kwargs +) -> pd.DataFrame: """espn_mbb_schedule - look up the men's college basketball scheduler for a given season Args: @@ -22,93 +23,99 @@ def espn_mbb_schedule(dates=None, groups=50, season_type=None, limit=500, """ url = "http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/scoreboard" params = { - 'dates': dates, - 'seasonType': season_type, - 'groups': groups if groups is not None else '50', - 'limit': limit + "dates": dates, + "seasonType": season_type, + "groups": groups if groups is not None else "50", + "limit": limit, } resp = download(url=url, params=params, **kwargs) ev = pl.DataFrame() events_txt = resp.json() - events = events_txt.get('events') + events = events_txt.get("events") if len(events) == 0: return pd.DataFrame() if return_as_pandas else pl.DataFrame() for event in events: - event.get('competitions')[0].get('competitors')[0].get('team').pop('links',None) - event.get('competitions')[0].get('competitors')[1].get('team').pop('links',None) - if event.get('competitions')[0].get('competitors')[0].get('homeAway') == 'home': - event = __extract_home_away(event, 0, 'home') - event = __extract_home_away(event, 1, 'away') + event.get("competitions")[0].get("competitors")[0].get("team").pop("links", None) + event.get("competitions")[0].get("competitors")[1].get("team").pop("links", None) + if event.get("competitions")[0].get("competitors")[0].get("homeAway") == "home": + event = __extract_home_away(event, 0, "home") + event = __extract_home_away(event, 1, "away") else: - event = __extract_home_away(event, 0, 'away') - event = __extract_home_away(event, 1, 'home') - del_keys = ['geoBroadcasts', 'headlines', 'series', 'situation', 'tickets', 'odds'] + event = __extract_home_away(event, 0, "away") + event = __extract_home_away(event, 1, "home") + del_keys = ["geoBroadcasts", "headlines", "series", "situation", "tickets", "odds"] for k in del_keys: - event.get('competitions')[0].pop(k, None) - event.get('competitions')[0]['notes_type'] = event.get('competitions')[0]['notes'][0].get("type") if len(event.get('competitions')[0]['notes']) > 0 else '' - event.get('competitions')[0]['notes_headline'] = event.get('competitions')[0]['notes'][0].get("headline").replace('"','') if len(event.get('competitions')[0]['notes']) > 0 else '' - event.get('competitions')[0]['broadcast_market'] = event.get('competitions')[0].get('broadcasts', [])[0].get('market', "") if len(event.get('competitions')[0].get('broadcasts')) > 0 else "" - event.get('competitions')[0]['broadcast_name'] = event.get('competitions')[0].get('broadcasts', [])[0].get('names', [])[0] if len(event.get('competitions')[0].get('broadcasts')) > 0 else "" - event.get('competitions')[0].pop('broadcasts', None) - event.get('competitions')[0].pop('notes', None) - event.get('competitions')[0].pop('competitors', None) - x = pl.from_pandas(pd.json_normalize(event.get('competitions')[0], sep = '_')) + event.get("competitions")[0].pop(k, None) + event.get("competitions")[0]["notes_type"] = ( + event.get("competitions")[0]["notes"][0].get("type") + if len(event.get("competitions")[0]["notes"]) > 0 + else "" + ) + event.get("competitions")[0]["notes_headline"] = ( + event.get("competitions")[0]["notes"][0].get("headline").replace('"', "") + if len(event.get("competitions")[0]["notes"]) > 0 + else "" + ) + event.get("competitions")[0]["broadcast_market"] = ( + event.get("competitions")[0].get("broadcasts", [])[0].get("market", "") + if len(event.get("competitions")[0].get("broadcasts")) > 0 + else "" + ) + event.get("competitions")[0]["broadcast_name"] = ( + event.get("competitions")[0].get("broadcasts", [])[0].get("names", [])[0] + if len(event.get("competitions")[0].get("broadcasts")) > 0 + else "" + ) + event.get("competitions")[0].pop("broadcasts", None) + event.get("competitions")[0].pop("notes", None) + event.get("competitions")[0].pop("competitors", None) + x = pl.from_pandas(pd.json_normalize(event.get("competitions")[0], sep="_")) x = x.with_columns( - game_id = (pl.col('id').cast(pl.Int32)), - season = (event.get('season').get('year')), - season_type = (event.get('season').get('type')), - home_linescores = pl.when(pl.col('status_type_description') == 'Postponed').then(None).otherwise(pl.col('home_linescores')), - away_linescores = pl.when(pl.col('status_type_description') == 'Postponed').then(None).otherwise(pl.col('away_linescores')), + game_id=(pl.col("id").cast(pl.Int32)), + season=(event.get("season").get("year")), + season_type=(event.get("season").get("type")), + home_linescores=pl.when(pl.col("status_type_description") == "Postponed") + .then(None) + .otherwise(pl.col("home_linescores")), + away_linescores=pl.when(pl.col("status_type_description") == "Postponed") + .then(None) + .otherwise(pl.col("away_linescores")), ) x = x[[s.name for s in x if s.null_count() != x.height]] - ev = pl.concat([ev, x], how = 'diagonal') + ev = pl.concat([ev, x], how="diagonal") ev.columns = [underscore(c) for c in ev.columns] return ev.to_pandas() if return_as_pandas else ev + def __extract_home_away(event, arg1, arg2): - event['competitions'][0][arg2] = ( - event.get('competitions')[0].get('competitors')[arg1].get('team') - ) - event['competitions'][0][arg2]['score'] = ( - event.get('competitions')[0].get('competitors')[arg1].get('score') - ) - event['competitions'][0][arg2]['winner'] = ( - event.get('competitions')[0].get('competitors')[arg1].get('winner') + event["competitions"][0][arg2] = event.get("competitions")[0].get("competitors")[arg1].get("team") + event["competitions"][0][arg2]["score"] = event.get("competitions")[0].get("competitors")[arg1].get("score") + event["competitions"][0][arg2]["winner"] = event.get("competitions")[0].get("competitors")[arg1].get("winner") + # add winner back to main competitors if does not exist + event["competitions"][0]["competitors"][arg1]["winner"] = ( + event.get("competitions")[0].get("competitors")[arg1].get("winner", False) ) - ## add winner back to main competitors if does not exist - event['competitions'][0]['competitors'][arg1]['winner'] = ( - event.get('competitions')[0].get('competitors')[arg1].get('winner', False) + event["competitions"][0][arg2]["currentRank"] = ( + event.get("competitions")[0].get("competitors")[arg1].get("curatedRank", {}).get("current", 99) ) - event['competitions'][0][arg2]['currentRank'] = ( - event.get('competitions')[0] - .get('competitors')[arg1] - .get('curatedRank', {}) - .get('current', 99) + event["competitions"][0][arg2]["linescores"] = ( + event.get("competitions")[0].get("competitors")[arg1].get("linescores", [{"value": "N/A"}]) ) - event['competitions'][0][arg2]['linescores'] = ( - event.get('competitions')[0] - .get('competitors')[arg1] - .get('linescores', [{'value': 'N/A'}]) + # add linescores back to main competitors if does not exist + event["competitions"][0]["competitors"][arg1]["linescores"] = ( + event.get("competitions")[0].get("competitors")[arg1].get("linescores", []) ) - ## add linescores back to main competitors if does not exist - event['competitions'][0]['competitors'][arg1]['linescores'] = ( - event.get('competitions')[0] - .get('competitors')[arg1] - .get('linescores', []) - ) - event['competitions'][0][arg2]['records'] = ( - event.get('competitions')[0] - .get('competitors')[arg1] - .get('records', []) + event["competitions"][0][arg2]["records"] = ( + event.get("competitions")[0].get("competitors")[arg1].get("records", []) ) return event -def espn_mbb_calendar(season=None, ondays=None, - return_as_pandas = True, **kwargs) -> pd.DataFrame: + +def espn_mbb_calendar(season=None, ondays=None, return_as_pandas=True, **kwargs) -> pd.DataFrame: """espn_mbb_calendar - look up the men's college basketball calendar for a given season Args: @@ -130,43 +137,45 @@ def espn_mbb_calendar(season=None, ondays=None, else: url = f"http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/scoreboard?dates={season}" resp = download(url=url, **kwargs) - txt = resp.json().get('leagues')[0].get('calendar') - datenum = list(map(lambda x: x[:10].replace("-",""),txt)) - date = list(map(lambda x: x[:10],txt)) - year = list(map(lambda x: x[:4],txt)) - month = list(map(lambda x: x[5:7],txt)) - day = list(map(lambda x: x[8:10],txt)) + txt = resp.json().get("leagues")[0].get("calendar") + datenum = list(map(lambda x: x[:10].replace("-", ""), txt)) + date = list(map(lambda x: x[:10], txt)) + year = list(map(lambda x: x[:4], txt)) + month = list(map(lambda x: x[5:7], txt)) + day = list(map(lambda x: x[8:10], txt)) data = { "season": season, - "datetime" : txt, - "date" : date, + "datetime": txt, + "date": date, "year": year, "month": month, "day": day, - "dateURL": datenum + "dateURL": datenum, } full_schedule = pl.DataFrame(data) full_schedule = full_schedule.with_columns( - url="http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/scoreboard?dates=" + - pl.col('dateURL') + url="http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/scoreboard?dates=" + + pl.col("dateURL") ) return full_schedule.to_pandas() if return_as_pandas else full_schedule + def __ondays_mbb_calendar(season, **kwargs): url = f"https://sports.core.api.espn.com/v2/sports/basketball/leagues/mens-college-basketball/seasons/{season}/types/2/calendar/ondays" resp = download(url=url, **kwargs) - txt = resp.json().get('eventDate').get('dates') - result = pl.DataFrame(txt, schema=['dates']) - result = result.with_columns(dateURL = pl.col('dates').str.slice(0, 10)) + txt = resp.json().get("eventDate").get("dates") + result = pl.DataFrame(txt, schema=["dates"]) + result = result.with_columns(dateURL=pl.col("dates").str.slice(0, 10)) result = result.with_columns( - url="http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/scoreboard?dates=" + - pl.col('dateURL') + url="http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/scoreboard?dates=" + + pl.col("dateURL") ) return result + def most_recent_mbb_season(): if datetime.datetime.now().month >= 10: return datetime.datetime.now().year + 1 else: - return datetime.datetime.now().year \ No newline at end of file + return datetime.datetime.now().year diff --git a/sportsdataverse/mbb/mbb_teams.py b/sportsdataverse/mbb/mbb_teams.py index e6e60ef..aade45d 100755 --- a/sportsdataverse/mbb/mbb_teams.py +++ b/sportsdataverse/mbb/mbb_teams.py @@ -1,8 +1,9 @@ import pandas as pd import polars as pl -import json + from sportsdataverse.dl_utils import download, underscore + def espn_mbb_teams(groups=None, return_as_pandas=True, **kwargs) -> pd.DataFrame: """espn_mbb_teams - look up the men's college basketball teams @@ -18,20 +19,16 @@ def espn_mbb_teams(groups=None, return_as_pandas=True, **kwargs) -> pd.DataFrame """ url = "http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/teams" - params = { - "groups": groups if groups is not None else "50", - "limit": 1000 - } - resp = download(url=url, params = params, **kwargs) + params = {"groups": groups if groups is not None else "50", "limit": 1000} + resp = download(url=url, params=params, **kwargs) if resp is not None: events_txt = resp.json() - teams = events_txt.get('sports')[0].get('leagues')[0].get('teams') - del_keys = ['record', 'links'] + teams = events_txt.get("sports")[0].get("leagues")[0].get("teams") + del_keys = ["record", "links"] for team in teams: for k in del_keys: - team.get('team').pop(k, None) - teams = pd.json_normalize(teams, sep='_') + team.get("team").pop(k, None) + teams = pd.json_normalize(teams, sep="_") teams.columns = [underscore(c) for c in teams.columns.tolist()] return teams if return_as_pandas else pl.from_pandas(teams) - diff --git a/sportsdataverse/mlb/mlb_loaders.py b/sportsdataverse/mlb/mlb_loaders.py index 73c0ca9..c481d2a 100755 --- a/sportsdataverse/mlb/mlb_loaders.py +++ b/sportsdataverse/mlb/mlb_loaders.py @@ -1,50 +1,39 @@ -import json -import pandas as pd -from pandas import json_normalize -from re import search -from datetime import datetime from sportsdataverse.dl_utils import download import os -def mlbam_copyright_info(saveFile=False,returnFile=False): - """ - Displays the copyright info for the MLBAM API. - - Args: - saveFile (boolean) = False - If saveFile is set to True, the copyright file generated is saved. - - returnFile (boolean) = False - If returnFile is set to True, the copyright file is returned. - - """ - url = "http://gdx.mlb.com/components/copyright.txt" - resp = download(url=url) - try: - if resp is not None: - l_string = str(resp, 'UTF-8') - with open("mlbam_copyright.txt","w+" ,encoding = "utf-8") as file: - file.writelines(str(l_string)) - - with open("mlbam_copyright.txt", "r" ,encoding = "utf-8") as file: - mlbam = file.read() - - if saveFile == False: - if os.path.exists("mlbam_copyright.txt"): - os.remove("mlbam_copyright.txt") - else: - pass - else: - pass - print(mlbam) - - if returnFile == True: - return mlbam - else: - pass - - - else: - print('Could not connect to the internet. \nPlease fix this issue to be able to use this package.') - except: - print('Could not connect to the internet. \nPlease fix this issue to be able to use this package.') \ No newline at end of file + + +def mlbam_copyright_info(saveFile=False, returnFile=False, **kwargs): + """ + Displays the copyright info for the MLBAM API. + + Args: + saveFile (boolean) = False + If saveFile is set to True, the copyright file generated is saved. + + returnFile (boolean) = False + If returnFile is set to True, the copyright file is returned. + + """ + url = "http://gdx.mlb.com/components/copyright.txt" + resp = download(url=url, **kwargs) + try: + if resp is not None: + l_string = str(resp.text(), "UTF-8") + with open("mlbam_copyright.txt", "w+", encoding="utf-8") as file: + file.writelines(l_string) + + with open("mlbam_copyright.txt", "r", encoding="utf-8") as file: + mlbam = file.read() + + if saveFile is False and os.path.exists("mlbam_copyright.txt"): + os.remove("mlbam_copyright.txt") + print(mlbam) + + if returnFile is True: + return mlbam + + else: + print("Could not connect to the internet. \nPlease fix this issue to be able to use this package.") + except Exception as e: + print("Could not connect to the internet. \nPlease fix this issue to be able to use this package.", e) diff --git a/sportsdataverse/nba/nba_game_rosters.py b/sportsdataverse/nba/nba_game_rosters.py index c9fc0b5..7cea3ed 100644 --- a/sportsdataverse/nba/nba_game_rosters.py +++ b/sportsdataverse/nba/nba_game_rosters.py @@ -1,13 +1,9 @@ import pandas as pd import polars as pl -import numpy as np -import os -import json -import re -from typing import List, Callable, Iterator, Union, Optional, Dict from sportsdataverse.dl_utils import download, underscore -def espn_nba_game_rosters(game_id: int, raw = False, return_as_pandas = True, **kwargs) -> pd.DataFrame: + +def espn_nba_game_rosters(game_id: int, raw=False, return_as_pandas=True, **kwargs) -> pd.DataFrame: """espn_nba_game_rosters() - Pull the game by id. Args: @@ -36,160 +32,145 @@ def espn_nba_game_rosters(game_id: int, raw = False, return_as_pandas = True, ** Example: `nba_df = sportsdataverse.nba.espn_nba_game_rosters(game_id=401307514)` """ - # play by play - pbp_txt = {} # summary endpoint for pickcenter array - summary_url = "https://sports.core.api.espn.com/v2/sports/basketball/leagues/nba/events/{x}/competitions/{x}/competitors".format(x=game_id) + summary_url = "https://sports.core.api.espn.com/v2/sports/basketball/leagues/nba/events/{x}/competitions/{x}/competitors".format( + x=game_id + ) summary_resp = download(summary_url, **kwargs) summary = summary_resp.json() items = helper_nba_game_items(summary) - team_rosters = helper_nba_roster_items(items = items, summary_url = summary_url, **kwargs) - team_rosters = team_rosters.join(items[['team_id', 'order', 'home_away', 'winner']], how = 'left', on = 'team_id') - teams_df = helper_nba_team_items(items = items, **kwargs) - teams_rosters = team_rosters.join(teams_df, how = 'left',on = 'team_id') - athletes = helper_nba_athlete_items(teams_rosters = team_rosters, **kwargs) - rosters = athletes.join(teams_rosters, how = 'left', left_on = 'athlete_id', right_on = 'player_id') - rosters = rosters.with_columns( - game_id = pl.lit(game_id).cast(pl.Int32) - ) + team_rosters = helper_nba_roster_items(items=items, summary_url=summary_url, **kwargs) + team_rosters = team_rosters.join(items[["team_id", "order", "home_away", "winner"]], how="left", on="team_id") + teams_df = helper_nba_team_items(items=items, **kwargs) + teams_rosters = team_rosters.join(teams_df, how="left", on="team_id") + athletes = helper_nba_athlete_items(teams_rosters=team_rosters, **kwargs) + rosters = athletes.join(teams_rosters, how="left", left_on="athlete_id", right_on="player_id") + rosters = rosters.with_columns(game_id=pl.lit(game_id).cast(pl.Int32)) rosters.columns = [underscore(c) for c in rosters.columns] return rosters.to_pandas() if return_as_pandas else rosters + def helper_nba_game_items(summary): - items = pl.from_pandas(pd.json_normalize(summary, record_path = "items", sep = '_')) + items = pl.from_pandas(pd.json_normalize(summary, record_path="items", sep="_")) items.columns = [col.replace("$ref", "href") for col in items.columns] items.columns = [underscore(c) for c in items.columns] - items = items.rename({ - "id": "team_id", - "uid": "team_uid", - "statistics_href": "team_statistics_href" - } - ) - items = items.with_columns( - team_id = pl.col("team_id").cast(pl.Int32) - ) + items = items.rename({"id": "team_id", "uid": "team_uid", "statistics_href": "team_statistics_href"}) + items = items.with_columns(team_id=pl.col("team_id").cast(pl.Int32)) return items + def helper_nba_team_items(items, **kwargs): pop_cols = [ - '$ref', - 'record', - 'athletes', - 'venue', - 'groups', - 'ranks', - 'statistics', - 'leaders', - 'links', - 'notes', - 'againstTheSpreadRecords', - 'franchise', - 'events', - 'college', - 'depthCharts', - 'transactions', - 'awards', - 'injuries', - 'coaches' + "$ref", + "record", + "athletes", + "venue", + "groups", + "ranks", + "statistics", + "leaders", + "links", + "notes", + "againstTheSpreadRecords", + "franchise", + "events", + "college", + "depthCharts", + "transactions", + "awards", + "injuries", + "coaches", ] teams_df = pl.DataFrame() - for x in items['team_href']: + for x in items["team_href"]: team = download(x, **kwargs).json() for k in pop_cols: team.pop(k, None) - team_row = pl.from_pandas(pd.json_normalize(team, sep = '_')) - teams_df = pl.concat([teams_df, team_row], how = 'vertical') + team_row = pl.from_pandas(pd.json_normalize(team, sep="_")) + teams_df = pl.concat([teams_df, team_row], how="vertical") teams_df.columns = [ - 'team_id', - 'team_guid', - 'team_uid', - 'team_slug', - 'team_location', - 'team_name', - 'team_abbreviation', - 'team_display_name', - 'team_short_display_name', - 'team_color', - 'team_alternate_color', - 'is_active', - 'is_all_star', - 'logos' + "team_id", + "team_guid", + "team_uid", + "team_slug", + "team_location", + "team_name", + "team_abbreviation", + "team_display_name", + "team_short_display_name", + "team_color", + "team_alternate_color", + "is_active", + "is_all_star", + "logos", ] - teams_df = teams_df.with_columns( - logo_href = pl.lit(""), - logo_dark_href = pl.lit("") - ) - for row in range(len(teams_df['logos'])): - team = teams_df['logos'][row] - teams_df[row, 'logo_href'] = team[0]['href'] - teams_df[row, 'logo_dark_href'] = team[1]['href'] - - teams_df = teams_df.drop(['logos']) - teams_df = teams_df.with_columns( - team_id = pl.col("team_id").cast(pl.Int32) - ) + teams_df = teams_df.with_columns(logo_href=pl.lit(""), logo_dark_href=pl.lit("")) + for row in range(len(teams_df["logos"])): + team = teams_df["logos"][row] + teams_df[row, "logo_href"] = team[0]["href"] + teams_df[row, "logo_dark_href"] = team[1]["href"] + + teams_df = teams_df.drop(["logos"]) + teams_df = teams_df.with_columns(team_id=pl.col("team_id").cast(pl.Int32)) return teams_df + def helper_nba_roster_items(items, summary_url, **kwargs): - team_ids = list(items['team_id']) + team_ids = list(items["team_id"]) game_rosters = pl.DataFrame() for tm in team_ids: - team_roster_url = "{x}/{t}/roster".format(x = summary_url, t = tm) + team_roster_url = "{x}/{t}/roster".format(x=summary_url, t=tm) team_roster_resp = download(team_roster_url, **kwargs) - team_roster = pl.from_pandas(pd.json_normalize(team_roster_resp.json().get('entries',[]), sep = '_')) + team_roster = pl.from_pandas(pd.json_normalize(team_roster_resp.json().get("entries", []), sep="_")) team_roster.columns = [col.replace("$ref", "href") for col in team_roster.columns] team_roster.columns = [underscore(c) for c in team_roster.columns] - team_roster= team_roster.with_columns( - team_id = pl.lit(tm).cast(pl.Int32) - ) - game_rosters = pl.concat([game_rosters, team_roster], how = 'vertical') + team_roster = team_roster.with_columns(team_id=pl.lit(tm).cast(pl.Int32)) + game_rosters = pl.concat([game_rosters, team_roster], how="vertical") game_rosters = game_rosters.drop(["period", "for_player_id", "active"]) game_rosters = game_rosters.with_columns( - player_id = pl.col('player_id').cast(pl.Int64), - team_id = pl.col('team_id').cast(pl.Int32) + player_id=pl.col("player_id").cast(pl.Int64), team_id=pl.col("team_id").cast(pl.Int32) ) return game_rosters + def helper_nba_athlete_items(teams_rosters, **kwargs): - athlete_hrefs = list(teams_rosters['athlete_href']) + athlete_hrefs = list(teams_rosters["athlete_href"]) game_athletes = pl.DataFrame() pop_cols = [ - 'links', - 'injuries', - 'teams', - 'team', - 'college', - 'proAthlete', - 'statistics', - 'notes', - 'eventLog', + "links", + "injuries", + "teams", + "team", + "college", + "proAthlete", + "statistics", + "notes", + "eventLog", "$ref", - "position" + "position", ] for athlete_href in athlete_hrefs: - athlete_res = download(athlete_href, **kwargs) athlete_resp = athlete_res.json() for k in pop_cols: athlete_resp.pop(k, None) - athlete = pl.from_pandas(pd.json_normalize(athlete_resp, sep='_')) + athlete = pl.from_pandas(pd.json_normalize(athlete_resp, sep="_")) athlete.columns = [col.replace("$ref", "href") for col in athlete.columns] athlete.columns = [underscore(c) for c in athlete.columns] - game_athletes = pl.concat([game_athletes, athlete], how = 'diagonal') + game_athletes = pl.concat([game_athletes, athlete], how="diagonal") - - game_athletes = game_athletes.rename({ - "id": "athlete_id", - "uid": "athlete_uid", - "guid": "athlete_guid", - "type": "athlete_type", - "display_name": "athlete_display_name" - }) - game_athletes = game_athletes.with_columns( - athlete_id = pl.col("athlete_id").cast(pl.Int64) + game_athletes = game_athletes.rename( + { + "id": "athlete_id", + "uid": "athlete_uid", + "guid": "athlete_guid", + "type": "athlete_type", + "display_name": "athlete_display_name", + } ) - return game_athletes \ No newline at end of file + game_athletes = game_athletes.with_columns(athlete_id=pl.col("athlete_id").cast(pl.Int64)) + return game_athletes diff --git a/sportsdataverse/nba/nba_loaders.py b/sportsdataverse/nba/nba_loaders.py index 1f7cfc6..48c3fd5 100755 --- a/sportsdataverse/nba/nba_loaders.py +++ b/sportsdataverse/nba/nba_loaders.py @@ -1,13 +1,12 @@ import pandas as pd import polars as pl -import json from tqdm import tqdm -from typing import List, Callable, Iterator, Union, Optional +from typing import List from sportsdataverse.config import NBA_BASE_URL, NBA_TEAM_BOX_URL, NBA_PLAYER_BOX_URL, NBA_TEAM_SCHEDULE_URL from sportsdataverse.errors import SeasonNotFoundError -from sportsdataverse.dl_utils import download -def load_nba_pbp(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: + +def load_nba_pbp(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: """Load NBA play by play data going back to 2002 Example: @@ -31,10 +30,11 @@ def load_nba_pbp(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: if int(i) < 2002: raise SeasonNotFoundError("season cannot be less than 2002") i_data = pl.read_parquet(NBA_BASE_URL.format(season=i), use_pyarrow=True, columns=None) - data = pl.concat([data, i_data], how = 'vertical') - return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data + data = pl.concat([data, i_data], how="vertical") + return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data + -def load_nba_team_boxscore(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: +def load_nba_team_boxscore(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: """Load NBA team boxscore data Example: @@ -57,11 +57,12 @@ def load_nba_team_boxscore(seasons: List[int], return_as_pandas = True) -> pd.Da for i in tqdm(seasons): if int(i) < 2002: raise SeasonNotFoundError("season cannot be less than 2002") - i_data = pl.read_parquet(NBA_TEAM_BOX_URL.format(season = i), use_pyarrow=True, columns=None) - data = pl.concat([data, i_data], how = 'vertical') - return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data + i_data = pl.read_parquet(NBA_TEAM_BOX_URL.format(season=i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how="vertical") + return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_nba_player_boxscore(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: + +def load_nba_player_boxscore(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: """Load NBA player boxscore data Example: @@ -84,11 +85,12 @@ def load_nba_player_boxscore(seasons: List[int], return_as_pandas = True) -> pd. for i in tqdm(seasons): if int(i) < 2002: raise SeasonNotFoundError("season cannot be less than 2002") - i_data = pl.read_parquet(NBA_PLAYER_BOX_URL.format(season = i), use_pyarrow=True, columns=None) - data = pl.concat([data, i_data], how = 'vertical') - return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data + i_data = pl.read_parquet(NBA_PLAYER_BOX_URL.format(season=i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how="vertical") + return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data + -def load_nba_schedule(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: +def load_nba_schedule(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: """Load NBA schedule data Example: @@ -111,6 +113,6 @@ def load_nba_schedule(seasons: List[int], return_as_pandas = True) -> pd.DataFra for i in tqdm(seasons): if int(i) < 2002: raise SeasonNotFoundError("season cannot be less than 2002") - i_data = pl.read_parquet(NBA_TEAM_SCHEDULE_URL.format(season = i), use_pyarrow=True, columns=None) - data = pl.concat([data, i_data], how = 'vertical') - return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data + i_data = pl.read_parquet(NBA_TEAM_SCHEDULE_URL.format(season=i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how="vertical") + return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data diff --git a/sportsdataverse/nba/nba_pbp.py b/sportsdataverse/nba/nba_pbp.py index aebce36..eebee04 100755 --- a/sportsdataverse/nba/nba_pbp.py +++ b/sportsdataverse/nba/nba_pbp.py @@ -4,47 +4,68 @@ import os import json import re -from typing import List, Callable, Iterator, Union, Optional, Dict -from sportsdataverse.dl_utils import download, flatten_json_iterative, key_check +from typing import Dict +from sportsdataverse.dl_utils import download, flatten_json_iterative -def espn_nba_pbp(game_id: int, raw = False, **kwargs) -> Dict: + +def espn_nba_pbp(game_id: int, raw=False, **kwargs) -> Dict: """espn_nba_pbp() - Pull the game by id - Data from API endpoints - `nba/playbyplay`, `nba/summary` - Args: - game_id (int): Unique game_id, can be obtained from nba_schedule(). - raw (bool): If True, returns the raw json from the API endpoint. If False, returns a cleaned dictionary of datasets. + Args: + game_id (int): Unique game_id, can be obtained from nba_schedule(). + raw (bool): If True, returns the raw json from the API endpoint. If False, returns a cleaned dictionary of datasets. - Returns: - Dict: Dictionary of game data with keys - "gameId", "plays", "winprobability", "boxscore", "header", "broadcasts", - "videos", "playByPlaySource", "standings", "leaders", "seasonseries", "timeouts", "pickcenter", "againstTheSpread", - "odds", "predictor", "espnWP", "gameInfo", "season" + Returns: + Dict: Dictionary of game data with keys - "gameId", "plays", "winprobability", "boxscore", "header", "broadcasts", + "videos", "playByPlaySource", "standings", "leaders", "seasonseries", "timeouts", "pickcenter", "againstTheSpread", + "odds", "predictor", "espnWP", "gameInfo", "season" - Example: - `nba_df = sportsdataverse.nba.espn_nba_pbp(game_id=401307514)` + Example: + `nba_df = sportsdataverse.nba.espn_nba_pbp(game_id=401307514)` """ # play by play - pbp_txt = {'timeouts': {}} + pbp_txt = {"timeouts": {}} # summary endpoint for pickcenter array summary_url = f"http://site.api.espn.com/apis/site/v2/sports/basketball/nba/summary?event={game_id}" summary_resp = download(summary_url, **kwargs) summary = summary_resp.json() incoming_keys_expected = [ - 'boxscore', 'format', 'gameInfo', 'leaders', 'seasonseries', - 'broadcasts', 'predictor', 'pickcenter', 'againstTheSpread', - 'odds', 'winprobability', 'header', 'plays', - 'article', 'videos', 'standings', - 'teamInfo', 'espnWP', 'season', 'timeouts' - ] - dict_keys_expected = [ - 'boxscore','format', 'gameInfo', 'predictor', - 'article', 'header', 'season', 'standings' + "boxscore", + "format", + "gameInfo", + "leaders", + "seasonseries", + "broadcasts", + "predictor", + "pickcenter", + "againstTheSpread", + "odds", + "winprobability", + "header", + "plays", + "article", + "videos", + "standings", + "teamInfo", + "espnWP", + "season", + "timeouts", ] + dict_keys_expected = ["boxscore", "format", "gameInfo", "predictor", "article", "header", "season", "standings"] array_keys_expected = [ - 'plays', 'seasonseries', 'videos', 'broadcasts', 'pickcenter', - 'againstTheSpread', 'odds', 'winprobability', 'teamInfo', 'espnWP', - 'leaders' + "plays", + "seasonseries", + "videos", + "broadcasts", + "pickcenter", + "againstTheSpread", + "odds", + "winprobability", + "teamInfo", + "espnWP", + "leaders", ] if raw == True: # reorder keys in raw format, appending empty keys which are defined later to the end @@ -62,91 +83,94 @@ def espn_nba_pbp(game_id: int, raw = False, **kwargs) -> Dict: else: pbp_txt[k] = {} if k in dict_keys_expected else [] - for k in ['news','shop']: - pbp_txt.pop(f'{k}', None) + for k in ["news", "shop"]: + pbp_txt.pop(f"{k}", None) return helper_nba_pbp(game_id, pbp_txt) + def nba_pbp_disk(game_id, path_to_json): with open(os.path.join(path_to_json, f"{game_id}.json")) as json_file: pbp_txt = json.load(json_file) return pbp_txt + def helper_nba_pbp(game_id, pbp_txt): init = helper_nba_pickcenter(pbp_txt) pbp_txt, init = helper_nba_game_data(pbp_txt, init) - if "plays" in pbp_txt.keys() and pbp_txt.get('header').get('competitions')[0].get('playByPlaySource') != 'none': + if "plays" in pbp_txt.keys() and pbp_txt.get("header").get("competitions")[0].get("playByPlaySource") != "none": pbp_txt = helper_nba_pbp_features(game_id, pbp_txt, init) else: - pbp_txt['plays'] = pl.DataFrame() - pbp_txt['timeouts'] = { - homeTeamId: {"1": [], "2": []}, - awayTeamId: {"1": [], "2": []}, + pbp_txt["plays"] = pl.DataFrame() + pbp_txt["timeouts"] = { + init["homeTeamId"]: {"1": [], "2": []}, + init["awayTeamId"]: {"1": [], "2": []}, } return { "gameId": game_id, - "plays": pbp_txt['plays'].to_dicts(), - "winprobability" : np.array(pbp_txt['winprobability']).tolist(), - "boxscore" : pbp_txt['boxscore'], - "header" : pbp_txt['header'], - "format": pbp_txt['format'], - "broadcasts" : np.array(pbp_txt['broadcasts']).tolist(), - "videos" : np.array(pbp_txt['videos']).tolist(), - "playByPlaySource": pbp_txt['playByPlaySource'], - "standings" : pbp_txt['standings'], - "article" : pbp_txt['article'], - "leaders" : np.array(pbp_txt['leaders']).tolist(), - "seasonseries" : np.array(pbp_txt['seasonseries']).tolist(), - "timeouts" : pbp_txt['timeouts'], - "pickcenter" : np.array(pbp_txt['pickcenter']).tolist(), - "againstTheSpread" : np.array(pbp_txt['againstTheSpread']).tolist(), - "odds" : np.array(pbp_txt['odds']).tolist(), - "predictor" : pbp_txt['predictor'], - "espnWP" : np.array(pbp_txt['espnWP']).tolist(), - "gameInfo" : pbp_txt['gameInfo'], - "teamInfo" : np.array(pbp_txt['teamInfo']).tolist(), - "season" : np.array(pbp_txt['season']).tolist() + "plays": pbp_txt["plays"].to_dicts(), + "winprobability": np.array(pbp_txt["winprobability"]).tolist(), + "boxscore": pbp_txt["boxscore"], + "header": pbp_txt["header"], + "format": pbp_txt["format"], + "broadcasts": np.array(pbp_txt["broadcasts"]).tolist(), + "videos": np.array(pbp_txt["videos"]).tolist(), + "playByPlaySource": pbp_txt["playByPlaySource"], + "standings": pbp_txt["standings"], + "article": pbp_txt["article"], + "leaders": np.array(pbp_txt["leaders"]).tolist(), + "seasonseries": np.array(pbp_txt["seasonseries"]).tolist(), + "timeouts": pbp_txt["timeouts"], + "pickcenter": np.array(pbp_txt["pickcenter"]).tolist(), + "againstTheSpread": np.array(pbp_txt["againstTheSpread"]).tolist(), + "odds": np.array(pbp_txt["odds"]).tolist(), + "predictor": pbp_txt["predictor"], + "espnWP": np.array(pbp_txt["espnWP"]).tolist(), + "gameInfo": pbp_txt["gameInfo"], + "teamInfo": np.array(pbp_txt["teamInfo"]).tolist(), + "season": np.array(pbp_txt["season"]).tolist(), } + def helper_nba_game_data(pbp_txt, init): - pbp_txt['timeouts'] = {} - pbp_txt['teamInfo'] = pbp_txt['header']['competitions'][0] - pbp_txt['season'] = pbp_txt['header']['season'] - pbp_txt['playByPlaySource'] = pbp_txt['header']['competitions'][0]['playByPlaySource'] - pbp_txt['boxscoreSource'] = pbp_txt['header']['competitions'][0]['boxscoreSource'] - pbp_txt['gameSpreadAvailable'] = init["gameSpreadAvailable"] - pbp_txt['gameSpread'] = init["gameSpread"] + pbp_txt["timeouts"] = {} + pbp_txt["teamInfo"] = pbp_txt["header"]["competitions"][0] + pbp_txt["season"] = pbp_txt["header"]["season"] + pbp_txt["playByPlaySource"] = pbp_txt["header"]["competitions"][0]["playByPlaySource"] + pbp_txt["boxscoreSource"] = pbp_txt["header"]["competitions"][0]["boxscoreSource"] + pbp_txt["gameSpreadAvailable"] = init["gameSpreadAvailable"] + pbp_txt["gameSpread"] = init["gameSpread"] pbp_txt["homeFavorite"] = init["homeFavorite"] pbp_txt["homeTeamSpread"] = np.where( init["homeFavorite"] == True, abs(init["gameSpread"]), -1 * abs(init["gameSpread"]) ) pbp_txt["overUnder"] = init["overUnder"] # Home and Away identification variables - if pbp_txt['header']['competitions'][0]['competitors'][0]['homeAway']=='home': - pbp_txt['header']['competitions'][0]['home'] = pbp_txt['header']['competitions'][0]['competitors'][0]['team'] - homeTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['id']) - homeTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['name']) - homeTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['location']) - homeTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['abbreviation']) + if pbp_txt["header"]["competitions"][0]["competitors"][0]["homeAway"] == "home": + pbp_txt["header"]["competitions"][0]["home"] = pbp_txt["header"]["competitions"][0]["competitors"][0]["team"] + homeTeamId = int(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["id"]) + homeTeamMascot = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["name"]) + homeTeamName = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["location"]) + homeTeamAbbrev = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["abbreviation"]) homeTeamNameAlt = re.sub("Stat(.+)", "St", homeTeamName) - pbp_txt['header']['competitions'][0]['away'] = pbp_txt['header']['competitions'][0]['competitors'][1]['team'] - awayTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['id']) - awayTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['name']) - awayTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['location']) - awayTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['abbreviation']) + pbp_txt["header"]["competitions"][0]["away"] = pbp_txt["header"]["competitions"][0]["competitors"][1]["team"] + awayTeamId = int(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["id"]) + awayTeamMascot = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["name"]) + awayTeamName = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["location"]) + awayTeamAbbrev = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["abbreviation"]) awayTeamNameAlt = re.sub("Stat(.+)", "St", awayTeamName) else: - pbp_txt['header']['competitions'][0]['away'] = pbp_txt['header']['competitions'][0]['competitors'][0]['team'] - awayTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['id']) - awayTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['name']) - awayTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['location']) - awayTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['abbreviation']) + pbp_txt["header"]["competitions"][0]["away"] = pbp_txt["header"]["competitions"][0]["competitors"][0]["team"] + awayTeamId = int(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["id"]) + awayTeamMascot = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["name"]) + awayTeamName = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["location"]) + awayTeamAbbrev = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["abbreviation"]) awayTeamNameAlt = re.sub("Stat(.+)", "St", awayTeamName) - pbp_txt['header']['competitions'][0]['home'] = pbp_txt['header']['competitions'][0]['competitors'][1]['team'] - homeTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['id']) - homeTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['name']) - homeTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['location']) - homeTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['abbreviation']) + pbp_txt["header"]["competitions"][0]["home"] = pbp_txt["header"]["competitions"][0]["competitors"][1]["team"] + homeTeamId = int(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["id"]) + homeTeamMascot = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["name"]) + homeTeamName = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["location"]) + homeTeamAbbrev = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["abbreviation"]) homeTeamNameAlt = re.sub("Stat(.+)", "St", homeTeamName) init["homeTeamId"] = homeTeamId init["homeTeamMascot"] = homeTeamMascot @@ -160,110 +184,124 @@ def helper_nba_game_data(pbp_txt, init): init["awayTeamNameAlt"] = awayTeamNameAlt return pbp_txt, init + def helper_nba_pbp_features(game_id, pbp_txt, init): - pbp_txt['plays_mod'] = [] - for play in pbp_txt['plays']: + pbp_txt["plays_mod"] = [] + for play in pbp_txt["plays"]: p = flatten_json_iterative(play) - pbp_txt['plays_mod'].append(p) - pbp_txt['plays'] = pl.from_pandas(pd.json_normalize(pbp_txt,'plays_mod')) - pbp_txt['plays'] = pbp_txt['plays'].with_columns( - game_id = pl.lit(game_id).cast(pl.Int32), - id = (pl.col('id').cast(pl.Int64)), - season = pl.lit(pbp_txt['header']['season']['year']).cast(pl.Int32), - seasonType = pl.lit(pbp_txt['header']['season']['type']), - homeTeamId = pl.lit(init["homeTeamId"]).cast(pl.Int32), - homeTeamName = pl.lit(init["homeTeamName"]), - homeTeamMascot = pl.lit(init["homeTeamMascot"]), - homeTeamAbbrev = pl.lit(init["homeTeamAbbrev"]), - homeTeamNameAlt = pl.lit(init["homeTeamNameAlt"]), - awayTeamId = pl.lit(init["awayTeamId"]).cast(pl.Int32), - awayTeamName = pl.lit(init["awayTeamName"]), - awayTeamMascot = pl.lit(init["awayTeamMascot"]), - awayTeamAbbrev = pl.lit(init["awayTeamAbbrev"]), - awayTeamNameAlt = pl.lit(init["awayTeamNameAlt"]), - gameSpread = pl.lit(init["gameSpread"]).abs(), - homeFavorite = pl.lit(init["homeFavorite"]), - gameSpreadAvailable = pl.lit(init["gameSpreadAvailable"]), - ).with_columns( - homeTeamSpread = pl.when(pl.col('homeFavorite') == True) - .then(pl.col('gameSpread')) - .otherwise(-1*pl.col('gameSpread')), - ).with_columns( - pl.col("period.number").cast(pl.Int32).alias("period.number"), - pl.col("period.number").cast(pl.Int32).alias("qtr"), - pl.when(pl.col("clock.displayValue").str.contains(r":") == False) - .then('0:'+ pl.col("clock.displayValue")) + pbp_txt["plays_mod"].append(p) + pbp_txt["plays"] = pl.from_pandas(pd.json_normalize(pbp_txt, "plays_mod")) + pbp_txt["plays"] = ( + pbp_txt["plays"] + .with_columns( + game_id=pl.lit(game_id).cast(pl.Int32), + id=(pl.col("id").cast(pl.Int64)), + season=pl.lit(pbp_txt["header"]["season"]["year"]).cast(pl.Int32), + seasonType=pl.lit(pbp_txt["header"]["season"]["type"]), + homeTeamId=pl.lit(init["homeTeamId"]).cast(pl.Int32), + homeTeamName=pl.lit(init["homeTeamName"]), + homeTeamMascot=pl.lit(init["homeTeamMascot"]), + homeTeamAbbrev=pl.lit(init["homeTeamAbbrev"]), + homeTeamNameAlt=pl.lit(init["homeTeamNameAlt"]), + awayTeamId=pl.lit(init["awayTeamId"]).cast(pl.Int32), + awayTeamName=pl.lit(init["awayTeamName"]), + awayTeamMascot=pl.lit(init["awayTeamMascot"]), + awayTeamAbbrev=pl.lit(init["awayTeamAbbrev"]), + awayTeamNameAlt=pl.lit(init["awayTeamNameAlt"]), + gameSpread=pl.lit(init["gameSpread"]).abs(), + homeFavorite=pl.lit(init["homeFavorite"]), + gameSpreadAvailable=pl.lit(init["gameSpreadAvailable"]), + ) + .with_columns( + homeTeamSpread=pl.when(pl.col("homeFavorite") == True) + .then(pl.col("gameSpread")) + .otherwise(-1 * pl.col("gameSpread")), + ) + .with_columns( + pl.col("period.number").cast(pl.Int32).alias("period.number"), + pl.col("period.number").cast(pl.Int32).alias("qtr"), + pl.when(pl.col("clock.displayValue").str.contains(r":") == False) + .then("0:" + pl.col("clock.displayValue")) .otherwise(pl.col("clock.displayValue")) .alias("clock.displayValue"), - ).with_columns( - pl.col("clock.displayValue").alias("time"), - pl.col("clock.displayValue").str.split(':').list.to_struct(n_field_strategy="first_non_null").alias("clock.mm") - ).with_columns( - pl.col("clock.mm").struct.rename_fields(["clock.minutes", "clock.seconds"]) - ).unnest( - "clock.mm" - ).with_columns( - pl.col("clock.minutes").cast(pl.Float32), - pl.col("clock.seconds").cast(pl.Float32), - pl.when((pl.col("type.text") == "ShortTimeOut") - .and_( - pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamAbbrev"]).lower()) - .or_( - pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamAbbrev"]).lower()), - pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamName"]).lower()), - pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamMascot"]).lower()), - pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamNameAlt"]).lower()) + ) + .with_columns( + pl.col("clock.displayValue").alias("time"), + pl.col("clock.displayValue") + .str.split(":") + .list.to_struct(n_field_strategy="first_non_null") + .alias("clock.mm"), + ) + .with_columns(pl.col("clock.mm").struct.rename_fields(["clock.minutes", "clock.seconds"])) + .unnest("clock.mm") + .with_columns( + pl.col("clock.minutes").cast(pl.Float32), + pl.col("clock.seconds").cast(pl.Float32), + pl.when( + (pl.col("type.text") == "ShortTimeOut").and_( + pl.col("text") + .str.to_lowercase() + .str.contains(str(init["homeTeamAbbrev"]).lower()) + .or_( + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamAbbrev"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamName"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamMascot"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamNameAlt"]).lower()), + ) ) - )) - .then(True) - .otherwise(False) - .alias("homeTimeoutCalled"), - pl.when((pl.col("type.text") == "ShortTimeOut") - .and_( - pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamAbbrev"]).lower()) - .or_( - pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamAbbrev"]).lower()), - pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamName"]).lower()), - pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamMascot"]).lower()), - pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamNameAlt"]).lower()) + ) + .then(True) + .otherwise(False) + .alias("homeTimeoutCalled"), + pl.when( + (pl.col("type.text") == "ShortTimeOut").and_( + pl.col("text") + .str.to_lowercase() + .str.contains(str(init["awayTeamAbbrev"]).lower()) + .or_( + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamAbbrev"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamName"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamMascot"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamNameAlt"]).lower()), + ) ) - )) - .then(True) - .otherwise(False) - .alias("awayTimeoutCalled"), - ).with_columns( - half = pl.when(pl.col('qtr') <= 2) - .then(1) - .otherwise(2), - game_half = pl.when(pl.col('qtr') <= 2) - .then(1) - .otherwise(2), - ).with_columns( - lag_qtr = pl.col('qtr').shift(1), - lead_qtr = pl.col('qtr').shift(-1), - lag_half = pl.col('half').shift(1), - lead_half = pl.col('half').shift(-1), - ).with_columns( - (60 * pl.col("clock.minutes") + pl.col("clock.seconds")).alias("start.quarter_seconds_remaining"), - pl.when(pl.col('qtr').is_in([1,3])) + ) + .then(True) + .otherwise(False) + .alias("awayTimeoutCalled"), + ) + .with_columns( + half=pl.when(pl.col("qtr") <= 2).then(1).otherwise(2), + game_half=pl.when(pl.col("qtr") <= 2).then(1).otherwise(2), + ) + .with_columns( + lag_qtr=pl.col("qtr").shift(1), + lead_qtr=pl.col("qtr").shift(-1), + lag_half=pl.col("half").shift(1), + lead_half=pl.col("half").shift(-1), + ) + .with_columns( + (60 * pl.col("clock.minutes") + pl.col("clock.seconds")).alias("start.quarter_seconds_remaining"), + pl.when(pl.col("qtr").is_in([1, 3])) .then(720 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) .otherwise(60 * pl.col("clock.minutes") + pl.col("clock.seconds")) .alias("start.half_seconds_remaining"), - pl.when(pl.col('qtr') == 1) + pl.when(pl.col("qtr") == 1) .then(2160 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) - .when(pl.col('qtr') == 2) + .when(pl.col("qtr") == 2) .then(1440 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) - .when(pl.col('qtr') == 3) + .when(pl.col("qtr") == 3) .then(720 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) .otherwise(60 * pl.col("clock.minutes") + pl.col("clock.seconds")) .alias("start.game_seconds_remaining"), - ).with_columns( - pl.col("start.quarter_seconds_remaining").shift(-1).alias("end.quarter_seconds_remaining"), - pl.col("start.half_seconds_remaining").shift(-1).alias("end.half_seconds_remaining"), - pl.col("start.game_seconds_remaining").shift(-1).alias("end.game_seconds_remaining"), + ) + .with_columns( + pl.col("start.quarter_seconds_remaining").shift(-1).alias("end.quarter_seconds_remaining"), + pl.col("start.half_seconds_remaining").shift(-1).alias("end.half_seconds_remaining"), + pl.col("start.game_seconds_remaining").shift(-1).alias("end.game_seconds_remaining"), + ) ) - pbp_txt["plays"] = pbp_txt["plays"].with_row_count("game_play_number", 1) pbp_txt["timeouts"] = { init["homeTeamId"]: {"1": [], "2": []}, @@ -271,107 +309,85 @@ def helper_nba_pbp_features(game_id, pbp_txt, init): } pbp_txt["timeouts"][init["homeTeamId"]]["1"] = ( pbp_txt["plays"] - .filter( - (pl.col("homeTimeoutCalled") == True) - .and_(pl.col("period.number") <= 2) - ) + .filter((pl.col("homeTimeoutCalled") == True).and_(pl.col("period.number") <= 2)) .get_column("id") .to_list() ) pbp_txt["timeouts"][init["homeTeamId"]]["2"] = ( pbp_txt["plays"] - .filter( - (pl.col("homeTimeoutCalled") == True) - .and_(pl.col("period.number") > 2) - ) + .filter((pl.col("homeTimeoutCalled") == True).and_(pl.col("period.number") > 2)) .get_column("id") .to_list() ) pbp_txt["timeouts"][init["awayTeamId"]]["1"] = ( pbp_txt["plays"] - .filter( - (pl.col("awayTimeoutCalled") == True) - .and_(pl.col("period.number") <= 2) - ) + .filter((pl.col("awayTimeoutCalled") == True).and_(pl.col("period.number") <= 2)) .get_column("id") .to_list() ) pbp_txt["timeouts"][init["awayTeamId"]]["2"] = ( pbp_txt["plays"] - .filter( - (pl.col("awayTimeoutCalled") == True) - .and_(pl.col("period.number") > 2) - ) + .filter((pl.col("awayTimeoutCalled") == True).and_(pl.col("period.number") > 2)) .get_column("id") .to_list() ) - # Pos Team - Start and End Id - pbp_txt['plays'] = pbp_txt['plays'].with_columns( - pl.when((pl.col("game_play_number") == 1) - .or_((pl.col("lag_qtr") == 1) - .and_(pl.col("period.number") == 2)) - .or_((pl.col("lag_qtr") == 2) - .and_(pl.col("period.number") == 3)) - .or_((pl.col("lag_qtr") == 3) - .and_(pl.col("period.number") == 4))) - .then(720) - .when((pl.col("lag_qtr") == (pl.col("period.number") - 1)) - .and_(pl.col("period.number") >= 5)) - .then(300) - .otherwise(pl.col("end.quarter_seconds_remaining")) - .alias("end.quarter_seconds_remaining"), + # Pos Team - Start and End Id + pbp_txt["plays"] = pbp_txt["plays"].with_columns( + pl.when( + (pl.col("game_play_number") == 1) + .or_((pl.col("lag_qtr") == 1).and_(pl.col("period.number") == 2)) + .or_((pl.col("lag_qtr") == 2).and_(pl.col("period.number") == 3)) + .or_((pl.col("lag_qtr") == 3).and_(pl.col("period.number") == 4)) + ) + .then(720) + .when((pl.col("lag_qtr") == (pl.col("period.number") - 1)).and_(pl.col("period.number") >= 5)) + .then(300) + .otherwise(pl.col("end.quarter_seconds_remaining")) + .alias("end.quarter_seconds_remaining"), pl.when((pl.col("game_play_number") == 1)) - .then(1440) - .when((pl.col("lag_half") == 1) - .and_(pl.col("half") == 2)) - .then(1440) - .when((pl.col("lag_qtr") == 1) - .and_(pl.col("period.number") == 2)) - .then(720) - .when((pl.col("lag_qtr") == 2) - .and_(pl.col("period.number") == 3)) - .then(1440) - .when((pl.col("lag_qtr") == 3) - .and_(pl.col("period.number") == 4)) - .then(720) - .when((pl.col("lag_qtr") == (pl.col("period.number") - 1)) - .and_(pl.col("period.number") >= 5)) - .then(300) - .otherwise(pl.col("end.half_seconds_remaining")) - .alias("end.half_seconds_remaining"), + .then(1440) + .when((pl.col("lag_half") == 1).and_(pl.col("half") == 2)) + .then(1440) + .when((pl.col("lag_qtr") == 1).and_(pl.col("period.number") == 2)) + .then(720) + .when((pl.col("lag_qtr") == 2).and_(pl.col("period.number") == 3)) + .then(1440) + .when((pl.col("lag_qtr") == 3).and_(pl.col("period.number") == 4)) + .then(720) + .when((pl.col("lag_qtr") == (pl.col("period.number") - 1)).and_(pl.col("period.number") >= 5)) + .then(300) + .otherwise(pl.col("end.half_seconds_remaining")) + .alias("end.half_seconds_remaining"), pl.when((pl.col("game_play_number") == 1)) - .then(2880) - .when((pl.col("lag_qtr") == 1) - .and_(pl.col("period.number") == 2)) - .then(2160) - .when((pl.col("lag_qtr") == 2) - .and_(pl.col("period.number") == 3)) - .then(1440) - .when((pl.col("lag_qtr") == 3) - .and_(pl.col("period.number") == 4)) - .then(720) - .when((pl.col("lag_qtr") == (pl.col("period.number") - 1)) - .and_(pl.col("period.number") >= 5)) - .then(300) - .otherwise(pl.col("end.game_seconds_remaining")) - .alias("end.game_seconds_remaining"), + .then(2880) + .when((pl.col("lag_qtr") == 1).and_(pl.col("period.number") == 2)) + .then(2160) + .when((pl.col("lag_qtr") == 2).and_(pl.col("period.number") == 3)) + .then(1440) + .when((pl.col("lag_qtr") == 3).and_(pl.col("period.number") == 4)) + .then(720) + .when((pl.col("lag_qtr") == (pl.col("period.number") - 1)).and_(pl.col("period.number") >= 5)) + .then(300) + .otherwise(pl.col("end.game_seconds_remaining")) + .alias("end.game_seconds_remaining"), pl.col("qtr").cast(pl.Int32).alias("period"), ) return pbp_txt + def helper_nba_pickcenter(pbp_txt): # Spread definition - if len(pbp_txt.get("pickcenter",[])) > 1: - homeFavorite = pbp_txt.get("pickcenter",{})[0].get("homeTeamOdds",{}).get("favorite","") - if "spread" in pbp_txt.get("pickcenter",{})[1].keys(): - gameSpread = pbp_txt.get("pickcenter",{})[1].get("spread","") - overUnder = pbp_txt.get("pickcenter",{})[1].get("overUnder","") + if len(pbp_txt.get("pickcenter", [])) > 1: + homeFavorite = pbp_txt.get("pickcenter", {})[0].get("homeTeamOdds", {}).get("favorite", "") + if "spread" in pbp_txt.get("pickcenter", {})[1].keys(): + gameSpread = pbp_txt.get("pickcenter", {})[1].get("spread", "") + overUnder = pbp_txt.get("pickcenter", {})[1].get("overUnder", "") else: - gameSpread = pbp_txt.get("pickcenter",{})[0].get("spread","") - overUnder = pbp_txt.get("pickcenter",{})[0].get("overUnder","") + gameSpread = pbp_txt.get("pickcenter", {})[0].get("spread", "") + overUnder = pbp_txt.get("pickcenter", {})[0].get("overUnder", "") gameSpreadAvailable = True - # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") + # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") else: gameSpread = 2.5 overUnder = 190.5 @@ -383,4 +399,4 @@ def helper_nba_pickcenter(pbp_txt): "overUnder": overUnder, "homeFavorite": homeFavorite, "gameSpreadAvailable": gameSpreadAvailable, - } \ No newline at end of file + } diff --git a/sportsdataverse/nba/nba_schedule.py b/sportsdataverse/nba/nba_schedule.py index 94d7fd6..c4280e5 100755 --- a/sportsdataverse/nba/nba_schedule.py +++ b/sportsdataverse/nba/nba_schedule.py @@ -1,18 +1,16 @@ import pandas as pd import polars as pl -import json import datetime -from typing import List, Callable, Iterator, Union, Optional -from sportsdataverse.errors import SeasonNotFoundError from sportsdataverse.dl_utils import download, underscore -def espn_nba_schedule(dates=None, season_type=None, limit=500, - return_as_pandas = True, **kwargs) -> pd.DataFrame: + +def espn_nba_schedule(dates=None, season_type=None, limit=500, return_as_pandas=True, **kwargs) -> pd.DataFrame: """espn_nba_schedule - look up the NBA schedule for a given date from ESPN Args: dates (int): Used to define different seasons. 2002 is the earliest available season. - season_type (int): season type, 1 for pre-season, 2 for regular season, 3 for post-season, 4 for all-star, 5 for off-season + season_type (int): season type, 1 for pre-season, 2 for regular season, 3 for post-season, + 4 for all-star, 5 for off-season limit (int): number of records to return, default: 500. return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. @@ -21,92 +19,102 @@ def espn_nba_schedule(dates=None, season_type=None, limit=500, schedule events for the requested season. """ url = "http://site.api.espn.com/apis/site/v2/sports/basketball/nba/scoreboard" - params = { - 'dates': dates, - 'seasonType': season_type, - 'limit': limit - } + params = {"dates": dates, "seasonType": season_type, "limit": limit} resp = download(url=url, params=params, **kwargs) ev = pl.DataFrame() events_txt = resp.json() - events = events_txt.get('events') + events = events_txt.get("events") if len(events) == 0: return pd.DataFrame() if return_as_pandas else pl.DataFrame() for event in events: - event.get('competitions')[0].get('competitors')[0].get('team').pop('links',None) - event.get('competitions')[0].get('competitors')[1].get('team').pop('links',None) - if event.get('competitions')[0].get('competitors')[0].get('homeAway') == 'home': - event = __extract_home_away(event, 0, 'home') - event = __extract_home_away(event, 1, 'away') + event.get("competitions")[0].get("competitors")[0].get("team").pop("links", None) + event.get("competitions")[0].get("competitors")[1].get("team").pop("links", None) + if event.get("competitions")[0].get("competitors")[0].get("homeAway") == "home": + event = __extract_home_away(event, 0, "home") + event = __extract_home_away(event, 1, "away") else: - event = __extract_home_away(event, 0, 'away') - event = __extract_home_away(event, 1, 'home') - - event.get('competitions')[0]['notes_type'] = event.get('competitions')[0]['notes'][0].get("type") if len(event.get('competitions')[0]['notes']) > 0 else '' - event.get('competitions')[0]['notes_headline'] = event.get('competitions')[0]['notes'][0].get("headline").replace('"','') if len(event.get('competitions')[0]['notes']) > 0 else '' - event.get('competitions')[0]['broadcast_market'] = event.get('competitions')[0].get('broadcasts', [])[0].get('market', "") if len(event.get('competitions')[0].get('broadcasts')) > 0 else "" - event.get('competitions')[0]['broadcast_name'] = event.get('competitions')[0].get('broadcasts', [])[0].get('names', [])[0] if len(event.get('competitions')[0].get('broadcasts')) > 0 else "" - del_keys = ['geoBroadcasts', 'headlines', 'series', 'situation', 'tickets', - 'odds', 'broadcasts', 'notes', 'competitors'] + event = __extract_home_away(event, 0, "away") + event = __extract_home_away(event, 1, "home") + + event.get("competitions")[0]["notes_type"] = ( + event.get("competitions")[0]["notes"][0].get("type") + if len(event.get("competitions")[0]["notes"]) > 0 + else "" + ) + event.get("competitions")[0]["notes_headline"] = ( + event.get("competitions")[0]["notes"][0].get("headline").replace('"', "") + if len(event.get("competitions")[0]["notes"]) > 0 + else "" + ) + event.get("competitions")[0]["broadcast_market"] = ( + event.get("competitions")[0].get("broadcasts", [])[0].get("market", "") + if len(event.get("competitions")[0].get("broadcasts")) > 0 + else "" + ) + event.get("competitions")[0]["broadcast_name"] = ( + event.get("competitions")[0].get("broadcasts", [])[0].get("names", [])[0] + if len(event.get("competitions")[0].get("broadcasts")) > 0 + else "" + ) + del_keys = [ + "geoBroadcasts", + "headlines", + "series", + "situation", + "tickets", + "odds", + "broadcasts", + "notes", + "competitors", + ] for k in del_keys: - event.get('competitions')[0].pop(k, None) + event.get("competitions")[0].pop(k, None) - x = pl.from_pandas(pd.json_normalize(event.get('competitions')[0], sep = '_')) + x = pl.from_pandas(pd.json_normalize(event.get("competitions")[0], sep="_")) x = x.with_columns( - game_id = (pl.col('id').cast(pl.Int32)), - season = (event.get('season').get('year')), - season_type = (event.get('season').get('type')), - home_linescores = pl.when(pl.col('status_type_description') == 'Postponed') - .then(None) - .otherwise(pl.col('home_linescores')), - away_linescores = pl.when(pl.col('status_type_description') == 'Postponed') - .then(None) - .otherwise(pl.col('away_linescores')), + game_id=(pl.col("id").cast(pl.Int32)), + season=(event.get("season").get("year")), + season_type=(event.get("season").get("type")), + home_linescores=pl.when(pl.col("status_type_description") == "Postponed") + .then(None) + .otherwise(pl.col("home_linescores")), + away_linescores=pl.when(pl.col("status_type_description") == "Postponed") + .then(None) + .otherwise(pl.col("away_linescores")), ) x = x[[s.name for s in x if s.null_count() != x.height]] - ev = pl.concat([ev, x], how = 'diagonal') + ev = pl.concat([ev, x], how="diagonal") ev.columns = [underscore(c) for c in ev.columns] return ev.to_pandas() if return_as_pandas else ev + def __extract_home_away(event, arg1, arg2): - event['competitions'][0][arg2] = ( - event.get('competitions')[0].get('competitors')[arg1].get('team') - ) - event['competitions'][0][arg2]['score'] = ( - event.get('competitions')[0].get('competitors')[arg1].get('score') - ) - event['competitions'][0][arg2]['winner'] = ( - event.get('competitions')[0].get('competitors')[arg1].get('winner') - ) - ## add winner back to main competitors if does not exist - event['competitions'][0]['competitors'][arg1]['winner'] = ( - event.get('competitions')[0].get('competitors')[arg1].get('winner', False) + event["competitions"][0][arg2] = event.get("competitions")[0].get("competitors")[arg1].get("team") + event["competitions"][0][arg2]["score"] = event.get("competitions")[0].get("competitors")[arg1].get("score") + event["competitions"][0][arg2]["winner"] = event.get("competitions")[0].get("competitors")[arg1].get("winner") + # add winner back to main competitors if does not exist + event["competitions"][0]["competitors"][arg1]["winner"] = ( + event.get("competitions")[0].get("competitors")[arg1].get("winner", False) ) - event['competitions'][0][arg2]['linescores'] = ( - event.get('competitions')[0] - .get('competitors')[arg1] - .get('linescores', [{'value': 'N/A'}]) + event["competitions"][0][arg2]["linescores"] = ( + event.get("competitions")[0].get("competitors")[arg1].get("linescores", [{"value": "N/A"}]) ) - ## add linescores back to main competitors if does not exist - event['competitions'][0]['competitors'][arg1]['linescores'] = ( - event.get('competitions')[0] - .get('competitors')[arg1] - .get('linescores', []) + # add linescores back to main competitors if does not exist + event["competitions"][0]["competitors"][arg1]["linescores"] = ( + event.get("competitions")[0].get("competitors")[arg1].get("linescores", []) ) - event['competitions'][0][arg2]['records'] = ( - event.get('competitions')[0] - .get('competitors')[arg1] - .get('records', []) + event["competitions"][0][arg2]["records"] = ( + event.get("competitions")[0].get("competitors")[arg1].get("records", []) ) return event -def espn_nba_calendar(season=None, ondays=None, return_as_pandas = True, **kwargs) -> pd.DataFrame: +def espn_nba_calendar(season=None, ondays=None, return_as_pandas=True, **kwargs) -> pd.DataFrame: """espn_nba_calendar - look up the NBA calendar for a given season from ESPN Args: @@ -126,25 +134,24 @@ def espn_nba_calendar(season=None, ondays=None, return_as_pandas = True, **kwarg else: url = f"http://site.api.espn.com/apis/site/v2/sports/basketball/nba/scoreboard?dates={season}" resp = download(url=url, **kwargs) - txt = resp.json().get('leagues')[0].get('calendar') - datenum = list(map(lambda x: x[:10].replace("-",""),txt)) - date = list(map(lambda x: x[:10],txt)) - year = list(map(lambda x: x[:4],txt)) - month = list(map(lambda x: x[5:7],txt)) - day = list(map(lambda x: x[8:10],txt)) + txt = resp.json().get("leagues")[0].get("calendar") + datenum = list(map(lambda x: x[:10].replace("-", ""), txt)) + date = list(map(lambda x: x[:10], txt)) + year = list(map(lambda x: x[:4], txt)) + month = list(map(lambda x: x[5:7], txt)) + day = list(map(lambda x: x[8:10], txt)) data = { "season": season, - "datetime" : txt, - "date" : date, + "datetime": txt, + "date": date, "year": year, "month": month, "day": day, - "dateURL": datenum + "dateURL": datenum, } full_schedule = pl.DataFrame(data) full_schedule = full_schedule.with_columns( - url="http://site.api.espn.com/apis/site/v2/sports/basketball/nba/scoreboard?dates=" + - pl.col('dateURL') + url="http://site.api.espn.com/apis/site/v2/sports/basketball/nba/scoreboard?dates=" + pl.col("dateURL") ) return full_schedule.to_pandas() if return_as_pandas else full_schedule @@ -152,22 +159,23 @@ def espn_nba_calendar(season=None, ondays=None, return_as_pandas = True, **kwarg def __ondays_nba_calendar(season, **kwargs): url = f"https://sports.core.api.espn.com/v2/sports/basketball/leagues/nba/seasons/{season}/types/2/calendar/ondays" resp = download(url=url, **kwargs) - txt = resp.json().get('eventDate').get('dates') - result = pl.DataFrame(txt, schema=['dates']) - result = result.with_columns(dateURL = pl.col('dates').str.slice(0, 10)) + txt = resp.json().get("eventDate").get("dates") + result = pl.DataFrame(txt, schema=["dates"]) + result = result.with_columns(dateURL=pl.col("dates").str.slice(0, 10)) result = result.with_columns( - url="http://site.api.espn.com/apis/site/v2/sports/basketball/nba/scoreboard?dates=" + - pl.col('dateURL') + url="http://site.api.espn.com/apis/site/v2/sports/basketball/nba/scoreboard?dates=" + pl.col("dateURL") ) return result + def most_recent_nba_season(): if int(str(datetime.date.today())[5:7]) >= 10: return int(str(datetime.date.today())[:4]) + 1 else: return int(str(datetime.date.today())[:4]) + def year_to_season(year): first_year = str(year)[2:4] next_year = int(first_year) + 1 @@ -177,4 +185,4 @@ def year_to_season(year): next_year_formatted = "00" else: next_year_formatted = str(next_year) - return f"{year}-{next_year_formatted}" \ No newline at end of file + return f"{year}-{next_year_formatted}" diff --git a/sportsdataverse/nba/nba_teams.py b/sportsdataverse/nba/nba_teams.py index e0e1dfe..43c3ce2 100755 --- a/sportsdataverse/nba/nba_teams.py +++ b/sportsdataverse/nba/nba_teams.py @@ -1,8 +1,8 @@ import pandas as pd import polars as pl -import json from sportsdataverse.dl_utils import download, underscore + def espn_nba_teams(return_as_pandas=True, **kwargs) -> pd.DataFrame: """espn_nba_teams - look up NBA teams @@ -17,19 +17,16 @@ def espn_nba_teams(return_as_pandas=True, **kwargs) -> pd.DataFrame: """ url = "http://site.api.espn.com/apis/site/v2/sports/basketball/nba/teams" - params = { - "limit": 1000 - } - resp = download(url=url, params = params, **kwargs) + params = {"limit": 1000} + resp = download(url=url, params=params, **kwargs) if resp is not None: events_txt = resp.json() - teams = events_txt.get('sports')[0].get('leagues')[0].get('teams') - del_keys = ['record', 'links'] + teams = events_txt.get("sports")[0].get("leagues")[0].get("teams") + del_keys = ["record", "links"] for team in teams: for k in del_keys: - team.get('team').pop(k, None) - teams = pd.json_normalize(teams, sep='_') + team.get("team").pop(k, None) + teams = pd.json_normalize(teams, sep="_") teams.columns = [underscore(c) for c in teams.columns.tolist()] return teams if return_as_pandas else pl.from_pandas(teams) - diff --git a/sportsdataverse/nfl/model_vars.py b/sportsdataverse/nfl/model_vars.py index 198f6a3..d415c4d 100755 --- a/sportsdataverse/nfl/model_vars.py +++ b/sportsdataverse/nfl/model_vars.py @@ -1,12 +1,4 @@ -ep_class_to_score_mapping = { - 0: 7, - 1: -7, - 2: 3, - 3: -3, - 4: 2, - 5: -2, - 6: 0 -} +ep_class_to_score_mapping = {0: 7, 1: -7, 2: 3, 3: -3, 4: 2, 5: -2, 6: 0} wp_start_touchback_columns = [ "start.pos_team_receives_2H_kickoff", @@ -21,7 +13,7 @@ "start.is_home", "start.posTeamTimeouts", "start.defPosTeamTimeouts", - "period" + "period", ] wp_start_columns = [ "start.pos_team_receives_2H_kickoff", @@ -36,7 +28,7 @@ "start.is_home", "start.posTeamTimeouts", "start.defPosTeamTimeouts", - "period" + "period", ] wp_end_columns = [ "end.pos_team_receives_2H_kickoff", @@ -51,7 +43,7 @@ "end.is_home", "end.posTeamTimeouts", "end.defPosTeamTimeouts", - "period" + "period", ] ep_start_touchback_columns = [ @@ -62,7 +54,7 @@ "down_2", "down_3", "down_4", - "pos_score_diff_start" + "pos_score_diff_start", ] ep_start_columns = [ "start.TimeSecsRem", @@ -72,7 +64,7 @@ "down_2", "down_3", "down_4", - "pos_score_diff_start" + "pos_score_diff_start", ] ep_end_columns = [ "end.TimeSecsRem", @@ -82,7 +74,7 @@ "down_2_end", "down_3_end", "down_4_end", - "pos_score_diff_end" + "pos_score_diff_end", ] ep_final_names = [ @@ -93,7 +85,7 @@ "down_2", "down_3", "down_4", - "pos_score_diff_start" + "pos_score_diff_start", ] wp_final_names = [ "pos_team_receives_2H_kickoff", @@ -108,10 +100,10 @@ "is_home", "pos_team_timeouts_rem_before", "def_pos_team_timeouts_rem_before", - "period" + "period", ] - #-------Play type vectors------------- +# -------Play type vectors------------- scores_vec = [ "Blocked Punt Touchdown", "Blocked Punt (Safety)", @@ -138,7 +130,7 @@ "Rushing Touchdown", "Field Goal Good", "Pass Reception Touchdown", - "Fumble Recovery (Own) Touchdown" + "Fumble Recovery (Own) Touchdown", ] defense_score_vec = [ "Blocked Punt Touchdown", @@ -147,13 +139,13 @@ "Punt Return Touchdown", "Fumble Recovery (Opponent) Touchdown", "Fumble Return Touchdown", - "Kickoff Touchdown", #<--- Kickoff Team recovers the return team fumble and scores + "Kickoff Touchdown", # <--- Kickoff Team recovers the return team fumble and scores "Defensive 2pt Conversion", "Safety", "Sack Touchdown", "Interception Return Touchdown", "Pass Interception Return Touchdown", - "Uncategorized Touchdown" + "Uncategorized Touchdown", ] turnover_vec = [ "Blocked Field Goal", @@ -177,7 +169,7 @@ "Punt Touchdown", "Punt Return Touchdown", "Sack Touchdown", - "Uncategorized Touchdown" + "Uncategorized Touchdown", ] normalplay = [ "Rush", @@ -186,23 +178,19 @@ "Pass Incompletion", "Pass Completion", "Sack", - "Fumble Recovery (Own)" -] -penalty = [ - 'Penalty', - 'Penalty (Kickoff)', - 'Penalty (Safety)' + "Fumble Recovery (Own)", ] +penalty = ["Penalty", "Penalty (Kickoff)", "Penalty (Safety)"] offense_score_vec = [ "Passing Touchdown", "Rushing Touchdown", "Field Goal Good", "Pass Reception Touchdown", "Fumble Recovery (Own) Touchdown", - "Punt Touchdown", #<--- Punting Team recovers the return team fumble and scores + "Punt Touchdown", # <--- Punting Team recovers the return team fumble and scores "Punt Team Fumble Recovery Touchdown", "Kickoff Return Touchdown", - "Kickoff Team Fumble Recovery Touchdown" + "Kickoff Team Fumble Recovery Touchdown", ] punt_vec = [ "Blocked Punt", @@ -214,7 +202,7 @@ "Punt Touchdown", "Punt Team Fumble Recovery", "Punt Team Fumble Recovery Touchdown", - "Punt Return Touchdown" + "Punt Return Touchdown", ] kickoff_vec = [ "Kickoff", @@ -224,7 +212,7 @@ "Kickoff Team Fumble Recovery", "Kickoff Team Fumble Recovery Touchdown", "Kickoff (Safety)", - "Penalty (Kickoff)" + "Penalty (Kickoff)", ] int_vec = [ "Interception", @@ -232,7 +220,7 @@ "Interception Return Touchdown", "Pass Interception", "Pass Interception Return", - "Pass Interception Return Touchdown" + "Pass Interception Return Touchdown", ] end_change_vec = [ "Blocked Field Goal", @@ -258,18 +246,8 @@ "Interception Return Touchdown", "Pass Interception Return", "Pass Interception Return Touchdown", - "Uncategorized Touchdown" -] -kickoff_turnovers = [ - "Kickoff Team Fumble Recovery", - "Kickoff Team Fumble Recovery Touchdown" + "Uncategorized Touchdown", ] +kickoff_turnovers = ["Kickoff Team Fumble Recovery", "Kickoff Team Fumble Recovery Touchdown"] -qbr_vars = [ - "qbr_epa", - "sack_epa", - "pass_epa", - "rush_epa", - "pen_epa", - "spread" -] \ No newline at end of file +qbr_vars = ["qbr_epa", "sack_epa", "pass_epa", "rush_epa", "pen_epa", "spread"] diff --git a/sportsdataverse/nfl/nfl_game_rosters.py b/sportsdataverse/nfl/nfl_game_rosters.py index 566107c..78ff179 100644 --- a/sportsdataverse/nfl/nfl_game_rosters.py +++ b/sportsdataverse/nfl/nfl_game_rosters.py @@ -1,13 +1,9 @@ import pandas as pd import polars as pl -import numpy as np -import os -import json -import re -from typing import List, Callable, Iterator, Union, Optional, Dict from sportsdataverse.dl_utils import download, underscore -def espn_nfl_game_rosters(game_id: int, raw = False, return_as_pandas = True, **kwargs) -> pd.DataFrame: + +def espn_nfl_game_rosters(game_id: int, raw=False, return_as_pandas=True, **kwargs) -> pd.DataFrame: """espn_nfl_game_rosters() - Pull the game by id. Args: @@ -36,165 +32,150 @@ def espn_nfl_game_rosters(game_id: int, raw = False, return_as_pandas = True, ** Example: `nfl_df = sportsdataverse.nfl.espn_nfl_game_rosters(game_id=401220403)` """ - # play by play - pbp_txt = {} # summary endpoint for pickcenter array - summary_url = "https://sports.core.api.espn.com/v2/sports/football/leagues/nfl/events/{x}/competitions/{x}/competitors".format(x=game_id) + summary_url = "https://sports.core.api.espn.com/v2/sports/football/leagues/nfl/events/{x}/competitions/{x}/competitors".format( + x=game_id + ) summary_resp = download(summary_url, **kwargs) summary = summary_resp.json() items = helper_nfl_game_items(summary) - team_rosters = helper_nfl_roster_items(items = items, summary_url = summary_url, **kwargs) - team_rosters = team_rosters.join(items[['team_id', 'order', 'home_away', 'winner']], how = 'left', on = 'team_id') - teams_df = helper_nfl_team_items(items = items, **kwargs) - teams_rosters = team_rosters.join(teams_df, how = 'left',on = 'team_id') - athletes = helper_nfl_athlete_items(teams_rosters = team_rosters, **kwargs) - rosters = athletes.join(teams_rosters, how = 'left', left_on = 'athlete_id', right_on = 'player_id') - rosters = rosters.with_columns( - game_id = pl.lit(game_id).cast(pl.Int32) - ) + team_rosters = helper_nfl_roster_items(items=items, summary_url=summary_url, **kwargs) + team_rosters = team_rosters.join(items[["team_id", "order", "home_away", "winner"]], how="left", on="team_id") + teams_df = helper_nfl_team_items(items=items, **kwargs) + teams_rosters = team_rosters.join(teams_df, how="left", on="team_id") + athletes = helper_nfl_athlete_items(teams_rosters=team_rosters, **kwargs) + rosters = athletes.join(teams_rosters, how="left", left_on="athlete_id", right_on="player_id") + rosters = rosters.with_columns(game_id=pl.lit(game_id).cast(pl.Int32)) rosters.columns = [underscore(c) for c in rosters.columns] return rosters.to_pandas() if return_as_pandas else rosters + def helper_nfl_game_items(summary): - items = pl.from_pandas(pd.json_normalize(summary, record_path = "items", sep = '_')) + items = pl.from_pandas(pd.json_normalize(summary, record_path="items", sep="_")) items.columns = [col.replace("$ref", "href") for col in items.columns] items.columns = [underscore(c) for c in items.columns] print(items.columns) - items = items.rename({ - "id": "team_id", - "uid": "team_uid", - "statistics_href": "team_statistics_href" - } - ) - items = items.with_columns( - team_id = pl.col("team_id").cast(pl.Int32) - ) + items = items.rename({"id": "team_id", "uid": "team_uid", "statistics_href": "team_statistics_href"}) + items = items.with_columns(team_id=pl.col("team_id").cast(pl.Int32)) return items + def helper_nfl_team_items(items, **kwargs): pop_cols = [ - '$ref', - 'record', - 'athletes', - 'venue', - 'groups', - 'ranks', - 'statistics', - 'leaders', - 'links', - 'notes', - 'againstTheSpreadRecords', - 'franchise', - 'events', - 'college', - 'awards', - 'coaches', - 'recruiting', - 'injuries', - 'transactions', - 'attendance' + "$ref", + "record", + "athletes", + "venue", + "groups", + "ranks", + "statistics", + "leaders", + "links", + "notes", + "againstTheSpreadRecords", + "franchise", + "events", + "college", + "awards", + "coaches", + "recruiting", + "injuries", + "transactions", + "attendance", ] teams_df = pl.DataFrame() - for x in items['team_href']: + for x in items["team_href"]: team = download(x, **kwargs).json() for k in pop_cols: team.pop(k, None) - team_row = pl.from_pandas(pd.json_normalize(team, sep = '_')) - teams_df = pl.concat([teams_df, team_row], how = 'vertical') + team_row = pl.from_pandas(pd.json_normalize(team, sep="_")) + teams_df = pl.concat([teams_df, team_row], how="vertical") print(teams_df.columns) teams_df.columns = [ - 'team_id', - 'team_guid', - 'team_uid', - 'team_slug', - 'team_location', - 'team_name', - 'team_nickname', - 'team_abbreviation', - 'team_display_name', - 'team_short_display_name', - 'team_color', - 'team_alternate_color', - 'is_active', - 'is_all_star', - 'logos', - 'team_alternate_ids_sdr' + "team_id", + "team_guid", + "team_uid", + "team_slug", + "team_location", + "team_name", + "team_nickname", + "team_abbreviation", + "team_display_name", + "team_short_display_name", + "team_color", + "team_alternate_color", + "is_active", + "is_all_star", + "logos", + "team_alternate_ids_sdr", ] - teams_df = teams_df.with_columns( - logo_href = pl.lit(""), - logo_dark_href = pl.lit("") - ) + teams_df = teams_df.with_columns(logo_href=pl.lit(""), logo_dark_href=pl.lit("")) - for row in range(len(teams_df['logos'])): - team = teams_df['logos'][row] - teams_df[row, 'logo_href'] = team[0]['href'] - teams_df[row, 'logo_dark_href'] = team[1]['href'] + for row in range(len(teams_df["logos"])): + team = teams_df["logos"][row] + teams_df[row, "logo_href"] = team[0]["href"] + teams_df[row, "logo_dark_href"] = team[1]["href"] - teams_df = teams_df.drop(['logos']) - teams_df = teams_df.with_columns( - team_id = pl.col("team_id").cast(pl.Int32) - ) + teams_df = teams_df.drop(["logos"]) + teams_df = teams_df.with_columns(team_id=pl.col("team_id").cast(pl.Int32)) return teams_df + def helper_nfl_roster_items(items, summary_url, **kwargs): - team_ids = list(items['team_id']) + team_ids = list(items["team_id"]) game_rosters = pl.DataFrame() for tm in team_ids: - team_roster_url = "{x}/{t}/roster".format(x = summary_url, t = tm) + team_roster_url = "{x}/{t}/roster".format(x=summary_url, t=tm) team_roster_resp = download(team_roster_url, **kwargs) - team_roster = pl.from_pandas(pd.json_normalize(team_roster_resp.json().get('entries',[]), sep = '_')) + team_roster = pl.from_pandas(pd.json_normalize(team_roster_resp.json().get("entries", []), sep="_")) team_roster.columns = [col.replace("$ref", "href") for col in team_roster.columns] team_roster.columns = [underscore(c) for c in team_roster.columns] - team_roster= team_roster.with_columns( - team_id = pl.lit(tm).cast(pl.Int32) - ) - game_rosters = pl.concat([game_rosters, team_roster], how = 'vertical') + team_roster = team_roster.with_columns(team_id=pl.lit(tm).cast(pl.Int32)) + game_rosters = pl.concat([game_rosters, team_roster], how="vertical") game_rosters = game_rosters.drop(["period", "for_player_id", "active"]) game_rosters = game_rosters.with_columns( - player_id = pl.col('player_id').cast(pl.Int64), - team_id = pl.col('team_id').cast(pl.Int32) + player_id=pl.col("player_id").cast(pl.Int64), team_id=pl.col("team_id").cast(pl.Int32) ) return game_rosters + def helper_nfl_athlete_items(teams_rosters, **kwargs): - athlete_hrefs = list(teams_rosters['athlete_href']) + athlete_hrefs = list(teams_rosters["athlete_href"]) game_athletes = pl.DataFrame() pop_cols = [ - 'links', - 'injuries', - 'teams', - 'team', - 'college', - 'proAthlete', - 'statistics', - 'notes', - 'eventLog', + "links", + "injuries", + "teams", + "team", + "college", + "proAthlete", + "statistics", + "notes", + "eventLog", "$ref", - "position" + "position", ] for athlete_href in athlete_hrefs: - athlete_res = download(athlete_href, **kwargs) athlete_resp = athlete_res.json() for k in pop_cols: athlete_resp.pop(k, None) - athlete = pl.from_pandas(pd.json_normalize(athlete_resp, sep='_')) + athlete = pl.from_pandas(pd.json_normalize(athlete_resp, sep="_")) athlete.columns = [col.replace("$ref", "href") for col in athlete.columns] athlete.columns = [underscore(c) for c in athlete.columns] - game_athletes = pl.concat([game_athletes, athlete], how = 'diagonal') - + game_athletes = pl.concat([game_athletes, athlete], how="diagonal") - game_athletes = game_athletes.rename({ - "id": "athlete_id", - "uid": "athlete_uid", - "guid": "athlete_guid", - "type": "athlete_type", - "display_name": "athlete_display_name" - }) - game_athletes = game_athletes.with_columns( - athlete_id = pl.col("athlete_id").cast(pl.Int64) + game_athletes = game_athletes.rename( + { + "id": "athlete_id", + "uid": "athlete_uid", + "guid": "athlete_guid", + "type": "athlete_type", + "display_name": "athlete_display_name", + } ) - return game_athletes \ No newline at end of file + game_athletes = game_athletes.with_columns(athlete_id=pl.col("athlete_id").cast(pl.Int64)) + return game_athletes diff --git a/sportsdataverse/nfl/nfl_games.py b/sportsdataverse/nfl/nfl_games.py index 9744aee..012801e 100755 --- a/sportsdataverse/nfl/nfl_games.py +++ b/sportsdataverse/nfl/nfl_games.py @@ -1,22 +1,19 @@ import json import requests -from typing import List, Callable, Iterator, Union, Optional, Dict -from sportsdataverse.dl_utils import download, flatten_json_iterative, key_check +from typing import Dict + def nfl_token_gen(): url = "https://api.nfl.com/v1/reroute" # TODO: resolve if DNT or x-domain-id are necessary. pulled them from chrome inspector - payload = 'grant_type=client_credentials' - headers = { - 'DNT': '1', - 'x-domain-id': '100', - 'Content-Type': 'application/x-www-form-urlencoded' - } + payload = "grant_type=client_credentials" + headers = {"DNT": "1", "x-domain-id": "100", "Content-Type": "application/x-www-form-urlencoded"} + + response = requests.request("POST", url, headers=headers, data=payload) - response = requests.request("POST", url, headers=headers, data = payload) + return json.loads(response.content)["access_token"] - return json.loads(response.content)['access_token'] def nfl_headers_gen(): token = nfl_token_gen() @@ -37,16 +34,17 @@ def nfl_headers_gen(): "Cache-Control": "no-cache", } + def nfl_game_details(game_id=None, headers=None, raw=False) -> Dict: """nfl_game_details() - Args: - game_id (int): Game ID - Returns: - Dict: Dictionary of odds and props data with keys - Example: - `nfl_df = nfl_game_details( - game_id = '7ae87c4c-d24c-11ec-b23d-d15a91047884' - )` + Args: + game_id (int): Game ID + Returns: + Dict: Dictionary of odds and props data with keys + Example: + `nfl_df = nfl_game_details( + game_id = '7ae87c4c-d24c-11ec-b23d-d15a91047884' + )` """ if headers is None: headers = nfl_headers_gen() @@ -56,58 +54,49 @@ def nfl_game_details(game_id=None, headers=None, raw=False) -> Dict: summary = summary_resp.json() incoming_keys_expected = [ - 'attendance', - 'distance', - 'down', - 'gameClock', - 'goalToGo', - 'homePointsOvertime', - 'homePointsQ1', - 'homePointsQ2', - 'homePointsQ3', - 'homePointsQ4', - 'homePointsTotal', - 'homeTeam', - 'homeTimeoutsRemaining', - 'homeTimeoutsUsed', - 'id', - 'offset', - 'period', - 'phase', - 'playReview', - 'possessionTeam', - 'quarter', - 'redzone', - 'scoringSummaries', - 'stadium', - 'startTime', - 'totalOffset', - 'visitorPointsOvertime', - 'visitorPointsQ1', - 'visitorPointsQ2', - 'visitorPointsQ3', - 'visitorPointsQ4', - 'visitorPointsTotal', - 'visitorTeam', - 'visitorTimeoutsRemaining', - 'visitorTimeoutsUsed', - 'weather', - 'yardLine', - 'yardsToGo', - 'drives', - 'plays' - ] - dict_keys_expected = [ - 'homeTeam', - 'possessionTeam', - 'visitorTeam', - 'weather' - ] - array_keys_expected = [ - 'scoringSummaries', - 'drives', - 'plays' + "attendance", + "distance", + "down", + "gameClock", + "goalToGo", + "homePointsOvertime", + "homePointsQ1", + "homePointsQ2", + "homePointsQ3", + "homePointsQ4", + "homePointsTotal", + "homeTeam", + "homeTimeoutsRemaining", + "homeTimeoutsUsed", + "id", + "offset", + "period", + "phase", + "playReview", + "possessionTeam", + "quarter", + "redzone", + "scoringSummaries", + "stadium", + "startTime", + "totalOffset", + "visitorPointsOvertime", + "visitorPointsQ1", + "visitorPointsQ2", + "visitorPointsQ3", + "visitorPointsQ4", + "visitorPointsTotal", + "visitorTeam", + "visitorTimeoutsRemaining", + "visitorTimeoutsUsed", + "weather", + "yardLine", + "yardsToGo", + "drives", + "plays", ] + dict_keys_expected = ["homeTeam", "possessionTeam", "visitorTeam", "weather"] + array_keys_expected = ["scoringSummaries", "drives", "plays"] if raw == True: return summary @@ -119,49 +108,47 @@ def nfl_game_details(game_id=None, headers=None, raw=False) -> Dict: return pbp_txt -def nfl_game_schedule(season=2021, - season_type="REG", - week=1, - headers=None, - raw=False) -> Dict: +def nfl_game_schedule(season=2021, season_type="REG", week=1, headers=None, raw=False) -> Dict: """nfl_game_schedule() - Args: - season (int): season - season_type (str): season type - REG, POST - week (int): week - Returns: - Dict: Dictionary of odds and props data with keys - Example: - `nfl_df = nfl_game_schedule( - season = 2021, seasonType='REG', week=1 - )` + Args: + season (int): season + season_type (str): season type - REG, POST + week (int): week + Returns: + Dict: Dictionary of odds and props data with keys + Example: + `nfl_df = nfl_game_schedule( + season = 2021, seasonType='REG', week=1 + )` """ if headers is None: headers = nfl_headers_gen() - params = { - "season": season, - "seasonType": season_type, - "week": week - } + params = {"season": season, "seasonType": season_type, "week": week} pbp_txt = {} summary_url = "https://api.nfl.com/experience/v1/games" - summary_resp = requests.get(summary_url, - headers=headers, - params=params) + summary_resp = requests.get(summary_url, headers=headers, params=params) summary = summary_resp.json() incoming_keys_expected = [ - 'id', 'homeTeam', 'awayTeam', 'category', 'date', 'time', 'broadcastInfo', 'neutralSite', 'venue', 'season', 'seasonType', 'status', 'week', 'weekType', 'externalIds', 'ticketUrl', 'ticketVendors', 'detail' - ] - dict_keys_expected = [ - 'homeTeam', - 'possessionTeam', - 'visitorTeam', - 'weather' - ] - array_keys_expected = [ - 'scoringSummaries', - 'drives', - 'plays' + "id", + "homeTeam", + "awayTeam", + "category", + "date", + "time", + "broadcastInfo", + "neutralSite", + "venue", + "season", + "seasonType", + "status", + "week", + "weekType", + "externalIds", + "ticketUrl", + "ticketVendors", + "detail", ] + dict_keys_expected = ["homeTeam", "possessionTeam", "visitorTeam", "weather"] + array_keys_expected = ["scoringSummaries", "drives", "plays"] return summary diff --git a/sportsdataverse/nfl/nfl_loaders.py b/sportsdataverse/nfl/nfl_loaders.py index 2304a47..31bd24a 100755 --- a/sportsdataverse/nfl/nfl_loaders.py +++ b/sportsdataverse/nfl/nfl_loaders.py @@ -4,20 +4,41 @@ import json from tqdm import tqdm from pyreadr import read_r, download_file -from typing import List, Callable, Iterator, Union, Optional -from sportsdataverse.config import NFL_BASE_URL, NFL_PLAYER_URL, NFL_ROSTER_URL,\ - NFL_WEEKLY_ROSTER_URL, NFL_TEAM_LOGO_URL, NFL_TEAM_SCHEDULE_URL,\ - NFL_PLAYER_STATS_URL, NFL_PLAYER_KICKING_STATS_URL, NFL_SNAP_COUNTS_URL,\ - NFL_PBP_PARTICIPATION_URL, NFL_CONTRACTS_URL, NFL_DRAFT_PICKS_URL,\ - NFL_COMBINE_URL, NFL_INJURIES_URL, NFL_DEPTH_CHARTS_URL, NFL_OFFICIALS_URL,\ - NFL_OTC_PLAYER_DETAILS_URL, NFL_NGS_PASSING_URL, NFL_NGS_RUSHING_URL,\ - NFL_NGS_RECEIVING_URL, NFL_PFR_SEASON_DEF_URL, NFL_PFR_WEEK_DEF_URL,\ - NFL_PFR_SEASON_PASS_URL, NFL_PFR_WEEK_PASS_URL, NFL_PFR_SEASON_REC_URL,\ - NFL_PFR_WEEK_REC_URL, NFL_PFR_SEASON_RUSH_URL, NFL_PFR_WEEK_RUSH_URL -from sportsdataverse.errors import SeasonNotFoundError, season_not_found_error -from sportsdataverse.dl_utils import download - -def load_nfl_pbp(seasons: List[int]) -> pd.DataFrame: +from typing import List +from sportsdataverse.config import ( + NFL_BASE_URL, + NFL_PLAYER_URL, + NFL_ROSTER_URL, + NFL_WEEKLY_ROSTER_URL, + NFL_TEAM_LOGO_URL, + NFL_TEAM_SCHEDULE_URL, + NFL_PLAYER_STATS_URL, + NFL_PLAYER_KICKING_STATS_URL, + NFL_SNAP_COUNTS_URL, + NFL_PBP_PARTICIPATION_URL, + NFL_CONTRACTS_URL, + NFL_DRAFT_PICKS_URL, + NFL_COMBINE_URL, + NFL_INJURIES_URL, + NFL_DEPTH_CHARTS_URL, + NFL_OFFICIALS_URL, + NFL_OTC_PLAYER_DETAILS_URL, + NFL_NGS_PASSING_URL, + NFL_NGS_RUSHING_URL, + NFL_NGS_RECEIVING_URL, + NFL_PFR_SEASON_DEF_URL, + NFL_PFR_WEEK_DEF_URL, + NFL_PFR_SEASON_PASS_URL, + NFL_PFR_WEEK_PASS_URL, + NFL_PFR_SEASON_REC_URL, + NFL_PFR_WEEK_REC_URL, + NFL_PFR_SEASON_RUSH_URL, + NFL_PFR_WEEK_RUSH_URL, +) +from sportsdataverse.errors import season_not_found_error + + +def load_nfl_pbp(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: """Load NFL play by play data going back to 1999 Example: @@ -25,6 +46,7 @@ def load_nfl_pbp(seasons: List[int]) -> pd.DataFrame: Args: seasons (list): Used to define different seasons. 1999 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: pd.DataFrame: Pandas dataframe containing the play-by-plays available for the requested seasons. @@ -38,10 +60,11 @@ def load_nfl_pbp(seasons: List[int]) -> pd.DataFrame: for i in tqdm(seasons): season_not_found_error(int(i), 1999) i_data = pl.read_parquet(NFL_BASE_URL.format(season=i), use_pyarrow=True, columns=None) - data = pl.concat([data, i_data], how = 'vertical') - return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data + data = pl.concat([data, i_data], how="vertical") + return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_nfl_schedule(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: + +def load_nfl_schedule(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: """Load NFL schedule data Example: @@ -63,15 +86,14 @@ def load_nfl_schedule(seasons: List[int], return_as_pandas = True) -> pd.DataFra for i in tqdm(seasons): season_not_found_error(int(i), 1999) schedule_url = NFL_TEAM_SCHEDULE_URL.format(season=i) - #i_data = pd.read_parquet(NFL_TEAM_SCHEDULE_URL.format(season = i), engine='auto', columns=None) - i_data = read_r( - download_file(schedule_url, f"{tempdirname}/nfl_sched_{i}.rds") - )[None] + # i_data = pd.read_parquet(NFL_TEAM_SCHEDULE_URL.format(season = i), engine='auto', columns=None) + i_data = read_r(download_file(schedule_url, f"{tempdirname}/nfl_sched_{i}.rds"))[None] i_data = pl.DataFrame(i_data) - data = pl.concat([data, i_data], how = "vertical") - return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data + data = pl.concat([data, i_data], how="vertical") + return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data + -def load_nfl_player_stats(kicking = False, return_as_pandas = True) -> pd.DataFrame: +def load_nfl_player_stats(kicking=False, return_as_pandas=True) -> pd.DataFrame: """Load NFL player stats data Example: @@ -89,9 +111,10 @@ def load_nfl_player_stats(kicking = False, return_as_pandas = True) -> pd.DataFr else: data = pl.read_parquet(NFL_PLAYER_KICKING_STATS_URL, use_pyarrow=True, columns=None) - return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data + return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_nfl_ngs_passing(return_as_pandas = True) -> pd.DataFrame: + +def load_nfl_ngs_passing(return_as_pandas=True) -> pd.DataFrame: """Load NFL NextGen Stats Passing data going back to 2016 Example: @@ -101,10 +124,16 @@ def load_nfl_ngs_passing(return_as_pandas = True) -> pd.DataFrame: pd.DataFrame: Pandas dataframe containing the NextGen Stats Passing data available. """ - return pl.read_parquet(NFL_NGS_PASSING_URL, use_pyarrow=True, columns=None).to_pandas(use_pyarrow_extension_array = True) \ - if return_as_pandas else pl.read_parquet(NFL_NGS_PASSING_URL, use_pyarrow=True, columns=None) + return ( + pl.read_parquet(NFL_NGS_PASSING_URL, use_pyarrow=True, columns=None).to_pandas( + use_pyarrow_extension_array=True + ) + if return_as_pandas + else pl.read_parquet(NFL_NGS_PASSING_URL, use_pyarrow=True, columns=None) + ) + -def load_nfl_ngs_rushing(return_as_pandas = True) -> pd.DataFrame: +def load_nfl_ngs_rushing(return_as_pandas=True) -> pd.DataFrame: """Load NFL NextGen Stats Rushing data going back to 2016 Example: @@ -114,10 +143,16 @@ def load_nfl_ngs_rushing(return_as_pandas = True) -> pd.DataFrame: pd.DataFrame: Pandas dataframe containing the NextGen Stats Rushing data available. """ - return pl.read_parquet(NFL_NGS_RUSHING_URL, use_pyarrow=True, columns=None).to_pandas(use_pyarrow_extension_array = True) \ - if return_as_pandas else pl.read_parquet(NFL_NGS_RUSHING_URL, use_pyarrow=True, columns=None) + return ( + pl.read_parquet(NFL_NGS_RUSHING_URL, use_pyarrow=True, columns=None).to_pandas( + use_pyarrow_extension_array=True + ) + if return_as_pandas + else pl.read_parquet(NFL_NGS_RUSHING_URL, use_pyarrow=True, columns=None) + ) -def load_nfl_ngs_receiving(return_as_pandas = True) -> pd.DataFrame: + +def load_nfl_ngs_receiving(return_as_pandas=True) -> pd.DataFrame: """Load NFL NextGen Stats Receiving data going back to 2016 Example: @@ -127,10 +162,16 @@ def load_nfl_ngs_receiving(return_as_pandas = True) -> pd.DataFrame: pd.DataFrame: Pandas dataframe containing the NextGen Stats Receiving data available. """ - return pl.read_parquet(NFL_NGS_RECEIVING_URL, use_pyarrow=True, columns=None).to_pandas(use_pyarrow_extension_array = True) \ - if return_as_pandas else pl.read_parquet(NFL_NGS_RECEIVING_URL, use_pyarrow=True, columns=None) + return ( + pl.read_parquet(NFL_NGS_RECEIVING_URL, use_pyarrow=True, columns=None).to_pandas( + use_pyarrow_extension_array=True + ) + if return_as_pandas + else pl.read_parquet(NFL_NGS_RECEIVING_URL, use_pyarrow=True, columns=None) + ) + -def load_nfl_pfr_pass(return_as_pandas = True) -> pd.DataFrame: +def load_nfl_pfr_pass(return_as_pandas=True) -> pd.DataFrame: """Load NFL Pro-Football Reference Advanced Passing data going back to 2018 Example: @@ -141,10 +182,16 @@ def load_nfl_pfr_pass(return_as_pandas = True) -> pd.DataFrame: advanced passing stats data available. """ - return pl.read_parquet(NFL_PFR_SEASON_PASS_URL, use_pyarrow=True, columns=None).to_pandas(use_pyarrow_extension_array = True) \ - if return_as_pandas else pl.read_parquet(NFL_PFR_SEASON_PASS_URL, use_pyarrow=True, columns=None) + return ( + pl.read_parquet(NFL_PFR_SEASON_PASS_URL, use_pyarrow=True, columns=None).to_pandas( + use_pyarrow_extension_array=True + ) + if return_as_pandas + else pl.read_parquet(NFL_PFR_SEASON_PASS_URL, use_pyarrow=True, columns=None) + ) + -def load_nfl_pfr_weekly_pass(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: +def load_nfl_pfr_weekly_pass(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: """Load NFL Pro-Football Reference Weekly Advanced Passing data going back to 2018 Example: @@ -164,10 +211,11 @@ def load_nfl_pfr_weekly_pass(seasons: List[int], return_as_pandas = True) -> pd. for i in tqdm(seasons): season_not_found_error(int(i), 2018) i_data = pl.read_parquet(NFL_PFR_WEEK_PASS_URL.format(season=i), use_pyarrow=True, columns=None) - data = pl.concat([data, i_data], how = 'vertical') - return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data + data = pl.concat([data, i_data], how="vertical") + return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_nfl_pfr_rush(return_as_pandas = True) -> pd.DataFrame: + +def load_nfl_pfr_rush(return_as_pandas=True) -> pd.DataFrame: """Load NFL Pro-Football Reference Advanced Rushing data going back to 2018 Example: @@ -178,10 +226,16 @@ def load_nfl_pfr_rush(return_as_pandas = True) -> pd.DataFrame: advanced rushing stats data available. """ - return pl.read_parquet(NFL_PFR_SEASON_RUSH_URL, use_pyarrow=True, columns=None).to_pandas(use_pyarrow_extension_array = True) \ - if return_as_pandas else pl.read_parquet(NFL_PFR_SEASON_RUSH_URL, use_pyarrow=True, columns=None) + return ( + pl.read_parquet(NFL_PFR_SEASON_RUSH_URL, use_pyarrow=True, columns=None).to_pandas( + use_pyarrow_extension_array=True + ) + if return_as_pandas + else pl.read_parquet(NFL_PFR_SEASON_RUSH_URL, use_pyarrow=True, columns=None) + ) + -def load_nfl_pfr_weekly_rush(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: +def load_nfl_pfr_weekly_rush(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: """Load NFL Pro-Football Reference Weekly Advanced Rushing data going back to 2018 Example: @@ -201,10 +255,11 @@ def load_nfl_pfr_weekly_rush(seasons: List[int], return_as_pandas = True) -> pd. for i in tqdm(seasons): season_not_found_error(int(i), 2018) i_data = pl.read_parquet(NFL_PFR_WEEK_RUSH_URL.format(season=i), use_pyarrow=True, columns=None) - data = pl.concat([data, i_data], how = 'vertical') - return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data + data = pl.concat([data, i_data], how="vertical") + return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data + -def load_nfl_pfr_rec(return_as_pandas = True) -> pd.DataFrame: +def load_nfl_pfr_rec(return_as_pandas=True) -> pd.DataFrame: """Load NFL Pro-Football Reference Advanced Receiving data going back to 2018 Example: @@ -215,10 +270,16 @@ def load_nfl_pfr_rec(return_as_pandas = True) -> pd.DataFrame: advanced receiving stats data available. """ - return pl.read_parquet(NFL_PFR_SEASON_REC_URL, use_pyarrow=True, columns=None).to_pandas(use_pyarrow_extension_array = True) \ - if return_as_pandas else pl.read_parquet(NFL_PFR_SEASON_REC_URL, use_pyarrow=True, columns=None) + return ( + pl.read_parquet(NFL_PFR_SEASON_REC_URL, use_pyarrow=True, columns=None).to_pandas( + use_pyarrow_extension_array=True + ) + if return_as_pandas + else pl.read_parquet(NFL_PFR_SEASON_REC_URL, use_pyarrow=True, columns=None) + ) -def load_nfl_pfr_weekly_rec(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: + +def load_nfl_pfr_weekly_rec(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: """Load NFL Pro-Football Reference Weekly Advanced Receiving data going back to 2018 Example: @@ -238,10 +299,11 @@ def load_nfl_pfr_weekly_rec(seasons: List[int], return_as_pandas = True) -> pd.D for i in tqdm(seasons): season_not_found_error(int(i), 2018) i_data = pl.read_parquet(NFL_PFR_WEEK_REC_URL.format(season=i), use_pyarrow=True, columns=None) - data = pl.concat([data, i_data], how = 'vertical') - return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data + data = pl.concat([data, i_data], how="vertical") + return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data + -def load_nfl_pfr_def(return_as_pandas = True) -> pd.DataFrame: +def load_nfl_pfr_def(return_as_pandas=True) -> pd.DataFrame: """Load NFL Pro-Football Reference Advanced Defensive data going back to 2018 Example: @@ -252,10 +314,16 @@ def load_nfl_pfr_def(return_as_pandas = True) -> pd.DataFrame: advanced defensive stats data available. """ - return pl.read_parquet(NFL_PFR_SEASON_DEF_URL, use_pyarrow=True, columns=None).to_pandas(use_pyarrow_extension_array = True) \ - if return_as_pandas else pl.read_parquet(NFL_PFR_SEASON_DEF_URL, use_pyarrow=True, columns=None) + return ( + pl.read_parquet(NFL_PFR_SEASON_DEF_URL, use_pyarrow=True, columns=None).to_pandas( + use_pyarrow_extension_array=True + ) + if return_as_pandas + else pl.read_parquet(NFL_PFR_SEASON_DEF_URL, use_pyarrow=True, columns=None) + ) + -def load_nfl_pfr_weekly_def(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: +def load_nfl_pfr_weekly_def(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: """Load NFL Pro-Football Reference Weekly Advanced Defensive data going back to 2018 Example: @@ -275,11 +343,11 @@ def load_nfl_pfr_weekly_def(seasons: List[int], return_as_pandas = True) -> pd.D for i in tqdm(seasons): season_not_found_error(int(i), 2018) i_data = pl.read_parquet(NFL_PFR_WEEK_DEF_URL.format(season=i), use_pyarrow=True, columns=None) - data = pl.concat([data, i_data], how = 'vertical') - return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data + data = pl.concat([data, i_data], how="vertical") + return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_nfl_rosters(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: +def load_nfl_rosters(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: """Load NFL roster data for all seasons Example: @@ -298,10 +366,11 @@ def load_nfl_rosters(seasons: List[int], return_as_pandas = True) -> pd.DataFram for i in tqdm(seasons): season_not_found_error(int(i), 1920) i_data = pl.read_parquet(NFL_ROSTER_URL.format(season=i), use_pyarrow=True, columns=None) - data = pl.concat([data, i_data], how = 'vertical') - return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data + data = pl.concat([data, i_data], how="vertical") + return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_nfl_weekly_rosters(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: + +def load_nfl_weekly_rosters(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: """Load NFL weekly roster data for selected seasons Example: @@ -320,10 +389,11 @@ def load_nfl_weekly_rosters(seasons: List[int], return_as_pandas = True) -> pd.D for i in tqdm(seasons): season_not_found_error(int(i), 2002) i_data = pl.read_parquet(NFL_WEEKLY_ROSTER_URL.format(season=i), use_pyarrow=True, columns=None) - data = pl.concat([data, i_data], how = 'vertical') - return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data + data = pl.concat([data, i_data], how="vertical") + return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data + -def load_nfl_teams(return_as_pandas = True) -> pd.DataFrame: +def load_nfl_teams(return_as_pandas=True) -> pd.DataFrame: """Load NFL team ID information and logos Example: @@ -334,11 +404,14 @@ def load_nfl_teams(return_as_pandas = True) -> pd.DataFrame: Returns: pd.DataFrame: Pandas dataframe containing teams available. """ - df = pd.read_csv(NFL_TEAM_LOGO_URL, low_memory=False) - return pl.read_csv(NFL_TEAM_LOGO_URL).to_pandas(use_pyarrow_extension_array = True) \ - if return_as_pandas else pl.read_csv(NFL_TEAM_LOGO_URL) + return ( + pl.read_csv(NFL_TEAM_LOGO_URL).to_pandas(use_pyarrow_extension_array=True) + if return_as_pandas + else pl.read_csv(NFL_TEAM_LOGO_URL) + ) + -def load_nfl_players(return_as_pandas = True) -> pd.DataFrame: +def load_nfl_players(return_as_pandas=True) -> pd.DataFrame: """Load NFL Player ID information Example: @@ -349,10 +422,14 @@ def load_nfl_players(return_as_pandas = True) -> pd.DataFrame: Returns: pd.DataFrame: Pandas dataframe containing players available. """ - return pl.read_parquet(NFL_PLAYER_URL, use_pyarrow=True, columns=None).to_pandas(use_pyarrow_extension_array = True) \ - if return_as_pandas else pl.read_parquet(NFL_OFFICIALS_URL, use_pyarrow=True, columns=None) + return ( + pl.read_parquet(NFL_PLAYER_URL, use_pyarrow=True, columns=None).to_pandas(use_pyarrow_extension_array=True) + if return_as_pandas + else pl.read_parquet(NFL_OFFICIALS_URL, use_pyarrow=True, columns=None) + ) -def load_nfl_snap_counts(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: + +def load_nfl_snap_counts(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: """Load NFL snap counts data for selected seasons Example: @@ -371,10 +448,11 @@ def load_nfl_snap_counts(seasons: List[int], return_as_pandas = True) -> pd.Data for i in tqdm(seasons): season_not_found_error(int(i), 2012) i_data = pl.read_parquet(NFL_SNAP_COUNTS_URL.format(season=i), use_pyarrow=True, columns=None) - data = pl.concat([data, i_data], how = 'vertical') - return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data + data = pl.concat([data, i_data], how="vertical") + return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data + -def load_nfl_pbp_participation(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: +def load_nfl_pbp_participation(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: """Load NFL play-by-play participation data for selected seasons Example: @@ -393,10 +471,11 @@ def load_nfl_pbp_participation(seasons: List[int], return_as_pandas = True) -> p for i in tqdm(seasons): season_not_found_error(int(i), 2016) i_data = pl.read_parquet(NFL_PBP_PARTICIPATION_URL.format(season=i), use_pyarrow=True, columns=None) - data = pl.concat([data, i_data], how = 'vertical') - return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data + data = pl.concat([data, i_data], how="vertical") + return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data + -def load_nfl_injuries(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: +def load_nfl_injuries(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: """Load NFL injuries data for selected seasons Example: @@ -415,10 +494,11 @@ def load_nfl_injuries(seasons: List[int], return_as_pandas = True) -> pd.DataFra for i in tqdm(seasons): season_not_found_error(int(i), 2009) i_data = pl.read_parquet(NFL_INJURIES_URL.format(season=i), use_pyarrow=True, columns=None) - data = pl.concat([data, i_data], how = 'vertical') - return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data + data = pl.concat([data, i_data], how="vertical") + return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_nfl_depth_charts(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: + +def load_nfl_depth_charts(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: """Load NFL Depth Chart data for selected seasons Example: @@ -437,10 +517,11 @@ def load_nfl_depth_charts(seasons: List[int], return_as_pandas = True) -> pd.Dat for i in tqdm(seasons): season_not_found_error(int(i), 2001) i_data = pl.read_parquet(NFL_DEPTH_CHARTS_URL.format(season=i), use_pyarrow=True, columns=None) - data = pl.concat([data, i_data], how = 'vertical') - return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data + data = pl.concat([data, i_data], how="vertical") + return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data + -def load_nfl_contracts(return_as_pandas = True) -> pd.DataFrame: +def load_nfl_contracts(return_as_pandas=True) -> pd.DataFrame: """Load NFL Historical contracts information Example: @@ -451,11 +532,14 @@ def load_nfl_contracts(return_as_pandas = True) -> pd.DataFrame: Returns: pd.DataFrame: Pandas dataframe containing historical contracts available. """ - return pl.read_parquet(NFL_CONTRACTS_URL, use_pyarrow=True, columns=None).to_pandas(use_pyarrow_extension_array = True) \ - if return_as_pandas else pl.read_parquet(NFL_CONTRACTS_URL, use_pyarrow=True, columns=None) + return ( + pl.read_parquet(NFL_CONTRACTS_URL, use_pyarrow=True, columns=None).to_pandas(use_pyarrow_extension_array=True) + if return_as_pandas + else pl.read_parquet(NFL_CONTRACTS_URL, use_pyarrow=True, columns=None) + ) -def load_nfl_combine(return_as_pandas = True) -> pd.DataFrame: +def load_nfl_combine(return_as_pandas=True) -> pd.DataFrame: """Load NFL Combine information Example: @@ -466,10 +550,14 @@ def load_nfl_combine(return_as_pandas = True) -> pd.DataFrame: Returns: pd.DataFrame: Pandas dataframe containing NFL combine data available. """ - return pl.read_parquet(NFL_COMBINE_URL, use_pyarrow=True, columns=None).to_pandas(use_pyarrow_extension_array = True) \ - if return_as_pandas else pl.read_parquet(NFL_COMBINE_URL, use_pyarrow=True, columns=None) + return ( + pl.read_parquet(NFL_COMBINE_URL, use_pyarrow=True, columns=None).to_pandas(use_pyarrow_extension_array=True) + if return_as_pandas + else pl.read_parquet(NFL_COMBINE_URL, use_pyarrow=True, columns=None) + ) + -def load_nfl_draft_picks(return_as_pandas = True) -> pd.DataFrame: +def load_nfl_draft_picks(return_as_pandas=True) -> pd.DataFrame: """Load NFL Draft picks information Example: @@ -480,10 +568,16 @@ def load_nfl_draft_picks(return_as_pandas = True) -> pd.DataFrame: Returns: pd.DataFrame: Pandas dataframe containing NFL Draft picks data available. """ - return pl.read_parquet(NFL_DRAFT_PICKS_URL, use_pyarrow=True, columns=None).to_pandas(use_pyarrow_extension_array = True) \ - if return_as_pandas else pl.read_parquet(NFL_DRAFT_PICKS_URL, use_pyarrow=True, columns=None) + return ( + pl.read_parquet(NFL_DRAFT_PICKS_URL, use_pyarrow=True, columns=None).to_pandas( + use_pyarrow_extension_array=True + ) + if return_as_pandas + else pl.read_parquet(NFL_DRAFT_PICKS_URL, use_pyarrow=True, columns=None) + ) -def load_nfl_officials(return_as_pandas = True) -> pd.DataFrame: + +def load_nfl_officials(return_as_pandas=True) -> pd.DataFrame: """Load NFL Officials information Example: @@ -494,8 +588,13 @@ def load_nfl_officials(return_as_pandas = True) -> pd.DataFrame: Returns: pd.DataFrame: Pandas dataframe containing officials available. """ - return pl.read_parquet(NFL_OFFICIALS_URL, use_pyarrow=True, columns=None).to_pandas(use_pyarrow_extension_array = True) \ - if return_as_pandas else pl.read_parquet(NFL_OFFICIALS_URL, use_pyarrow=True, columns=None) + return ( + pl.read_parquet(NFL_OFFICIALS_URL, use_pyarrow=True, columns=None).to_pandas(use_pyarrow_extension_array=True) + if return_as_pandas + else pl.read_parquet(NFL_OFFICIALS_URL, use_pyarrow=True, columns=None) + ) + + ## Currently removed due to unsupported features of pyreadr's method. ## there is a list-column of nested tibbles within the data ## that is not supported by pyreadr @@ -519,4 +618,4 @@ def load_nfl_officials(return_as_pandas = True) -> pd.DataFrame: # data = pd.concat([data, df], ignore_index=True) # #Give each row a unique index # data.reset_index(drop=True, inplace=True) -# return df \ No newline at end of file +# return df diff --git a/sportsdataverse/nfl/nfl_schedule.py b/sportsdataverse/nfl/nfl_schedule.py index 7d6c90b..8f5fbb1 100755 --- a/sportsdataverse/nfl/nfl_schedule.py +++ b/sportsdataverse/nfl/nfl_schedule.py @@ -1,14 +1,12 @@ import pandas as pd import polars as pl -import json -import time import datetime from sportsdataverse.dl_utils import download, underscore -from urllib.error import URLError, HTTPError, ContentTooShortError -def espn_nfl_schedule(dates=None, week=None, season_type=None, groups=None, limit=500, - return_as_pandas = True, - **kwargs) -> pd.DataFrame: + +def espn_nfl_schedule( + dates=None, week=None, season_type=None, groups=None, limit=500, return_as_pandas=True, **kwargs +) -> pd.DataFrame: """espn_nfl_schedule - look up the NFL schedule for a given season Args: @@ -22,101 +20,98 @@ def espn_nfl_schedule(dates=None, week=None, season_type=None, groups=None, limi pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. """ - params = { - 'week': week, - 'dates': dates, - 'seasonType': season_type, - 'limit': limit - } + params = {"week": week, "dates": dates, "seasonType": season_type, "limit": limit} url = "http://site.api.espn.com/apis/site/v2/sports/football/nfl/scoreboard" resp = download(url=url, params=params, **kwargs) ev = pl.DataFrame() events_txt = resp.json() - events = events_txt.get('events') + events = events_txt.get("events") if len(events) == 0: return pd.DataFrame() if return_as_pandas else pl.DataFrame() for event in events: - event.get('competitions')[0].get('competitors')[0].get('team').pop('links',None) - event.get('competitions')[0].get('competitors')[1].get('team').pop('links',None) - if event.get('competitions')[0].get('competitors')[0].get('homeAway') == 'home': - event = _extract_home_away(event, 0, 'home') - event = _extract_home_away(event, 1, 'away') + event.get("competitions")[0].get("competitors")[0].get("team").pop("links", None) + event.get("competitions")[0].get("competitors")[1].get("team").pop("links", None) + if event.get("competitions")[0].get("competitors")[0].get("homeAway") == "home": + event = _extract_home_away(event, 0, "home") + event = _extract_home_away(event, 1, "away") else: - event = _extract_home_away(event, 0, 'away') - event = _extract_home_away(event, 1, 'home') - del_keys = ['geoBroadcasts', 'headlines', 'series', 'situation', 'tickets', 'odds'] + event = _extract_home_away(event, 0, "away") + event = _extract_home_away(event, 1, "home") + del_keys = ["geoBroadcasts", "headlines", "series", "situation", "tickets", "odds"] for k in del_keys: - event.get('competitions')[0].pop(k, None) - event.get('competitions')[0]['notes_type'] = event.get('competitions')[0]['notes'][0].get("type") if len(event.get('competitions')[0]['notes']) > 0 else '' - event.get('competitions')[0]['notes_headline'] = event.get('competitions')[0]['notes'][0].get("headline").replace('"','') if len(event.get('competitions')[0]['notes']) > 0 else '' - event.get('competitions')[0]['broadcast_market'] = event.get('competitions')[0].get('broadcasts', [])[0].get('market', "") if len(event.get('competitions')[0].get('broadcasts')) > 0 else "" - event.get('competitions')[0]['broadcast_name'] = event.get('competitions')[0].get('broadcasts', [])[0].get('names', [])[0] if len(event.get('competitions')[0].get('broadcasts')) > 0 else "" - event.get('competitions')[0].pop('broadcasts', None) - event.get('competitions')[0].pop('notes', None) - event.get('competitions')[0].pop('competitors', None) - x = pl.from_pandas(pd.json_normalize(event.get('competitions')[0], sep = '_')) + event.get("competitions")[0].pop(k, None) + event.get("competitions")[0]["notes_type"] = ( + event.get("competitions")[0]["notes"][0].get("type") + if len(event.get("competitions")[0]["notes"]) > 0 + else "" + ) + event.get("competitions")[0]["notes_headline"] = ( + event.get("competitions")[0]["notes"][0].get("headline").replace('"', "") + if len(event.get("competitions")[0]["notes"]) > 0 + else "" + ) + event.get("competitions")[0]["broadcast_market"] = ( + event.get("competitions")[0].get("broadcasts", [])[0].get("market", "") + if len(event.get("competitions")[0].get("broadcasts")) > 0 + else "" + ) + event.get("competitions")[0]["broadcast_name"] = ( + event.get("competitions")[0].get("broadcasts", [])[0].get("names", [])[0] + if len(event.get("competitions")[0].get("broadcasts")) > 0 + else "" + ) + event.get("competitions")[0].pop("broadcasts", None) + event.get("competitions")[0].pop("notes", None) + event.get("competitions")[0].pop("competitors", None) + x = pl.from_pandas(pd.json_normalize(event.get("competitions")[0], sep="_")) x = x.with_columns( - game_id = (pl.col('id').cast(pl.Int32)), - season = (event.get('season').get('year')), - season_type = (event.get('season').get('type')), - week = (event.get('week', {}).get('number')), - home_linescores = pl.when(pl.col('status_type_description') == 'Postponed') - .then(None) - .otherwise(pl.col('home_linescores')), - away_linescores = pl.when(pl.col('status_type_description') == 'Postponed') - .then(None) - .otherwise(pl.col('away_linescores')), + game_id=(pl.col("id").cast(pl.Int32)), + season=(event.get("season").get("year")), + season_type=(event.get("season").get("type")), + week=(event.get("week", {}).get("number")), + home_linescores=pl.when(pl.col("status_type_description") == "Postponed") + .then(None) + .otherwise(pl.col("home_linescores")), + away_linescores=pl.when(pl.col("status_type_description") == "Postponed") + .then(None) + .otherwise(pl.col("away_linescores")), ) x = x[[s.name for s in x if s.null_count() != x.height]] - ev = pl.concat([ev, x], how = 'diagonal') + ev = pl.concat([ev, x], how="diagonal") ev.columns = [underscore(c) for c in ev.columns] return ev.to_pandas() if return_as_pandas else ev + def _extract_home_away(event, arg1, arg2): - event['competitions'][0][arg2] = ( - event.get('competitions')[0].get('competitors')[arg1].get('team') - ) - event['competitions'][0][arg2]['score'] = ( - event.get('competitions')[0].get('competitors')[arg1].get('score') + event["competitions"][0][arg2] = event.get("competitions")[0].get("competitors")[arg1].get("team") + event["competitions"][0][arg2]["score"] = event.get("competitions")[0].get("competitors")[arg1].get("score") + event["competitions"][0][arg2]["winner"] = event.get("competitions")[0].get("competitors")[arg1].get("winner") + # add winner back to main competitors if does not exist + event["competitions"][0]["competitors"][arg1]["winner"] = ( + event.get("competitions")[0].get("competitors")[arg1].get("winner", False) ) - event['competitions'][0][arg2]['winner'] = ( - event.get('competitions')[0].get('competitors')[arg1].get('winner') + event["competitions"][0][arg2]["currentRank"] = ( + event.get("competitions")[0].get("competitors")[arg1].get("curatedRank", {}).get("current", 99) ) - ## add winner back to main competitors if does not exist - event['competitions'][0]['competitors'][arg1]['winner'] = ( - event.get('competitions')[0].get('competitors')[arg1].get('winner', False) + event["competitions"][0][arg2]["linescores"] = ( + event.get("competitions")[0].get("competitors")[arg1].get("linescores", [{"value": "N/A"}]) ) - event['competitions'][0][arg2]['currentRank'] = ( - event.get('competitions')[0] - .get('competitors')[arg1] - .get('curatedRank', {}) - .get('current', 99) + # add linescores back to main competitors if does not exist + event["competitions"][0]["competitors"][arg1]["linescores"] = ( + event.get("competitions")[0].get("competitors")[arg1].get("linescores", []) ) - event['competitions'][0][arg2]['linescores'] = ( - event.get('competitions')[0] - .get('competitors')[arg1] - .get('linescores', [{'value': 'N/A'}]) - ) - ## add linescores back to main competitors if does not exist - event['competitions'][0]['competitors'][arg1]['linescores'] = ( - event.get('competitions')[0] - .get('competitors')[arg1] - .get('linescores', []) - ) - event['competitions'][0][arg2]['records'] = ( - event.get('competitions')[0] - .get('competitors')[arg1] - .get('records', []) + event["competitions"][0][arg2]["records"] = ( + event.get("competitions")[0].get("competitors")[arg1].get("records", []) ) return event -def espn_nfl_calendar(season = None, ondays = None, - return_as_pandas = True, **kwargs) -> pd.DataFrame: + +def espn_nfl_calendar(season=None, ondays=None, return_as_pandas=True, **kwargs) -> pd.DataFrame: """espn_nfl_calendar - look up the NFL calendar for a given season Args: @@ -134,26 +129,24 @@ def espn_nfl_calendar(season = None, ondays = None, full_schedule = __ondays_nfl_calendar(season, **kwargs) else: url = "http://site.api.espn.com/apis/site/v2/sports/football/nfl/scoreboard" - params = { - 'dates': season - } - resp = download(url = url, params = params, **kwargs) + params = {"dates": season} + resp = download(url=url, params=params, **kwargs) txt = resp.json() - txt = txt.get('leagues')[0].get('calendar') + txt = txt.get("leagues")[0].get("calendar") full_schedule = pl.DataFrame() for i in range(len(txt)): - if txt[i].get('entries', None) is not None: - reg = pd.json_normalize(data = txt[i], - record_path = 'entries', - meta=["label","value","startDate","endDate"], - meta_prefix='season_type_', - record_prefix='week_', - errors="ignore", - sep='_') - full_schedule = pl.concat([full_schedule, pl.from_pandas(reg)], how = 'vertical') - full_schedule = full_schedule.with_columns( - season = season - ) + if txt[i].get("entries", None) is not None: + reg = pd.json_normalize( + data=txt[i], + record_path="entries", + meta=["label", "value", "startDate", "endDate"], + meta_prefix="season_type_", + record_prefix="week_", + errors="ignore", + sep="_", + ) + full_schedule = pl.concat([full_schedule, pl.from_pandas(reg)], how="vertical") + full_schedule = full_schedule.with_columns(season=season) full_schedule.columns = [underscore(c) for c in full_schedule.columns] full_schedule = full_schedule.rename({"week_value": "week", "season_type_value": "season_type"}) return full_schedule.to_pandas() if return_as_pandas else full_schedule @@ -163,25 +156,25 @@ def __ondays_nfl_calendar(season, **kwargs): url = f"https://sports.core.api.espn.com/v2/sports/football/leagues/nfl/seasons/{season}/types/2/calendar/ondays" resp = download(url=url, **kwargs) if resp is not None: - txt = resp.json().get('eventDate').get('dates') - result = pl.DataFrame(txt, schema=['dates']) - result = result.with_columns(dateURL = pl.col('dates').str.slice(0, 10)) + txt = resp.json().get("eventDate").get("dates") + result = pl.DataFrame(txt, schema=["dates"]) + result = result.with_columns(dateURL=pl.col("dates").str.slice(0, 10)) result = result.with_columns( - url="http://site.api.espn.com/apis/site/v2/sports/football/nfl/scoreboard?dates=" - + pl.col('dateURL') + url="http://site.api.espn.com/apis/site/v2/sports/football/nfl/scoreboard?dates=" + pl.col("dateURL") ) return result + def most_recent_nfl_season(): today = datetime.datetime.now() current_year = today.year current_month = today.month - current_day = today.day + # current_day = today.day return current_year if current_month >= 9 else current_year - 1 -def get_current_week(): +def get_current_week(): # Find first Monday of September in current season week1_sep = pd.to_datetime([f"{most_recent_nfl_season()}-09-0{num}" for num in range(1, 8)]).to_series() monday1_sep = week1_sep[week1_sep.dt.dayofweek == 0] @@ -195,4 +188,4 @@ def get_current_week(): # hardcoded week bounds because this whole date based thing has assumptions anyway current_week = max(current_week, 1) - return min(current_week, 22) \ No newline at end of file + return min(current_week, 22) diff --git a/sportsdataverse/nfl/nfl_teams.py b/sportsdataverse/nfl/nfl_teams.py index c57e7e5..ae6a122 100755 --- a/sportsdataverse/nfl/nfl_teams.py +++ b/sportsdataverse/nfl/nfl_teams.py @@ -1,9 +1,9 @@ import pandas as pd import polars as pl -import json from sportsdataverse.dl_utils import download, underscore -def espn_nfl_teams(return_as_pandas = True, **kwargs) -> pd.DataFrame: + +def espn_nfl_teams(return_as_pandas=True, **kwargs) -> pd.DataFrame: """espn_nfl_teams - look up NFL teams Args: @@ -17,19 +17,16 @@ def espn_nfl_teams(return_as_pandas = True, **kwargs) -> pd.DataFrame: """ url = "http://site.api.espn.com/apis/site/v2/sports/football/nfl/teams" - params = { - "limit": 1000 - } - resp = download(url=url, params = params, **kwargs) + params = {"limit": 1000} + resp = download(url=url, params=params, **kwargs) if resp is not None: events_txt = resp.json() - teams = events_txt.get('sports')[0].get('leagues')[0].get('teams') - del_keys = ['record', 'links'] + teams = events_txt.get("sports")[0].get("leagues")[0].get("teams") + del_keys = ["record", "links"] for team in teams: for k in del_keys: - team.get('team').pop(k, None) - teams = pd.json_normalize(teams, sep='_') + team.get("team").pop(k, None) + teams = pd.json_normalize(teams, sep="_") teams.columns = [underscore(c) for c in teams.columns.tolist()] return teams if return_as_pandas else pl.from_pandas(teams) - diff --git a/sportsdataverse/nhl/nhl_api.py b/sportsdataverse/nhl/nhl_api.py index 43a2c87..acaef3a 100755 --- a/sportsdataverse/nhl/nhl_api.py +++ b/sportsdataverse/nhl/nhl_api.py @@ -3,7 +3,7 @@ import numpy as np import re import json -from typing import List, Callable, Iterator, Union, Optional, Dict +from typing import Dict from sportsdataverse.dl_utils import download, flatten_json_iterative, key_check @@ -25,18 +25,18 @@ def nhl_api_pbp(game_id: int, **kwargs) -> Dict: summary_url = f"https://statsapi.web.nhl.com/api/v1/game/{game_id}/feed/live?site=en_nhl" summary_resp = download(summary_url, **kwargs) summary = summary_resp.json() - pbp_txt = {'datetime': summary.get("gameData").get("datetime")} - pbp_txt['game'] = summary.get("gameData").get("game") - pbp_txt['players'] = summary.get("gameData").get("players") - pbp_txt['status'] = summary.get("gameData").get("status") - pbp_txt['teams'] = summary.get("gameData").get("teams") - pbp_txt['venues'] = summary.get("gameData").get("venues") - pbp_txt['gameId'] = summary.get("gameData").get("gamePk") - pbp_txt['gameLink'] = summary.get("gameData").get("link") + pbp_txt = {"datetime": summary.get("gameData").get("datetime")} + pbp_txt["game"] = summary.get("gameData").get("game") + pbp_txt["players"] = summary.get("gameData").get("players") + pbp_txt["status"] = summary.get("gameData").get("status") + pbp_txt["teams"] = summary.get("gameData").get("teams") + pbp_txt["venues"] = summary.get("gameData").get("venues") + pbp_txt["gameId"] = summary.get("gameData").get("gamePk") + pbp_txt["gameLink"] = summary.get("gameData").get("link") return pbp_txt -def nhl_api_schedule(start_date: str, end_date: str, return_as_pandas = True) -> pd.DataFrame: +def nhl_api_schedule(start_date: str, end_date: str, return_as_pandas=True) -> pd.DataFrame: """nhl_api_schedule() - Pull the game by id. Data from API endpoints - `nhl/schedule` Args: @@ -50,16 +50,12 @@ def nhl_api_schedule(start_date: str, end_date: str, return_as_pandas = True) -> """ # summary endpoint for pickcenter array summary_url = "https://statsapi.web.nhl.com/api/v1/schedule" - params = { - "site": "en_nhl", - "startDate": start_date, - "endDate": end_date - } - summary_resp = download(summary_url, params = params, **kwargs) + params = {"site": "en_nhl", "startDate": start_date, "endDate": end_date} + summary_resp = download(summary_url, params=params, **kwargs) summary = summary_resp.json() - pbp_txt = {'dates': summary.get("dates")} + pbp_txt = {"dates": summary.get("dates")} pbp_txt_games = pl.DataFrame() - for date in pbp_txt['dates']: + for date in pbp_txt["dates"]: game = pl.from_pandas(pd.json_normalize(date, record_path="games", meta=["date"])) - pbp_txt_games = pl.concat([pbp_txt_games, game], how = 'vertical') - return pbp_txt_games.to_pandas() if return_as_pandas else pbp_txt_games \ No newline at end of file + pbp_txt_games = pl.concat([pbp_txt_games, game], how="vertical") + return pbp_txt_games.to_pandas() if return_as_pandas else pbp_txt_games diff --git a/sportsdataverse/nhl/nhl_game_rosters.py b/sportsdataverse/nhl/nhl_game_rosters.py index 3353207..05fcad3 100644 --- a/sportsdataverse/nhl/nhl_game_rosters.py +++ b/sportsdataverse/nhl/nhl_game_rosters.py @@ -1,13 +1,9 @@ import pandas as pd import polars as pl -import numpy as np -import os -import json -import re -from typing import List, Callable, Iterator, Union, Optional, Dict from sportsdataverse.dl_utils import download, underscore -def espn_nhl_game_rosters(game_id: int, raw = False, return_as_pandas = True, **kwargs) -> pd.DataFrame: + +def espn_nhl_game_rosters(game_id: int, raw=False, return_as_pandas=True, **kwargs) -> pd.DataFrame: """espn_nhl_game_rosters() - Pull the game by id. Args: @@ -36,163 +32,150 @@ def espn_nhl_game_rosters(game_id: int, raw = False, return_as_pandas = True, ** Example: `nhl_df = sportsdataverse.nhl.espn_nhl_game_rosters(game_id=401247153)` """ - # play by play - pbp_txt = {} # summary endpoint for pickcenter array - summary_url = "https://sports.core.api.espn.com/v2/sports/hockey/leagues/nhl/events/{x}/competitions/{x}/competitors".format(x=game_id) + summary_url = ( + "https://sports.core.api.espn.com/v2/sports/hockey/leagues/nhl/events/{x}/competitions/{x}/competitors".format( + x=game_id + ) + ) summary_resp = download(summary_url, **kwargs) summary = summary_resp.json() items = helper_nhl_game_items(summary) - team_rosters = helper_nhl_roster_items(items = items, summary_url = summary_url, **kwargs) - team_rosters = team_rosters.join(items[['team_id', 'order', 'home_away', 'winner']], how = 'left', on = 'team_id') - teams_df = helper_nhl_team_items(items = items, **kwargs) - teams_rosters = team_rosters.join(teams_df, how = 'left',on = 'team_id') - athletes = helper_nhl_athlete_items(teams_rosters = team_rosters, **kwargs) - rosters = athletes.join(teams_rosters, how = 'left', left_on = 'athlete_id', right_on = 'player_id') - rosters = rosters.with_columns( - game_id = pl.lit(game_id).cast(pl.Int32) - ) + team_rosters = helper_nhl_roster_items(items=items, summary_url=summary_url, **kwargs) + team_rosters = team_rosters.join(items[["team_id", "order", "home_away", "winner"]], how="left", on="team_id") + teams_df = helper_nhl_team_items(items=items, **kwargs) + teams_rosters = team_rosters.join(teams_df, how="left", on="team_id") + athletes = helper_nhl_athlete_items(teams_rosters=team_rosters, **kwargs) + rosters = athletes.join(teams_rosters, how="left", left_on="athlete_id", right_on="player_id") + rosters = rosters.with_columns(game_id=pl.lit(game_id).cast(pl.Int32)) rosters.columns = [underscore(c) for c in rosters.columns] return rosters.to_pandas() if return_as_pandas else rosters + def helper_nhl_game_items(summary): - items = pl.from_pandas(pd.json_normalize(summary, record_path = "items", sep = '_')) + items = pl.from_pandas(pd.json_normalize(summary, record_path="items", sep="_")) items.columns = [col.replace("$ref", "href") for col in items.columns] items.columns = [underscore(c) for c in items.columns] - items = items.rename({ - "id": "team_id", - "uid": "team_uid", - "statistics_href": "team_statistics_href" - } - ) - items = items.with_columns( - team_id = pl.col("team_id").cast(pl.Int32) - ) + items = items.rename({"id": "team_id", "uid": "team_uid", "statistics_href": "team_statistics_href"}) + items = items.with_columns(team_id=pl.col("team_id").cast(pl.Int32)) return items + def helper_nhl_team_items(items, **kwargs): pop_cols = [ - '$ref', - 'record', - 'athletes', - 'venue', - 'groups', - 'ranks', - 'statistics', - 'leaders', - 'links', - 'notes', - 'againstTheSpreadRecords', - 'franchise', - 'events', - 'college', - 'depthCharts', - 'transactions', - 'awards', - 'injuries', - 'coaches', - 'ranks', - 'attendance' + "$ref", + "record", + "athletes", + "venue", + "groups", + "ranks", + "statistics", + "leaders", + "links", + "notes", + "againstTheSpreadRecords", + "franchise", + "events", + "college", + "depthCharts", + "transactions", + "awards", + "injuries", + "coaches", + "ranks", + "attendance", ] teams_df = pl.DataFrame() - for x in items['team_href']: + for x in items["team_href"]: team = download(x, **kwargs).json() for k in pop_cols: team.pop(k, None) - team_row = pl.from_pandas(pd.json_normalize(team, sep = '_')) - teams_df = pl.concat([teams_df, team_row], how = 'vertical') + team_row = pl.from_pandas(pd.json_normalize(team, sep="_")) + teams_df = pl.concat([teams_df, team_row], how="vertical") teams_df.columns = [ - 'team_id', - 'team_guid', - 'team_uid', - 'team_slug', - 'team_location', - 'team_name', - 'team_nickname', - 'team_abbreviation', - 'team_display_name', - 'team_short_display_name', - 'team_color', - 'team_alternate_color', - 'is_active', - 'is_all_star', - 'logos', - 'team_alternate_ids_sdr' + "team_id", + "team_guid", + "team_uid", + "team_slug", + "team_location", + "team_name", + "team_nickname", + "team_abbreviation", + "team_display_name", + "team_short_display_name", + "team_color", + "team_alternate_color", + "is_active", + "is_all_star", + "logos", + "team_alternate_ids_sdr", ] - teams_df = teams_df.with_columns( - logo_href = pl.lit(""), - logo_dark_href = pl.lit("") - ) - for row in range(len(teams_df['logos'])): - team = teams_df['logos'][row] - teams_df[row, 'logo_href'] = team[0]['href'] - teams_df[row, 'logo_dark_href'] = team[1]['href'] - - teams_df = teams_df.drop(['logos']) - teams_df = teams_df.with_columns( - team_id = pl.col("team_id").cast(pl.Int32) - ) + teams_df = teams_df.with_columns(logo_href=pl.lit(""), logo_dark_href=pl.lit("")) + for row in range(len(teams_df["logos"])): + team = teams_df["logos"][row] + teams_df[row, "logo_href"] = team[0]["href"] + teams_df[row, "logo_dark_href"] = team[1]["href"] + + teams_df = teams_df.drop(["logos"]) + teams_df = teams_df.with_columns(team_id=pl.col("team_id").cast(pl.Int32)) return teams_df + def helper_nhl_roster_items(items, summary_url, **kwargs): - team_ids = list(items['team_id']) + team_ids = list(items["team_id"]) game_rosters = pl.DataFrame() for tm in team_ids: - team_roster_url = "{x}/{t}/roster".format(x = summary_url, t = tm) + team_roster_url = "{x}/{t}/roster".format(x=summary_url, t=tm) team_roster_resp = download(team_roster_url, **kwargs) - team_roster = pl.from_pandas(pd.json_normalize(team_roster_resp.json().get('entries',[]), sep = '_')) + team_roster = pl.from_pandas(pd.json_normalize(team_roster_resp.json().get("entries", []), sep="_")) team_roster.columns = [col.replace("$ref", "href") for col in team_roster.columns] team_roster.columns = [underscore(c) for c in team_roster.columns] - team_roster= team_roster.with_columns( - team_id = pl.lit(tm).cast(pl.Int32) - ) - game_rosters = pl.concat([game_rosters, team_roster], how = 'diagonal') + team_roster = team_roster.with_columns(team_id=pl.lit(tm).cast(pl.Int32)) + game_rosters = pl.concat([game_rosters, team_roster], how="diagonal") game_rosters = game_rosters.with_columns( - player_id = pl.col('player_id').cast(pl.Int64), - team_id = pl.col('team_id').cast(pl.Int32) + player_id=pl.col("player_id").cast(pl.Int64), team_id=pl.col("team_id").cast(pl.Int32) ) return game_rosters + def helper_nhl_athlete_items(teams_rosters, **kwargs): - athlete_hrefs = list(teams_rosters['athlete_href']) + athlete_hrefs = list(teams_rosters["athlete_href"]) game_athletes = pl.DataFrame() pop_cols = [ - 'links', - 'injuries', - 'teams', - 'team', - 'college', - 'proAthlete', - 'statistics', - 'notes', - 'eventLog', + "links", + "injuries", + "teams", + "team", + "college", + "proAthlete", + "statistics", + "notes", + "eventLog", "$ref", - "position" + "position", ] for athlete_href in athlete_hrefs: - athlete_res = download(athlete_href, **kwargs) athlete_resp = athlete_res.json() for k in pop_cols: athlete_resp.pop(k, None) - athlete = pl.from_pandas(pd.json_normalize(athlete_resp, sep='_')) + athlete = pl.from_pandas(pd.json_normalize(athlete_resp, sep="_")) athlete.columns = [col.replace("$ref", "href") for col in athlete.columns] athlete.columns = [underscore(c) for c in athlete.columns] - game_athletes = pl.concat([game_athletes, athlete], how = 'diagonal') + game_athletes = pl.concat([game_athletes, athlete], how="diagonal") - - game_athletes = game_athletes.rename({ - "id": "athlete_id", - "uid": "athlete_uid", - "guid": "athlete_guid", - "type": "athlete_type", - "display_name": "athlete_display_name" - }) - game_athletes = game_athletes.with_columns( - athlete_id = pl.col("athlete_id").cast(pl.Int64) + game_athletes = game_athletes.rename( + { + "id": "athlete_id", + "uid": "athlete_uid", + "guid": "athlete_guid", + "type": "athlete_type", + "display_name": "athlete_display_name", + } ) - return game_athletes \ No newline at end of file + game_athletes = game_athletes.with_columns(athlete_id=pl.col("athlete_id").cast(pl.Int64)) + return game_athletes diff --git a/sportsdataverse/nhl/nhl_loaders.py b/sportsdataverse/nhl/nhl_loaders.py index 75f3d1d..73c57ff 100755 --- a/sportsdataverse/nhl/nhl_loaders.py +++ b/sportsdataverse/nhl/nhl_loaders.py @@ -1,13 +1,18 @@ import pandas as pd import polars as pl -import json from tqdm import tqdm -from typing import List, Callable, Iterator, Union, Optional -from sportsdataverse.config import NHL_BASE_URL, NHL_TEAM_BOX_URL, NHL_TEAM_SCHEDULE_URL, NHL_TEAM_LOGO_URL, NHL_PLAYER_BOX_URL +from typing import List +from sportsdataverse.config import ( + NHL_BASE_URL, + NHL_TEAM_BOX_URL, + NHL_TEAM_SCHEDULE_URL, + NHL_TEAM_LOGO_URL, + NHL_PLAYER_BOX_URL, +) from sportsdataverse.errors import SeasonNotFoundError -from sportsdataverse.dl_utils import download -def load_nhl_pbp(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: + +def load_nhl_pbp(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: """Load NHL play by play data going back to 2011 Example: @@ -30,10 +35,11 @@ def load_nhl_pbp(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: if int(i) < 2011: raise SeasonNotFoundError("season cannot be less than 2011") i_data = pl.read_parquet(NHL_BASE_URL.format(season=i), use_pyarrow=True, columns=None) - data = pl.concat([data, i_data], how = 'vertical') - return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data + data = pl.concat([data, i_data], how="vertical") + return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data + -def load_nhl_schedule(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: +def load_nhl_schedule(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: """Load NHL schedule data Example: @@ -55,11 +61,12 @@ def load_nhl_schedule(seasons: List[int], return_as_pandas = True) -> pd.DataFra for i in tqdm(seasons): if int(i) < 2002: raise SeasonNotFoundError("season cannot be less than 2002") - i_data = pl.read_parquet(NHL_TEAM_SCHEDULE_URL.format(season = i), use_pyarrow=True, columns=None) - data = pl.concat([data, i_data], how = 'vertical') - return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data + i_data = pl.read_parquet(NHL_TEAM_SCHEDULE_URL.format(season=i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how="vertical") + return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data + -def load_nhl_team_boxscore(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: +def load_nhl_team_boxscore(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: """Load NHL team boxscore data Example: @@ -82,11 +89,12 @@ def load_nhl_team_boxscore(seasons: List[int], return_as_pandas = True) -> pd.Da for i in tqdm(seasons): if int(i) < 2011: raise SeasonNotFoundError("season cannot be less than 2011") - i_data = pl.read_parquet(NHL_TEAM_BOX_URL.format(season = i), use_pyarrow=True, columns=None) - data = pl.concat([data, i_data], how = 'vertical') - return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data + i_data = pl.read_parquet(NHL_TEAM_BOX_URL.format(season=i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how="vertical") + return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_nhl_player_boxscore(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: + +def load_nhl_player_boxscore(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: """Load NHL player boxscore data Example: @@ -109,11 +117,12 @@ def load_nhl_player_boxscore(seasons: List[int], return_as_pandas = True) -> pd. for i in tqdm(seasons): if int(i) < 2011: raise SeasonNotFoundError("season cannot be less than 2011") - i_data = pl.read_parquet(NHL_PLAYER_BOX_URL.format(season = i), use_pyarrow=True, columns=None) - data = pl.concat([data, i_data], how = 'vertical') - return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data + i_data = pl.read_parquet(NHL_PLAYER_BOX_URL.format(season=i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how="vertical") + return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data + -def nhl_teams(return_as_pandas = True) -> pd.DataFrame: +def nhl_teams(return_as_pandas=True) -> pd.DataFrame: """Load NHL team ID information and logos Example: @@ -125,4 +134,4 @@ def nhl_teams(return_as_pandas = True) -> pd.DataFrame: Returns: pd.DataFrame: Pandas dataframe containing teams available for the requested seasons. """ - return pl.read_csv(NHL_TEAM_LOGO_URL).to_pandas if return_as_pandas else pl.read_csv(NHL_TEAM_LOGO_URL) \ No newline at end of file + return pl.read_csv(NHL_TEAM_LOGO_URL).to_pandas if return_as_pandas else pl.read_csv(NHL_TEAM_LOGO_URL) diff --git a/sportsdataverse/nhl/nhl_pbp.py b/sportsdataverse/nhl/nhl_pbp.py index 9e5a0b4..d907b38 100755 --- a/sportsdataverse/nhl/nhl_pbp.py +++ b/sportsdataverse/nhl/nhl_pbp.py @@ -4,11 +4,11 @@ import os import json import re -from typing import List, Callable, Iterator, Union, Optional, Dict +from typing import Dict from sportsdataverse.dl_utils import download, flatten_json_iterative, key_check -def espn_nhl_pbp(game_id: int, raw = False, **kwargs) -> Dict: +def espn_nhl_pbp(game_id: int, raw=False, **kwargs) -> Dict: """espn_nhl_pbp() - Pull the game by id. Data from API endpoints - `nhl/playbyplay`, `nhl/summary` Args: @@ -26,21 +26,47 @@ def espn_nhl_pbp(game_id: int, raw = False, **kwargs) -> Dict: summary_url = f"http://site.api.espn.com/apis/site/v2/sports/hockey/nhl/summary?event={game_id}" summary_resp = download(summary_url, **kwargs) summary = summary_resp.json() - for k in ['plays', 'seasonseries', 'videos', 'broadcasts', 'pickcenter', - 'onIce', 'againstTheSpread', 'odds', 'winprobability', - 'teamInfo', 'espnWP', 'leaders']: - pbp_txt[k]=key_check(obj=summary, key = k, replacement = np.array([])) - for k in ['boxscore','format', 'gameInfo', 'article', - 'header', 'season', 'standings']: - pbp_txt[k] = key_check(obj=summary, key = k, replacement = {}) - for k in ['news','shop']: + for k in [ + "plays", + "seasonseries", + "videos", + "broadcasts", + "pickcenter", + "onIce", + "againstTheSpread", + "odds", + "winprobability", + "teamInfo", + "espnWP", + "leaders", + ]: + pbp_txt[k] = key_check(obj=summary, key=k, replacement=np.array([])) + for k in ["boxscore", "format", "gameInfo", "article", "header", "season", "standings"]: + pbp_txt[k] = key_check(obj=summary, key=k, replacement={}) + for k in ["news", "shop"]: if k in pbp_txt.keys(): del pbp_txt[k] incoming_keys_expected = [ - 'boxscore', 'format', 'gameInfo', 'leaders', 'seasonseries', - 'broadcasts', 'pickcenter', 'againstTheSpread', 'odds', - 'winprobability', 'header', 'onIce', 'article', 'videos', - 'plays', 'standings', 'teamInfo', 'espnWP', 'season', 'timeouts' + "boxscore", + "format", + "gameInfo", + "leaders", + "seasonseries", + "broadcasts", + "pickcenter", + "againstTheSpread", + "odds", + "winprobability", + "header", + "onIce", + "article", + "videos", + "plays", + "standings", + "teamInfo", + "espnWP", + "season", + "timeouts", ] if raw == True: # reorder keys in raw format, appending empty keys which are defined later to the end @@ -53,80 +79,82 @@ def espn_nhl_pbp(game_id: int, raw = False, **kwargs) -> Dict: return pbp_json return helper_nhl_pbp(game_id, pbp_txt) + def nhl_pbp_disk(game_id, path_to_json): with open(os.path.join(path_to_json, f"{game_id}.json")) as json_file: pbp_txt = json.load(json_file) return pbp_txt -def helper_nhl_pbp(game_id, pbp_txt): +def helper_nhl_pbp(game_id, pbp_txt): init = helper_nhl_pickcenter(pbp_txt) pbp_txt, init = helper_nhl_game_data(pbp_txt, init) - if (pbp_txt['playByPlaySource'] != "none") & (len(pbp_txt['plays'])>1): + if (pbp_txt["playByPlaySource"] != "none") & (len(pbp_txt["plays"]) > 1): pbp_txt = helper_nhl_pbp_features(game_id, pbp_txt, init) else: - pbp_txt['plays'] = pl.DataFrame() + pbp_txt["plays"] = pl.DataFrame() return { "gameId": game_id, - "plays": pbp_txt['plays'].to_dicts(), - "boxscore": pbp_txt['boxscore'], - "header": pbp_txt['header'], - "format": pbp_txt['format'], - "broadcasts": np.array(pbp_txt['broadcasts']).tolist(), - "videos": np.array(pbp_txt['videos']).tolist(), - "playByPlaySource": pbp_txt['playByPlaySource'], - "standings": pbp_txt['standings'], - "leaders": np.array(pbp_txt['leaders']).tolist(), - "seasonseries": np.array(pbp_txt['seasonseries']).tolist(), - "pickcenter": np.array(pbp_txt['pickcenter']).tolist(), - "againstTheSpread": np.array(pbp_txt['againstTheSpread']).tolist(), - "odds": np.array(pbp_txt['odds']).tolist(), - "onIce": np.array(pbp_txt['onIce']).tolist(), - "gameInfo": pbp_txt['gameInfo'], - "teamInfo": np.array(pbp_txt['teamInfo']).tolist(), - "season": np.array(pbp_txt['season']).tolist(), + "plays": pbp_txt["plays"].to_dicts(), + "boxscore": pbp_txt["boxscore"], + "header": pbp_txt["header"], + "format": pbp_txt["format"], + "broadcasts": np.array(pbp_txt["broadcasts"]).tolist(), + "videos": np.array(pbp_txt["videos"]).tolist(), + "playByPlaySource": pbp_txt["playByPlaySource"], + "standings": pbp_txt["standings"], + "leaders": np.array(pbp_txt["leaders"]).tolist(), + "seasonseries": np.array(pbp_txt["seasonseries"]).tolist(), + "pickcenter": np.array(pbp_txt["pickcenter"]).tolist(), + "againstTheSpread": np.array(pbp_txt["againstTheSpread"]).tolist(), + "odds": np.array(pbp_txt["odds"]).tolist(), + "onIce": np.array(pbp_txt["onIce"]).tolist(), + "gameInfo": pbp_txt["gameInfo"], + "teamInfo": np.array(pbp_txt["teamInfo"]).tolist(), + "season": np.array(pbp_txt["season"]).tolist(), } + def helper_nhl_game_data(pbp_txt, init): - pbp_txt['timeouts'] = {} - pbp_txt['teamInfo'] = pbp_txt['header']['competitions'][0] - pbp_txt['season'] = pbp_txt['header']['season'] - pbp_txt['playByPlaySource'] = pbp_txt['header']['competitions'][0]['playByPlaySource'] - pbp_txt['boxscoreSource'] = pbp_txt['header']['competitions'][0]['boxscoreSource'] - pbp_txt['gameSpreadAvailable'] = init["gameSpreadAvailable"] - pbp_txt['gameSpread'] = init["gameSpread"] + pbp_txt["timeouts"] = {} + pbp_txt["teamInfo"] = pbp_txt["header"]["competitions"][0] + pbp_txt["season"] = pbp_txt["header"]["season"] + pbp_txt["playByPlaySource"] = pbp_txt["header"]["competitions"][0]["playByPlaySource"] + pbp_txt["boxscoreSource"] = pbp_txt["header"]["competitions"][0]["boxscoreSource"] + pbp_txt["gameSpreadAvailable"] = init["gameSpreadAvailable"] + pbp_txt["gameSpread"] = init["gameSpread"] pbp_txt["homeFavorite"] = init["homeFavorite"] pbp_txt["homeTeamSpread"] = np.where( init["homeFavorite"] == True, abs(init["gameSpread"]), -1 * abs(init["gameSpread"]) ) pbp_txt["overUnder"] = init["overUnder"] # Home and Away identification variables - if pbp_txt['header']['competitions'][0]['competitors'][0]['homeAway']=='home': - pbp_txt['header']['competitions'][0]['home'] = pbp_txt['header']['competitions'][0]['competitors'][0]['team'] - homeTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['id']) - homeTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['name']) - homeTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['location']) - homeTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['abbreviation']) + if pbp_txt["header"]["competitions"][0]["competitors"][0]["homeAway"] == "home": + pbp_txt["header"]["competitions"][0]["home"] = pbp_txt["header"]["competitions"][0]["competitors"][0]["team"] + homeTeamId = int(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["id"]) + homeTeamMascot = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["name"]) + homeTeamName = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["location"]) + homeTeamAbbrev = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["abbreviation"]) homeTeamNameAlt = re.sub("Stat(.+)", "St", homeTeamName) - pbp_txt['header']['competitions'][0]['away'] = pbp_txt['header']['competitions'][0]['competitors'][1]['team'] - awayTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['id']) - awayTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['name']) - awayTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['location']) - awayTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['abbreviation']) + pbp_txt["header"]["competitions"][0]["away"] = pbp_txt["header"]["competitions"][0]["competitors"][1]["team"] + awayTeamId = int(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["id"]) + awayTeamMascot = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["name"]) + awayTeamName = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["location"]) + awayTeamAbbrev = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["abbreviation"]) awayTeamNameAlt = re.sub("Stat(.+)", "St", awayTeamName) else: - pbp_txt['header']['competitions'][0]['away'] = pbp_txt['header']['competitions'][0]['competitors'][0]['team'] - awayTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['id']) - awayTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['name']) - awayTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['location']) - awayTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['abbreviation']) + pbp_txt["header"]["competitions"][0]["away"] = pbp_txt["header"]["competitions"][0]["competitors"][0]["team"] + awayTeamId = int(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["id"]) + awayTeamMascot = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["name"]) + awayTeamName = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["location"]) + awayTeamAbbrev = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["abbreviation"]) awayTeamNameAlt = re.sub("Stat(.+)", "St", awayTeamName) - pbp_txt['header']['competitions'][0]['home'] = pbp_txt['header']['competitions'][0]['competitors'][1]['team'] - homeTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['id']) - homeTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['name']) - homeTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['location']) - homeTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['abbreviation']) + pbp_txt["header"]["competitions"][0]["home"] = pbp_txt["header"]["competitions"][0]["competitors"][1]["team"] + homeTeamId = int(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["id"]) + homeTeamMascot = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["name"]) + homeTeamName = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["location"]) + homeTeamAbbrev = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["abbreviation"]) homeTeamNameAlt = re.sub("Stat(.+)", "St", homeTeamName) init["homeTeamId"] = homeTeamId init["homeTeamMascot"] = homeTeamMascot @@ -140,18 +168,19 @@ def helper_nhl_game_data(pbp_txt, init): init["awayTeamNameAlt"] = awayTeamNameAlt return pbp_txt, init + def helper_nhl_pickcenter(pbp_txt): # Spread definition - if len(pbp_txt.get("pickcenter",[])) > 1: - homeFavorite = pbp_txt.get("pickcenter",{})[0].get("homeTeamOdds",{}).get("favorite","") - if "spread" in pbp_txt.get("pickcenter",{})[1].keys(): - gameSpread = pbp_txt.get("pickcenter",{})[1].get("spread","") - overUnder = pbp_txt.get("pickcenter",{})[1].get("overUnder","") + if len(pbp_txt.get("pickcenter", [])) > 1: + homeFavorite = pbp_txt.get("pickcenter", {})[0].get("homeTeamOdds", {}).get("favorite", "") + if "spread" in pbp_txt.get("pickcenter", {})[1].keys(): + gameSpread = pbp_txt.get("pickcenter", {})[1].get("spread", "") + overUnder = pbp_txt.get("pickcenter", {})[1].get("overUnder", "") else: - gameSpread = pbp_txt.get("pickcenter",{})[0].get("spread","") - overUnder = pbp_txt.get("pickcenter",{})[0].get("overUnder","") + gameSpread = pbp_txt.get("pickcenter", {})[0].get("spread", "") + overUnder = pbp_txt.get("pickcenter", {})[0].get("overUnder", "") gameSpreadAvailable = True - # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") + # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") else: gameSpread = 2.5 overUnder = 190.5 @@ -165,57 +194,67 @@ def helper_nhl_pickcenter(pbp_txt): "gameSpreadAvailable": gameSpreadAvailable, } + def helper_nhl_pbp_features(game_id, pbp_txt, init): - pbp_txt['plays_mod'] = [] - for play in pbp_txt['plays']: + pbp_txt["plays_mod"] = [] + for play in pbp_txt["plays"]: p = flatten_json_iterative(play) - pbp_txt['plays_mod'].append(p) - pbp_txt['plays'] = pl.from_pandas(pd.json_normalize(pbp_txt,'plays_mod')) - pbp_txt['plays'] = pbp_txt['plays'].with_columns( - game_id = pl.lit(game_id).cast(pl.Int32), - id = (pl.col('id').cast(pl.Int64)), - season = pl.lit(pbp_txt['header']['season']['year']).cast(pl.Int32), - seasonType = pl.lit(pbp_txt['header']['season']['type']), - homeTeamId = pl.lit(init["homeTeamId"]).cast(pl.Int32), - homeTeamName = pl.lit(init["homeTeamName"]), - homeTeamMascot = pl.lit(init["homeTeamMascot"]), - homeTeamAbbrev = pl.lit(init["homeTeamAbbrev"]), - homeTeamNameAlt = pl.lit(init["homeTeamNameAlt"]), - awayTeamId = pl.lit(init["awayTeamId"]).cast(pl.Int32), - awayTeamName = pl.lit(init["awayTeamName"]), - awayTeamMascot = pl.lit(init["awayTeamMascot"]), - awayTeamAbbrev = pl.lit(init["awayTeamAbbrev"]), - awayTeamNameAlt = pl.lit(init["awayTeamNameAlt"]), - gameSpread = pl.lit(init["gameSpread"]).abs(), - homeFavorite = pl.lit(init["homeFavorite"]), - gameSpreadAvailable = pl.lit(init["gameSpreadAvailable"]), - ).with_columns( - homeTeamSpread = pl.when(pl.col('homeFavorite') == True) - .then(pl.col('gameSpread')) - .otherwise(-1*pl.col('gameSpread')), - ).with_columns( - pl.col("period.number").cast(pl.Int32).alias("period.number"), - pl.col("period.number").cast(pl.Int32).alias("qtr"), - pl.when(pl.col("clock.displayValue").str.contains(r":") == False) - .then('0:'+ pl.col("clock.displayValue")) + pbp_txt["plays_mod"].append(p) + pbp_txt["plays"] = pl.from_pandas(pd.json_normalize(pbp_txt, "plays_mod")) + pbp_txt["plays"] = ( + pbp_txt["plays"] + .with_columns( + game_id=pl.lit(game_id).cast(pl.Int32), + id=(pl.col("id").cast(pl.Int64)), + season=pl.lit(pbp_txt["header"]["season"]["year"]).cast(pl.Int32), + seasonType=pl.lit(pbp_txt["header"]["season"]["type"]), + homeTeamId=pl.lit(init["homeTeamId"]).cast(pl.Int32), + homeTeamName=pl.lit(init["homeTeamName"]), + homeTeamMascot=pl.lit(init["homeTeamMascot"]), + homeTeamAbbrev=pl.lit(init["homeTeamAbbrev"]), + homeTeamNameAlt=pl.lit(init["homeTeamNameAlt"]), + awayTeamId=pl.lit(init["awayTeamId"]).cast(pl.Int32), + awayTeamName=pl.lit(init["awayTeamName"]), + awayTeamMascot=pl.lit(init["awayTeamMascot"]), + awayTeamAbbrev=pl.lit(init["awayTeamAbbrev"]), + awayTeamNameAlt=pl.lit(init["awayTeamNameAlt"]), + gameSpread=pl.lit(init["gameSpread"]).abs(), + homeFavorite=pl.lit(init["homeFavorite"]), + gameSpreadAvailable=pl.lit(init["gameSpreadAvailable"]), + ) + .with_columns( + homeTeamSpread=pl.when(pl.col("homeFavorite") == True) + .then(pl.col("gameSpread")) + .otherwise(-1 * pl.col("gameSpread")), + ) + .with_columns( + pl.col("period.number").cast(pl.Int32).alias("period.number"), + pl.col("period.number").cast(pl.Int32).alias("qtr"), + pl.when(pl.col("clock.displayValue").str.contains(r":") == False) + .then("0:" + pl.col("clock.displayValue")) .otherwise(pl.col("clock.displayValue")) .alias("clock.displayValue"), - ).with_columns( - pl.col("clock.displayValue").alias("time"), - pl.col("clock.displayValue").str.split(':').list.to_struct(n_field_strategy="first_non_null").alias("clock.mm") - ).with_columns( - pl.col("clock.mm").struct.rename_fields(["clock.minutes", "clock.seconds"]) - ).unnest( - "clock.mm" - ).with_columns( - pl.col("clock.minutes").cast(pl.Float32), - pl.col("clock.seconds").cast(pl.Float32), - ).with_columns( - lag_period = pl.col("period.number").shift(1), - lead_period = pl.col("period.number").shift(-1), - ).with_columns( - (60 * pl.col("clock.minutes") + pl.col("clock.seconds")).alias("start.period_seconds_remaining"), - pl.when(pl.col("period.number") == 1) + ) + .with_columns( + pl.col("clock.displayValue").alias("time"), + pl.col("clock.displayValue") + .str.split(":") + .list.to_struct(n_field_strategy="first_non_null") + .alias("clock.mm"), + ) + .with_columns(pl.col("clock.mm").struct.rename_fields(["clock.minutes", "clock.seconds"])) + .unnest("clock.mm") + .with_columns( + pl.col("clock.minutes").cast(pl.Float32), + pl.col("clock.seconds").cast(pl.Float32), + ) + .with_columns( + lag_period=pl.col("period.number").shift(1), + lead_period=pl.col("period.number").shift(-1), + ) + .with_columns( + (60 * pl.col("clock.minutes") + pl.col("clock.seconds")).alias("start.period_seconds_remaining"), + pl.when(pl.col("period.number") == 1) .then(2400 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) .when(pl.col("period.number") == 2) .then(1200 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) @@ -223,40 +262,36 @@ def helper_nhl_pbp_features(game_id, pbp_txt, init): .then(60 * pl.col("clock.minutes") + pl.col("clock.seconds")) .otherwise(60 * pl.col("clock.minutes") + pl.col("clock.seconds")) .alias("start.game_seconds_remaining"), - ).with_columns( - pl.col("start.period_seconds_remaining").shift(-1).alias("end.period_seconds_remaining"), - pl.col("start.game_seconds_remaining").shift(-1).alias("end.game_seconds_remaining"), + ) + .with_columns( + pl.col("start.period_seconds_remaining").shift(-1).alias("end.period_seconds_remaining"), + pl.col("start.game_seconds_remaining").shift(-1).alias("end.game_seconds_remaining"), + ) ) pbp_txt["plays"] = pbp_txt["plays"].with_row_count("game_play_number", 1) pbp_txt["plays"] = pbp_txt["plays"].with_columns( pl.when(pl.col("game_play_number") == 1) - .then(1200) - .when((pl.col("lag_period") == 1) - .and_(pl.col("period.number") == 2)) - .then(1200) - .when((pl.col("lag_period") == 2) - .and_(pl.col("period.number") == 3)) - .then(1200) - .when((pl.col("lag_period") == pl.col("period.number") - 1) - .and_(pl.col("period.number") >= 4)) - .then(1200) - .otherwise(pl.col("end.period_seconds_remaining")) - .alias("end.period_seconds_remaining"), + .then(1200) + .when((pl.col("lag_period") == 1).and_(pl.col("period.number") == 2)) + .then(1200) + .when((pl.col("lag_period") == 2).and_(pl.col("period.number") == 3)) + .then(1200) + .when((pl.col("lag_period") == pl.col("period.number") - 1).and_(pl.col("period.number") >= 4)) + .then(1200) + .otherwise(pl.col("end.period_seconds_remaining")) + .alias("end.period_seconds_remaining"), pl.when(pl.col("game_play_number") == 1) - .then(3600) - .when((pl.col("lag_period") == 1) - .and_(pl.col("period.number") == 2)) - .then(2400) - .when((pl.col("lag_period") == 2) - .and_(pl.col("period.number") == 3)) - .then(1200) - .when((pl.col("lag_period") == pl.col("period.number") - 1) - .and_(pl.col("period.number") >= 4)) - .then(1200) - .otherwise(pl.col("end.game_seconds_remaining")) - .alias("end.game_seconds_remaining"), + .then(3600) + .when((pl.col("lag_period") == 1).and_(pl.col("period.number") == 2)) + .then(2400) + .when((pl.col("lag_period") == 2).and_(pl.col("period.number") == 3)) + .then(1200) + .when((pl.col("lag_period") == pl.col("period.number") - 1).and_(pl.col("period.number") >= 4)) + .then(1200) + .otherwise(pl.col("end.game_seconds_remaining")) + .alias("end.game_seconds_remaining"), ) - return pbp_txt \ No newline at end of file + return pbp_txt diff --git a/sportsdataverse/nhl/nhl_schedule.py b/sportsdataverse/nhl/nhl_schedule.py index 2fe5408..8aae4f2 100755 --- a/sportsdataverse/nhl/nhl_schedule.py +++ b/sportsdataverse/nhl/nhl_schedule.py @@ -1,11 +1,10 @@ import pandas as pd import polars as pl -import json import datetime from sportsdataverse.dl_utils import download, underscore -def espn_nhl_schedule(dates=None, season_type=None, limit=500, - return_as_pandas = True, **kwargs) -> pd.DataFrame: + +def espn_nhl_schedule(dates=None, season_type=None, limit=500, return_as_pandas=True, **kwargs) -> pd.DataFrame: """espn_nhl_schedule - look up the NHL schedule for a given date Args: @@ -21,91 +20,102 @@ def espn_nhl_schedule(dates=None, season_type=None, limit=500, """ url = "http://site.api.espn.com/apis/site/v2/sports/hockey/nhl/scoreboard" - params = { - 'dates': dates, - 'seasonType': season_type, - 'limit': limit - } + params = {"dates": dates, "seasonType": season_type, "limit": limit} resp = download(url=url, params=params, **kwargs) ev = pl.DataFrame() events_txt = resp.json() - events = events_txt.get('events') + events = events_txt.get("events") if len(events) == 0: return pd.DataFrame() if return_as_pandas else pl.DataFrame() for event in events: - event.get('competitions')[0].get('competitors')[0].get('team').pop('links',None) - event.get('competitions')[0].get('competitors')[1].get('team').pop('links',None) - if event.get('competitions')[0].get('competitors')[0].get('homeAway') == 'home': - event = __extract_home_away(event, 0, 'home') - event = __extract_home_away(event, 1, 'away') + event.get("competitions")[0].get("competitors")[0].get("team").pop("links", None) + event.get("competitions")[0].get("competitors")[1].get("team").pop("links", None) + if event.get("competitions")[0].get("competitors")[0].get("homeAway") == "home": + event = __extract_home_away(event, 0, "home") + event = __extract_home_away(event, 1, "away") else: - event = __extract_home_away(event, 0, 'away') - event = __extract_home_away(event, 1, 'home') - - event.get('competitions')[0]['notes_type'] = event.get('competitions')[0]['notes'][0].get("type") if len(event.get('competitions')[0]['notes']) > 0 else '' - event.get('competitions')[0]['notes_headline'] = event.get('competitions')[0]['notes'][0].get("headline").replace('"','') if len(event.get('competitions')[0]['notes']) > 0 else '' - event.get('competitions')[0]['broadcast_market'] = event.get('competitions')[0].get('broadcasts', [])[0].get('market', "") if len(event.get('competitions')[0].get('broadcasts')) > 0 else "" - event.get('competitions')[0]['broadcast_name'] = event.get('competitions')[0].get('broadcasts', [])[0].get('names', [])[0] if len(event.get('competitions')[0].get('broadcasts')) > 0 else "" - del_keys = ['geoBroadcasts', 'headlines', 'series', 'situation', 'tickets', - 'odds', 'broadcasts', 'notes', 'competitors'] + event = __extract_home_away(event, 0, "away") + event = __extract_home_away(event, 1, "home") + + event.get("competitions")[0]["notes_type"] = ( + event.get("competitions")[0]["notes"][0].get("type") + if len(event.get("competitions")[0]["notes"]) > 0 + else "" + ) + event.get("competitions")[0]["notes_headline"] = ( + event.get("competitions")[0]["notes"][0].get("headline").replace('"', "") + if len(event.get("competitions")[0]["notes"]) > 0 + else "" + ) + event.get("competitions")[0]["broadcast_market"] = ( + event.get("competitions")[0].get("broadcasts", [])[0].get("market", "") + if len(event.get("competitions")[0].get("broadcasts")) > 0 + else "" + ) + event.get("competitions")[0]["broadcast_name"] = ( + event.get("competitions")[0].get("broadcasts", [])[0].get("names", [])[0] + if len(event.get("competitions")[0].get("broadcasts")) > 0 + else "" + ) + del_keys = [ + "geoBroadcasts", + "headlines", + "series", + "situation", + "tickets", + "odds", + "broadcasts", + "notes", + "competitors", + ] for k in del_keys: - event.get('competitions')[0].pop(k, None) + event.get("competitions")[0].pop(k, None) - x = pl.from_pandas(pd.json_normalize(event.get('competitions')[0], sep = '_')) + x = pl.from_pandas(pd.json_normalize(event.get("competitions")[0], sep="_")) x = x.with_columns( - game_id = (pl.col('id').cast(pl.Int32)), - season = (event.get('season').get('year')), - season_type = (event.get('season').get('type')), - home_linescores = pl.when(pl.col('status_type_description') == 'Postponed') - .then(None) - .otherwise(pl.col('home_linescores')), - away_linescores = pl.when(pl.col('status_type_description') == 'Postponed') - .then(None) - .otherwise(pl.col('away_linescores')), + game_id=(pl.col("id").cast(pl.Int32)), + season=(event.get("season").get("year")), + season_type=(event.get("season").get("type")), + home_linescores=pl.when(pl.col("status_type_description") == "Postponed") + .then(None) + .otherwise(pl.col("home_linescores")), + away_linescores=pl.when(pl.col("status_type_description") == "Postponed") + .then(None) + .otherwise(pl.col("away_linescores")), ) x = x[[s.name for s in x if s.null_count() != x.height]] - ev = pl.concat([ev, x], how = 'diagonal') + ev = pl.concat([ev, x], how="diagonal") ev.columns = [underscore(c) for c in ev.columns] return ev.to_pandas() if return_as_pandas else ev + def __extract_home_away(event, arg1, arg2): - event['competitions'][0][arg2] = ( - event.get('competitions')[0].get('competitors')[arg1].get('team') - ) - event['competitions'][0][arg2]['score'] = ( - event.get('competitions')[0].get('competitors')[arg1].get('score') - ) - event['competitions'][0][arg2]['winner'] = ( - event.get('competitions')[0].get('competitors')[arg1].get('winner') - ) - ## add winner back to main competitors if does not exist - event['competitions'][0]['competitors'][arg1]['winner'] = ( - event.get('competitions')[0].get('competitors')[arg1].get('winner', False) + event["competitions"][0][arg2] = event.get("competitions")[0].get("competitors")[arg1].get("team") + event["competitions"][0][arg2]["score"] = event.get("competitions")[0].get("competitors")[arg1].get("score") + event["competitions"][0][arg2]["winner"] = event.get("competitions")[0].get("competitors")[arg1].get("winner") + # add winner back to main competitors if does not exist + event["competitions"][0]["competitors"][arg1]["winner"] = ( + event.get("competitions")[0].get("competitors")[arg1].get("winner", False) ) - event['competitions'][0][arg2]['linescores'] = ( - event.get('competitions')[0] - .get('competitors')[arg1] - .get('linescores', [{'value': 'N/A'}]) + event["competitions"][0][arg2]["linescores"] = ( + event.get("competitions")[0].get("competitors")[arg1].get("linescores", [{"value": "N/A"}]) ) - ## add linescores back to main competitors if does not exist - event['competitions'][0]['competitors'][arg1]['linescores'] = ( - event.get('competitions')[0] - .get('competitors')[arg1] - .get('linescores', []) + # add linescores back to main competitors if does not exist + event["competitions"][0]["competitors"][arg1]["linescores"] = ( + event.get("competitions")[0].get("competitors")[arg1].get("linescores", []) ) - event['competitions'][0][arg2]['records'] = ( - event.get('competitions')[0] - .get('competitors')[arg1] - .get('records', []) + event["competitions"][0][arg2]["records"] = ( + event.get("competitions")[0].get("competitors")[arg1].get("records", []) ) return event -def espn_nhl_calendar(season=None, ondays=None, return_as_pandas = True, **kwargs) -> pd.DataFrame: + +def espn_nhl_calendar(season=None, ondays=None, return_as_pandas=True, **kwargs) -> pd.DataFrame: """espn_nhl_calendar - look up the NHL calendar for a given season Args: @@ -125,37 +135,36 @@ def espn_nhl_calendar(season=None, ondays=None, return_as_pandas = True, **kwarg else: url = f"http://site.api.espn.com/apis/site/v2/sports/hockey/nhl/scoreboard?dates={season}" resp = download(url=url, **kwargs) - txt = resp.json().get('leagues')[0].get('calendar') - datenum = list(map(lambda x: x[:10].replace("-",""),txt)) - date = list(map(lambda x: x[:10],txt)) - year = list(map(lambda x: x[:4],txt)) - month = list(map(lambda x: x[5:7],txt)) - day = list(map(lambda x: x[8:10],txt)) + txt = resp.json().get("leagues")[0].get("calendar") + datenum = list(map(lambda x: x[:10].replace("-", ""), txt)) + date = list(map(lambda x: x[:10], txt)) + year = list(map(lambda x: x[:4], txt)) + month = list(map(lambda x: x[5:7], txt)) + day = list(map(lambda x: x[8:10], txt)) data = { "season": season, - "datetime" : txt, - "date" : date, + "datetime": txt, + "date": date, "year": year, "month": month, "day": day, - "dateURL": datenum + "dateURL": datenum, } full_schedule = pl.DataFrame(data) full_schedule = full_schedule.with_columns( - url="http://site.api.espn.com/apis/site/v2/sports/hockey/nhl/scoreboard?dates=" + - pl.col('dateURL') + url="http://site.api.espn.com/apis/site/v2/sports/hockey/nhl/scoreboard?dates=" + pl.col("dateURL") ) return full_schedule.to_pandas() if return_as_pandas else full_schedule + def __ondays_nhl_calendar(season, **kwargs): url = f"https://sports.core.api.espn.com/v2/sports/hockey/leagues/nhl/seasons/{season}/types/2/calendar/ondays" resp = download(url=url, **kwargs) - txt = resp.json().get('eventDate').get('dates') - result = pl.DataFrame(txt, schema=['dates']) - result = result.with_columns(dateURL = pl.col('dates').str.slice(0, 10)) + txt = resp.json().get("eventDate").get("dates") + result = pl.DataFrame(txt, schema=["dates"]) + result = result.with_columns(dateURL=pl.col("dates").str.slice(0, 10)) result = result.with_columns( - url="http://site.api.espn.com/apis/site/v2/sports/hockey/nhl/scoreboard?dates=" + - pl.col('dateURL') + url="http://site.api.espn.com/apis/site/v2/sports/hockey/nhl/scoreboard?dates=" + pl.col("dateURL") ) return result @@ -167,6 +176,7 @@ def most_recent_nhl_season(): else: return int(str(datetime.date.today())[:4]) + def year_to_season(year): first_year = str(year)[2:4] next_year = int(first_year) + 1 @@ -176,4 +186,4 @@ def year_to_season(year): next_year_formatted = "00" else: next_year_formatted = str(next_year) - return f"{year}-{next_year_formatted}" \ No newline at end of file + return f"{year}-{next_year_formatted}" diff --git a/sportsdataverse/nhl/nhl_teams.py b/sportsdataverse/nhl/nhl_teams.py index 89dfe40..27c9676 100755 --- a/sportsdataverse/nhl/nhl_teams.py +++ b/sportsdataverse/nhl/nhl_teams.py @@ -1,9 +1,9 @@ import pandas as pd import polars as pl -import json from sportsdataverse.dl_utils import download, underscore -def espn_nhl_teams(return_as_pandas = True, **kwargs) -> pd.DataFrame: + +def espn_nhl_teams(return_as_pandas=True, **kwargs) -> pd.DataFrame: """espn_nhl_teams - look up NHL teams Args: @@ -17,19 +17,16 @@ def espn_nhl_teams(return_as_pandas = True, **kwargs) -> pd.DataFrame: """ url = "http://site.api.espn.com/apis/site/v2/sports/hockey/nhl/teams" - params = { - "limit": 1000 - } - resp = download(url=url, params = params, **kwargs) + params = {"limit": 1000} + resp = download(url=url, params=params, **kwargs) if resp is not None: events_txt = resp.json() - teams = events_txt.get('sports')[0].get('leagues')[0].get('teams') - del_keys = ['record', 'links'] + teams = events_txt.get("sports")[0].get("leagues")[0].get("teams") + del_keys = ["record", "links"] for team in teams: for k in del_keys: - team.get('team').pop(k, None) - teams = pd.json_normalize(teams, sep='_') + team.get("team").pop(k, None) + teams = pd.json_normalize(teams, sep="_") teams.columns = [underscore(c) for c in teams.columns.tolist()] return teams if return_as_pandas else pl.from_pandas(teams) - diff --git a/sportsdataverse/wbb/wbb_game_rosters.py b/sportsdataverse/wbb/wbb_game_rosters.py index 95cd6a0..b100c2c 100644 --- a/sportsdataverse/wbb/wbb_game_rosters.py +++ b/sportsdataverse/wbb/wbb_game_rosters.py @@ -1,13 +1,9 @@ import pandas as pd import polars as pl -import numpy as np -import os -import json -import re -from typing import List, Callable, Iterator, Union, Optional, Dict from sportsdataverse.dl_utils import download, underscore -def espn_wbb_game_rosters(game_id: int, raw = False, return_as_pandas = True, **kwargs) -> pd.DataFrame: + +def espn_wbb_game_rosters(game_id: int, raw=False, return_as_pandas=True, **kwargs) -> pd.DataFrame: """espn_wbb_game_rosters() - Pull the game by id. Args: @@ -36,158 +32,143 @@ def espn_wbb_game_rosters(game_id: int, raw = False, return_as_pandas = True, ** Example: `wbb_df = sportsdataverse.wbb.espn_wbb_game_rosters(game_id=401266534)` """ - # play by play - pbp_txt = {} # summary endpoint for pickcenter array - summary_url = "https://sports.core.api.espn.com/v2/sports/basketball/leagues/womens-college-basketball/events/{x}/competitions/{x}/competitors".format(x=game_id) + summary_url = "https://sports.core.api.espn.com/v2/sports/basketball/leagues/womens-college-basketball/events/{x}/competitions/{x}/competitors".format( + x=game_id + ) summary_resp = download(summary_url, **kwargs) summary = summary_resp.json() items = helper_wbb_game_items(summary) - team_rosters = helper_wbb_roster_items(items = items, summary_url = summary_url, **kwargs) - team_rosters = team_rosters.join(items[['team_id', 'order', 'home_away', 'winner']], how = 'left', on = 'team_id') - teams_df = helper_wbb_team_items(items = items, **kwargs) - teams_rosters = team_rosters.join(teams_df, how = 'left',on = 'team_id') - athletes = helper_wbb_athlete_items(teams_rosters = team_rosters, **kwargs) - rosters = athletes.join(teams_rosters, how = 'left', left_on = 'athlete_id', right_on = 'player_id') - rosters = rosters.with_columns( - game_id = pl.lit(game_id).cast(pl.Int32) - ) + team_rosters = helper_wbb_roster_items(items=items, summary_url=summary_url, **kwargs) + team_rosters = team_rosters.join(items[["team_id", "order", "home_away", "winner"]], how="left", on="team_id") + teams_df = helper_wbb_team_items(items=items, **kwargs) + teams_rosters = team_rosters.join(teams_df, how="left", on="team_id") + athletes = helper_wbb_athlete_items(teams_rosters=team_rosters, **kwargs) + rosters = athletes.join(teams_rosters, how="left", left_on="athlete_id", right_on="player_id") + rosters = rosters.with_columns(game_id=pl.lit(game_id).cast(pl.Int32)) rosters.columns = [underscore(c) for c in rosters.columns] return rosters.to_pandas() if return_as_pandas else rosters + def helper_wbb_game_items(summary): - items = pl.from_pandas(pd.json_normalize(summary, record_path = "items", sep = '_')) + items = pl.from_pandas(pd.json_normalize(summary, record_path="items", sep="_")) items.columns = [col.replace("$ref", "href") for col in items.columns] items.columns = [underscore(c) for c in items.columns] - items = items.rename({ - "id": "team_id", - "uid": "team_uid", - "statistics_href": "team_statistics_href" - } - ) - items = items.with_columns( - team_id = pl.col("team_id").cast(pl.Int32) - ) + items = items.rename({"id": "team_id", "uid": "team_uid", "statistics_href": "team_statistics_href"}) + items = items.with_columns(team_id=pl.col("team_id").cast(pl.Int32)) return items + def helper_wbb_team_items(items, **kwargs): pop_cols = [ - '$ref', - 'record', - 'athletes', - 'venue', - 'groups', - 'ranks', - 'statistics', - 'leaders', - 'links', - 'notes', - 'againstTheSpreadRecords', - 'franchise', - 'events', - 'college' + "$ref", + "record", + "athletes", + "venue", + "groups", + "ranks", + "statistics", + "leaders", + "links", + "notes", + "againstTheSpreadRecords", + "franchise", + "events", + "college", ] teams_df = pl.DataFrame() - for x in items['team_href']: + for x in items["team_href"]: team = download(x, **kwargs).json() for k in pop_cols: team.pop(k, None) - team_row = pl.from_pandas(pd.json_normalize(team, sep = '_')) - teams_df = pl.concat([teams_df, team_row], how = 'vertical') + team_row = pl.from_pandas(pd.json_normalize(team, sep="_")) + teams_df = pl.concat([teams_df, team_row], how="vertical") teams_df.columns = [ - 'team_id', - 'team_guid', - 'team_uid', - 'team_slug', - 'team_location', - 'team_name', - 'team_nickname', - 'team_abbreviation', - 'team_display_name', - 'team_short_display_name', - 'team_color', - 'team_alternate_color', - 'is_active', - 'is_all_star', - 'logos', - 'team_alternate_ids_sdr' + "team_id", + "team_guid", + "team_uid", + "team_slug", + "team_location", + "team_name", + "team_nickname", + "team_abbreviation", + "team_display_name", + "team_short_display_name", + "team_color", + "team_alternate_color", + "is_active", + "is_all_star", + "logos", + "team_alternate_ids_sdr", ] - teams_df = teams_df.with_columns( - logo_href = pl.lit(""), - logo_dark_href = pl.lit("") - ) + teams_df = teams_df.with_columns(logo_href=pl.lit(""), logo_dark_href=pl.lit("")) - for row in range(len(teams_df['logos'])): - team = teams_df['logos'][row] - teams_df[row, 'logo_href'] = team[0]['href'] - teams_df[row, 'logo_dark_href'] = team[1]['href'] + for row in range(len(teams_df["logos"])): + team = teams_df["logos"][row] + teams_df[row, "logo_href"] = team[0]["href"] + teams_df[row, "logo_dark_href"] = team[1]["href"] - teams_df = teams_df.drop(['logos']) - teams_df = teams_df.with_columns( - team_id = pl.col("team_id").cast(pl.Int32) - ) + teams_df = teams_df.drop(["logos"]) + teams_df = teams_df.with_columns(team_id=pl.col("team_id").cast(pl.Int32)) return teams_df + def helper_wbb_roster_items(items, summary_url, **kwargs): - team_ids = list(items['team_id']) + team_ids = list(items["team_id"]) game_rosters = pl.DataFrame() for tm in team_ids: - team_roster_url = "{x}/{t}/roster".format(x = summary_url, t = tm) + team_roster_url = "{x}/{t}/roster".format(x=summary_url, t=tm) team_roster_resp = download(team_roster_url, **kwargs) - team_roster = pl.from_pandas(pd.json_normalize(team_roster_resp.json().get('entries',[]), sep = '_')) + team_roster = pl.from_pandas(pd.json_normalize(team_roster_resp.json().get("entries", []), sep="_")) team_roster.columns = [col.replace("$ref", "href") for col in team_roster.columns] team_roster.columns = [underscore(c) for c in team_roster.columns] - team_roster= team_roster.with_columns( - team_id = pl.lit(tm).cast(pl.Int32) - ) - game_rosters = pl.concat([game_rosters, team_roster], how = 'vertical') + team_roster = team_roster.with_columns(team_id=pl.lit(tm).cast(pl.Int32)) + game_rosters = pl.concat([game_rosters, team_roster], how="vertical") game_rosters = game_rosters.drop(["period", "for_player_id", "active"]) game_rosters = game_rosters.with_columns( - player_id = pl.col('player_id').cast(pl.Int64), - team_id = pl.col('team_id').cast(pl.Int32) + player_id=pl.col("player_id").cast(pl.Int64), team_id=pl.col("team_id").cast(pl.Int32) ) return game_rosters + def helper_wbb_athlete_items(teams_rosters, **kwargs): - athlete_hrefs = list(teams_rosters['athlete_href']) + athlete_hrefs = list(teams_rosters["athlete_href"]) game_athletes = pl.DataFrame() pop_cols = [ - 'links', - 'injuries', - 'teams', - 'team', - 'college', - 'proAthlete', - 'statistics', - 'notes', - 'eventLog', + "links", + "injuries", + "teams", + "team", + "college", + "proAthlete", + "statistics", + "notes", + "eventLog", "$ref", - "position" + "position", ] for athlete_href in athlete_hrefs: - athlete_res = download(athlete_href, **kwargs) athlete_resp = athlete_res.json() for k in pop_cols: athlete_resp.pop(k, None) - athlete = pl.from_pandas(pd.json_normalize(athlete_resp, sep='_')) + athlete = pl.from_pandas(pd.json_normalize(athlete_resp, sep="_")) athlete.columns = [col.replace("$ref", "href") for col in athlete.columns] athlete.columns = [underscore(c) for c in athlete.columns] - game_athletes = pl.concat([game_athletes, athlete], how = 'diagonal') - + game_athletes = pl.concat([game_athletes, athlete], how="diagonal") - game_athletes = game_athletes.rename({ - "id": "athlete_id", - "uid": "athlete_uid", - "guid": "athlete_guid", - "type": "athlete_type", - "display_name": "athlete_display_name" - }) - game_athletes = game_athletes.with_columns( - athlete_id = pl.col("athlete_id").cast(pl.Int64) + game_athletes = game_athletes.rename( + { + "id": "athlete_id", + "uid": "athlete_uid", + "guid": "athlete_guid", + "type": "athlete_type", + "display_name": "athlete_display_name", + } ) - return game_athletes \ No newline at end of file + game_athletes = game_athletes.with_columns(athlete_id=pl.col("athlete_id").cast(pl.Int64)) + return game_athletes diff --git a/sportsdataverse/wbb/wbb_loaders.py b/sportsdataverse/wbb/wbb_loaders.py index b007076..bdf7cf0 100755 --- a/sportsdataverse/wbb/wbb_loaders.py +++ b/sportsdataverse/wbb/wbb_loaders.py @@ -1,13 +1,12 @@ import pandas as pd import polars as pl -import json from tqdm import tqdm -from typing import List, Callable, Iterator, Union, Optional +from typing import List from sportsdataverse.config import WBB_BASE_URL, WBB_TEAM_BOX_URL, WBB_PLAYER_BOX_URL, WBB_TEAM_SCHEDULE_URL from sportsdataverse.errors import SeasonNotFoundError -from sportsdataverse.dl_utils import download -def load_wbb_pbp(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: + +def load_wbb_pbp(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: """Load women's college basketball play by play data going back to 2002 Example: @@ -31,10 +30,11 @@ def load_wbb_pbp(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: if int(i) < 2002: raise SeasonNotFoundError("season cannot be less than 2002") i_data = pl.read_parquet(WBB_BASE_URL.format(season=i), use_pyarrow=True, columns=None) - data = pl.concat([data, i_data], how = 'vertical') - return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data + data = pl.concat([data, i_data], how="vertical") + return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data + -def load_wbb_team_boxscore(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: +def load_wbb_team_boxscore(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: """Load women's college basketball team boxscore data Example: @@ -57,11 +57,12 @@ def load_wbb_team_boxscore(seasons: List[int], return_as_pandas = True) -> pd.Da for i in tqdm(seasons): if int(i) < 2002: raise ValueError("season cannot be less than 2002") - i_data = pl.read_parquet(WBB_TEAM_BOX_URL.format(season = i), use_pyarrow=True, columns=None) - data = pl.concat([data, i_data], how = 'vertical') - return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data + i_data = pl.read_parquet(WBB_TEAM_BOX_URL.format(season=i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how="vertical") + return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_wbb_player_boxscore(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: + +def load_wbb_player_boxscore(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: """Load women's college basketball player boxscore data Example: @@ -84,11 +85,12 @@ def load_wbb_player_boxscore(seasons: List[int], return_as_pandas = True) -> pd. for i in tqdm(seasons): if int(i) < 2002: raise ValueError("season cannot be less than 2002") - i_data = pl.read_parquet(WBB_PLAYER_BOX_URL.format(season = i), use_pyarrow=True, columns=None) - data = pl.concat([data, i_data], how = 'vertical') - return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data + i_data = pl.read_parquet(WBB_PLAYER_BOX_URL.format(season=i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how="vertical") + return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data + -def load_wbb_schedule(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: +def load_wbb_schedule(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: """Load women's college basketball schedule data Example: @@ -111,6 +113,6 @@ def load_wbb_schedule(seasons: List[int], return_as_pandas = True) -> pd.DataFra for i in tqdm(seasons): if int(i) < 2002: raise ValueError("season cannot be less than 2002") - i_data = pl.read_parquet(WBB_TEAM_SCHEDULE_URL.format(season = i), use_pyarrow=True, columns=None) - data = pl.concat([data, i_data], how = 'vertical') - return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data + i_data = pl.read_parquet(WBB_TEAM_SCHEDULE_URL.format(season=i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how="vertical") + return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data diff --git a/sportsdataverse/wbb/wbb_pbp.py b/sportsdataverse/wbb/wbb_pbp.py index 6d0320f..2525a7e 100755 --- a/sportsdataverse/wbb/wbb_pbp.py +++ b/sportsdataverse/wbb/wbb_pbp.py @@ -4,44 +4,68 @@ import os import json import re -from typing import List, Callable, Iterator, Union, Optional, Dict -from sportsdataverse.dl_utils import download, flatten_json_iterative, key_check +from typing import Dict +from sportsdataverse.dl_utils import download, flatten_json_iterative -def espn_wbb_pbp(game_id: int, raw = False, **kwargs) -> Dict: - """espn_wbb_pbp() - Pull the game by id. Data from API endpoints - `womens-college-basketball/playbyplay`, `womens-college-basketball/summary` - Args: - game_id (int): Unique game_id, can be obtained from wbb_schedule(). - raw (bool): If True, returns the raw json from the API endpoint. If False, returns a cleaned dictionary of datasets. +def espn_wbb_pbp(game_id: int, raw=False, **kwargs) -> Dict: + """espn_wbb_pbp() - Pull the game by id. Data from API endpoints - `womens-college-basketball/playbyplay`, + `womens-college-basketball/summary` - Returns: - Dict: Dictionary of game data with keys - "gameId", "plays", "winprobability", "boxscore", "header", "broadcasts", - "videos", "playByPlaySource", "standings", "leaders", "timeouts", "pickcenter", "againstTheSpread", "odds", "predictor", - "espnWP", "gameInfo", "season" + Args: + game_id (int): Unique game_id, can be obtained from wbb_schedule(). + raw (bool): If True, returns the raw json from the API endpoint. If False, returns a cleaned dictionary of datasets. - Example: - `wbb_df = sportsdataverse.wb.espn_wbb_pbp(game_id=401266534)` + Returns: + Dict: Dictionary of game data with keys - "gameId", "plays", "winprobability", "boxscore", "header", + "broadcasts", "videos", "playByPlaySource", "standings", "leaders", "timeouts", "pickcenter", + "againstTheSpread", "odds", "predictor","espnWP", "gameInfo", "season" + + Example: + `wbb_df = sportsdataverse.wb.espn_wbb_pbp(game_id=401266534)` """ # play by play - pbp_txt = {'timeouts': {}} + pbp_txt = {"timeouts": {}} # summary endpoint for pickcenter array - summary_url = f"http://site.api.espn.com/apis/site/v2/sports/basketball/womens-college-basketball/summary?event={game_id}" + summary_url = ( + f"http://site.api.espn.com/apis/site/v2/sports/basketball/womens-college-basketball/summary?event={game_id}" + ) summary_resp = download(summary_url, **kwargs) summary = summary_resp.json() incoming_keys_expected = [ - 'boxscore', 'format', 'gameInfo', 'leaders', 'broadcasts', - 'predictor', 'pickcenter', 'againstTheSpread', 'odds', 'winprobability', - 'header', 'plays', 'article', 'videos', 'standings', - 'teamInfo', 'espnWP', 'season', 'timeouts' + "boxscore", + "format", + "gameInfo", + "leaders", + "broadcasts", + "predictor", + "pickcenter", + "againstTheSpread", + "odds", + "winprobability", + "header", + "plays", + "article", + "videos", + "standings", + "teamInfo", + "espnWP", + "season", + "timeouts", ] dict_keys_expected = [ - 'plays', 'videos', 'broadcasts', 'pickcenter', 'againstTheSpread', - 'odds', 'winprobability', 'teamInfo', 'espnWP', 'leaders' - ] - array_keys_expected = [ - 'boxscore','format', 'gameInfo', - 'predictor', 'article', 'header', 'season', 'standings' + "plays", + "videos", + "broadcasts", + "pickcenter", + "againstTheSpread", + "odds", + "winprobability", + "teamInfo", + "espnWP", + "leaders", ] + array_keys_expected = ["boxscore", "format", "gameInfo", "predictor", "article", "header", "season", "standings"] if raw == True: # reorder keys in raw format, appending empty keys which are defined later to the end pbp_json = {} @@ -58,92 +82,95 @@ def espn_wbb_pbp(game_id: int, raw = False, **kwargs) -> Dict: else: pbp_txt[k] = {} if k in dict_keys_expected else [] - for k in ['news','shop']: - pbp_txt.pop(f'{k}', None) + for k in ["news", "shop"]: + pbp_txt.pop(f"{k}", None) return helper_wbb_pbp(game_id, pbp_txt) + def mbb_pbp_disk(game_id, path_to_json): with open(os.path.join(path_to_json, f"{game_id}.json")) as json_file: pbp_txt = json.load(json_file) return pbp_txt + def helper_wbb_pbp(game_id, pbp_txt): init = helper_wbb_pickcenter(pbp_txt) pbp_txt, init = helper_wbb_game_data(pbp_txt, init) - if "plays" in pbp_txt.keys() and pbp_txt.get('header').get('competitions')[0].get('playByPlaySource') != 'none': + if "plays" in pbp_txt.keys() and pbp_txt.get("header").get("competitions")[0].get("playByPlaySource") != "none": pbp_txt = helper_wbb_pbp_features(game_id, pbp_txt, init) else: - pbp_txt['plays'] = pl.DataFrame() - pbp_txt['timeouts'] = { - homeTeamId: {"1": [], "2": []}, - awayTeamId: {"1": [], "2": []}, + pbp_txt["plays"] = pl.DataFrame() + pbp_txt["timeouts"] = { + init["homeTeamId"]: {"1": [], "2": []}, + init["awayTeamId"]: {"1": [], "2": []}, } # pbp_txt['plays'] = pbp_txt['plays'].replace({np.nan: None}) return { "gameId": int(game_id), - "plays": pbp_txt['plays'].to_dicts(), - "winprobability": np.array(pbp_txt['winprobability']).tolist(), - "boxscore": pbp_txt['boxscore'], - "header": pbp_txt['header'], - "format": pbp_txt['format'], - "broadcasts": np.array(pbp_txt['broadcasts']).tolist(), - "videos": np.array(pbp_txt['videos']).tolist(), - "playByPlaySource": pbp_txt['playByPlaySource'], - "standings": pbp_txt['standings'], - "article": pbp_txt['article'], - "leaders": np.array(pbp_txt['leaders']).tolist(), - "timeouts": pbp_txt['timeouts'], - "pickcenter": np.array(pbp_txt['pickcenter']).tolist(), - "againstTheSpread": np.array(pbp_txt['againstTheSpread']).tolist(), - "odds": np.array(pbp_txt['odds']).tolist(), - "predictor": pbp_txt['predictor'], - "espnWP": np.array(pbp_txt['espnWP']).tolist(), - "gameInfo": pbp_txt['gameInfo'], - "teamInfo": np.array(pbp_txt['teamInfo']).tolist(), - "season": np.array(pbp_txt['season']).tolist(), + "plays": pbp_txt["plays"].to_dicts(), + "winprobability": np.array(pbp_txt["winprobability"]).tolist(), + "boxscore": pbp_txt["boxscore"], + "header": pbp_txt["header"], + "format": pbp_txt["format"], + "broadcasts": np.array(pbp_txt["broadcasts"]).tolist(), + "videos": np.array(pbp_txt["videos"]).tolist(), + "playByPlaySource": pbp_txt["playByPlaySource"], + "standings": pbp_txt["standings"], + "article": pbp_txt["article"], + "leaders": np.array(pbp_txt["leaders"]).tolist(), + "timeouts": pbp_txt["timeouts"], + "pickcenter": np.array(pbp_txt["pickcenter"]).tolist(), + "againstTheSpread": np.array(pbp_txt["againstTheSpread"]).tolist(), + "odds": np.array(pbp_txt["odds"]).tolist(), + "predictor": pbp_txt["predictor"], + "espnWP": np.array(pbp_txt["espnWP"]).tolist(), + "gameInfo": pbp_txt["gameInfo"], + "teamInfo": np.array(pbp_txt["teamInfo"]).tolist(), + "season": np.array(pbp_txt["season"]).tolist(), } + def helper_wbb_game_data(pbp_txt, init): - pbp_txt['timeouts'] = {} - pbp_txt['teamInfo'] = pbp_txt['header']['competitions'][0] - pbp_txt['season'] = pbp_txt['header']['season'] - pbp_txt['playByPlaySource'] = pbp_txt['header']['competitions'][0]['playByPlaySource'] - pbp_txt['boxscoreSource'] = pbp_txt['header']['competitions'][0]['boxscoreSource'] - pbp_txt['gameSpreadAvailable'] = init["gameSpreadAvailable"] - pbp_txt['gameSpread'] = init["gameSpread"] + pbp_txt["timeouts"] = {} + pbp_txt["teamInfo"] = pbp_txt["header"]["competitions"][0] + pbp_txt["season"] = pbp_txt["header"]["season"] + pbp_txt["playByPlaySource"] = pbp_txt["header"]["competitions"][0]["playByPlaySource"] + pbp_txt["boxscoreSource"] = pbp_txt["header"]["competitions"][0]["boxscoreSource"] + pbp_txt["gameSpreadAvailable"] = init["gameSpreadAvailable"] + pbp_txt["gameSpread"] = init["gameSpread"] pbp_txt["homeFavorite"] = init["homeFavorite"] pbp_txt["homeTeamSpread"] = np.where( init["homeFavorite"] == True, abs(init["gameSpread"]), -1 * abs(init["gameSpread"]) ) pbp_txt["overUnder"] = init["overUnder"] # Home and Away identification variables - if pbp_txt['header']['competitions'][0]['competitors'][0]['homeAway']=='home': - pbp_txt['header']['competitions'][0]['home'] = pbp_txt['header']['competitions'][0]['competitors'][0]['team'] - homeTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['id']) - homeTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['name']) - homeTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['location']) - homeTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['abbreviation']) + if pbp_txt["header"]["competitions"][0]["competitors"][0]["homeAway"] == "home": + pbp_txt["header"]["competitions"][0]["home"] = pbp_txt["header"]["competitions"][0]["competitors"][0]["team"] + homeTeamId = int(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["id"]) + homeTeamMascot = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["name"]) + homeTeamName = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["location"]) + homeTeamAbbrev = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["abbreviation"]) homeTeamNameAlt = re.sub("Stat(.+)", "St", homeTeamName) - pbp_txt['header']['competitions'][0]['away'] = pbp_txt['header']['competitions'][0]['competitors'][1]['team'] - awayTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['id']) - awayTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['name']) - awayTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['location']) - awayTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['abbreviation']) + pbp_txt["header"]["competitions"][0]["away"] = pbp_txt["header"]["competitions"][0]["competitors"][1]["team"] + awayTeamId = int(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["id"]) + awayTeamMascot = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["name"]) + awayTeamName = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["location"]) + awayTeamAbbrev = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["abbreviation"]) awayTeamNameAlt = re.sub("Stat(.+)", "St", awayTeamName) else: - pbp_txt['header']['competitions'][0]['away'] = pbp_txt['header']['competitions'][0]['competitors'][0]['team'] - awayTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['id']) - awayTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['name']) - awayTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['location']) - awayTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['abbreviation']) + pbp_txt["header"]["competitions"][0]["away"] = pbp_txt["header"]["competitions"][0]["competitors"][0]["team"] + awayTeamId = int(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["id"]) + awayTeamMascot = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["name"]) + awayTeamName = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["location"]) + awayTeamAbbrev = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["abbreviation"]) awayTeamNameAlt = re.sub("Stat(.+)", "St", awayTeamName) - pbp_txt['header']['competitions'][0]['home'] = pbp_txt['header']['competitions'][0]['competitors'][1]['team'] - homeTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['id']) - homeTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['name']) - homeTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['location']) - homeTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['abbreviation']) + pbp_txt["header"]["competitions"][0]["home"] = pbp_txt["header"]["competitions"][0]["competitors"][1]["team"] + homeTeamId = int(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["id"]) + homeTeamMascot = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["name"]) + homeTeamName = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["location"]) + homeTeamAbbrev = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["abbreviation"]) homeTeamNameAlt = re.sub("Stat(.+)", "St", homeTeamName) init["homeTeamId"] = homeTeamId init["homeTeamMascot"] = homeTeamMascot @@ -157,18 +184,19 @@ def helper_wbb_game_data(pbp_txt, init): init["awayTeamNameAlt"] = awayTeamNameAlt return pbp_txt, init + def helper_wbb_pickcenter(pbp_txt): # Spread definition - if len(pbp_txt.get("pickcenter",[])) > 1: - homeFavorite = pbp_txt.get("pickcenter",{})[0].get("homeTeamOdds",{}).get("favorite","") - if "spread" in pbp_txt.get("pickcenter",{})[1].keys(): - gameSpread = pbp_txt.get("pickcenter",{})[1].get("spread","") - overUnder = pbp_txt.get("pickcenter",{})[1].get("overUnder","") + if len(pbp_txt.get("pickcenter", [])) > 1: + homeFavorite = pbp_txt.get("pickcenter", {})[0].get("homeTeamOdds", {}).get("favorite", "") + if "spread" in pbp_txt.get("pickcenter", {})[1].keys(): + gameSpread = pbp_txt.get("pickcenter", {})[1].get("spread", "") + overUnder = pbp_txt.get("pickcenter", {})[1].get("overUnder", "") else: - gameSpread = pbp_txt.get("pickcenter",{})[0].get("spread","") - overUnder = pbp_txt.get("pickcenter",{})[0].get("overUnder","") + gameSpread = pbp_txt.get("pickcenter", {})[0].get("spread", "") + overUnder = pbp_txt.get("pickcenter", {})[0].get("overUnder", "") gameSpreadAvailable = True - # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") + # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") else: gameSpread = 2.5 overUnder = 140.5 @@ -179,113 +207,127 @@ def helper_wbb_pickcenter(pbp_txt): "gameSpread": gameSpread, "overUnder": overUnder, "homeFavorite": homeFavorite, - "gameSpreadAvailable": gameSpreadAvailable + "gameSpreadAvailable": gameSpreadAvailable, } + def helper_wbb_pbp_features(game_id, pbp_txt, init): - pbp_txt['plays_mod'] = [] - for play in pbp_txt['plays']: + pbp_txt["plays_mod"] = [] + for play in pbp_txt["plays"]: p = flatten_json_iterative(play) - pbp_txt['plays_mod'].append(p) - pbp_txt['plays'] = pl.from_pandas(pd.json_normalize(pbp_txt,'plays_mod')) - pbp_txt['plays'] = pbp_txt['plays'].with_columns( - game_id = pl.lit(game_id).cast(pl.Int32), - id = (pl.col('id').cast(pl.Int64)), - season = pl.lit(pbp_txt['header']['season']['year']), - seasonType = pl.lit(pbp_txt['header']['season']['type']), - homeTeamId = pl.lit(init["homeTeamId"]), - homeTeamName = pl.lit(init["homeTeamName"]), - homeTeamMascot = pl.lit(init["homeTeamMascot"]), - homeTeamAbbrev = pl.lit(init["homeTeamAbbrev"]), - homeTeamNameAlt = pl.lit(init["homeTeamNameAlt"]), - awayTeamId = pl.lit(init["awayTeamId"]), - awayTeamName = pl.lit(init["awayTeamName"]), - awayTeamMascot = pl.lit(init["awayTeamMascot"]), - awayTeamAbbrev = pl.lit(init["awayTeamAbbrev"]), - awayTeamNameAlt = pl.lit(init["awayTeamNameAlt"]), - gameSpread = pl.lit(init["gameSpread"]).abs(), - homeFavorite = pl.lit(init["homeFavorite"]), - gameSpreadAvailable = pl.lit(init["gameSpreadAvailable"]), - ).with_columns( - homeTeamSpread = pl.when(pl.col('homeFavorite') == True) - .then(pl.col('gameSpread')) - .otherwise(-1*pl.col('gameSpread')), - ).with_columns( - pl.col("period.number").cast(pl.Int32).alias("period.number"), - pl.col("period.number").cast(pl.Int32).alias("qtr"), - pl.when(pl.col("clock.displayValue").str.contains(r":") == False) - .then('0:'+ pl.col("clock.displayValue")) + pbp_txt["plays_mod"].append(p) + pbp_txt["plays"] = pl.from_pandas(pd.json_normalize(pbp_txt, "plays_mod")) + pbp_txt["plays"] = ( + pbp_txt["plays"] + .with_columns( + game_id=pl.lit(game_id).cast(pl.Int32), + id=(pl.col("id").cast(pl.Int64)), + season=pl.lit(pbp_txt["header"]["season"]["year"]), + seasonType=pl.lit(pbp_txt["header"]["season"]["type"]), + homeTeamId=pl.lit(init["homeTeamId"]), + homeTeamName=pl.lit(init["homeTeamName"]), + homeTeamMascot=pl.lit(init["homeTeamMascot"]), + homeTeamAbbrev=pl.lit(init["homeTeamAbbrev"]), + homeTeamNameAlt=pl.lit(init["homeTeamNameAlt"]), + awayTeamId=pl.lit(init["awayTeamId"]), + awayTeamName=pl.lit(init["awayTeamName"]), + awayTeamMascot=pl.lit(init["awayTeamMascot"]), + awayTeamAbbrev=pl.lit(init["awayTeamAbbrev"]), + awayTeamNameAlt=pl.lit(init["awayTeamNameAlt"]), + gameSpread=pl.lit(init["gameSpread"]).abs(), + homeFavorite=pl.lit(init["homeFavorite"]), + gameSpreadAvailable=pl.lit(init["gameSpreadAvailable"]), + ) + .with_columns( + homeTeamSpread=pl.when(pl.col("homeFavorite") == True) + .then(pl.col("gameSpread")) + .otherwise(-1 * pl.col("gameSpread")), + ) + .with_columns( + pl.col("period.number").cast(pl.Int32).alias("period.number"), + pl.col("period.number").cast(pl.Int32).alias("qtr"), + pl.when(pl.col("clock.displayValue").str.contains(r":") == False) + .then("0:" + pl.col("clock.displayValue")) .otherwise(pl.col("clock.displayValue")) .alias("clock.displayValue"), - ).with_columns( - pl.col("clock.displayValue").alias("time"), - pl.col("clock.displayValue").str.split(':').list.to_struct(n_field_strategy="first_non_null").alias("clock.mm") - ).with_columns( - pl.col("clock.mm").struct.rename_fields(["clock.minutes", "clock.seconds"]) - ).unnest( - "clock.mm" - ).with_columns( - pl.col("clock.minutes").cast(pl.Float32), - pl.col("clock.seconds").cast(pl.Float32), - pl.when((pl.col("type.text") == "ShortTimeOut") - .and_( - pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamAbbrev"]).lower()) - .or_( - pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamAbbrev"]).lower()), - pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamName"]).lower()), - pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamMascot"]).lower()), - pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamNameAlt"]).lower()) + ) + .with_columns( + pl.col("clock.displayValue").alias("time"), + pl.col("clock.displayValue") + .str.split(":") + .list.to_struct(n_field_strategy="first_non_null") + .alias("clock.mm"), + ) + .with_columns(pl.col("clock.mm").struct.rename_fields(["clock.minutes", "clock.seconds"])) + .unnest("clock.mm") + .with_columns( + pl.col("clock.minutes").cast(pl.Float32), + pl.col("clock.seconds").cast(pl.Float32), + pl.when( + (pl.col("type.text") == "ShortTimeOut").and_( + pl.col("text") + .str.to_lowercase() + .str.contains(str(init["homeTeamAbbrev"]).lower()) + .or_( + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamAbbrev"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamName"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamMascot"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamNameAlt"]).lower()), + ) ) - )) - .then(True) - .otherwise(False) - .alias("homeTimeoutCalled"), - pl.when((pl.col("type.text") == "ShortTimeOut") - .and_( - pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamAbbrev"]).lower()) - .or_( - pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamAbbrev"]).lower()), - pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamName"]).lower()), - pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamMascot"]).lower()), - pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamNameAlt"]).lower()) + ) + .then(True) + .otherwise(False) + .alias("homeTimeoutCalled"), + pl.when( + (pl.col("type.text") == "ShortTimeOut").and_( + pl.col("text") + .str.to_lowercase() + .str.contains(str(init["awayTeamAbbrev"]).lower()) + .or_( + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamAbbrev"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamName"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamMascot"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamNameAlt"]).lower()), + ) ) - )) - .then(True) - .otherwise(False) - .alias("awayTimeoutCalled"), - ).with_columns( - half = pl.when(pl.col('qtr') <= 2) - .then(1) - .otherwise(2), - game_half = pl.when(pl.col('qtr') <= 2) - .then(1) - .otherwise(2), - ).with_columns( - lag_qtr = pl.col('qtr').shift(1), - lead_qtr = pl.col('qtr').shift(-1), - lag_half = pl.col('half').shift(1), - lead_half = pl.col('half').shift(-1), - ).with_columns( - (60 * pl.col("clock.minutes") + pl.col("clock.seconds")).alias("start.quarter_seconds_remaining"), - pl.when(pl.col('qtr').is_in([1,3])) + ) + .then(True) + .otherwise(False) + .alias("awayTimeoutCalled"), + ) + .with_columns( + half=pl.when(pl.col("qtr") <= 2).then(1).otherwise(2), + game_half=pl.when(pl.col("qtr") <= 2).then(1).otherwise(2), + ) + .with_columns( + lag_qtr=pl.col("qtr").shift(1), + lead_qtr=pl.col("qtr").shift(-1), + lag_half=pl.col("half").shift(1), + lead_half=pl.col("half").shift(-1), + ) + .with_columns( + (60 * pl.col("clock.minutes") + pl.col("clock.seconds")).alias("start.quarter_seconds_remaining"), + pl.when(pl.col("qtr").is_in([1, 3])) .then(600 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) .otherwise(60 * pl.col("clock.minutes") + pl.col("clock.seconds")) .alias("start.half_seconds_remaining"), - pl.when(pl.col('qtr') == 1) + pl.when(pl.col("qtr") == 1) .then(1800 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) - .when(pl.col('qtr') == 2) + .when(pl.col("qtr") == 2) .then(1200 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) - .when(pl.col('qtr') == 3) + .when(pl.col("qtr") == 3) .then(600 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) .otherwise(60 * pl.col("clock.minutes") + pl.col("clock.seconds")) .alias("start.game_seconds_remaining"), - ).with_columns( - pl.col("start.quarter_seconds_remaining").shift(-1).alias("end.quarter_seconds_remaining"), - pl.col("start.half_seconds_remaining").shift(-1).alias("end.half_seconds_remaining"), - pl.col("start.game_seconds_remaining").shift(-1).alias("end.game_seconds_remaining"), + ) + .with_columns( + pl.col("start.quarter_seconds_remaining").shift(-1).alias("end.quarter_seconds_remaining"), + pl.col("start.half_seconds_remaining").shift(-1).alias("end.half_seconds_remaining"), + pl.col("start.game_seconds_remaining").shift(-1).alias("end.game_seconds_remaining"), + ) ) - pbp_txt["plays"] = pbp_txt["plays"].with_row_count("game_play_number", 1) pbp_txt["timeouts"] = { init["homeTeamId"]: {"1": [], "2": []}, @@ -293,90 +335,67 @@ def helper_wbb_pbp_features(game_id, pbp_txt, init): } pbp_txt["timeouts"][init["homeTeamId"]]["1"] = ( pbp_txt["plays"] - .filter( - (pl.col("homeTimeoutCalled") == True) - .and_(pl.col("period.number") <= 2) - ) + .filter((pl.col("homeTimeoutCalled") == True).and_(pl.col("period.number") <= 2)) .get_column("id") .to_list() ) pbp_txt["timeouts"][init["homeTeamId"]]["2"] = ( pbp_txt["plays"] - .filter( - (pl.col("homeTimeoutCalled") == True) - .and_(pl.col("period.number") > 2) - ) + .filter((pl.col("homeTimeoutCalled") == True).and_(pl.col("period.number") > 2)) .get_column("id") .to_list() ) pbp_txt["timeouts"][init["awayTeamId"]]["1"] = ( pbp_txt["plays"] - .filter( - (pl.col("awayTimeoutCalled") == True) - .and_(pl.col("period.number") <= 2) - ) + .filter((pl.col("awayTimeoutCalled") == True).and_(pl.col("period.number") <= 2)) .get_column("id") .to_list() ) pbp_txt["timeouts"][init["awayTeamId"]]["2"] = ( pbp_txt["plays"] - .filter( - (pl.col("awayTimeoutCalled") == True) - .and_(pl.col("period.number") > 2) - ) + .filter((pl.col("awayTimeoutCalled") == True).and_(pl.col("period.number") > 2)) .get_column("id") .to_list() ) - # Pos Team - Start and End Id - pbp_txt['plays'] = pbp_txt['plays'].with_columns( - pl.when((pl.col("game_play_number") == 1) - .or_((pl.col("lag_qtr") == 1) - .and_(pl.col("period.number") == 2)) - .or_((pl.col("lag_qtr") == 2) - .and_(pl.col("period.number") == 3)) - .or_((pl.col("lag_qtr") == 3) - .and_(pl.col("period.number") == 4))) - .then(600) - .when((pl.col("lag_qtr") == (pl.col("period.number") - 1)) - .and_(pl.col("period.number") >= 5)) - .then(300) - .otherwise(pl.col("end.quarter_seconds_remaining")) - .alias("end.quarter_seconds_remaining"), + # Pos Team - Start and End Id + pbp_txt["plays"] = pbp_txt["plays"].with_columns( + pl.when( + (pl.col("game_play_number") == 1) + .or_((pl.col("lag_qtr") == 1).and_(pl.col("period.number") == 2)) + .or_((pl.col("lag_qtr") == 2).and_(pl.col("period.number") == 3)) + .or_((pl.col("lag_qtr") == 3).and_(pl.col("period.number") == 4)) + ) + .then(600) + .when((pl.col("lag_qtr") == (pl.col("period.number") - 1)).and_(pl.col("period.number") >= 5)) + .then(300) + .otherwise(pl.col("end.quarter_seconds_remaining")) + .alias("end.quarter_seconds_remaining"), pl.when((pl.col("game_play_number") == 1)) - .then(1200) - .when((pl.col("lag_half") == 1) - .and_(pl.col("half") == 2)) - .then(1200) - .when((pl.col("lag_qtr") == 1) - .and_(pl.col("period.number") == 2)) - .then(600) - .when((pl.col("lag_qtr") == 2) - .and_(pl.col("period.number") == 3)) - .then(1200) - .when((pl.col("lag_qtr") == 3) - .and_(pl.col("period.number") == 4)) - .then(600) - .when((pl.col("lag_qtr") == (pl.col("period.number") - 1)) - .and_(pl.col("period.number") >= 5)) - .then(300) - .otherwise(pl.col("end.half_seconds_remaining")) - .alias("end.half_seconds_remaining"), + .then(1200) + .when((pl.col("lag_half") == 1).and_(pl.col("half") == 2)) + .then(1200) + .when((pl.col("lag_qtr") == 1).and_(pl.col("period.number") == 2)) + .then(600) + .when((pl.col("lag_qtr") == 2).and_(pl.col("period.number") == 3)) + .then(1200) + .when((pl.col("lag_qtr") == 3).and_(pl.col("period.number") == 4)) + .then(600) + .when((pl.col("lag_qtr") == (pl.col("period.number") - 1)).and_(pl.col("period.number") >= 5)) + .then(300) + .otherwise(pl.col("end.half_seconds_remaining")) + .alias("end.half_seconds_remaining"), pl.when((pl.col("game_play_number") == 1)) - .then(2400) - .when((pl.col("lag_qtr") == 1) - .and_(pl.col("period.number") == 2)) - .then(1800) - .when((pl.col("lag_qtr") == 2) - .and_(pl.col("period.number") == 3)) - .then(1200) - .when((pl.col("lag_qtr") == 3) - .and_(pl.col("period.number") == 4)) - .then(600) - .when((pl.col("lag_qtr") == (pl.col("period.number") - 1)) - .and_(pl.col("period.number") >= 5)) - .then(300) - .otherwise(pl.col("end.game_seconds_remaining")) - .alias("end.game_seconds_remaining"), + .then(2400) + .when((pl.col("lag_qtr") == 1).and_(pl.col("period.number") == 2)) + .then(1800) + .when((pl.col("lag_qtr") == 2).and_(pl.col("period.number") == 3)) + .then(1200) + .when((pl.col("lag_qtr") == 3).and_(pl.col("period.number") == 4)) + .then(600) + .when((pl.col("lag_qtr") == (pl.col("period.number") - 1)).and_(pl.col("period.number") >= 5)) + .then(300) + .otherwise(pl.col("end.game_seconds_remaining")) + .alias("end.game_seconds_remaining"), pl.col("qtr").cast(pl.Int32).alias("period"), ) - return pbp_txt \ No newline at end of file + return pbp_txt diff --git a/sportsdataverse/wbb/wbb_schedule.py b/sportsdataverse/wbb/wbb_schedule.py index 5b8654b..ce34d75 100755 --- a/sportsdataverse/wbb/wbb_schedule.py +++ b/sportsdataverse/wbb/wbb_schedule.py @@ -1,12 +1,13 @@ import pandas as pd -import json +import polars as pl import datetime -from typing import List, Callable, Iterator, Union, Optional from sportsdataverse.errors import SeasonNotFoundError from sportsdataverse.dl_utils import download, underscore -def espn_wbb_schedule(dates=None, groups=50, season_type=None, limit=500, - return_as_pandas=True, **kwargs) -> pd.DataFrame: + +def espn_wbb_schedule( + dates=None, groups=50, season_type=None, limit=500, return_as_pandas=True, **kwargs +) -> pd.DataFrame: """espn_wbb_schedule - look up the women's college basketball schedule for a given season Args: @@ -21,93 +22,99 @@ def espn_wbb_schedule(dates=None, groups=50, season_type=None, limit=500, """ url = "http://site.api.espn.com/apis/site/v2/sports/basketball/womens-college-basketball/scoreboard" params = { - 'dates': dates, - 'seasonType': season_type, - 'groups': groups if groups is not None else '50', - 'limit': limit + "dates": dates, + "seasonType": season_type, + "groups": groups if groups is not None else "50", + "limit": limit, } resp = download(url=url, params=params, **kwargs) ev = pl.DataFrame() events_txt = resp.json() - events = events_txt.get('events') + events = events_txt.get("events") if len(events) == 0: return pd.DataFrame() if return_as_pandas else pl.DataFrame() for event in events: - event.get('competitions')[0].get('competitors')[0].get('team').pop('links',None) - event.get('competitions')[0].get('competitors')[1].get('team').pop('links',None) - if event.get('competitions')[0].get('competitors')[0].get('homeAway') == 'home': - event = __extract_home_away(event, 0, 'home') - event = __extract_home_away(event, 1, 'away') + event.get("competitions")[0].get("competitors")[0].get("team").pop("links", None) + event.get("competitions")[0].get("competitors")[1].get("team").pop("links", None) + if event.get("competitions")[0].get("competitors")[0].get("homeAway") == "home": + event = __extract_home_away(event, 0, "home") + event = __extract_home_away(event, 1, "away") else: - event = __extract_home_away(event, 0, 'away') - event = __extract_home_away(event, 1, 'home') - del_keys = ['geoBroadcasts', 'headlines', 'series', 'situation', 'tickets', 'odds'] + event = __extract_home_away(event, 0, "away") + event = __extract_home_away(event, 1, "home") + del_keys = ["geoBroadcasts", "headlines", "series", "situation", "tickets", "odds"] for k in del_keys: - event.get('competitions')[0].pop(k, None) - event.get('competitions')[0]['notes_type'] = event.get('competitions')[0]['notes'][0].get("type") if len(event.get('competitions')[0]['notes']) > 0 else '' - event.get('competitions')[0]['notes_headline'] = event.get('competitions')[0]['notes'][0].get("headline").replace('"','') if len(event.get('competitions')[0]['notes']) > 0 else '' - event.get('competitions')[0]['broadcast_market'] = event.get('competitions')[0].get('broadcasts', [])[0].get('market', "") if len(event.get('competitions')[0].get('broadcasts')) > 0 else "" - event.get('competitions')[0]['broadcast_name'] = event.get('competitions')[0].get('broadcasts', [])[0].get('names', [])[0] if len(event.get('competitions')[0].get('broadcasts')) > 0 else "" - event.get('competitions')[0].pop('broadcasts', None) - event.get('competitions')[0].pop('notes', None) - event.get('competitions')[0].pop('competitors', None) - x = pl.from_pandas(pd.json_normalize(event.get('competitions')[0], sep = '_')) + event.get("competitions")[0].pop(k, None) + event.get("competitions")[0]["notes_type"] = ( + event.get("competitions")[0]["notes"][0].get("type") + if len(event.get("competitions")[0]["notes"]) > 0 + else "" + ) + event.get("competitions")[0]["notes_headline"] = ( + event.get("competitions")[0]["notes"][0].get("headline").replace('"', "") + if len(event.get("competitions")[0]["notes"]) > 0 + else "" + ) + event.get("competitions")[0]["broadcast_market"] = ( + event.get("competitions")[0].get("broadcasts", [])[0].get("market", "") + if len(event.get("competitions")[0].get("broadcasts")) > 0 + else "" + ) + event.get("competitions")[0]["broadcast_name"] = ( + event.get("competitions")[0].get("broadcasts", [])[0].get("names", [])[0] + if len(event.get("competitions")[0].get("broadcasts")) > 0 + else "" + ) + event.get("competitions")[0].pop("broadcasts", None) + event.get("competitions")[0].pop("notes", None) + event.get("competitions")[0].pop("competitors", None) + x = pl.from_pandas(pd.json_normalize(event.get("competitions")[0], sep="_")) x = x.with_columns( - game_id = (pl.col('id').cast(pl.Int32)), - season = (event.get('season').get('year')), - season_type = (event.get('season').get('type')), - home_linescores = pl.when(pl.col('status_type_description') == 'Postponed').then(None).otherwise(pl.col('home_linescores')), - away_linescores = pl.when(pl.col('status_type_description') == 'Postponed').then(None).otherwise(pl.col('away_linescores')), + game_id=(pl.col("id").cast(pl.Int32)), + season=(event.get("season").get("year")), + season_type=(event.get("season").get("type")), + home_linescores=pl.when(pl.col("status_type_description") == "Postponed") + .then(None) + .otherwise(pl.col("home_linescores")), + away_linescores=pl.when(pl.col("status_type_description") == "Postponed") + .then(None) + .otherwise(pl.col("away_linescores")), ) x = x[[s.name for s in x if s.null_count() != x.height]] - ev = pl.concat([ev, x], how = 'diagonal') + ev = pl.concat([ev, x], how="diagonal") ev.columns = [underscore(c) for c in ev.columns] return ev.to_pandas() if return_as_pandas else ev + def __extract_home_away(event, arg1, arg2): - event['competitions'][0][arg2] = ( - event.get('competitions')[0].get('competitors')[arg1].get('team') + event["competitions"][0][arg2] = event.get("competitions")[0].get("competitors")[arg1].get("team") + event["competitions"][0][arg2]["score"] = event.get("competitions")[0].get("competitors")[arg1].get("score") + event["competitions"][0][arg2]["winner"] = event.get("competitions")[0].get("competitors")[arg1].get("winner") + # add winner back to main competitors if does not exist + event["competitions"][0]["competitors"][arg1]["winner"] = ( + event.get("competitions")[0].get("competitors")[arg1].get("winner", False) ) - event['competitions'][0][arg2]['score'] = ( - event.get('competitions')[0].get('competitors')[arg1].get('score') + event["competitions"][0][arg2]["currentRank"] = ( + event.get("competitions")[0].get("competitors")[arg1].get("curatedRank", {}).get("current", 99) ) - event['competitions'][0][arg2]['winner'] = ( - event.get('competitions')[0].get('competitors')[arg1].get('winner') + event["competitions"][0][arg2]["linescores"] = ( + event.get("competitions")[0].get("competitors")[arg1].get("linescores", [{"value": "N/A"}]) ) - ## add winner back to main competitors if does not exist - event['competitions'][0]['competitors'][arg1]['winner'] = ( - event.get('competitions')[0].get('competitors')[arg1].get('winner', False) + # add linescores back to main competitors if does not exist + event["competitions"][0]["competitors"][arg1]["linescores"] = ( + event.get("competitions")[0].get("competitors")[arg1].get("linescores", []) ) - event['competitions'][0][arg2]['currentRank'] = ( - event.get('competitions')[0] - .get('competitors')[arg1] - .get('curatedRank', {}) - .get('current', 99) - ) - event['competitions'][0][arg2]['linescores'] = ( - event.get('competitions')[0] - .get('competitors')[arg1] - .get('linescores', [{'value': 'N/A'}]) - ) - ## add linescores back to main competitors if does not exist - event['competitions'][0]['competitors'][arg1]['linescores'] = ( - event.get('competitions')[0] - .get('competitors')[arg1] - .get('linescores', []) - ) - event['competitions'][0][arg2]['records'] = ( - event.get('competitions')[0] - .get('competitors')[arg1] - .get('records', []) + event["competitions"][0][arg2]["records"] = ( + event.get("competitions")[0].get("competitors")[arg1].get("records", []) ) return event -def espn_wbb_calendar(season=None, ondays=None, - return_as_pandas = True, **kwargs) -> pd.DataFrame: + +def espn_wbb_calendar(season=None, ondays=None, return_as_pandas=True, **kwargs) -> pd.DataFrame: """espn_wbb_calendar - look up the women's college basketball calendar for a given season Args: @@ -129,43 +136,45 @@ def espn_wbb_calendar(season=None, ondays=None, else: url = f"http://site.api.espn.com/apis/site/v2/sports/basketball/womens-college-basketball/scoreboard?dates={season}" resp = download(url=url, **kwargs) - txt = resp.json().get('leagues')[0].get('calendar') - datenum = list(map(lambda x: x[:10].replace("-",""),txt)) - date = list(map(lambda x: x[:10],txt)) - year = list(map(lambda x: x[:4],txt)) - month = list(map(lambda x: x[5:7],txt)) - day = list(map(lambda x: x[8:10],txt)) + txt = resp.json().get("leagues")[0].get("calendar") + datenum = list(map(lambda x: x[:10].replace("-", ""), txt)) + date = list(map(lambda x: x[:10], txt)) + year = list(map(lambda x: x[:4], txt)) + month = list(map(lambda x: x[5:7], txt)) + day = list(map(lambda x: x[8:10], txt)) data = { "season": season, - "datetime" : txt, - "date" : date, + "datetime": txt, + "date": date, "year": year, "month": month, "day": day, - "dateURL": datenum + "dateURL": datenum, } full_schedule = pl.DataFrame(data) full_schedule = full_schedule.with_columns( - url="http://site.api.espn.com/apis/site/v2/sports/basketball/womens-college-basketball/scoreboard?dates=" + - pl.col('dateURL') + url="http://site.api.espn.com/apis/site/v2/sports/basketball/womens-college-basketball/scoreboard?dates=" + + pl.col("dateURL") ) return full_schedule.to_pandas() if return_as_pandas else full_schedule + def __ondays_wbb_calendar(season, **kwargs): url = f"https://sports.core.api.espn.com/v2/sports/basketball/leagues/womens-college-basketball/seasons/{season}/types/2/calendar/ondays" resp = download(url=url, **kwargs) - txt = resp.json().get('eventDate').get('dates') - result = pl.DataFrame(txt, schema=['dates']) - result = result.with_columns(dateURL = pl.col('dates').str.slice(0, 10)) + txt = resp.json().get("eventDate").get("dates") + result = pl.DataFrame(txt, schema=["dates"]) + result = result.with_columns(dateURL=pl.col("dates").str.slice(0, 10)) result = result.with_columns( - url="http://site.api.espn.com/apis/site/v2/sports/basketball/womens-college-basketball/scoreboard?dates=" + - pl.col('dateURL') + url="http://site.api.espn.com/apis/site/v2/sports/basketball/womens-college-basketball/scoreboard?dates=" + + pl.col("dateURL") ) return result + def most_recent_wbb_season(): if datetime.datetime.now().month >= 10: return datetime.datetime.now().year + 1 else: - return datetime.datetime.now().year \ No newline at end of file + return datetime.datetime.now().year diff --git a/sportsdataverse/wbb/wbb_teams.py b/sportsdataverse/wbb/wbb_teams.py index 7d7bf55..95cbce9 100755 --- a/sportsdataverse/wbb/wbb_teams.py +++ b/sportsdataverse/wbb/wbb_teams.py @@ -1,8 +1,7 @@ import pandas as pd import polars as pl -import json from sportsdataverse.dl_utils import download, underscore -from urllib.error import URLError, HTTPError, ContentTooShortError + def espn_wbb_teams(groups=None, return_as_pandas=True, **kwargs) -> pd.DataFrame: """espn_wbb_teams - look up the women's college basketball teams @@ -19,20 +18,16 @@ def espn_wbb_teams(groups=None, return_as_pandas=True, **kwargs) -> pd.DataFrame """ url = "http://site.api.espn.com/apis/site/v2/sports/basketball/womens-college-basketball/teams" - params = { - "groups": groups if groups is not None else "50", - "limit": 1000 - } - resp = download(url=url, params = params, **kwargs) + params = {"groups": groups if groups is not None else "50", "limit": 1000} + resp = download(url=url, params=params, **kwargs) if resp is not None: events_txt = resp.json() - teams = events_txt.get('sports')[0].get('leagues')[0].get('teams') - del_keys = ['record', 'links'] + teams = events_txt.get("sports")[0].get("leagues")[0].get("teams") + del_keys = ["record", "links"] for team in teams: for k in del_keys: - team.get('team').pop(k, None) - teams = pd.json_normalize(teams, sep='_') + team.get("team").pop(k, None) + teams = pd.json_normalize(teams, sep="_") teams.columns = [underscore(c) for c in teams.columns.tolist()] return teams if return_as_pandas else pl.from_pandas(teams) - diff --git a/sportsdataverse/wnba/wnba_game_rosters.py b/sportsdataverse/wnba/wnba_game_rosters.py index 99bacb5..bacbe7a 100644 --- a/sportsdataverse/wnba/wnba_game_rosters.py +++ b/sportsdataverse/wnba/wnba_game_rosters.py @@ -1,13 +1,10 @@ import pandas as pd import polars as pl -import numpy as np -import os -import json -import re -from typing import List, Callable, Iterator, Union, Optional, Dict +from typing import Dict from sportsdataverse.dl_utils import download, underscore -def espn_wnba_game_rosters(game_id: int, raw = False, return_as_pandas = True, **kwargs) -> pd.DataFrame: + +def espn_wnba_game_rosters(game_id: int, raw=False, return_as_pandas=True, **kwargs) -> pd.DataFrame: """espn_wnba_game_rosters() - Pull the game by id. Args: @@ -36,161 +33,146 @@ def espn_wnba_game_rosters(game_id: int, raw = False, return_as_pandas = True, * Example: `wnba_df = sportsdataverse.wnba.espn_wnba_game_rosters(game_id=401370395)` """ - # play by play - pbp_txt = {} # summary endpoint for pickcenter array - summary_url = "https://sports.core.api.espn.com/v2/sports/basketball/leagues/wnba/events/{x}/competitions/{x}/competitors".format(x=game_id) + summary_url = "https://sports.core.api.espn.com/v2/sports/basketball/leagues/wnba/events/{x}/competitions/{x}/competitors".format( + x=game_id + ) summary_resp = download(summary_url, **kwargs) summary = summary_resp.json() items = helper_wnba_game_items(summary) - team_rosters = helper_wnba_roster_items(items = items, summary_url = summary_url, **kwargs) - team_rosters = team_rosters.join(items[['team_id', 'order', 'home_away', 'winner']], how = 'left', on = 'team_id') - teams_df = helper_wnba_team_items(items = items, **kwargs) - teams_rosters = team_rosters.join(teams_df, how = 'left',on = 'team_id') - athletes = helper_wnba_athlete_items(teams_rosters = team_rosters, **kwargs) - rosters = athletes.join(teams_rosters, how = 'left', left_on = 'athlete_id', right_on = 'player_id') - rosters = rosters.with_columns( - game_id = pl.lit(game_id).cast(pl.Int32) - ) + team_rosters = helper_wnba_roster_items(items=items, summary_url=summary_url, **kwargs) + team_rosters = team_rosters.join(items[["team_id", "order", "home_away", "winner"]], how="left", on="team_id") + teams_df = helper_wnba_team_items(items=items, **kwargs) + teams_rosters = team_rosters.join(teams_df, how="left", on="team_id") + athletes = helper_wnba_athlete_items(teams_rosters=team_rosters, **kwargs) + rosters = athletes.join(teams_rosters, how="left", left_on="athlete_id", right_on="player_id") + rosters = rosters.with_columns(game_id=pl.lit(game_id).cast(pl.Int32)) rosters.columns = [underscore(c) for c in rosters.columns] return rosters.to_pandas() if return_as_pandas else rosters + def helper_wnba_game_items(summary): - items = pl.from_pandas(pd.json_normalize(summary, record_path = "items", sep = '_')) + items = pl.from_pandas(pd.json_normalize(summary, record_path="items", sep="_")) items.columns = [col.replace("$ref", "href") for col in items.columns] items.columns = [underscore(c) for c in items.columns] - items = items.rename({ - "id": "team_id", - "uid": "team_uid", - "statistics_href": "team_statistics_href" - } - ) - items = items.with_columns( - team_id = pl.col("team_id").cast(pl.Int32) - ) + items = items.rename({"id": "team_id", "uid": "team_uid", "statistics_href": "team_statistics_href"}) + items = items.with_columns(team_id=pl.col("team_id").cast(pl.Int32)) return items + def helper_wnba_team_items(items, **kwargs): pop_cols = [ - '$ref', - 'record', - 'athletes', - 'venue', - 'groups', - 'ranks', - 'statistics', - 'leaders', - 'links', - 'notes', - 'againstTheSpreadRecords', - 'franchise', - 'events', - 'college', - 'depthCharts', - 'transactions', - 'awards', - 'injuries', - 'coaches' + "$ref", + "record", + "athletes", + "venue", + "groups", + "ranks", + "statistics", + "leaders", + "links", + "notes", + "againstTheSpreadRecords", + "franchise", + "events", + "college", + "depthCharts", + "transactions", + "awards", + "injuries", + "coaches", ] teams_df = pl.DataFrame() - for x in items['team_href']: + for x in items["team_href"]: team = download(x, **kwargs).json() for k in pop_cols: team.pop(k, None) - team_row = pl.from_pandas(pd.json_normalize(team, sep = '_')) - teams_df = pl.concat([teams_df, team_row], how = 'vertical') + team_row = pl.from_pandas(pd.json_normalize(team, sep="_")) + teams_df = pl.concat([teams_df, team_row], how="vertical") teams_df.columns = [ - 'team_id', - 'team_guid', - 'team_uid', - 'team_slug', - 'team_location', - 'team_name', - 'team_abbreviation', - 'team_display_name', - 'team_short_display_name', - 'team_color', - 'team_alternate_color', - 'is_active', - 'is_all_star', - 'logos', - 'team_alternate_ids_sdr' + "team_id", + "team_guid", + "team_uid", + "team_slug", + "team_location", + "team_name", + "team_abbreviation", + "team_display_name", + "team_short_display_name", + "team_color", + "team_alternate_color", + "is_active", + "is_all_star", + "logos", + "team_alternate_ids_sdr", ] - teams_df = teams_df.with_columns( - logo_href = pl.lit(""), - logo_dark_href = pl.lit("") - ) - for row in range(len(teams_df['logos'])): - team = teams_df['logos'][row] - teams_df[row, 'logo_href'] = team[0]['href'] - teams_df[row, 'logo_dark_href'] = team[1]['href'] - - teams_df = teams_df.drop(['logos']) - teams_df = teams_df.with_columns( - team_id = pl.col("team_id").cast(pl.Int32) - ) + teams_df = teams_df.with_columns(logo_href=pl.lit(""), logo_dark_href=pl.lit("")) + for row in range(len(teams_df["logos"])): + team = teams_df["logos"][row] + teams_df[row, "logo_href"] = team[0]["href"] + teams_df[row, "logo_dark_href"] = team[1]["href"] + + teams_df = teams_df.drop(["logos"]) + teams_df = teams_df.with_columns(team_id=pl.col("team_id").cast(pl.Int32)) return teams_df + def helper_wnba_roster_items(items, summary_url, **kwargs): - team_ids = list(items['team_id']) + team_ids = list(items["team_id"]) game_rosters = pl.DataFrame() for tm in team_ids: - team_roster_url = "{x}/{t}/roster".format(x = summary_url, t = tm) + team_roster_url = "{x}/{t}/roster".format(x=summary_url, t=tm) team_roster_resp = download(team_roster_url, **kwargs) - team_roster = pl.from_pandas(pd.json_normalize(team_roster_resp.json().get('entries',[]), sep = '_')) + team_roster = pl.from_pandas(pd.json_normalize(team_roster_resp.json().get("entries", []), sep="_")) team_roster.columns = [col.replace("$ref", "href") for col in team_roster.columns] team_roster.columns = [underscore(c) for c in team_roster.columns] - team_roster= team_roster.with_columns( - team_id = pl.lit(tm).cast(pl.Int32) - ) - game_rosters = pl.concat([game_rosters, team_roster], how = 'vertical') + team_roster = team_roster.with_columns(team_id=pl.lit(tm).cast(pl.Int32)) + game_rosters = pl.concat([game_rosters, team_roster], how="vertical") game_rosters = game_rosters.drop(["period", "for_player_id", "active"]) game_rosters = game_rosters.with_columns( - player_id = pl.col('player_id').cast(pl.Int64), - team_id = pl.col('team_id').cast(pl.Int32) + player_id=pl.col("player_id").cast(pl.Int64), team_id=pl.col("team_id").cast(pl.Int32) ) return game_rosters + def helper_wnba_athlete_items(teams_rosters, **kwargs): - athlete_hrefs = list(teams_rosters['athlete_href']) + athlete_hrefs = list(teams_rosters["athlete_href"]) game_athletes = pl.DataFrame() pop_cols = [ - 'links', - 'injuries', - 'teams', - 'team', - 'college', - 'proAthlete', - 'statistics', - 'notes', - 'eventLog', + "links", + "injuries", + "teams", + "team", + "college", + "proAthlete", + "statistics", + "notes", + "eventLog", "$ref", - "position" + "position", ] for athlete_href in athlete_hrefs: - athlete_res = download(athlete_href, **kwargs) athlete_resp = athlete_res.json() for k in pop_cols: athlete_resp.pop(k, None) - athlete = pl.from_pandas(pd.json_normalize(athlete_resp, sep='_')) + athlete = pl.from_pandas(pd.json_normalize(athlete_resp, sep="_")) athlete.columns = [col.replace("$ref", "href") for col in athlete.columns] athlete.columns = [underscore(c) for c in athlete.columns] - game_athletes = pl.concat([game_athletes, athlete], how = 'diagonal') + game_athletes = pl.concat([game_athletes, athlete], how="diagonal") - - game_athletes = game_athletes.rename({ - "id": "athlete_id", - "uid": "athlete_uid", - "guid": "athlete_guid", - "type": "athlete_type", - "display_name": "athlete_display_name" - }) - game_athletes = game_athletes.with_columns( - athlete_id = pl.col("athlete_id").cast(pl.Int64) + game_athletes = game_athletes.rename( + { + "id": "athlete_id", + "uid": "athlete_uid", + "guid": "athlete_guid", + "type": "athlete_type", + "display_name": "athlete_display_name", + } ) - return game_athletes \ No newline at end of file + game_athletes = game_athletes.with_columns(athlete_id=pl.col("athlete_id").cast(pl.Int64)) + return game_athletes diff --git a/sportsdataverse/wnba/wnba_loaders.py b/sportsdataverse/wnba/wnba_loaders.py index 1959bd6..06c6743 100755 --- a/sportsdataverse/wnba/wnba_loaders.py +++ b/sportsdataverse/wnba/wnba_loaders.py @@ -1,13 +1,12 @@ import pandas as pd import polars as pl -import json from tqdm import tqdm -from typing import List, Callable, Iterator, Union, Optional +from typing import List from sportsdataverse.config import WNBA_BASE_URL, WNBA_TEAM_BOX_URL, WNBA_PLAYER_BOX_URL, WNBA_TEAM_SCHEDULE_URL from sportsdataverse.errors import SeasonNotFoundError -from sportsdataverse.dl_utils import download -def load_wnba_pbp(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: + +def load_wnba_pbp(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: """Load WNBA play by play data going back to 2002 Example: @@ -31,10 +30,11 @@ def load_wnba_pbp(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: if int(i) < 2002: raise SeasonNotFoundError("season cannot be less than 2002") i_data = pd.read_parquet(WNBA_BASE_URL.format(season=i), use_pyarrow=True, columns=None) - data = pl.concat([data, i_data], how = 'vertical') - return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data + data = pl.concat([data, i_data], how="vertical") + return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data + -def load_wnba_team_boxscore(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: +def load_wnba_team_boxscore(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: """Load WNBA team boxscore data Example: @@ -57,11 +57,12 @@ def load_wnba_team_boxscore(seasons: List[int], return_as_pandas = True) -> pd.D for i in tqdm(seasons): if int(i) < 2002: raise ValueError("season cannot be less than 2002") - i_data = pd.read_parquet(WNBA_TEAM_BOX_URL.format(season = i), use_pyarrow=True, columns=None) - data = pl.concat([data, i_data], how = 'vertical') - return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data + i_data = pd.read_parquet(WNBA_TEAM_BOX_URL.format(season=i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how="vertical") + return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_wnba_player_boxscore(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: + +def load_wnba_player_boxscore(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: """Load WNBA player boxscore data Example: @@ -84,11 +85,12 @@ def load_wnba_player_boxscore(seasons: List[int], return_as_pandas = True) -> pd for i in tqdm(seasons): if int(i) < 2002: raise ValueError("season cannot be less than 2002") - i_data = pd.read_parquet(WNBA_PLAYER_BOX_URL.format(season = i), use_pyarrow=True, columns=None) - data = pl.concat([data, i_data], how = 'vertical') - return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data + i_data = pd.read_parquet(WNBA_PLAYER_BOX_URL.format(season=i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how="vertical") + return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data + -def load_wnba_schedule(seasons: List[int], return_as_pandas = True) -> pd.DataFrame: +def load_wnba_schedule(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: """Load WNBA schedule data Example: @@ -111,6 +113,6 @@ def load_wnba_schedule(seasons: List[int], return_as_pandas = True) -> pd.DataFr for i in tqdm(seasons): if int(i) < 2002: raise ValueError("season cannot be less than 2002") - i_data = pd.read_parquet(WNBA_TEAM_SCHEDULE_URL.format(season = i), use_pyarrow=True, columns=None) - data = pl.concat([data, i_data], how = 'vertical') - return data.to_pandas(use_pyarrow_extension_array = True) if return_as_pandas else data + i_data = pd.read_parquet(WNBA_TEAM_SCHEDULE_URL.format(season=i), use_pyarrow=True, columns=None) + data = pl.concat([data, i_data], how="vertical") + return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data diff --git a/sportsdataverse/wnba/wnba_pbp.py b/sportsdataverse/wnba/wnba_pbp.py index 7b23c61..0629c4e 100755 --- a/sportsdataverse/wnba/wnba_pbp.py +++ b/sportsdataverse/wnba/wnba_pbp.py @@ -4,46 +4,67 @@ import os import json import re -from typing import List, Callable, Iterator, Union, Optional, Dict -from sportsdataverse.dl_utils import download, flatten_json_iterative, key_check +from typing import Dict +from sportsdataverse.dl_utils import download, flatten_json_iterative -def espn_wnba_pbp(game_id: int, raw = False, **kwargs) -> Dict: + +def espn_wnba_pbp(game_id: int, raw=False, **kwargs) -> Dict: """espn_wnba_pbp() - Pull the game by id. Data from API endpoints - `wnba/playbyplay`, `wnba/summary` - Args: - game_id (int): Unique game_id, can be obtained from wnba_schedule(). + Args: + game_id (int): Unique game_id, can be obtained from wnba_schedule(). - Returns: - Dict: Dictionary of game data with keys - "gameId", "plays", "winprobability", "boxscore", "header", - "broadcasts", "videos", "playByPlaySource", "standings", "leaders", "seasonseries", "timeouts", - "pickcenter", "againstTheSpread", "odds", "predictor", "espnWP", "gameInfo", "season" + Returns: + Dict: Dictionary of game data with keys - "gameId", "plays", "winprobability", "boxscore", "header", + "broadcasts", "videos", "playByPlaySource", "standings", "leaders", "seasonseries", "timeouts", + "pickcenter", "againstTheSpread", "odds", "predictor", "espnWP", "gameInfo", "season" - Example: - `wnba_df = sportsdataverse.wnba.espn_wnba_pbp(game_id=401370395)` + Example: + `wnba_df = sportsdataverse.wnba.espn_wnba_pbp(game_id=401370395)` """ # play by play # play by play - pbp_txt = {'timeouts': {}} + pbp_txt = {"timeouts": {}} # summary endpoint for pickcenter array summary_url = f"http://site.api.espn.com/apis/site/v2/sports/basketball/wnba/summary?event={game_id}" summary_resp = download(summary_url, **kwargs) summary = summary_resp.json() incoming_keys_expected = [ - 'boxscore', 'format', 'gameInfo', 'leaders', 'seasonseries', - 'broadcasts', 'predictor', 'pickcenter', 'againstTheSpread', - 'odds', 'winprobability', 'header', 'plays', - 'article', 'videos', 'standings', - 'teamInfo', 'espnWP', 'season', 'timeouts' - ] - dict_keys_expected = [ - 'boxscore','format', 'gameInfo', 'predictor', - 'article', 'header', 'season', 'standings' + "boxscore", + "format", + "gameInfo", + "leaders", + "seasonseries", + "broadcasts", + "predictor", + "pickcenter", + "againstTheSpread", + "odds", + "winprobability", + "header", + "plays", + "article", + "videos", + "standings", + "teamInfo", + "espnWP", + "season", + "timeouts", ] + dict_keys_expected = ["boxscore", "format", "gameInfo", "predictor", "article", "header", "season", "standings"] array_keys_expected = [ - 'plays', 'seasonseries', 'videos', 'broadcasts', 'pickcenter', - 'againstTheSpread', 'odds', 'winprobability', 'teamInfo', 'espnWP', - 'leaders' + "plays", + "seasonseries", + "videos", + "broadcasts", + "pickcenter", + "againstTheSpread", + "odds", + "winprobability", + "teamInfo", + "espnWP", + "leaders", ] if raw == True: # reorder keys in raw format, appending empty keys which are defined later to the end @@ -61,91 +82,94 @@ def espn_wnba_pbp(game_id: int, raw = False, **kwargs) -> Dict: else: pbp_txt[k] = {} if k in dict_keys_expected else [] - for k in ['news','shop']: - pbp_txt.pop(f'{k}', None) + for k in ["news", "shop"]: + pbp_txt.pop(f"{k}", None) return helper_wnba_pbp(game_id, pbp_txt) + def wnba_pbp_disk(game_id, path_to_json): with open(os.path.join(path_to_json, f"{game_id}.json")) as json_file: pbp_txt = json.load(json_file) return pbp_txt + def helper_wnba_pbp(game_id, pbp_txt): init = helper_wnba_pickcenter(pbp_txt) pbp_txt, init = helper_wnba_game_data(pbp_txt, init) - if "plays" in pbp_txt.keys() and pbp_txt.get('header').get('competitions')[0].get('playByPlaySource') != 'none': + if "plays" in pbp_txt.keys() and pbp_txt.get("header").get("competitions")[0].get("playByPlaySource") != "none": pbp_txt = helper_wnba_pbp_features(game_id, pbp_txt, init) else: - pbp_txt['plays'] = pl.DataFrame() - pbp_txt['timeouts'] = { - homeTeamId: {"1": [], "2": []}, - awayTeamId: {"1": [], "2": []}, + pbp_txt["plays"] = pl.DataFrame() + pbp_txt["timeouts"] = { + init["homeTeamId"]: {"1": [], "2": []}, + init["awayTeamId"]: {"1": [], "2": []}, } return { "gameId": game_id, - "plays": pbp_txt['plays'].to_dicts(), - "winprobability" : np.array(pbp_txt['winprobability']).tolist(), - "boxscore" : pbp_txt['boxscore'], - "header" : pbp_txt['header'], - "format": pbp_txt['format'], - "broadcasts" : np.array(pbp_txt['broadcasts']).tolist(), - "videos" : np.array(pbp_txt['videos']).tolist(), - "playByPlaySource": pbp_txt['playByPlaySource'], - "standings" : pbp_txt['standings'], - "article" : pbp_txt['article'], - "leaders" : np.array(pbp_txt['leaders']).tolist(), - "seasonseries" : np.array(pbp_txt['seasonseries']).tolist(), - "timeouts" : pbp_txt['timeouts'], - "pickcenter" : np.array(pbp_txt['pickcenter']).tolist(), - "againstTheSpread" : np.array(pbp_txt['againstTheSpread']).tolist(), - "odds" : np.array(pbp_txt['odds']).tolist(), - "predictor" : pbp_txt['predictor'], - "espnWP" : np.array(pbp_txt['espnWP']).tolist(), - "gameInfo" : pbp_txt['gameInfo'], - "teamInfo" : np.array(pbp_txt['teamInfo']).tolist(), - "season" : np.array(pbp_txt['season']).tolist() + "plays": pbp_txt["plays"].to_dicts(), + "winprobability": np.array(pbp_txt["winprobability"]).tolist(), + "boxscore": pbp_txt["boxscore"], + "header": pbp_txt["header"], + "format": pbp_txt["format"], + "broadcasts": np.array(pbp_txt["broadcasts"]).tolist(), + "videos": np.array(pbp_txt["videos"]).tolist(), + "playByPlaySource": pbp_txt["playByPlaySource"], + "standings": pbp_txt["standings"], + "article": pbp_txt["article"], + "leaders": np.array(pbp_txt["leaders"]).tolist(), + "seasonseries": np.array(pbp_txt["seasonseries"]).tolist(), + "timeouts": pbp_txt["timeouts"], + "pickcenter": np.array(pbp_txt["pickcenter"]).tolist(), + "againstTheSpread": np.array(pbp_txt["againstTheSpread"]).tolist(), + "odds": np.array(pbp_txt["odds"]).tolist(), + "predictor": pbp_txt["predictor"], + "espnWP": np.array(pbp_txt["espnWP"]).tolist(), + "gameInfo": pbp_txt["gameInfo"], + "teamInfo": np.array(pbp_txt["teamInfo"]).tolist(), + "season": np.array(pbp_txt["season"]).tolist(), } + def helper_wnba_game_data(pbp_txt, init): - pbp_txt['timeouts'] = {} - pbp_txt['teamInfo'] = pbp_txt['header']['competitions'][0] - pbp_txt['season'] = pbp_txt['header']['season'] - pbp_txt['playByPlaySource'] = pbp_txt['header']['competitions'][0]['playByPlaySource'] - pbp_txt['boxscoreSource'] = pbp_txt['header']['competitions'][0]['boxscoreSource'] - pbp_txt['gameSpreadAvailable'] = init["gameSpreadAvailable"] - pbp_txt['gameSpread'] = init["gameSpread"] + pbp_txt["timeouts"] = {} + pbp_txt["teamInfo"] = pbp_txt["header"]["competitions"][0] + pbp_txt["season"] = pbp_txt["header"]["season"] + pbp_txt["playByPlaySource"] = pbp_txt["header"]["competitions"][0]["playByPlaySource"] + pbp_txt["boxscoreSource"] = pbp_txt["header"]["competitions"][0]["boxscoreSource"] + pbp_txt["gameSpreadAvailable"] = init["gameSpreadAvailable"] + pbp_txt["gameSpread"] = init["gameSpread"] pbp_txt["homeFavorite"] = init["homeFavorite"] pbp_txt["homeTeamSpread"] = np.where( init["homeFavorite"] == True, abs(init["gameSpread"]), -1 * abs(init["gameSpread"]) ) pbp_txt["overUnder"] = init["overUnder"] # Home and Away identification variables - if pbp_txt['header']['competitions'][0]['competitors'][0]['homeAway']=='home': - pbp_txt['header']['competitions'][0]['home'] = pbp_txt['header']['competitions'][0]['competitors'][0]['team'] - homeTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['id']) - homeTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['name']) - homeTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['location']) - homeTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['abbreviation']) + if pbp_txt["header"]["competitions"][0]["competitors"][0]["homeAway"] == "home": + pbp_txt["header"]["competitions"][0]["home"] = pbp_txt["header"]["competitions"][0]["competitors"][0]["team"] + homeTeamId = int(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["id"]) + homeTeamMascot = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["name"]) + homeTeamName = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["location"]) + homeTeamAbbrev = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["abbreviation"]) homeTeamNameAlt = re.sub("Stat(.+)", "St", homeTeamName) - pbp_txt['header']['competitions'][0]['away'] = pbp_txt['header']['competitions'][0]['competitors'][1]['team'] - awayTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['id']) - awayTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['name']) - awayTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['location']) - awayTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['abbreviation']) + pbp_txt["header"]["competitions"][0]["away"] = pbp_txt["header"]["competitions"][0]["competitors"][1]["team"] + awayTeamId = int(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["id"]) + awayTeamMascot = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["name"]) + awayTeamName = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["location"]) + awayTeamAbbrev = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["abbreviation"]) awayTeamNameAlt = re.sub("Stat(.+)", "St", awayTeamName) else: - pbp_txt['header']['competitions'][0]['away'] = pbp_txt['header']['competitions'][0]['competitors'][0]['team'] - awayTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['id']) - awayTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['name']) - awayTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['location']) - awayTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['abbreviation']) + pbp_txt["header"]["competitions"][0]["away"] = pbp_txt["header"]["competitions"][0]["competitors"][0]["team"] + awayTeamId = int(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["id"]) + awayTeamMascot = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["name"]) + awayTeamName = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["location"]) + awayTeamAbbrev = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["abbreviation"]) awayTeamNameAlt = re.sub("Stat(.+)", "St", awayTeamName) - pbp_txt['header']['competitions'][0]['home'] = pbp_txt['header']['competitions'][0]['competitors'][1]['team'] - homeTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['id']) - homeTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['name']) - homeTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['location']) - homeTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['abbreviation']) + pbp_txt["header"]["competitions"][0]["home"] = pbp_txt["header"]["competitions"][0]["competitors"][1]["team"] + homeTeamId = int(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["id"]) + homeTeamMascot = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["name"]) + homeTeamName = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["location"]) + homeTeamAbbrev = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["abbreviation"]) homeTeamNameAlt = re.sub("Stat(.+)", "St", homeTeamName) init["homeTeamId"] = homeTeamId init["homeTeamMascot"] = homeTeamMascot @@ -159,110 +183,124 @@ def helper_wnba_game_data(pbp_txt, init): init["awayTeamNameAlt"] = awayTeamNameAlt return pbp_txt, init + def helper_wnba_pbp_features(game_id, pbp_txt, init): - pbp_txt['plays_mod'] = [] - for play in pbp_txt['plays']: + pbp_txt["plays_mod"] = [] + for play in pbp_txt["plays"]: p = flatten_json_iterative(play) - pbp_txt['plays_mod'].append(p) - pbp_txt['plays'] = pl.from_pandas(pd.json_normalize(pbp_txt,'plays_mod')) - pbp_txt['plays'] = pbp_txt['plays'].with_columns( - game_id = pl.lit(game_id).cast(pl.Int32), - id = (pl.col('id').cast(pl.Int64)), - season = pl.lit(pbp_txt['header']['season']['year']).cast(pl.Int32), - seasonType = pl.lit(pbp_txt['header']['season']['type']), - homeTeamId = pl.lit(init["homeTeamId"]).cast(pl.Int32), - homeTeamName = pl.lit(init["homeTeamName"]), - homeTeamMascot = pl.lit(init["homeTeamMascot"]), - homeTeamAbbrev = pl.lit(init["homeTeamAbbrev"]), - homeTeamNameAlt = pl.lit(init["homeTeamNameAlt"]), - awayTeamId = pl.lit(init["awayTeamId"]).cast(pl.Int32), - awayTeamName = pl.lit(init["awayTeamName"]), - awayTeamMascot = pl.lit(init["awayTeamMascot"]), - awayTeamAbbrev = pl.lit(init["awayTeamAbbrev"]), - awayTeamNameAlt = pl.lit(init["awayTeamNameAlt"]), - gameSpread = pl.lit(init["gameSpread"]).abs(), - homeFavorite = pl.lit(init["homeFavorite"]), - gameSpreadAvailable = pl.lit(init["gameSpreadAvailable"]), - ).with_columns( - homeTeamSpread = pl.when(pl.col('homeFavorite') == True) - .then(pl.col('gameSpread')) - .otherwise(-1*pl.col('gameSpread')), - ).with_columns( - pl.col("period.number").cast(pl.Int32).alias("period.number"), - pl.col("period.number").cast(pl.Int32).alias("qtr"), - pl.when(pl.col("clock.displayValue").str.contains(r":") == False) - .then('0:'+ pl.col("clock.displayValue")) + pbp_txt["plays_mod"].append(p) + pbp_txt["plays"] = pl.from_pandas(pd.json_normalize(pbp_txt, "plays_mod")) + pbp_txt["plays"] = ( + pbp_txt["plays"] + .with_columns( + game_id=pl.lit(game_id).cast(pl.Int32), + id=(pl.col("id").cast(pl.Int64)), + season=pl.lit(pbp_txt["header"]["season"]["year"]).cast(pl.Int32), + seasonType=pl.lit(pbp_txt["header"]["season"]["type"]), + homeTeamId=pl.lit(init["homeTeamId"]).cast(pl.Int32), + homeTeamName=pl.lit(init["homeTeamName"]), + homeTeamMascot=pl.lit(init["homeTeamMascot"]), + homeTeamAbbrev=pl.lit(init["homeTeamAbbrev"]), + homeTeamNameAlt=pl.lit(init["homeTeamNameAlt"]), + awayTeamId=pl.lit(init["awayTeamId"]).cast(pl.Int32), + awayTeamName=pl.lit(init["awayTeamName"]), + awayTeamMascot=pl.lit(init["awayTeamMascot"]), + awayTeamAbbrev=pl.lit(init["awayTeamAbbrev"]), + awayTeamNameAlt=pl.lit(init["awayTeamNameAlt"]), + gameSpread=pl.lit(init["gameSpread"]).abs(), + homeFavorite=pl.lit(init["homeFavorite"]), + gameSpreadAvailable=pl.lit(init["gameSpreadAvailable"]), + ) + .with_columns( + homeTeamSpread=pl.when(pl.col("homeFavorite") == True) + .then(pl.col("gameSpread")) + .otherwise(-1 * pl.col("gameSpread")), + ) + .with_columns( + pl.col("period.number").cast(pl.Int32).alias("period.number"), + pl.col("period.number").cast(pl.Int32).alias("qtr"), + pl.when(pl.col("clock.displayValue").str.contains(r":") == False) + .then("0:" + pl.col("clock.displayValue")) .otherwise(pl.col("clock.displayValue")) .alias("clock.displayValue"), - ).with_columns( - pl.col("clock.displayValue").alias("time"), - pl.col("clock.displayValue").str.split(':').list.to_struct(n_field_strategy="first_non_null").alias("clock.mm") - ).with_columns( - pl.col("clock.mm").struct.rename_fields(["clock.minutes", "clock.seconds"]) - ).unnest( - "clock.mm" - ).with_columns( - pl.col("clock.minutes").cast(pl.Float32), - pl.col("clock.seconds").cast(pl.Float32), - pl.when((pl.col("type.text") == "ShortTimeOut") - .and_( - pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamAbbrev"]).lower()) - .or_( - pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamAbbrev"]).lower()), - pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamName"]).lower()), - pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamMascot"]).lower()), - pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamNameAlt"]).lower()) + ) + .with_columns( + pl.col("clock.displayValue").alias("time"), + pl.col("clock.displayValue") + .str.split(":") + .list.to_struct(n_field_strategy="first_non_null") + .alias("clock.mm"), + ) + .with_columns(pl.col("clock.mm").struct.rename_fields(["clock.minutes", "clock.seconds"])) + .unnest("clock.mm") + .with_columns( + pl.col("clock.minutes").cast(pl.Float32), + pl.col("clock.seconds").cast(pl.Float32), + pl.when( + (pl.col("type.text") == "ShortTimeOut").and_( + pl.col("text") + .str.to_lowercase() + .str.contains(str(init["homeTeamAbbrev"]).lower()) + .or_( + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamAbbrev"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamName"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamMascot"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamNameAlt"]).lower()), + ) ) - )) - .then(True) - .otherwise(False) - .alias("homeTimeoutCalled"), - pl.when((pl.col("type.text") == "ShortTimeOut") - .and_( - pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamAbbrev"]).lower()) - .or_( - pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamAbbrev"]).lower()), - pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamName"]).lower()), - pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamMascot"]).lower()), - pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamNameAlt"]).lower()) + ) + .then(True) + .otherwise(False) + .alias("homeTimeoutCalled"), + pl.when( + (pl.col("type.text") == "ShortTimeOut").and_( + pl.col("text") + .str.to_lowercase() + .str.contains(str(init["awayTeamAbbrev"]).lower()) + .or_( + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamAbbrev"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamName"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamMascot"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamNameAlt"]).lower()), + ) ) - )) - .then(True) - .otherwise(False) - .alias("awayTimeoutCalled"), - ).with_columns( - half = pl.when(pl.col('qtr') <= 2) - .then(1) - .otherwise(2), - game_half = pl.when(pl.col('qtr') <= 2) - .then(1) - .otherwise(2), - ).with_columns( - lag_qtr = pl.col('qtr').shift(1), - lead_qtr = pl.col('qtr').shift(-1), - lag_half = pl.col('half').shift(1), - lead_half = pl.col('half').shift(-1), - ).with_columns( - (60 * pl.col("clock.minutes") + pl.col("clock.seconds")).alias("start.quarter_seconds_remaining"), - pl.when(pl.col('qtr').is_in([1,3])) + ) + .then(True) + .otherwise(False) + .alias("awayTimeoutCalled"), + ) + .with_columns( + half=pl.when(pl.col("qtr") <= 2).then(1).otherwise(2), + game_half=pl.when(pl.col("qtr") <= 2).then(1).otherwise(2), + ) + .with_columns( + lag_qtr=pl.col("qtr").shift(1), + lead_qtr=pl.col("qtr").shift(-1), + lag_half=pl.col("half").shift(1), + lead_half=pl.col("half").shift(-1), + ) + .with_columns( + (60 * pl.col("clock.minutes") + pl.col("clock.seconds")).alias("start.quarter_seconds_remaining"), + pl.when(pl.col("qtr").is_in([1, 3])) .then(600 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) .otherwise(60 * pl.col("clock.minutes") + pl.col("clock.seconds")) .alias("start.half_seconds_remaining"), - pl.when(pl.col('qtr') == 1) + pl.when(pl.col("qtr") == 1) .then(1800 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) - .when(pl.col('qtr') == 2) + .when(pl.col("qtr") == 2) .then(1200 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) - .when(pl.col('qtr') == 3) + .when(pl.col("qtr") == 3) .then(600 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) .otherwise(60 * pl.col("clock.minutes") + pl.col("clock.seconds")) .alias("start.game_seconds_remaining"), - ).with_columns( - pl.col("start.quarter_seconds_remaining").shift(-1).alias("end.quarter_seconds_remaining"), - pl.col("start.half_seconds_remaining").shift(-1).alias("end.half_seconds_remaining"), - pl.col("start.game_seconds_remaining").shift(-1).alias("end.game_seconds_remaining"), + ) + .with_columns( + pl.col("start.quarter_seconds_remaining").shift(-1).alias("end.quarter_seconds_remaining"), + pl.col("start.half_seconds_remaining").shift(-1).alias("end.half_seconds_remaining"), + pl.col("start.game_seconds_remaining").shift(-1).alias("end.game_seconds_remaining"), + ) ) - pbp_txt["plays"] = pbp_txt["plays"].with_row_count("game_play_number", 1) pbp_txt["timeouts"] = { init["homeTeamId"]: {"1": [], "2": []}, @@ -270,107 +308,85 @@ def helper_wnba_pbp_features(game_id, pbp_txt, init): } pbp_txt["timeouts"][init["homeTeamId"]]["1"] = ( pbp_txt["plays"] - .filter( - (pl.col("homeTimeoutCalled") == True) - .and_(pl.col("period.number") <= 2) - ) + .filter((pl.col("homeTimeoutCalled") == True).and_(pl.col("period.number") <= 2)) .get_column("id") .to_list() ) pbp_txt["timeouts"][init["homeTeamId"]]["2"] = ( pbp_txt["plays"] - .filter( - (pl.col("homeTimeoutCalled") == True) - .and_(pl.col("period.number") > 2) - ) + .filter((pl.col("homeTimeoutCalled") == True).and_(pl.col("period.number") > 2)) .get_column("id") .to_list() ) pbp_txt["timeouts"][init["awayTeamId"]]["1"] = ( pbp_txt["plays"] - .filter( - (pl.col("awayTimeoutCalled") == True) - .and_(pl.col("period.number") <= 2) - ) + .filter((pl.col("awayTimeoutCalled") == True).and_(pl.col("period.number") <= 2)) .get_column("id") .to_list() ) pbp_txt["timeouts"][init["awayTeamId"]]["2"] = ( pbp_txt["plays"] - .filter( - (pl.col("awayTimeoutCalled") == True) - .and_(pl.col("period.number") > 2) - ) + .filter((pl.col("awayTimeoutCalled") == True).and_(pl.col("period.number") > 2)) .get_column("id") .to_list() ) - # Pos Team - Start and End Id - pbp_txt['plays'] = pbp_txt['plays'].with_columns( - pl.when((pl.col("game_play_number") == 1) - .or_((pl.col("lag_qtr") == 1) - .and_(pl.col("period.number") == 2)) - .or_((pl.col("lag_qtr") == 2) - .and_(pl.col("period.number") == 3)) - .or_((pl.col("lag_qtr") == 3) - .and_(pl.col("period.number") == 4))) - .then(600) - .when((pl.col("lag_qtr") == (pl.col("period.number") - 1)) - .and_(pl.col("period.number") >= 5)) - .then(300) - .otherwise(pl.col("end.quarter_seconds_remaining")) - .alias("end.quarter_seconds_remaining"), + # Pos Team - Start and End Id + pbp_txt["plays"] = pbp_txt["plays"].with_columns( + pl.when( + (pl.col("game_play_number") == 1) + .or_((pl.col("lag_qtr") == 1).and_(pl.col("period.number") == 2)) + .or_((pl.col("lag_qtr") == 2).and_(pl.col("period.number") == 3)) + .or_((pl.col("lag_qtr") == 3).and_(pl.col("period.number") == 4)) + ) + .then(600) + .when((pl.col("lag_qtr") == (pl.col("period.number") - 1)).and_(pl.col("period.number") >= 5)) + .then(300) + .otherwise(pl.col("end.quarter_seconds_remaining")) + .alias("end.quarter_seconds_remaining"), pl.when((pl.col("game_play_number") == 1)) - .then(1200) - .when((pl.col("lag_half") == 1) - .and_(pl.col("half") == 2)) - .then(1200) - .when((pl.col("lag_qtr") == 1) - .and_(pl.col("period.number") == 2)) - .then(600) - .when((pl.col("lag_qtr") == 2) - .and_(pl.col("period.number") == 3)) - .then(1200) - .when((pl.col("lag_qtr") == 3) - .and_(pl.col("period.number") == 4)) - .then(600) - .when((pl.col("lag_qtr") == (pl.col("period.number") - 1)) - .and_(pl.col("period.number") >= 5)) - .then(300) - .otherwise(pl.col("end.half_seconds_remaining")) - .alias("end.half_seconds_remaining"), + .then(1200) + .when((pl.col("lag_half") == 1).and_(pl.col("half") == 2)) + .then(1200) + .when((pl.col("lag_qtr") == 1).and_(pl.col("period.number") == 2)) + .then(600) + .when((pl.col("lag_qtr") == 2).and_(pl.col("period.number") == 3)) + .then(1200) + .when((pl.col("lag_qtr") == 3).and_(pl.col("period.number") == 4)) + .then(600) + .when((pl.col("lag_qtr") == (pl.col("period.number") - 1)).and_(pl.col("period.number") >= 5)) + .then(300) + .otherwise(pl.col("end.half_seconds_remaining")) + .alias("end.half_seconds_remaining"), pl.when((pl.col("game_play_number") == 1)) - .then(2400) - .when((pl.col("lag_qtr") == 1) - .and_(pl.col("period.number") == 2)) - .then(1800) - .when((pl.col("lag_qtr") == 2) - .and_(pl.col("period.number") == 3)) - .then(1200) - .when((pl.col("lag_qtr") == 3) - .and_(pl.col("period.number") == 4)) - .then(600) - .when((pl.col("lag_qtr") == (pl.col("period.number") - 1)) - .and_(pl.col("period.number") >= 5)) - .then(300) - .otherwise(pl.col("end.game_seconds_remaining")) - .alias("end.game_seconds_remaining"), + .then(2400) + .when((pl.col("lag_qtr") == 1).and_(pl.col("period.number") == 2)) + .then(1800) + .when((pl.col("lag_qtr") == 2).and_(pl.col("period.number") == 3)) + .then(1200) + .when((pl.col("lag_qtr") == 3).and_(pl.col("period.number") == 4)) + .then(600) + .when((pl.col("lag_qtr") == (pl.col("period.number") - 1)).and_(pl.col("period.number") >= 5)) + .then(300) + .otherwise(pl.col("end.game_seconds_remaining")) + .alias("end.game_seconds_remaining"), pl.col("qtr").cast(pl.Int32).alias("period"), ) return pbp_txt + def helper_wnba_pickcenter(pbp_txt): # Spread definition - if len(pbp_txt.get("pickcenter",[])) > 1: - homeFavorite = pbp_txt.get("pickcenter",{})[0].get("homeTeamOdds",{}).get("favorite","") - if "spread" in pbp_txt.get("pickcenter",{})[1].keys(): - gameSpread = pbp_txt.get("pickcenter",{})[1].get("spread","") - overUnder = pbp_txt.get("pickcenter",{})[1].get("overUnder","") + if len(pbp_txt.get("pickcenter", [])) > 1: + homeFavorite = pbp_txt.get("pickcenter", {})[0].get("homeTeamOdds", {}).get("favorite", "") + if "spread" in pbp_txt.get("pickcenter", {})[1].keys(): + gameSpread = pbp_txt.get("pickcenter", {})[1].get("spread", "") + overUnder = pbp_txt.get("pickcenter", {})[1].get("overUnder", "") else: - gameSpread = pbp_txt.get("pickcenter",{})[0].get("spread","") - overUnder = pbp_txt.get("pickcenter",{})[0].get("overUnder","") + gameSpread = pbp_txt.get("pickcenter", {})[0].get("spread", "") + overUnder = pbp_txt.get("pickcenter", {})[0].get("overUnder", "") gameSpreadAvailable = True - # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") + # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") else: gameSpread = 2.5 overUnder = 160.5 @@ -382,4 +398,4 @@ def helper_wnba_pickcenter(pbp_txt): "overUnder": overUnder, "homeFavorite": homeFavorite, "gameSpreadAvailable": gameSpreadAvailable, - } \ No newline at end of file + } diff --git a/sportsdataverse/wnba/wnba_schedule.py b/sportsdataverse/wnba/wnba_schedule.py index 6118fdf..3c299f9 100755 --- a/sportsdataverse/wnba/wnba_schedule.py +++ b/sportsdataverse/wnba/wnba_schedule.py @@ -1,13 +1,12 @@ import pandas as pd -import json +import polars as pl from datetime import datetime -from typing import List, Callable, Iterator, Union, Optional from sportsdataverse.config import WNBA_BASE_URL, WNBA_TEAM_BOX_URL, WNBA_PLAYER_BOX_URL, WNBA_TEAM_SCHEDULE_URL from sportsdataverse.errors import SeasonNotFoundError from sportsdataverse.dl_utils import download, underscore -def espn_wnba_schedule(dates=None, season_type=None, limit = 500, - return_as_pandas = True, **kwargs) -> pd.DataFrame: + +def espn_wnba_schedule(dates=None, season_type=None, limit=500, return_as_pandas=True, **kwargs) -> pd.DataFrame: """espn_wnba_schedule - look up the WNBA schedule for a given season Args: @@ -19,92 +18,102 @@ def espn_wnba_schedule(dates=None, season_type=None, limit = 500, pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. """ url = "http://site.api.espn.com/apis/site/v2/sports/basketball/wnba/scoreboard" - params = { - 'dates': dates, - 'seasonType': season_type, - 'limit': limit - } + params = {"dates": dates, "seasonType": season_type, "limit": limit} resp = download(url=url, params=params, **kwargs) ev = pl.DataFrame() events_txt = resp.json() - events = events_txt.get('events') + events = events_txt.get("events") if len(events) == 0: return pd.DataFrame() if return_as_pandas else pl.DataFrame() for event in events: - event.get('competitions')[0].get('competitors')[0].get('team').pop('links',None) - event.get('competitions')[0].get('competitors')[1].get('team').pop('links',None) - if event.get('competitions')[0].get('competitors')[0].get('homeAway') == 'home': - event = __extract_home_away(event, 0, 'home') - event = __extract_home_away(event, 1, 'away') + event.get("competitions")[0].get("competitors")[0].get("team").pop("links", None) + event.get("competitions")[0].get("competitors")[1].get("team").pop("links", None) + if event.get("competitions")[0].get("competitors")[0].get("homeAway") == "home": + event = __extract_home_away(event, 0, "home") + event = __extract_home_away(event, 1, "away") else: - event = __extract_home_away(event, 0, 'away') - event = __extract_home_away(event, 1, 'home') - - event.get('competitions')[0]['notes_type'] = event.get('competitions')[0]['notes'][0].get("type") if len(event.get('competitions')[0]['notes']) > 0 else '' - event.get('competitions')[0]['notes_headline'] = event.get('competitions')[0]['notes'][0].get("headline").replace('"','') if len(event.get('competitions')[0]['notes']) > 0 else '' - event.get('competitions')[0]['broadcast_market'] = event.get('competitions')[0].get('broadcasts', [])[0].get('market', "") if len(event.get('competitions')[0].get('broadcasts')) > 0 else "" - event.get('competitions')[0]['broadcast_name'] = event.get('competitions')[0].get('broadcasts', [])[0].get('names', [])[0] if len(event.get('competitions')[0].get('broadcasts')) > 0 else "" - del_keys = ['geoBroadcasts', 'headlines', 'series', 'situation', 'tickets', - 'odds', 'broadcasts', 'notes', 'competitors'] + event = __extract_home_away(event, 0, "away") + event = __extract_home_away(event, 1, "home") + + event.get("competitions")[0]["notes_type"] = ( + event.get("competitions")[0]["notes"][0].get("type") + if len(event.get("competitions")[0]["notes"]) > 0 + else "" + ) + event.get("competitions")[0]["notes_headline"] = ( + event.get("competitions")[0]["notes"][0].get("headline").replace('"', "") + if len(event.get("competitions")[0]["notes"]) > 0 + else "" + ) + event.get("competitions")[0]["broadcast_market"] = ( + event.get("competitions")[0].get("broadcasts", [])[0].get("market", "") + if len(event.get("competitions")[0].get("broadcasts")) > 0 + else "" + ) + event.get("competitions")[0]["broadcast_name"] = ( + event.get("competitions")[0].get("broadcasts", [])[0].get("names", [])[0] + if len(event.get("competitions")[0].get("broadcasts")) > 0 + else "" + ) + del_keys = [ + "geoBroadcasts", + "headlines", + "series", + "situation", + "tickets", + "odds", + "broadcasts", + "notes", + "competitors", + ] for k in del_keys: - event.get('competitions')[0].pop(k, None) + event.get("competitions")[0].pop(k, None) - x = pl.from_pandas(pd.json_normalize(event.get('competitions')[0], sep = '_')) + x = pl.from_pandas(pd.json_normalize(event.get("competitions")[0], sep="_")) x = x.with_columns( - game_id = (pl.col('id').cast(pl.Int32)), - season = (event.get('season').get('year')), - season_type = (event.get('season').get('type')), - home_linescores = pl.when(pl.col('status_type_description') == 'Postponed') - .then(None) - .otherwise(pl.col('home_linescores')), - away_linescores = pl.when(pl.col('status_type_description') == 'Postponed') - .then(None) - .otherwise(pl.col('away_linescores')), + game_id=(pl.col("id").cast(pl.Int32)), + season=(event.get("season").get("year")), + season_type=(event.get("season").get("type")), + home_linescores=pl.when(pl.col("status_type_description") == "Postponed") + .then(None) + .otherwise(pl.col("home_linescores")), + away_linescores=pl.when(pl.col("status_type_description") == "Postponed") + .then(None) + .otherwise(pl.col("away_linescores")), ) x = x[[s.name for s in x if s.null_count() != x.height]] - ev = pl.concat([ev, x], how = 'diagonal') + ev = pl.concat([ev, x], how="diagonal") ev.columns = [underscore(c) for c in ev.columns] return ev.to_pandas() if return_as_pandas else ev + def __extract_home_away(event, arg1, arg2): - event['competitions'][0][arg2] = ( - event.get('competitions')[0].get('competitors')[arg1].get('team') - ) - event['competitions'][0][arg2]['score'] = ( - event.get('competitions')[0].get('competitors')[arg1].get('score') - ) - event['competitions'][0][arg2]['winner'] = ( - event.get('competitions')[0].get('competitors')[arg1].get('winner') - ) + event["competitions"][0][arg2] = event.get("competitions")[0].get("competitors")[arg1].get("team") + event["competitions"][0][arg2]["score"] = event.get("competitions")[0].get("competitors")[arg1].get("score") + event["competitions"][0][arg2]["winner"] = event.get("competitions")[0].get("competitors")[arg1].get("winner") ## add winner back to main competitors if does not exist - event['competitions'][0]['competitors'][arg1]['winner'] = ( - event.get('competitions')[0].get('competitors')[arg1].get('winner', False) + event["competitions"][0]["competitors"][arg1]["winner"] = ( + event.get("competitions")[0].get("competitors")[arg1].get("winner", False) ) - event['competitions'][0][arg2]['linescores'] = ( - event.get('competitions')[0] - .get('competitors')[arg1] - .get('linescores', [{'value': 'N/A'}]) + event["competitions"][0][arg2]["linescores"] = ( + event.get("competitions")[0].get("competitors")[arg1].get("linescores", [{"value": "N/A"}]) ) ## add linescores back to main competitors if does not exist - event['competitions'][0]['competitors'][arg1]['linescores'] = ( - event.get('competitions')[0] - .get('competitors')[arg1] - .get('linescores', []) + event["competitions"][0]["competitors"][arg1]["linescores"] = ( + event.get("competitions")[0].get("competitors")[arg1].get("linescores", []) ) - event['competitions'][0][arg2]['records'] = ( - event.get('competitions')[0] - .get('competitors')[arg1] - .get('records', []) + event["competitions"][0][arg2]["records"] = ( + event.get("competitions")[0].get("competitors")[arg1].get("records", []) ) return event -def espn_wnba_calendar(season=None, ondays=None, - return_as_pandas = True, **kwargs) -> pd.DataFrame: + +def espn_wnba_calendar(season=None, ondays=None, return_as_pandas=True, **kwargs) -> pd.DataFrame: """espn_wnba_calendar - look up the WNBA calendar for a given season Args: @@ -122,42 +131,43 @@ def espn_wnba_calendar(season=None, ondays=None, else: url = f"http://site.api.espn.com/apis/site/v2/sports/basketball/wnba/scoreboard?dates={season}" resp = download(url=url, **kwargs) - txt = resp.json().get('leagues')[0].get('calendar') - datenum = list(map(lambda x: x[:10].replace("-",""),txt)) - date = list(map(lambda x: x[:10],txt)) - year = list(map(lambda x: x[:4],txt)) - month = list(map(lambda x: x[5:7],txt)) - day = list(map(lambda x: x[8:10],txt)) + txt = resp.json().get("leagues")[0].get("calendar") + datenum = list(map(lambda x: x[:10].replace("-", ""), txt)) + date = list(map(lambda x: x[:10], txt)) + year = list(map(lambda x: x[:4], txt)) + month = list(map(lambda x: x[5:7], txt)) + day = list(map(lambda x: x[8:10], txt)) data = { "season": season, - "datetime" : txt, - "date" : date, + "datetime": txt, + "date": date, "year": year, "month": month, "day": day, - "dateURL": datenum + "dateURL": datenum, } full_schedule = pl.DataFrame(data) full_schedule = full_schedule.with_columns( - url="http://site.api.espn.com/apis/site/v2/sports/basketball/wnba/scoreboard?dates=" + - pl.col('dateURL') + url="http://site.api.espn.com/apis/site/v2/sports/basketball/wnba/scoreboard?dates=" + pl.col("dateURL") ) return full_schedule.to_pandas() if return_as_pandas else full_schedule def __ondays_wnba_calendar(season, **kwargs): - url = f"https://sports.core.api.espn.com/v2/sports/basketball/leagues/wnba/seasons/{season}/types/2/calendar/ondays" + url = ( + f"https://sports.core.api.espn.com/v2/sports/basketball/leagues/wnba/seasons/{season}/types/2/calendar/ondays" + ) resp = download(url=url, **kwargs) - txt = resp.json().get('eventDate').get('dates') - result = pl.DataFrame(txt, schema=['dates']) - result = result.with_columns(dateURL = pl.col('dates').str.slice(0, 10)) + txt = resp.json().get("eventDate").get("dates") + result = pl.DataFrame(txt, schema=["dates"]) + result = result.with_columns(dateURL=pl.col("dates").str.slice(0, 10)) result = result.with_columns( - url="http://site.api.espn.com/apis/site/v2/sports/basketball/wnba/scoreboard?dates=" + - pl.col('dateURL') + url="http://site.api.espn.com/apis/site/v2/sports/basketball/wnba/scoreboard?dates=" + pl.col("dateURL") ) return result + def most_recent_wnba_season(): today = datetime.date(datetime.now()) - return today.year if today.month >= 5 else today.year - 1 \ No newline at end of file + return today.year if today.month >= 5 else today.year - 1 diff --git a/sportsdataverse/wnba/wnba_teams.py b/sportsdataverse/wnba/wnba_teams.py index 3826299..f01468c 100755 --- a/sportsdataverse/wnba/wnba_teams.py +++ b/sportsdataverse/wnba/wnba_teams.py @@ -1,10 +1,9 @@ import pandas as pd import polars as pl -import json from sportsdataverse.dl_utils import download, underscore -from urllib.error import URLError, HTTPError, ContentTooShortError -def espn_wnba_teams(return_as_pandas = True, **kwargs) -> pd.DataFrame: + +def espn_wnba_teams(return_as_pandas=True, **kwargs) -> pd.DataFrame: """espn_wnba_teams - look up WNBA teams Args: @@ -18,19 +17,16 @@ def espn_wnba_teams(return_as_pandas = True, **kwargs) -> pd.DataFrame: """ url = "http://site.api.espn.com/apis/site/v2/sports/basketball/wnba/teams" - params = { - "limit": 1000 - } - resp = download(url=url, params = params, **kwargs) + params = {"limit": 1000} + resp = download(url=url, params=params, **kwargs) if resp is not None: events_txt = resp.json() - teams = events_txt.get('sports')[0].get('leagues')[0].get('teams') - del_keys = ['record', 'links'] + teams = events_txt.get("sports")[0].get("leagues")[0].get("teams") + del_keys = ["record", "links"] for team in teams: for k in del_keys: - team.get('team').pop(k, None) - teams = pd.json_normalize(teams, sep='_') + team.get("team").pop(k, None) + teams = pd.json_normalize(teams, sep="_") teams.columns = [underscore(c) for c in teams.columns.tolist()] return teams if return_as_pandas else pl.from_pandas(teams) - From a98074f24433f7a6741eca60e5dbd9f9424b408c Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Thu, 27 Jul 2023 04:12:38 -0400 Subject: [PATCH 38/79] black --- .flake8 | 5 + .gitignore | 2 +- .pre-commit-config.yaml | 47 +- MANIFEST.in | 2 +- .../_build/doctrees/environment.pickle | Bin 925711 -> 924089 bytes Sphinx-docs/_build/doctrees/setup.doctree | Bin 2328 -> 2329 bytes .../doctrees/sportsdataverse.cfb.doctree | Bin 102043 -> 102044 bytes .../_build/doctrees/sportsdataverse.doctree | Bin 57651 -> 57652 bytes .../doctrees/sportsdataverse.mbb.doctree | Bin 85580 -> 85581 bytes .../doctrees/sportsdataverse.mlb.doctree | Bin 167270 -> 167255 bytes .../doctrees/sportsdataverse.nba.doctree | Bin 85789 -> 85790 bytes .../doctrees/sportsdataverse.nfl.doctree | Bin 206449 -> 206450 bytes .../doctrees/sportsdataverse.nhl.doctree | Bin 102345 -> 102346 bytes .../doctrees/sportsdataverse.wbb.doctree | Bin 86055 -> 86056 bytes .../doctrees/sportsdataverse.wnba.doctree | Bin 83926 -> 83927 bytes Sphinx-docs/_build/doctrees/tests.cfb.doctree | Bin 3131 -> 3132 bytes Sphinx-docs/_build/doctrees/tests.mbb.doctree | Bin 3131 -> 3132 bytes .../_build/markdown/sportsdataverse.cfb.md | 6 +- Sphinx-docs/conf.py | 38 +- Sphinx-docs/copy_docs.sh | 2 +- create_docs.sh | 2 +- docs/CNAME | 2 +- docs/blog/authors.yml | 2 - docs/docs/cfb/index.md | 6 +- docs/docusaurus.config.js | 2 +- docs/package.json | 2 +- docs/src/css/custom.css | 2 +- docs/src/pages/index.js | 2 +- docs/src/pages/styles.module.css | 2 +- docs/static/img/logo.svg | 2 +- docs/static/img/undraw_docusaurus_tree.svg | 2 +- examples/SportsDataVerse.py | 393 +- examples/cfb_pbp_testing.py | 49 +- examples/ex_load_cfb_pbp.py | 9 +- examples/ex_load_nfl.py | 7 +- examples/ex_nhl_api_pbp.py | 10 +- examples/ex_rosters_test.py | 2 + examples/nfl_sched_testing.py | 22 +- examples/test_mlbam.py | 76 +- examples/test_sdv_mbb_schedule.py | 3 +- examples/test_sdv_pbp.py | 1 + examples/test_sdv_schedule.py | 1 + examples/timing.py | 18 +- pyproject.toml | 26 + setup.py | 43 +- sportsdataverse.egg-info/SOURCES.txt | 2 +- sportsdataverse.egg-info/dependency_links.txt | 1 - sportsdataverse/cfb/__init__.py | 2 +- sportsdataverse/cfb/cfb_game_rosters.py | 1 + sportsdataverse/cfb/cfb_pbp.py | 6188 +++++++++-------- sportsdataverse/cfb/cfb_teams.py | 4 + sportsdataverse/config.py | 124 +- sportsdataverse/decorators.py | 19 +- sportsdataverse/dl_utils.py | 68 +- sportsdataverse/errors.py | 1 + sportsdataverse/mbb/__init__.py | 3 +- sportsdataverse/mbb/mbb_loaders.py | 11 +- sportsdataverse/mbb/mbb_pbp.py | 10 +- sportsdataverse/mbb/mbb_teams.py | 3 + sportsdataverse/mlb/__init__.py | 4 +- sportsdataverse/mlb/mlb_loaders.py | 4 +- sportsdataverse/mlb/mlbam_games.py | 144 +- sportsdataverse/mlb/mlbam_players.py | 236 +- sportsdataverse/mlb/mlbam_reports.py | 251 +- sportsdataverse/mlb/mlbam_stats.py | 637 +- sportsdataverse/mlb/mlbam_teams.py | 229 +- sportsdataverse/mlb/retrosheet.py | 422 +- sportsdataverse/mlb/retrosplits.py | 317 +- sportsdataverse/nba/__init__.py | 2 +- sportsdataverse/nba/nba_game_rosters.py | 1 + sportsdataverse/nba/nba_loaders.py | 11 +- sportsdataverse/nba/nba_pbp.py | 10 +- sportsdataverse/nba/nba_schedule.py | 4 +- sportsdataverse/nba/nba_teams.py | 4 + sportsdataverse/nfl/__init__.py | 2 +- sportsdataverse/nfl/nfl_game_rosters.py | 1 + sportsdataverse/nfl/nfl_games.py | 3 +- sportsdataverse/nfl/nfl_loaders.py | 42 +- sportsdataverse/nfl/nfl_pbp.py | 3879 ++++------- sportsdataverse/nfl/nfl_schedule.py | 4 +- sportsdataverse/nfl/nfl_teams.py | 4 + sportsdataverse/nhl/__init__.py | 2 +- sportsdataverse/nhl/nhl_api.py | 9 +- sportsdataverse/nhl/nhl_game_rosters.py | 1 + sportsdataverse/nhl/nhl_loaders.py | 8 +- sportsdataverse/nhl/nhl_pbp.py | 10 +- sportsdataverse/nhl/nhl_schedule.py | 4 +- sportsdataverse/nhl/nhl_teams.py | 4 + sportsdataverse/wbb/__init__.py | 2 +- sportsdataverse/wbb/wbb_game_rosters.py | 1 + sportsdataverse/wbb/wbb_loaders.py | 11 +- sportsdataverse/wbb/wbb_pbp.py | 10 +- sportsdataverse/wbb/wbb_schedule.py | 6 +- sportsdataverse/wbb/wbb_teams.py | 4 + sportsdataverse/wnba/__init__.py | 2 +- sportsdataverse/wnba/wnba_game_rosters.py | 2 +- sportsdataverse/wnba/wnba_loaders.py | 11 +- sportsdataverse/wnba/wnba_pbp.py | 10 +- sportsdataverse/wnba/wnba_schedule.py | 6 +- sportsdataverse/wnba/wnba_teams.py | 4 + tests/cfb/__init__.py | 2 +- tests/cfb/test_cfb_schedule.py | 20 +- tests/cfb/test_pbp.py | 168 +- tests/mbb/__init__.py | 2 +- tests/mbb/test_pbp.py | 12 +- tests/nfl/__init__.py | 2 +- tests/nfl/test_espn_pbp.py | 10 +- tests/nfl/test_roster.py | 3 +- tests/test_dl_utils.py | 22 +- 109 files changed, 7075 insertions(+), 6709 deletions(-) create mode 100644 .flake8 create mode 100644 pyproject.toml diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..055c73c --- /dev/null +++ b/.flake8 @@ -0,0 +1,5 @@ +[flake8] +max-line-length = 120 +max-complexity = 18 +select = B,C,E,F,W,T4,B9 +extend-ignore = E203, E266, E400, E501, W503, B905, B907 diff --git a/.gitignore b/.gitignore index 44e91c1..29fa79b 100755 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,4 @@ dist *.ipynb .ipynb_checkpoints ./.ipynb_checkpoints -*/.ipynb_checkpoints \ No newline at end of file +*/.ipynb_checkpoints diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b0daf5f..79c80e4 100755 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,52 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks repos: + - repo: local + hooks: + - id: update-docs + name: update-docs + entry: bash create_docs.sh + language: system + types: [python] + pass_filenames: false + - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.0.1 hooks: - - id: trailing-whitespace + - id: trailing-whitespace + - id: check-merge-conflict + - id: check-ast + - id: check-toml + - id: check-json + - id: check-xml + - id: check-yaml + - id: check-symlinks + - id: check-vcs-permalinks + - id: end-of-file-fixer + - id: requirements-txt-fixer + - id: check-added-large-files + args: [--maxkb=10000] + - id: debug-statements + + - repo: https://github.com/psf/black + rev: '23.1.0' + hooks: + - id: black + # - repo: https://github.com/pycqa/flake8 + # rev: '3.9.2' + # hooks: + # - id: flake8 + - repo: https://github.com/pycqa/isort + rev: '5.12.0' + hooks: + - id: isort + - repo: https://github.com/hadialqattan/pycln + rev: 'v2.1.5' + hooks: + - id: pycln + args: ['.', "--all"] + # - repo: https://github.com/pycqa/pydocstyle + # rev: '6.3.0' + # hooks: + # - id: pydocstyle + # args: ['.', '--ignore=D100,D203,D405'] diff --git a/MANIFEST.in b/MANIFEST.in index cdf9187..7794599 100755 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -8,4 +8,4 @@ recursive-include *.model recursive-include bin *.py *.cmd *.zip recursive-include docs *.py *.rst *.css Makefile recursive-include features *.feature *.py -recursive-include paver_ext *.py \ No newline at end of file +recursive-include paver_ext *.py diff --git a/Sphinx-docs/_build/doctrees/environment.pickle b/Sphinx-docs/_build/doctrees/environment.pickle index 89a32264b7074b7c24fb8d1354e12fb1d9972bb6..0fabdbe4fe5dadf701701c4ecc94ca1f9a3d865f 100755 GIT binary patch literal 924089 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 literal 925711 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 z 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

Y2A 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�JYs&Q{!1zeqSyxffn#1g$P3;nHjMd z-dX=>!Kaes$`NC>kUmX?N8Ch4MnBp3m?BL@q(wY^hr>0NBNE*0_U+FPfG;J^% z^sw;C1K{T3gT~DQpKvSk;RQVGziI$E}98;PA!Cm%hIJ(yOmP z#iaIO8s_ESj}!~j6>KSJ&X#I+TTdQ8-LeBq9tS50JcYEkkUb+j-gUhtDXP82PeP)X z2Gd6qdF#v4VR*<5%r1+4dVUHYp$qK!=Vzj6xm&Psm=&*9yC&clTFL0RVoHGUhU<8s zjEiV$PYcQacjf=k-(1Bsi$uS3N;BN?r@oFR==L?5JEj(OR(a@;KfE6!KkOLV90$h3 zBff1go$N{^c$N*#nJdS|O_8b#4M`2*gqrt4XH*GRh`|wKy*}D)*OKAn4BZ%FGEDp^ zB}sL=abty%N=$#?SUb>nMPBl>w>?%u*YL0VhRAaa` z_ZcFCy)|JpF&zU#u-cU)gG_XJ%WGt7b->OL7I(Q)$_e55UbQ9}12i|=6jb*tj=_27 z0;bmiJWLiRyKO0s9-R-*cH2FRZz!qpUIu*-&OW{j<$&DG)d!wN+;1VDyRoF4b8AV` zKxo~*D1p;1lq3|$IjM9-XVl z+hMKVD$|HObXzzL57rAZi^6+h&8HC%CQbxza9?xG;W_!C#4e&G8P@7ccnC^XO=__ zw31j#qefZ*G>x>9L1|G*j5X9sW~_!*GOLLHYYO$?|a;BZ?$5B?`c`_Qf9<%Lu*x^MQ!@u8FLJ^57 za4s{!mlc?O@{;c^pN&+Aa8H{!6ETyHin{77FXUDGP?E2h=(J2ZHKM9Jn8Cj}&@Xfu zO)*^3g9L!y)?j(ypv4A0sCR`0LRElRTTS%TNOEZRy85|Zh}(j!DnS)>Cyk;$IPC(E zHG*v6O{!?oJRLr#PxkaQx6N(Clf~05#pyQ*k5fH*vAZ}`o?+vKRM$SSI7cEe&MRyd zvf|o=pEveq#C*nERvFBc{J=O@m1+=(V%lOpYll%x1r95si;*;WuWLnQX8W zQ+_%pMfUwJNJW`aA|#mlr!#}L*Z65?(kt#-?}BcI_m1zV z9XAFnYnBzQQMA9er^!N}tG{ONN)|h6PH`V{eTu&$fo92&3+NKBmsv74^zy-Fa}=;9 zC8EZ%aDPF0$>dCa+al-x{zs0>AZ87JM5W*1pZ_EJc4DOyr}HKh*vHJFng8y>{olH+t|)+-%9``)}O2 z+k5T1KZug~e(fA4K6U#@UCo5^h(CHOt@2z+O{c$23!Z!piT z&Fqn$^bG>eIM3(#?G{^t(25I)nEd!u4uk8j1H27{(jv$&_}U&^=wBT2!NGsZ&N}EH zK41_G#2&k)dylox7m8Hx>fVcJH*am%94% z1ds_qKVXxTH%T}Ka2oPro#S-#;Nm9#&RhM5{n423UW?%+buMyU=e&!q=}LfbMsWuQ ze-Ew;#+A7G$2*FPunn`J#A@q)>WWOoXGCp>6V(?F<$xG?I-}|Z1TH*RzJx#|qn@>E zfE5>W3;{jh$gt=nbKj1Vl=|HfH1G5Iuy=OR#}s|!+%BM-@aCkrt4KY0`N%Xx)E3GKHvH zhFbdI$`YhV?Q>buJQPqYOOo+~`GU5jrwqr$=9=@o8p)$*#cx{f?OK{!#$?|J%E^-o}pP>;-Z`CU6YwB3J|nk`vAvp=KrOG9IrZ zjK^3|;` z=ky_|+l&{8U=PW@R8@CZS65eeSC!NOEw;^%;C|?f8K-Lg^PQJ!kSmkIhO$}kxj+&! z=hV|LMz#ekXUW%VxQ8fjA7DemlZRh_zPN2oEoyk5JbIIL@Ze^2OOu9$eWSM~_I<2Q zeq+PiWnM|7V}9#q)|+w3EXL;JUbp&C!d)}WRvy3s?V;KxaGx~>Q1<{L`;5$DY%$z^ z@+gCkjuA{`lL8Osz9ysGDsj-k8~XR_Wlhp+Yf0Od5_+M>6Kwo57NmC*Y#4f4b`8s| z<2J%LfXk`w4Fj0~Lf@#*ADtcz>AS2ejZ4}jVxG~loN-y2de;oB zJZQHgn=2=g8*zE7b_V~LgM2u}^)Q4GI+x>;%ip-Zg6j)nY~|ul`EYaVOdG2{F6`=Y z`A&qTC+;bZ{%#r}!cF zX^!uFl4=?1tkkE6yzNs1_6ic=(v;qPN3$crrM`x6tN)D~*3r!KIh9%0(CO-n;4)$F zupeC-v#IC{;TYSdpHHOqQD%itBmSm2<5P6L1SyV3B~9$PH0`RR77P~Om*7E)=VQ>P znA_GEFL2z9rMc^xAxPQ&S;gv!uk=>kcvriuh68SJuooMTn2Dty$oAX8CCaUgBIJ4_ z`fXT~`BHzza-Z+My&`2Bb8ky-RY7)_qLRGZ9r}5hKGszhq6%WltQN&yJ+53WOGsX-*R;-~J5M{tNPkr={`3 z(Lx1dPYCI%XXo^ESYFyzIhGw`?|Z0fi#O~dqZV$UA)3`VmHoBts7smmJyf z6P&-|br`opM~&dhuY%^w9Y>9`7w1?n#o;DH(B23St5L%=uVo9Hg=@99a8Q@oi6uQs zdl8hv0e2e@3y5>{wj~|*XG1V!b6B+Kiod+Dpl?=kIRinq0`bKaT>B?wlpi@G_+7yD z+6*uXWg-l9r`vSJqT2ylQVE{hFyUPa)Cbs6X(tt$3R2MM51jZ^>u_U8F|#UCN-BbN zRMOKCnS#;T!2!%Hi5APLXu@QxNm$g`kyX{q1DW~#-jm~jC+&J48y#E72^`Ic-5 z$EoH4Vwd(jK&ZSC6w{QjkgN#MIo&vDN?0gZ5L09a*=dq>iA~kjtaQB!;|63_@G~vBU8;$KgmYzH5nMqdH(l9s<2u#3nXVnJSM1lB zm%eDN5|zx6W0Fqo3yZdC-)%>%VoWi0^e*_U)7`ckHvAYvR*__G9e`v#bqC{`LC(8n009}Kah2QI--BXSS+=nXXD||-PI^ape?_m|vX`B4 zk1jG5To_u9?;Ei<;RQ^BKP*WcPV)mBU?BuV%9cD}u4*-SO*0EB^({B$vP6(L2weWy zSUu`;_e}f}Ka-4V;{@(Ly&=`7F{IxZ_{SYw;vtQ+n2fKCg5RGdkbHruaZM=P+qVRo zZT7EmO^Bjra|vYBrfv3p2&az4^q|?;h3AQ8>!(X(C)}k+?$*atRJfmdelh+at@G0C zL{Gm;?BuoBE3lJ?TV^K%e`R(GQIy%qkX(_SLKG=>GBj6arw~ycI~lSC*eNV3>{PR1 znkgU%%B2~Lgyl^zmWO*|j1{7|A;t<(+$dv(h;D?j7MD|3TK4<|;T4>)n#{b?vghHJ z*~!4)sGKq+H!Pj6&6*46f2CDPKFdLLjI?yZ&4W;pQxN9CCU;>QAtWj zl#_mnN=j0qtTr<$tZ6}MlgQd4|1(i0Xpxnxw8%1Ci^7Bi%%DY9veF{U2wPN2W5|KE zn*BGEMxNMClJ;Uzi|dSerL85KwAzEA47(c0$)HRbfrZjp`T~O@_#}OdkKpN@HwmRG zKn7RabVSh{NinB)aSH!uw4kB4Z}-Nj9g0Z;Q_}& zAnBzC?G&R%dm&&j{PuHCjjpq)FbKroX&5q_Y7omDOjooiTT@l0TDq#u%$lk?8Pj#$ zOf6NmS^J1xFA$GNU-|TTBW5M5eHG?Z2FWs0eHG``R!#MlRjuhO%TD%HDYJst8zsfi&J29c_!HOmh+TmQh!2zagCR~35QA=T0P|0Idbi*uvQso0 zrBWUjk#{jr-%fz00wC(ULn!PoTKe*sZ&ZRX*hJ`sHhH2gn&ZZ%1R4CigFrF9!Q5MT zB6HeotZg=ez!WKvrE0#Dtw9S^(*8-6^lkKr78sBeC>sVc3@17dZ`PWPRJ};NOX!9X z$tvo-IFF8{=$s3^JUyH=PuoYmyx*mtxHEA4W~-r2ZSho%!WEXQN8K=hOY8ZKk2{&_ z5P?9hZ{~%90v>UsWw=Z)_`FyLYwTzqvYB94%1Q0kUI%sijk>6L1qq$3dfcG%;Urv$KB9F3OyP=x) zXI(0FD+s6 z<;L_TNV_-{@dyzT`@1qSTKj0BvIgHEDyQtc4P;)$nS;$wCF|2TCH^IgTVm1kayBKr zRgy5D$qmZCgF7;cAshEMhVyrL8kx%UZl_T_6>(qNaMS0dr;#Ch3)9F@y_IQX$iA1; zh>5G4Mt;7|P)kla{iBL<%Q{?ke=FO-sXu93>q{*^t;_2qZ>FyZXCYeqo#Qr0BXU_K zVlrFBN$+R^3S*w8OCGg_S1P3e*;>G$=?7^n^|0%UN6;5)So7mG9OzoJN?J-q%XOLv zbOll0aaCqKX?;7zzi@G;`EYnm@q%${v`)4fLg`01hU}d-_Yu+gZm4pRP!J~gcx?@| zT$0pCTM3xyTEIo!Km&8hKA;onbw6Uh>Fb|dU)!Utkjylo-AJN^`<3hJ@-*7JDYU;M z1I==0CbRp-&5(dL3+7HDl?IsJHt%5ZywI#;(IR;2xHOHVu5>K(;+E8PPZ~pAX^%lV zQ(f^ogJxjhl}o11vMgN=&ns{^jSwJW0AV+b*kZCZ0s#gEsDl>YjB+xgYNwoiIkal4 zt1n-^ekH3Zww>_UL|1QYM?~iiIFVgvW47P(IMTHv9L+6Ub%r!844LLpwk0^mWr$+qvncZM=h~J%Hc~=)bO(nbLOUK}#rD z?|9HkO}qS{UA}2o8noNllVOq3c>`WI;u;S;z`^2?J;eWy1kS18 z*E3@l*Q7fB2yn<|2wGW?R_!fXt~7+9w(UXT$lIK&rZOo6^WuTLED7v$6n z67L4{ZSrTGb`~&6z?^lZuYof+KAOXsZWJ!XLbMAC+EitH2mb$gh~%|xoEMRCb^3SE z-vJNRm5z8Qu#p5F$#KkK+ZUN5W*vxQX6uTZx|LFWLMN9$PZq&EN5_%zmz@5*8N=+(v|IG*f0Nys%Pg*2-_eTrm26~rBEz*SfS7Cj_l$- zu9gJKGQHd@_zgiLxEND|WBqyZ#Vd`2HrgpE^$_?8prVXvAzpD8^5-@f#4spVgKj|w zEsL+J66_1mX}x(3bQ-8SIz3hHoE~kbBL%mMf=h+Ge}sdJcKNa^Qa*`jxC)B za4t8%Gh-TLhCGDq;S_0(l5_2@xT`!o(6Q0F=g4sNkiP!$T96ZBDHHF(h3(^05qY4T ze!eKp!y7+e-_PsdEyt#3nIJzT2r$B*K`9T~krp9zsZ!@$9CE)OIn_`PJAb_MOL+5n zKYB#0S^GO*A?t(a0WO@{eDnOz-+OPTv-4|Y{%M#=RI#g9E%KY4|3;C>*)>TKyA!9S z{^#+lAD~#|G26=*DfIYPKY08vC^coLM5Si1JD)m2IJ4RWPM$#-=hu?Nx#cdS#Aza2 za^w&QE`uPoa9~`}i)6g(DRTA}Brb!@^9=k3{TKRJo&Sum(ZB<2{5R^F>z~oEkHD&& z))%MOfR~f~#r$&H@3c$o@1YOwq_6Nx1$MS>Y*HVHuV4y&(7_{hj{M5rgO?Ez2O-k8 z?yarW`1KBgJdjJH$nzSDmdLe{A;q@1KODZurX%*_KrW6mmq%AQ;rbiVNKcg@ktEyW z;~)S9lkNN#6^QH%rx4fXk~`=K!?D56B*yvi6kMf*;T(Ze5lv&VB6XItk2q;~bfhUh zurzn3!4m@^c}0>?ba3f9RDa9M)R4}!KiSOC^K}lEqQ1^%Iq!B|E1gCijO`eZ!ND}* z&vdmlLa4=7)i6aTlgVgvbrsQZb6kM!wMeeuXlk&E9|b3`zT8-q>AHH*DPH801DtlR zvNtMr=zK6l;J5}e1AJN|ehb!+vj&p_dCZ?!+x_iNBnI7&Ebca>@~1tDFGc|4;*NmW zEq3xF7`EgiU`}D7{3R-3Q~Vj^N*LWm;^nE|qtFi!DHf86ioTTSCh}WXS=9g~jjb+g zUe~kB(A^L-`TmfMM0om;Lhd>mGzLzJoM7?r`L_^d5B4~GpI00$nI0+nRDOH2#4a{0 zmW}$@poY`mMIoEsHXkxte^KD3Y|(=YuHDt z;;g`WpD$j=ofb0xJUFxsT60u0dfb16a_`bS>_yr{*Ub@0Q=V-2LyL>I%K=()0m}Zm zfhgVKwu7L)o5nhi7Hvd&hLFABFv_SnAhdbJmeh@@!n{;tE1-F&dp$$!2yUP9)iq40 z#fP{;SlniU==;HO--1J20e+!Yck@1b3PXFmGdaTCIL?PZ4_p~WWbun};&qT6XPD2z zQfhB9<)`iC;&9Ri7OFrgxq@SQ+z3=kvq&jJ4%Zn^qj*dpc(ja(!z=9pzlc%xtO*~7 z`Eo6NjXuPZKKtR#EV!wNUj+1Z%?S+A$-B5&uVp9(wr@XG5uIjBmLK6BNx~f*Hg)hD znRG4D$m^?$NG>7d6_2?E(W;^1+hG7c?T~&ll{KN=&5OfvZ$xeobWz0#)?@^COySup zHA9Tm;)siW+h1_s#SoMTfRC)WM`-QCSuWW59T8X}AR^E&Ld*;M`$dZc1~V{4m7$o$ zM;Q`vm0VB%*@xBZ)=5sW)t`HwH?0%4Rwshb13BjgUZkg%vqC6SUi*t zOZ$XD&3vLR@|>QHX1~GA!mHmE@CgtZ<9P_-CF@k(id+GsLJ^d=Z)cru7fx^qvWss; zSJ@DAX)uC~0$yXmWk=ZIAiyFdZI-{rrH9!at7=kBIGv{`bY|cH>+fZA889Yus?pdqg+Y+^Bi;dSA^9 z*GG#1WVMP}Q~KEnBY4+QIpeL7h;@$R>&{rC6{wjBuVCQ#S$~u_#TwQs@|_(SKNxpp zXfiA*LzBx}%+Mr@v{`L_aov_CX}N@a%R6vFnHdW0MmCV}vf~JsIp4oe4jv<7vWLb~^2BQI27`|EQ=q)(#9zvMk~v_c1JSfnmlP8kiUe zJ#2Y2K6;W2Fn&m6r2`dW<)GQ?9Rt3gU;5z8r)T5a24cB%rQEO?hT4!;+Kp?zX$mO z+v)n6%9WX*oaB3}j2B(0}awr=BJ!k?{d#Ae@_6RpWT-Go^&C>zInW-f`lPhWFUz6*xgq;F(ipL0eSv2o4kMow(=~P&CK!ZQ z)L9FeZOo-2R7=(?`i8Y@G#oWwm?wWKL4ORJmai^1x_wGPA^N>v-Og6-tiUQnp)oeI>EvkT6H2i=E&qXXXLp*dJ4-*&g9Ui&Ixe=Lpgmr`g{i=?9B{agnAeJ+0E=LcBaDgF zn)U}JGVx3a=7oM=O3Hk~n>zOSyT$uYW-ETeh1zm*MXW#Q;>sPm2iuU|1Od)53kZXs z1hnK+j!z+OjsZ~${dtUB#T*s*JzGXIupJ)h0{o!MPe+J~6q29fuAZB%goSO?@HzZp z(f)AlZnu$rm|?TbZCw_^ybz%BfWL)*92L{X|NiD*iI8XT(-A=?5>S>t7AXCOe6KJy z%dQ(Se24uGVi)fslF}Y{e0PdlCDN?F<(xr2IT^|e84B9L5$_5VZXWgv2myTKb3h$! zqMwR&CX>$LNt<1PoO*OYDJv=P(INe#QZAL?YnY%fALH#A`4zG84`eWvvp%M&)jm6K zSS-WSxWfcltIZ_0+vq#081<!)z>2^TQbfqKI8cg%zKn=; zBC56;EUIvrge!SRG3(OQQaW{l(ab8%>LV~2LZvJv#tGAc3`b~ht9J}Rmg~@?>50-b zViAW(4=HBtqa^!8o@VbBmH(i+(1MVSqg5-d75-Ac@*8Li@938&XO(vC3L@@mc zC!(pk7P6#;w!XE&L=9k7}XD z&N>SHSzIU>Y%O+o=U%l~ARe^X&vyO>#a3fzb30Wl-{1KZmEVslfA`H(s0urOMOku} px0L-`WS7a0=t>oc+Q$Xzdx^6z-XgA685c^OnocwMZaO_^{U82>zn}mB diff --git a/Sphinx-docs/_build/doctrees/setup.doctree b/Sphinx-docs/_build/doctrees/setup.doctree index 9a8de415341d074559b2b0af7dbeb33ffc2ca1e1..bd9edbacd4734d042c1d1bb69eec7bda5ecab05d 100755 GIT binary patch delta 26 hcmbOsG*gJBfpse1MiyokcDCdKLnBk8&Acq!tN>T_1-t+N delta 22 ecmbO!G((7`fpseHMiyq4$!C~3HVd$DvH}1{odtXV diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.cfb.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.cfb.doctree index cd389b60400f07dc2740b34d8ae280ab7bfac95d..1d610387293c6c9ebf5934ab1b4627b943a35163 100755 GIT binary patch delta 230 zcmbO|mu=2mwhhbQGI1qLj(ab$`QTd*Mp3RQty3DO)K1aJVC`Yj;hK^mofRxRS+HAX zvDJGfMy|=$AE&Xg0|ncKCw~l9o&4wHOt7H*>{n%K6N@faAPpo9rn*8On zAtU={wJ&EG*?57%+dw)cHVc01V`Ajpyx>P56B{>B_<```i3OUI6@D9mRhc3whhbQGASfXj(ab$`QTd*Mp1<+ty3DO)K1aJVC`YjQJ9h;ofRxRS+HAX zvDJGfMuo}NAE&Xg0tMTJCw~l9o&4wHOt7H*>{n%K5N@faAPpo9rn*8On zAtUQ%wJ&EG*|>qi+dw)cHVc01V`Ajqyx>P56B{Q`_<```i3OUI6@D9mRh1q^8MfM8F@FK{}aW?#s^fcC<0a<@y}@Tz2AJBXZ&krWGve%$0*Y@{ZAt! L$9Cr?#z-Cj@9bNS diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.doctree index 234a04ba781d90d2bf003d3998dc5bdae65285bd..3b3e087c5719440e2e6aae6b0e99fc82c2c0e070 100755 GIT binary patch delta 28 kcmdmdhzhxP0n)`lsJVI1#$8Ngt=p*5q(^nVtNDqE*9b~y2~B^MYPnHo*$kx4Bo$}gJ! O-ic9SyNfd;2R8s*r4sf4 delta 60 zcmX>*hxN=H)`lsJVI1##8Ngt=p*5q(^nVtNDqE*9b~y2|mX@UHT1@GYNi8bMFPi?| NiBV#^t1}}9Hvm$j6AJ(U diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.mlb.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.mlb.doctree index d4e1c2f25e043f1d330c80903755bac113424e2f..c75e1ad4cc3c6b078addf381042fc2ff21b5c75f 100755 GIT binary patch delta 313 zcmaDhi|hIp2aG{1(Vx&64@xY5~Fyg*VruDod;F&X#2%trU-6!w&VgsBU7X8 J`t?lad;lD8Xq*55 delta 360 zcmcaUi|g4eu7)X$yo{M!`4}hPV@%)f$i|ex$fz*gU=O3ocDo0R#w?6erca#2I0-2B z!Gy_gd(czHU)<;lUw*<=`1dzNMG0;I diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.nfl.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.nfl.doctree index 360ca3d9741edf8b55a383b7e83c24e583c57f30..5eb0161dd9f3874cf367af97bc321703119d7730 100755 GIT binary patch delta 48 zcmex(hUe26o`x327N#l8X^bpe)0w}E@UtZs7#f)xP3e(IEh@?{nyxR(EU~>sl=-ay E0Jx11sQ>@~ delta 47 zcmex#hUeoMo`x327N#l8X^hNU)0w}E@UfPbr0H5r>5)k-D#|aKt}n_gvAtE4`K26d diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.nhl.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.nhl.doctree index 499c688c45b2634d60ab586bcd0937e8ac16874b..2be2255eeb9ab02d0d51b14c20e28264783db811 100755 GIT binary patch delta 31 ncmX>(pY7CqwuUK;o{W`Sy%>F(+1Zi{42?{Uw$Ev1)a3yHw?_&; delta 30 mcmX>#pY7y)wuUK;o{SY+y%>F(*;q?U(sV7h&uwPZ}<&ehDN4F+h^D?+HnH_u_Fn- delta 30 mcmccK&w8z&wP6Y)KjZ$b0*p;IY^!k23Y_A delta 25 gcmdlZv0H+rfpx0EMiwS^HrCRTG+m3$yzKg{0A9TYZU6uP diff --git a/Sphinx-docs/_build/doctrees/tests.mbb.doctree b/Sphinx-docs/_build/doctrees/tests.mbb.doctree index dbc0874a42c5835b9752290f276256d553db20dd..2924dc3b058d88e28ba60cfc24bf8b1df896d05b 100755 GIT binary patch delta 26 hcmdlju}6ZXfpx0kMiwS^cDCdKLnBk8%{=V-tN>!k23Y_A delta 25 gcmdlZv0H+rfpx0EMiwS^HrCRTG+m3$yzKg{0A9TYZU6uP diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.cfb.md b/Sphinx-docs/_build/markdown/sportsdataverse.cfb.md index d02a889..e01d45c 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.cfb.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.cfb.md @@ -164,12 +164,14 @@ Initialize self. See help(type(self)) for accurate signature. #### create_box_score(play_df) #### espn_cfb_pbp(\*\*kwargs) -espn_cfb_pbp() - Pull the game by id. Data from API endpoints: college-football/playbyplay, college-football/summary +espn_cfb_pbp() - Pull the game by id. Data from API endpoints: college-football/playbyplay, +college-football/summary Args: game_id (int): Unique game_id, can be obtained from cfb_schedule(). - raw (bool): If True, returns the raw json from the API endpoint. If False, returns a cleaned dictionary of datasets. + raw (bool): If True, returns the raw json from the API endpoint. If False, returns a + cleaned dictionary of datasets. Returns: diff --git a/Sphinx-docs/conf.py b/Sphinx-docs/conf.py index bf10e35..55062e2 100755 --- a/Sphinx-docs/conf.py +++ b/Sphinx-docs/conf.py @@ -17,9 +17,9 @@ # -- Project information ----------------------------------------------------- -project = 'sdv-py' -copyright = '2022, Saiem Gilani' -author = 'Saiem Gilani' +project = "sdv-py" +copyright = "2022, Saiem Gilani" +author = "Saiem Gilani" # -- General configuration --------------------------------------------------- @@ -28,26 +28,26 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.doctest', - 'sphinx.ext.viewcode', - 'sphinx.ext.todo', + "sphinx.ext.autodoc", + "sphinx.ext.doctest", + "sphinx.ext.viewcode", + "sphinx.ext.todo", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = 'en' +language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', 'setup.py'] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "setup.py"] # -- Options for HTML output ------------------------------------------------- @@ -55,12 +55,12 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'sphinx-material' +html_theme = "sphinx-material" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # -- Extension configuration ------------------------------------------------- @@ -71,11 +71,15 @@ todo_include_todos = True import os import sys -sys.path.insert(0,os.path.abspath('../')) -def skip(app, what, name, obj,would_skip, options): - if name in ( '__init__',): + +sys.path.insert(0, os.path.abspath("../")) + + +def skip(app, what, name, obj, would_skip, options): + if name in ("__init__",): return False return would_skip -def setup(app): - app.connect('autodoc-skip-member', skip) + +def setup(app): + app.connect("autodoc-skip-member", skip) diff --git a/Sphinx-docs/copy_docs.sh b/Sphinx-docs/copy_docs.sh index f110881..9991730 100755 --- a/Sphinx-docs/copy_docs.sh +++ b/Sphinx-docs/copy_docs.sh @@ -8,4 +8,4 @@ cp _build/markdown/sportsdataverse.nfl.md ../docs/docs/nfl/index.md cp _build/markdown/sportsdataverse.nhl.md ../docs/docs/nhl/index.md cp _build/markdown/sportsdataverse.wbb.md ../docs/docs/wbb/index.md cp _build/markdown/sportsdataverse.wnba.md ../docs/docs/wnba/index.md -cp ../CHANGELOG.md ../docs/src/pages/CHANGELOG.md \ No newline at end of file +cp ../CHANGELOG.md ../docs/src/pages/CHANGELOG.md diff --git a/create_docs.sh b/create_docs.sh index 67d13fd..544cbe1 100755 --- a/create_docs.sh +++ b/create_docs.sh @@ -12,4 +12,4 @@ cp _build/markdown/sportsdataverse.nhl.md ../docs/docs/nhl/index.md cp _build/markdown/sportsdataverse.wbb.md ../docs/docs/wbb/index.md cp _build/markdown/sportsdataverse.wnba.md ../docs/docs/wnba/index.md cp ../CHANGELOG.md ../docs/src/pages/CHANGELOG.md -cd ../ \ No newline at end of file +cd ../ diff --git a/docs/CNAME b/docs/CNAME index a4b81ac..71a3e32 100755 --- a/docs/CNAME +++ b/docs/CNAME @@ -1 +1 @@ -py.sportsdataverse.org \ No newline at end of file +py.sportsdataverse.org diff --git a/docs/blog/authors.yml b/docs/blog/authors.yml index d11161c..c918d45 100755 --- a/docs/blog/authors.yml +++ b/docs/blog/authors.yml @@ -39,5 +39,3 @@ john: title: Author of SportsDataverse url: https://github.com/john-b-edwards image_url: https://github.com/john-b-edwards.png - - diff --git a/docs/docs/cfb/index.md b/docs/docs/cfb/index.md index d02a889..e01d45c 100755 --- a/docs/docs/cfb/index.md +++ b/docs/docs/cfb/index.md @@ -164,12 +164,14 @@ Initialize self. See help(type(self)) for accurate signature. #### create_box_score(play_df) #### espn_cfb_pbp(\*\*kwargs) -espn_cfb_pbp() - Pull the game by id. Data from API endpoints: college-football/playbyplay, college-football/summary +espn_cfb_pbp() - Pull the game by id. Data from API endpoints: college-football/playbyplay, +college-football/summary Args: game_id (int): Unique game_id, can be obtained from cfb_schedule(). - raw (bool): If True, returns the raw json from the API endpoint. If False, returns a cleaned dictionary of datasets. + raw (bool): If True, returns the raw json from the API endpoint. If False, returns a + cleaned dictionary of datasets. Returns: diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index df675de..22b786a 100755 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -248,4 +248,4 @@ module.exports = { }, ], ], -}; \ No newline at end of file +}; diff --git a/docs/package.json b/docs/package.json index 2b57f3d..6e956ce 100755 --- a/docs/package.json +++ b/docs/package.json @@ -37,4 +37,4 @@ "last 1 safari version" ] } -} \ No newline at end of file +} diff --git a/docs/src/css/custom.css b/docs/src/css/custom.css index 8f7b397..2624f42 100755 --- a/docs/src/css/custom.css +++ b/docs/src/css/custom.css @@ -290,4 +290,4 @@ html[data-theme='dark'] { .grid { display: grid; } .grid-cols-12 { grid-template-columns: repeat(12, minmax(0, 1fr)); } .gap-2 { gap: 0.5rem; } -.col-auto { grid-column: auto; } \ No newline at end of file +.col-auto { grid-column: auto; } diff --git a/docs/src/pages/index.js b/docs/src/pages/index.js index 4779834..cd758fd 100755 --- a/docs/src/pages/index.js +++ b/docs/src/pages/index.js @@ -117,4 +117,4 @@ export default function Home() { ); -} \ No newline at end of file +} diff --git a/docs/src/pages/styles.module.css b/docs/src/pages/styles.module.css index 0e11bd0..acce3bc 100755 --- a/docs/src/pages/styles.module.css +++ b/docs/src/pages/styles.module.css @@ -72,4 +72,4 @@ display: block; margin-bottom: 8px; margin-right: 8px; -} \ No newline at end of file +} diff --git a/docs/static/img/logo.svg b/docs/static/img/logo.svg index 9db6d0d..ad9d11a 100755 --- a/docs/static/img/logo.svg +++ b/docs/static/img/logo.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/docs/static/img/undraw_docusaurus_tree.svg b/docs/static/img/undraw_docusaurus_tree.svg index a05cc03..4bbfbf1 100755 --- a/docs/static/img/undraw_docusaurus_tree.svg +++ b/docs/static/img/undraw_docusaurus_tree.svg @@ -1 +1 @@ -docu_tree \ No newline at end of file +docu_tree diff --git a/examples/SportsDataVerse.py b/examples/SportsDataVerse.py index d6551e0..a4a57ed 100755 --- a/examples/SportsDataVerse.py +++ b/examples/SportsDataVerse.py @@ -1,82 +1,90 @@ -import pandas as pd -import cfbfastR import sportsdataverse ###################################################################################################### ## CFB Secion ## ###################################################################################################### + def getCfbEspnTeams(): -############################################################################# -# module 'sportsdataverse.cfb' has no attribute 'espn_cfb_teams' ## -############################################################################# + ############################################################################# + # module 'sportsdataverse.cfb' has no attribute 'espn_cfb_teams' ## + ############################################################################# - print('sportsdataverse.cfb.espn_cfb_teams()') - print('Load college football team ID information and logos') + print("sportsdataverse.cfb.espn_cfb_teams()") + print("Load college football team ID information and logos") cfbEspnTeams_df = sportsdataverse.cfb.espn_cfb_teams() print(cfbEspnTeams_df) + def getEspnPbp(): - print('sportsdataverse.cfb.CFBPlayProcess(gameId=401256137).espn_cfb_pbp()') - print('espn_cfb_pbp() - Pull the game by id. Data from API endpoints: college-football/playbyplay, college-football/summary') + print("sportsdataverse.cfb.CFBPlayProcess(gameId=401256137).espn_cfb_pbp()") + print( + "espn_cfb_pbp() - Pull the game by id. Data from API endpoints: college-football/playbyplay, college-football/summary" + ) espnPbp = sportsdataverse.cfb.CFBPlayProcess(gameId=401256137).espn_cfb_pbp() print(espnPbp) + def getCfbPbp(): - print('sportsdataverse.cfb.load_cfb_pbp(seasons=range(2009,2010))') - print('Load college football play by play data .') - cfbPbp_df = sportsdataverse.cfb.load_cfb_pbp(seasons=range(2009,2010)) + print("sportsdataverse.cfb.load_cfb_pbp(seasons=range(2009,2010))") + print("Load college football play by play data .") + cfbPbp_df = sportsdataverse.cfb.load_cfb_pbp(seasons=range(2009, 2010)) print(cfbPbp_df) + def getEspnCfbCalendar(): - print('sportsdataverse.cfb.espn_cfb_calendar(season=2020, groups=80)') - print('cfb_calendar - look up the men’s college football calendar for a given season') + print("sportsdataverse.cfb.espn_cfb_calendar(season=2020, groups=80)") + print("cfb_calendar - look up the men’s college football calendar for a given season") espnCfbCalendar = sportsdataverse.cfb.espn_cfb_calendar(season=2020, groups=80) print(espnCfbCalendar) + def getEspnSchedule(): - print('sportsdataverse.cfb.espn_cfb_schedule(dates=2020, week=5, season_type=2, groups=80)') - print('cfb_schedule - look up the college football schedule for a given season') - espnSchedule = sportsdataverse.cfb.espn_cfb_schedule(dates=2020, week=5, season_type=2, groups=80) + print("sportsdataverse.cfb.espn_cfb_schedule(dates=2020, week=5, season_type=2, groups=80)") + print("cfb_schedule - look up the college football schedule for a given season") + espnSchedule = sportsdataverse.cfb.espn_cfb_schedule(dates=2020, week=5, season_type=2, groups=80) print(espnSchedule) + def loadCfbPbp(): - print('sportsdataverse.cfb.load_cfb_pbp(seasons=range(2009,2010))') - print('Load college football play by play data .') - loadedCfbPbp = sportsdataverse.cfb.load_cfb_pbp(seasons=range(2009,2010)) + print("sportsdataverse.cfb.load_cfb_pbp(seasons=range(2009,2010))") + print("Load college football play by play data .") + loadedCfbPbp = sportsdataverse.cfb.load_cfb_pbp(seasons=range(2009, 2010)) print(loadedCfbPbp) -def loadCfbRosters(): -################################# -# Load roster data ## -# Empty DataFrame ## -# Columns: [] ## -# Index: [] ## -################################# - - print('sportsdataverse.cfb.load_cfb_rosters(seasons=range(2014,2010))') - print('Load roster data') - loadedCfbRosters = cfb_df = sportsdataverse.cfb.load_cfb_rosters(seasons=range(2014,2010)) +def loadCfbRosters(): + ################################# + # Load roster data ## + # Empty DataFrame ## + # Columns: [] ## + # Index: [] ## + ################################# + + print("sportsdataverse.cfb.load_cfb_rosters(seasons=range(2014,2010))") + print("Load roster data") + loadedCfbRosters = cfb_df = sportsdataverse.cfb.load_cfb_rosters(seasons=range(2014, 2010)) print(loadedCfbRosters) + def loadCfbSchedule(): -################################# -# Load roster data ## -# Empty DataFrame ## -# Columns: [] ## -# Index: [] ## -################################# - - print('sportsdataverse.cfb.load_cfb_schedule(seasons=range(2009,2010))') - print('Load college football schedule data') - loadedCfbSchedule = sportsdataverse.cfb.load_cfb_schedule(seasons=range(2009,2010)) + ################################# + # Load roster data ## + # Empty DataFrame ## + # Columns: [] ## + # Index: [] ## + ################################# + + print("sportsdataverse.cfb.load_cfb_schedule(seasons=range(2009,2010))") + print("Load college football schedule data") + loadedCfbSchedule = sportsdataverse.cfb.load_cfb_schedule(seasons=range(2009, 2010)) print(loadedCfbSchedule) + def loadCfbTeamInfo(): - print('sportsdataverse.cfb.load_cfb_team_info(seasons=range(2009,2010))') - print('Load college football team info') - loadedCfbTeamInfo = cfb_df = sportsdataverse.cfb.load_cfb_team_info(seasons=range(2009,2010)) + print("sportsdataverse.cfb.load_cfb_team_info(seasons=range(2009,2010))") + print("Load college football team info") + loadedCfbTeamInfo = cfb_df = sportsdataverse.cfb.load_cfb_team_info(seasons=range(2009, 2010)) print(loadedCfbTeamInfo) @@ -84,306 +92,359 @@ def loadCfbTeamInfo(): ## MBB Secion ## ###################################################################################################### + def loadMbbPbp(): - print('sportsdataverse.mbb.load_mbb_pbp(seasons=range(2020,2022))') - print('Load men’s college basketball play by play data going back to 2002') - mbbPbp = sportsdataverse.mbb.load_mbb_pbp(seasons=range(2020,2022)) + print("sportsdataverse.mbb.load_mbb_pbp(seasons=range(2020,2022))") + print("Load men’s college basketball play by play data going back to 2002") + mbbPbp = sportsdataverse.mbb.load_mbb_pbp(seasons=range(2020, 2022)) print(mbbPbp) + def loadMbbPlayerBoxscore(): - print('sportsdataverse.mbb.load_mbb_player_boxscore(seasons=range(2020,2022))') - print('Load men’s college basketball player boxscore data') - mbbPlayerBoxscore = sportsdataverse.mbb.load_mbb_player_boxscore(seasons=range(2020,2022)) + print("sportsdataverse.mbb.load_mbb_player_boxscore(seasons=range(2020,2022))") + print("Load men’s college basketball player boxscore data") + mbbPlayerBoxscore = sportsdataverse.mbb.load_mbb_player_boxscore(seasons=range(2020, 2022)) print(mbbPlayerBoxscore) + def loadMbbSchedule(): - print('sportsdataverse.mbb.load_mbb_schedule(seasons=range(2020,2022))') - print('Load men’s college basketball schedule data') - mbbSchedule = sportsdataverse.mbb.load_mbb_schedule(seasons=range(2020,2022)) + print("sportsdataverse.mbb.load_mbb_schedule(seasons=range(2020,2022))") + print("Load men’s college basketball schedule data") + mbbSchedule = sportsdataverse.mbb.load_mbb_schedule(seasons=range(2020, 2022)) print(mbbSchedule) + def loadMbbTeamBoxscore(): -########################################## -## HTTP Error 404: Not Found ## -########################################## + ########################################## + ## HTTP Error 404: Not Found ## + ########################################## - print('sportsdataverse.mbb.load_mbb_team_boxscore(seasons=range(2002,2022))') - print('Load men’s college basketball team boxscore data') - mbbTeamBoxscore = sportsdataverse.mbb.load_mbb_team_boxscore(seasons=range(2002,2022)) + print("sportsdataverse.mbb.load_mbb_team_boxscore(seasons=range(2002,2022))") + print("Load men’s college basketball team boxscore data") + mbbTeamBoxscore = sportsdataverse.mbb.load_mbb_team_boxscore(seasons=range(2002, 2022)) print(mbbTeamBoxscore) + def loadEspnMbbPbp(): - print('sportsdataverse.mbb.espn_mbb_pbp(game_id=401265031)') - print('mbb_pbp() - Pull the game by id. Data from API endpoints: mens-college-basketball/playbyplay, mens-college-basketball/summary') - espnMbbPbp = sportsdataverse.mbb.espn_mbb_pbp(game_id=401265031) - print(espnMbbPbp) + print("sportsdataverse.mbb.espn_mbb_pbp(game_id=401265031)") + print( + "mbb_pbp() - Pull the game by id. Data from API endpoints: mens-college-basketball/playbyplay, mens-college-basketball/summary" + ) + espnMbbPbp = sportsdataverse.mbb.espn_mbb_pbp(game_id=401265031) + print(espnMbbPbp) + def loadEspnMbbSchedule(): - print('sportsdataverse.mbb.espn_mbb_schedule(dates=2020, groups=50, season_type=2)') - print('mbb_schedule - look up the men’s college basketball scheduler for a given season') + print("sportsdataverse.mbb.espn_mbb_schedule(dates=2020, groups=50, season_type=2)") + print("mbb_schedule - look up the men’s college basketball scheduler for a given season") espnMbbSchedule = sportsdataverse.mbb.espn_mbb_schedule(dates=2020, groups=50, season_type=2) print(espnMbbSchedule) + ###################################################################################################### ## MBB Secion ## ###################################################################################################### + def loadNbaPbp(): - print('sportsdataverse.nba.load_nba_pbp(seasons=range(2020,2022))') - print('Load NBA play by play data going back to 2002') - nbaPbp = sportsdataverse.nba.load_nba_pbp(seasons=range(2020,2022)) + print("sportsdataverse.nba.load_nba_pbp(seasons=range(2020,2022))") + print("Load NBA play by play data going back to 2002") + nbaPbp = sportsdataverse.nba.load_nba_pbp(seasons=range(2020, 2022)) print(nbaPbp) + def loadNbaPlayerBoxscores(): - print('sportsdataverse.nba.load_nba_player_boxscore(seasons=range(2020,2022))') - print('Load NBA player boxscore data') - nbaBoxscore = sportsdataverse.nba.load_nba_player_boxscore(seasons=range(2020,2022)) + print("sportsdataverse.nba.load_nba_player_boxscore(seasons=range(2020,2022))") + print("Load NBA player boxscore data") + nbaBoxscore = sportsdataverse.nba.load_nba_player_boxscore(seasons=range(2020, 2022)) print(nbaBoxscore) + def loadNbaSchedule(): - print('sportsdataverse.nba.load_nba_schedule(seasons=range(2020,2022))') - print('Load NBA schedule data') - nbaSchedule = sportsdataverse.nba.load_nba_schedule(seasons=range(2020,2022)) + print("sportsdataverse.nba.load_nba_schedule(seasons=range(2020,2022))") + print("Load NBA schedule data") + nbaSchedule = sportsdataverse.nba.load_nba_schedule(seasons=range(2020, 2022)) print(nbaSchedule) + def loadNbaTeamBoxscores(): - print('sportsdataverse.nba.load_nba_team_boxscore(seasons=range(2020,2022))') - print('Load NBA team boxscore data') - nbaTeamBoxscores = sportsdataverse.nba.load_nba_team_boxscore(seasons=range(2020,2022)) + print("sportsdataverse.nba.load_nba_team_boxscore(seasons=range(2020,2022))") + print("Load NBA team boxscore data") + nbaTeamBoxscores = sportsdataverse.nba.load_nba_team_boxscore(seasons=range(2020, 2022)) print(nbaTeamBoxscores) + def loadEspnNbaPbp(): - print('sportsdataverse.nba.espn_nba_pbp(game_id=401307514)') - print('nba_pbp() - Pull the game by id - Data from API endpoints - nba/playbyplay, nba/summary') + print("sportsdataverse.nba.espn_nba_pbp(game_id=401307514)") + print("nba_pbp() - Pull the game by id - Data from API endpoints - nba/playbyplay, nba/summary") espnNbaPbp = sportsdataverse.nba.espn_nba_pbp(game_id=401307514) print(espnNbaPbp) + def loadEspnNbaCalendar(): ########################################## ## Does not work as advertised ## ########################################## - print('nba_calendar - look up the NBA calendar for a given season from ESPN') - print('sportsdataverse.nba.espn_nba_schedule(dates=range(2020,2022), season_type=2)') - espnNbaCalendar = sportsdataverse.nba.espn_nba_schedule(dates=range(2020,2022), season_type=2) + print("nba_calendar - look up the NBA calendar for a given season from ESPN") + print("sportsdataverse.nba.espn_nba_schedule(dates=range(2020,2022), season_type=2)") + espnNbaCalendar = sportsdataverse.nba.espn_nba_schedule(dates=range(2020, 2022), season_type=2) print(espnNbaCalendar) + ###################################################################################################### ## NFL Secion ## ###################################################################################################### + def loadNflPbp(): - print('sportsdataverse.nfl.load_nfl_pbp(seasons=range(2019,2021))') - print('Load NFL play by play data going back to 1999') - nflPbp = sportsdataverse.nfl.load_nfl_pbp(seasons=range(2019,2021)) + print("sportsdataverse.nfl.load_nfl_pbp(seasons=range(2019,2021))") + print("Load NFL play by play data going back to 1999") + nflPbp = sportsdataverse.nfl.load_nfl_pbp(seasons=range(2019, 2021)) print(nflPbp) + def loadNflPlayerStats(): - print('sportsdataverse.nfl.load_nfl_player_stats()') - print('Load NFL player stats data') + print("sportsdataverse.nfl.load_nfl_player_stats()") + print("Load NFL player stats data") nflPlayerStats = sportsdataverse.nfl.load_nfl_player_stats() print(nflPlayerStats) + def loadNflRosters(): - print('sportsdataverse.nfl.load_nfl_rosters()') - print('Load NFL roster data for all seasons') + print("sportsdataverse.nfl.load_nfl_rosters()") + print("Load NFL roster data for all seasons") nflRosters = sportsdataverse.nfl.load_nfl_rosters() print(nflRosters) + def loadNflSchedules(): - print('sportsdataverse.nfl.load_nfl_schedule(seasons=range(2019,2021))') - print('Load NFL schedule data') - nflSchedules = sportsdataverse.nfl.load_nfl_schedule(seasons=range(2019,2021)) + print("sportsdataverse.nfl.load_nfl_schedule(seasons=range(2019,2021))") + print("Load NFL schedule data") + nflSchedules = sportsdataverse.nfl.load_nfl_schedule(seasons=range(2019, 2021)) print(nflSchedules) + def loadNflTeams(): - print('sportsdataverse.nfl.load_nfl_teams()') - print('Load NFL team ID information and logos') + print("sportsdataverse.nfl.load_nfl_teams()") + print("Load NFL team ID information and logos") nflTeams = sportsdataverse.nfl.load_nfl_teams() print(nflTeams) + def isNflPlayInProgress(): ## Idk how this one is suppose to be implemented. ## TBD how saiemgilani wants this to be implemented - print('') + print("") + def loadEspnNflPbp(): ########################################## ## Does not work as advertised ## ########################################## - print('sportsdataverse.nfl.NFLPlayProcess(game_id=401220403).espn_nfl_pbp()') - print('espn_nfl_pbp() - Pull the game by id - Data from API endpoints - nfl/playbyplay, nfl/summary') + print("sportsdataverse.nfl.NFLPlayProcess(game_id=401220403).espn_nfl_pbp()") + print("espn_nfl_pbp() - Pull the game by id - Data from API endpoints - nfl/playbyplay, nfl/summary") espnNflPbp = sportsdataverse.nfl.NFLPlayProcess(game_id=401220403).espn_nfl_pbp() print(espnNflPbp) + def loadEspnNflCalendar(): - print('portsdataverse.nfl.espn_nfl_calendar(season=2020)') - print('espn_nfl_calendar - look up the NFL calendar for a given season from ESPN') + print("portsdataverse.nfl.espn_nfl_calendar(season=2020)") + print("espn_nfl_calendar - look up the NFL calendar for a given season from ESPN") espnNflCalendar = sportsdataverse.nfl.espn_nfl_calendar(season=2020) print(espnNflCalendar) + def loadEspnNflSchedule(): - print('sportsdataverse.nfl.espn_nfl_schedule(dates=2020, week=1, season_type=2)') - print('espn_nfl_schedule - look up the NFL schedule for a given date from ESPN') + print("sportsdataverse.nfl.espn_nfl_schedule(dates=2020, week=1, season_type=2)") + print("espn_nfl_schedule - look up the NFL schedule for a given date from ESPN") espnNflSchedule = sportsdataverse.nfl.espn_nfl_schedule(dates=2020, week=1, season_type=2) print(espnNflSchedule) + ###################################################################################################### ## NFL Secion ## ###################################################################################################### + def loadNhlPbp(): - print('sportsdataverse.nhl.load_nhl_pbp(seasons=range(2019,2021))') - print('Load NHL play by play data going back to 1999') - nhlPbp = sportsdataverse.nhl.load_nhl_pbp(seasons=range(2019,2021)) + print("sportsdataverse.nhl.load_nhl_pbp(seasons=range(2019,2021))") + print("Load NHL play by play data going back to 1999") + nhlPbp = sportsdataverse.nhl.load_nhl_pbp(seasons=range(2019, 2021)) print(nhlPbp) + def loadNhlPlayerStats(): ###################################### ## name 'i' is not defined ## ###################################### - print('') - print('Load NHL player stats data') + print("") + print("Load NHL player stats data") nhlPlayerStats = sportsdataverse.nhl.load_nhl_player_stats() print(nhlPlayerStats) + def loadNhlSchedules(): - print('sportsdataverse.nhl.load_nhl_schedule(seasons=range(2020,2021))') - print('Load NHL schedule data') - nhlSchedules = sportsdataverse.nhl.load_nhl_schedule(seasons=range(2020,2021)) + print("sportsdataverse.nhl.load_nhl_schedule(seasons=range(2020,2021))") + print("Load NHL schedule data") + nhlSchedules = sportsdataverse.nhl.load_nhl_schedule(seasons=range(2020, 2021)) print(nhlSchedules) + def loadEspnNhlTeamInfo(): ############################################################################## ## module 'sportsdataverse.nhl' has no attribute 'espn_nhl_teams' ## ############################################################################## - print('sportsdataverse.nhl.espn_nhl_teams()') - print('Load NHL team ID information and logos') + print("sportsdataverse.nhl.espn_nhl_teams()") + print("Load NHL team ID information and logos") espnNhlTeamInfo = sportsdataverse.nhl.espn_nhl_teams() print(espnNhlTeamInfo) + def loadEspnNhlPbp(): - print('sportsdataverse.nhl.espn_nhl_pbp(game_id=401247153)') - print('espn_nhl_pbp() - Pull the game by id. Data from API endpoints - nhl/playbyplay, nhl/summary') + print("sportsdataverse.nhl.espn_nhl_pbp(game_id=401247153)") + print("espn_nhl_pbp() - Pull the game by id. Data from API endpoints - nhl/playbyplay, nhl/summary") espnNhlPbp = sportsdataverse.nhl.espn_nhl_pbp(game_id=401247153) print(espnNhlPbp) + def loadEspnNhlCalendar(): - print('sportsdataverse.nhl.espn_nhl_calendar(season=2010)') - print('espn_nhl_calendar - look up the NHL calendar for a given season') + print("sportsdataverse.nhl.espn_nhl_calendar(season=2010)") + print("espn_nhl_calendar - look up the NHL calendar for a given season") espnNhlCalendar = sportsdataverse.nhl.espn_nhl_calendar(season=2010) print(espnNhlCalendar) + def loadEspnNhlSchedule(): - print('sportsdataverse.nhl.espn_nhl_schedule(dates=2010, season_type=2)') - print('espn_nhl_schedule - look up the NHL schedule for a given date') + print("sportsdataverse.nhl.espn_nhl_schedule(dates=2010, season_type=2)") + print("espn_nhl_schedule - look up the NHL schedule for a given date") espnNhlSchedule = sportsdataverse.nhl.espn_nhl_schedule(dates=2010, season_type=2) print(espnNhlSchedule) + ###################################################################################################### ## WBB Secion ## ###################################################################################################### + def loadWbbPbp(): - print('sportsdataverse.wbb.load_wbb_pbp(seasons=range(2020,2022))') - print('Load women’s college basketball play by play data going back to 2002') - wbbPbp = sportsdataverse.wbb.load_wbb_pbp(seasons=range(2020,2022)) + print("sportsdataverse.wbb.load_wbb_pbp(seasons=range(2020,2022))") + print("Load women’s college basketball play by play data going back to 2002") + wbbPbp = sportsdataverse.wbb.load_wbb_pbp(seasons=range(2020, 2022)) print(wbbPbp) + def loadWbbPlayerBoxscores(): - print('sportsdataverse.wbb.load_wbb_player_boxscore(seasons=range(2020,2022))') - print('Load women’s college basketball player boxscore data') - wbbPlayerBoxscore = sportsdataverse.wbb.load_wbb_player_boxscore(seasons=range(2020,2022)) + print("sportsdataverse.wbb.load_wbb_player_boxscore(seasons=range(2020,2022))") + print("Load women’s college basketball player boxscore data") + wbbPlayerBoxscore = sportsdataverse.wbb.load_wbb_player_boxscore(seasons=range(2020, 2022)) print(wbbPlayerBoxscore) + def loadWbbSchedule(): - print('sportsdataverse.wbb.load_wbb_schedule(seasons=range(2020,2022))') - print('Load women’s college basketball schedule data') - wbbSchedule = sportsdataverse.wbb.load_wbb_schedule(seasons=range(2020,2022)) + print("sportsdataverse.wbb.load_wbb_schedule(seasons=range(2020,2022))") + print("Load women’s college basketball schedule data") + wbbSchedule = sportsdataverse.wbb.load_wbb_schedule(seasons=range(2020, 2022)) print(wbbSchedule) + def loadWbbTeamBoxscores(): - print('sportsdataverse.wbb.load_wbb_team_boxscore(seasons=range(2020,2022))') - print('Load women’s college basketball team boxscore data') - wbbTeamBoxscores = sportsdataverse.wbb.load_wbb_team_boxscore(seasons=range(2020,2022)) + print("sportsdataverse.wbb.load_wbb_team_boxscore(seasons=range(2020,2022))") + print("Load women’s college basketball team boxscore data") + wbbTeamBoxscores = sportsdataverse.wbb.load_wbb_team_boxscore(seasons=range(2020, 2022)) print(wbbTeamBoxscores) + def loadWbbPbpModule(): ################################################################## ## There's nothing there, but the docs reference this. ## ## Idk how it's suppose to be implemented right now. ## ## ¯\_(ツ)_/¯ ## ################################################################## - print('') + print("") + def loadEspnWbbPbp(): - print('sportsdataverse.wbb.espn_wbb_pbp(game_id=401266534)') - print('espn_wbb_pbp() - Pull the game by id. Data from API endpoints - womens-college-basketball/playbyplay, womens-college-basketball/summary') + print("sportsdataverse.wbb.espn_wbb_pbp(game_id=401266534)") + print( + "espn_wbb_pbp() - Pull the game by id. Data from API endpoints - womens-college-basketball/playbyplay, womens-college-basketball/summary" + ) espnWbbPbp = sportsdataverse.wbb.espn_wbb_pbp(game_id=401266534) print(espnWbbPbp) + def loadEspnWbbCalendar(): - print('sportsdataverse.wbb.espn_wbb_calendar(season=2020)') - print('espn_wbb_calendar - look up the women’s college basketball calendar for a given season') + print("sportsdataverse.wbb.espn_wbb_calendar(season=2020)") + print("espn_wbb_calendar - look up the women’s college basketball calendar for a given season") espnWbbCalendar = sportsdataverse.wbb.espn_wbb_calendar(season=2020) print(espnWbbCalendar) + def loadEspnWbbSchedule(): - print('sportsdataverse.wbb.espn_wbb_schedule(dates=2020, groups=50, season_type=2)') - print('espn_wbb_schedule - look up the women’s college basketball schedule for a given season') + print("sportsdataverse.wbb.espn_wbb_schedule(dates=2020, groups=50, season_type=2)") + print("espn_wbb_schedule - look up the women’s college basketball schedule for a given season") espnWbbSchedule = sportsdataverse.wbb.espn_wbb_schedule(dates=2020, groups=50, season_type=2) print(espnWbbSchedule) + ####################################################################################################### ## WNBA Secion ## ####################################################################################################### + def loadWnbaPbp(): - print('sportsdataverse.wnba.load_wnba_pbp(seasons=range(2020,2022))') - print('Load WNBA play by play data going back to 2002') - wnba_df = sportsdataverse.wnba.load_wnba_pbp(seasons=range(2020,2022)) + print("sportsdataverse.wnba.load_wnba_pbp(seasons=range(2020,2022))") + print("Load WNBA play by play data going back to 2002") + wnba_df = sportsdataverse.wnba.load_wnba_pbp(seasons=range(2020, 2022)) print(wnba_df) + def loadWnbaPlayerBoxscores(): - print('sportsdataverse.wnba.load_wnba_player_boxscore(seasons=range(2020,2022))') - print('Load WNBA player boxscore data') - wnbaPlayerBoxscore = sportsdataverse.wnba.load_wnba_player_boxscore(seasons=range(2020,2022)) + print("sportsdataverse.wnba.load_wnba_player_boxscore(seasons=range(2020,2022))") + print("Load WNBA player boxscore data") + wnbaPlayerBoxscore = sportsdataverse.wnba.load_wnba_player_boxscore(seasons=range(2020, 2022)) print(wnbaPlayerBoxscore) + def loadWnbaSchedules(): - print('sportsdataverse.wnba.load_wnba_schedule(seasons=range(2020,2022))') - print('Load WNBA schedule data') - wnbaSchedules = sportsdataverse.wnba.load_wnba_schedule(seasons=range(2020,2022)) + print("sportsdataverse.wnba.load_wnba_schedule(seasons=range(2020,2022))") + print("Load WNBA schedule data") + wnbaSchedules = sportsdataverse.wnba.load_wnba_schedule(seasons=range(2020, 2022)) print(wnbaSchedules) + def loadWnbaTeamBoxscores(): - print('sportsdataverse.wnba.load_wnba_team_boxscore(seasons=range(2020,2022))') - print('Load WNBA team boxscore data') - wnbaTeamBoxscores = sportsdataverse.wnba.load_wnba_team_boxscore(seasons=range(2020,2022)) + print("sportsdataverse.wnba.load_wnba_team_boxscore(seasons=range(2020,2022))") + print("Load WNBA team boxscore data") + wnbaTeamBoxscores = sportsdataverse.wnba.load_wnba_team_boxscore(seasons=range(2020, 2022)) print(wnbaTeamBoxscores) + def loadEspnWnbaPbp(): - print('sportsdataverse.wnba.espn_wnba_pbp(game_id=401370395)') - print('espn_wnba_pbp() - Pull the game by id. Data from API endpoints - wnba/playbyplay, wnba/summary') + print("sportsdataverse.wnba.espn_wnba_pbp(game_id=401370395)") + print("espn_wnba_pbp() - Pull the game by id. Data from API endpoints - wnba/playbyplay, wnba/summary") espnWnbaPbp = sportsdataverse.wnba.espn_wnba_pbp(game_id=401370395) print(espnWnbaPbp) + def loadEspnWnbaCalendar(): - print('sportsdataverse.wnba.espn_wnba_schedule(dates=2020, season_type=2)') - print('espn_wnba_calendar - look up the WNBA calendar for a given season') + print("sportsdataverse.wnba.espn_wnba_schedule(dates=2020, season_type=2)") + print("espn_wnba_calendar - look up the WNBA calendar for a given season") espnWnbaCalendar = sportsdataverse.wnba.espn_wnba_calendar(season=2020) print(espnWnbaCalendar) + def loadEspnWnbaSchedule(): - print('sportsdataverse.wnba.espn_wnba_schedule(dates=2020, season_type=2)') - print('espn_wnba_schedule - look up the WNBA schedule for a given season') + print("sportsdataverse.wnba.espn_wnba_schedule(dates=2020, season_type=2)") + print("espn_wnba_schedule - look up the WNBA schedule for a given season") espnWnbaSchedule = sportsdataverse.wnba.espn_wnba_schedule(dates=2020, season_type=2) print(espnWnbaSchedule) + def main(): ## If it's commented out, it didn't work. - print('starting up') + print("starting up") # ## CFB getCfbEspnTeams() @@ -396,7 +457,6 @@ def main(): # loadCfbSchedule() # loadCfbTeamInfo() - # ## MBB # loadMbbPbp() # loadMbbPlayerBoxscore() @@ -453,13 +513,14 @@ def main(): # loadEspnWnbaSchedule() # Failed to work properly - #getCfbEspnTeams() - #loadCfbRosters() - #loadMbbTeamBoxscore() - #loadEspnNbaCalendar() - #isNflPlayInProgress() - #loadNhlPlayerStats() - #loadEspnNhlTeamInfo() - -if __name__ == '__main__': - main() \ No newline at end of file + # getCfbEspnTeams() + # loadCfbRosters() + # loadMbbTeamBoxscore() + # loadEspnNbaCalendar() + # isNflPlayInProgress() + # loadNhlPlayerStats() + # loadEspnNhlTeamInfo() + + +if __name__ == "__main__": + main() diff --git a/examples/cfb_pbp_testing.py b/examples/cfb_pbp_testing.py index 085d3a7..da0420e 100755 --- a/examples/cfb_pbp_testing.py +++ b/examples/cfb_pbp_testing.py @@ -1,26 +1,41 @@ -import sportsdataverse as sdv import pandas as pd -pd.set_option('display.max_columns', None) -pd.set_option('display.max_rows', None) +import sportsdataverse as sdv + +pd.set_option("display.max_columns", None) +pd.set_option("display.max_rows", None) + def main(): processor = sdv.cfb.CFBPlayProcess(gameId=401403867) pbp_init = processor.espn_cfb_pbp() pbp_fin = processor.run_processing_pipeline() - plays_df = pd.DataFrame(pbp_fin['plays']) - special_teams = plays_df[plays_df["sp"]==True] - special_teams = special_teams[[ -"text","type.text","fg_kicker_player_name","yds_fg", -"fg_attempt", "fg_made", -"fg_return_player_name", "fg_block_player_name", -"kickoff_player_name","yds_kickoff", -"kickoff_return_player_name", "yds_kickoff_return", -"punt_return_player_name","yds_punt_return", -"punter_player_name","yds_punted", -"punt_block_player_name","yds_punt_gained" -]] + plays_df = pd.DataFrame(pbp_fin["plays"]) + special_teams = plays_df[plays_df["sp"] == True] + special_teams = special_teams[ + [ + "text", + "type.text", + "fg_kicker_player_name", + "yds_fg", + "fg_attempt", + "fg_made", + "fg_return_player_name", + "fg_block_player_name", + "kickoff_player_name", + "yds_kickoff", + "kickoff_return_player_name", + "yds_kickoff_return", + "punt_return_player_name", + "yds_punt_return", + "punter_player_name", + "yds_punted", + "punt_block_player_name", + "yds_punt_gained", + ] + ] print(special_teams) -if __name__ == '__main__': - main() \ No newline at end of file + +if __name__ == "__main__": + main() diff --git a/examples/ex_load_cfb_pbp.py b/examples/ex_load_cfb_pbp.py index 834311c..ee12e0d 100755 --- a/examples/ex_load_cfb_pbp.py +++ b/examples/ex_load_cfb_pbp.py @@ -1,9 +1,12 @@ import sportsdataverse + + def main(): - cfb_df = sportsdataverse.cfb.load_cfb_pbp(seasons=range(2011,2021)) + cfb_df = sportsdataverse.cfb.load_cfb_pbp(seasons=range(2011, 2021)) print(cfb_df.head()) - nfl_df = sportsdataverse.nfl.load_nfl_pbp(seasons=range(2011,2021)) + nfl_df = sportsdataverse.nfl.load_nfl_pbp(seasons=range(2011, 2021)) print(nfl_df.head()) + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/examples/ex_load_nfl.py b/examples/ex_load_nfl.py index 6ef2cc4..eb0976f 100755 --- a/examples/ex_load_nfl.py +++ b/examples/ex_load_nfl.py @@ -1,17 +1,18 @@ import sportsdataverse as sdv -def main(): - nfl_pbp_df = sdv.nfl.load_nfl_pbp(seasons=range(2015,2021)) +def main(): + nfl_pbp_df = sdv.nfl.load_nfl_pbp(seasons=range(2015, 2021)) print(nfl_pbp_df.head()) nfl_player_stats_df = sdv.nfl.load_nfl_player_stats() print(nfl_player_stats_df.head()) nfl_rosters_df = sdv.nfl.load_nfl_player_stats() print(nfl_rosters_df.head()) - nfl_schedules_df = sdv.nfl.load_nfl_schedule(seasons=range(2015,2021)) + nfl_schedules_df = sdv.nfl.load_nfl_schedule(seasons=range(2015, 2021)) print(nfl_schedules_df.head()) nfl_teams_df = sdv.nfl.load_nfl_teams() print(nfl_teams_df.head()) + if __name__ == "__main__": main() diff --git a/examples/ex_nhl_api_pbp.py b/examples/ex_nhl_api_pbp.py index cac21a6..cc284fc 100755 --- a/examples/ex_nhl_api_pbp.py +++ b/examples/ex_nhl_api_pbp.py @@ -1,9 +1,13 @@ import sportsdataverse + + def main(): nhl_df = sportsdataverse.nhl.nhl_api_pbp(game_id=2021020079) print(nhl_df) - nhl_sched_df = sportsdataverse.nhl.nhl_api_schedule(start_date='2021-08-01', end_date='2021-10-01') + nhl_sched_df = sportsdataverse.nhl.nhl_api_schedule(start_date="2021-08-01", end_date="2021-10-01") print(nhl_sched_df) - nhl_sched_df.to_csv('../nhl_sched.csv', index=False) + nhl_sched_df.to_csv("../nhl_sched.csv", index=False) + + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/examples/ex_rosters_test.py b/examples/ex_rosters_test.py index 90c4ce4..fe6d52d 100755 --- a/examples/ex_rosters_test.py +++ b/examples/ex_rosters_test.py @@ -1,7 +1,9 @@ from sportsdataverse.nfl import * + def main(): print(load_nfl_rosters()) + if __name__ == "__main__": main() diff --git a/examples/nfl_sched_testing.py b/examples/nfl_sched_testing.py index 9c8bceb..2bf2c25 100755 --- a/examples/nfl_sched_testing.py +++ b/examples/nfl_sched_testing.py @@ -1,20 +1,26 @@ -import sportsdataverse as sdv +import tempfile + import pandas as pd import pyreadr -import tempfile -pd.set_option('display.max_columns', None) -pd.set_option('display.max_rows', None) + +pd.set_option("display.max_columns", None) +pd.set_option("display.max_rows", None) + def main(): data = pd.DataFrame() with tempfile.TemporaryDirectory() as tempdirname: - for i in range(2018,2022): + for i in range(2018, 2022): nfl_schedule_df = pyreadr.read_r( pyreadr.download_file( f"https://raw.githubusercontent.com/nflverse/nflverse-pbp/master/schedules/sched_{i}.rds", - f"{tempdirname}/nfl_sched_{i}.rds"))[None] + f"{tempdirname}/nfl_sched_{i}.rds", + ) + )[None] nfl_season_sched = pd.DataFrame(nfl_schedule_df) data = pd.concat([data, nfl_season_sched], ignore_index=True) print(data) -if __name__ == '__main__': - main() \ No newline at end of file + + +if __name__ == "__main__": + main() diff --git a/examples/test_mlbam.py b/examples/test_mlbam.py index e607fce..6275b8d 100755 --- a/examples/test_mlbam.py +++ b/examples/test_mlbam.py @@ -18,9 +18,9 @@ MLBAM_copyright_info = sdv.mlb.mlbam_copyright_info() print(MLBAM_copyright_info) """ -print('mlbam_copyright_info') +print("mlbam_copyright_info") print(sdv.mlb.mlbam_copyright_info()) -print('') +print("") """ mlbam_schedule(season:int,gameType="R"): @@ -53,9 +53,9 @@ df = sdv.mlb.mlbam_schedule(2020) print(df) """ -print('mlbam_schedule') +print("mlbam_schedule") print(sdv.mlb.mlbam_schedule(2017)) -print('') +print("") """ mlbam_search_mlb_players(search:str,isActive=""): @@ -80,10 +80,10 @@ df = sdv.mlb.mlbam_search_mlb_players(search="Votto",isActive="y") print(df) """ -print('mlbam_search_mlb_players') -print(sdv.mlb.mlbam_search_mlb_players(search="Votto",isActive="y")) -print(sdv.mlb.mlbam_search_mlb_players(search="Joe",isActive="y")) -print('') +print("mlbam_search_mlb_players") +print(sdv.mlb.mlbam_search_mlb_players(search="Votto", isActive="y")) +print(sdv.mlb.mlbam_search_mlb_players(search="Joe", isActive="y")) +print("") """ mlbam_player_info(playerID:int): @@ -100,9 +100,9 @@ df = sdv.mlb.mlbam_player_info(playerID=458015) print(df) """ -print('getPlayerInfo') +print("getPlayerInfo") print(sdv.mlb.mlbam_player_info(playerID=458015)) -print('') +print("") """ def mlbam_player_teams(playerID:int,season:int): @@ -124,9 +124,9 @@ def mlbam_player_teams(playerID:int,season:int): df = sdv.mlb.mlbam_player_teams(playerID=523260,season=2014) print(df) """ -print('getPlayerTeams') -print(sdv.mlb.mlbam_player_teams(playerID=523260,season=2014)) -print('') +print("getPlayerTeams") +print(sdv.mlb.mlbam_player_teams(playerID=523260, season=2014)) +print("") """ def mlbam_player_season_hitting_stats(playerID:int,season:int,gameType="R"): @@ -159,9 +159,9 @@ def mlbam_player_season_hitting_stats(playerID:int,season:int,gameType="R"): df = sdv.mlb.mlbam_player_season_hitting_stats(playerID=458015,season=2021,gameType="R") print(df) """ -print('mlbam_player_season_hitting_stats') -print(sdv.mlb.mlbam_player_season_hitting_stats(playerID=458015,season=2021,gameType="R")) -print('') +print("mlbam_player_season_hitting_stats") +print(sdv.mlb.mlbam_player_season_hitting_stats(playerID=458015, season=2021, gameType="R")) +print("") """ def mlbam_player_season_pitching_stats(playerID:int,season:int,gameType="R"): @@ -194,9 +194,9 @@ def mlbam_player_season_pitching_stats(playerID:int,season:int,gameType="R"): df = sdv.mlb.mlbam_player_season_pitching_stats(playerID=642840,season=2019,gameType="R") print(df) """ -print('mlbam_player_season_pitching_stats') -print(sdv.mlb.mlbam_player_season_pitching_stats(playerID=642840,season=2019,gameType="R")) -print('') +print("mlbam_player_season_pitching_stats") +print(sdv.mlb.mlbam_player_season_pitching_stats(playerID=642840, season=2019, gameType="R")) +print("") """ def mlbam_player_career_hitting_stats(playerID:int,gameType="R"): @@ -229,9 +229,9 @@ def mlbam_player_career_hitting_stats(playerID:int,gameType="R"): df = sdv.mlb.mlbam_player_career_hitting_stats(playerID=458015,gameType="R") print(df) """ -print('mlbam_player_career_hitting_stats') -print(sdv.mlb.mlbam_player_career_hitting_stats(playerID=458015,gameType="R")) -print('') +print("mlbam_player_career_hitting_stats") +print(sdv.mlb.mlbam_player_career_hitting_stats(playerID=458015, gameType="R")) +print("") """ mlbam_player_career_pitching_stats(playerID:int,gameType="R"): @@ -261,9 +261,9 @@ def mlbam_player_career_hitting_stats(playerID:int,gameType="R"): df = sdv.mlb.mlbam_player_career_pitching_stats(playerID=642840,gameType="R") print(df) """ -print('mlbam_player_career_pitching_stats') -print(sdv.mlb.mlbam_player_career_pitching_stats(playerID=642840,gameType="R")) -print('') +print("mlbam_player_career_pitching_stats") +print(sdv.mlb.mlbam_player_career_pitching_stats(playerID=642840, gameType="R")) +print("") """ mlbam_transactions(startDate=0,endDate=0): @@ -288,9 +288,9 @@ def mlbam_player_career_hitting_stats(playerID:int,gameType="R"): df =sdv.mlb.mlbam_transactions(startDate=20200901,endDate=20200914) print(df) """ -print('mlbam_transactions') -print(sdv.mlb.mlbam_transactions(startDate="09/01/2020",endDate="09/01/2020")) -print('') +print("mlbam_transactions") +print(sdv.mlb.mlbam_transactions(startDate="09/01/2020", endDate="09/01/2020")) +print("") """ mlbam_broadcast_info(season:int,home_away="e"): @@ -317,9 +317,9 @@ def mlbam_player_career_hitting_stats(playerID:int,gameType="R"): df = sdv.mlb.mlbam_broadcast_info(season=2020,home_away="e") print(df) """ -print('mlbam_broadcast_info') -print(sdv.mlb.mlbam_broadcast_info(season=2020,home_away="e")) -print('') +print("mlbam_broadcast_info") +print(sdv.mlb.mlbam_broadcast_info(season=2020, home_away="e")) +print("") """ mlbam_teams(season:int,retriveAllStarRosters=False): @@ -340,9 +340,9 @@ def mlbam_player_career_hitting_stats(playerID:int,gameType="R"): df = sdv.mlb.mlbam_teams(season=2020) print(df) """ -print('mlbam_teams') +print("mlbam_teams") print(sdv.mlb.mlbam_teams(season=2020)) -print('') +print("") """ mlbam_40_man_roster(teamID=113): @@ -359,9 +359,9 @@ def mlbam_player_career_hitting_stats(playerID:int,gameType="R"): df = sdv.mlb.mlbam_40_man_roster(teamID=113) print(df) """ -print('mlbam_40_man_roster') +print("mlbam_40_man_roster") print(sdv.mlb.mlbam_40_man_roster(teamID=113)) -print('') +print("") """ mlbam_team_roster(teamID=113,startSeason=2020,endSeason=2021): @@ -384,7 +384,7 @@ def mlbam_player_career_hitting_stats(playerID:int,gameType="R"): df = sdv.mlb.mlbam_team_roster(teamID=113,startSeason=2020,endSeason=2021) print(df) """ -print('mlbam_team_roster') -df = sdv.mlb.mlbam_team_roster(teamID=113,startSeason=2020,endSeason=2021) +print("mlbam_team_roster") +df = sdv.mlb.mlbam_team_roster(teamID=113, startSeason=2020, endSeason=2021) print(df) -print('') \ No newline at end of file +print("") diff --git a/examples/test_sdv_mbb_schedule.py b/examples/test_sdv_mbb_schedule.py index e94b00e..909b94f 100755 --- a/examples/test_sdv_mbb_schedule.py +++ b/examples/test_sdv_mbb_schedule.py @@ -1,6 +1,7 @@ import pandas as pd + import sportsdataverse as sdv -from functools import reduce + def main(): df = pd.Series(sdv.mbb.espn_mbb_calendar(season=2022).dateURL) diff --git a/examples/test_sdv_pbp.py b/examples/test_sdv_pbp.py index 246985a..8f43b8c 100755 --- a/examples/test_sdv_pbp.py +++ b/examples/test_sdv_pbp.py @@ -1,5 +1,6 @@ import sportsdataverse as sdv + def main(): print(sdv.wbb.espn_wbb_pbp(game_id=401266534)) print(sdv.wnba.espn_wnba_pbp(game_id=401370395)) diff --git a/examples/test_sdv_schedule.py b/examples/test_sdv_schedule.py index 816b77d..31bb6e4 100755 --- a/examples/test_sdv_schedule.py +++ b/examples/test_sdv_schedule.py @@ -1,5 +1,6 @@ import sportsdataverse as sdv + def main(): print(sdv.wbb.espn_wbb_calendar(season=2020)) print(sdv.wbb.espn_wbb_schedule(dates=2020)) diff --git a/examples/timing.py b/examples/timing.py index f8ae48c..1576c22 100644 --- a/examples/timing.py +++ b/examples/timing.py @@ -1,35 +1,41 @@ -import sportsdataverse as sdv import logging -from sportsdataverse.decorators import * -logging.basicConfig(level=logging.DEBUG, filename = 'wehoop_wnba_raw_logfile.txt') +import sportsdataverse as sdv + +logging.basicConfig(level=logging.DEBUG, filename="wehoop_wnba_raw_logfile.txt") logger = logging.getLogger(__name__) + @timer(number=10) @record_mem_usage def test_polars_cfb_schedule(): sdv.cfb.espn_cfb_schedule() + # @timer(number=10) # @record_mem_usage # def test_pandas_cfb_schedule(): # sdv.cfb.espn_cfb_schedule_pandas() + @timer(number=10) @record_mem_usage def test_polars_cfb_calendar_ondays(): sdv.cfb.espn_cfb_calendar(season=2022, ondays=True) + # @timer(number=10) # @record_mem_usage # def test_pandas_cfb_calendar_ondays(): # sdv.cfb.espn_cfb_calendar_pandas(season=2022, ondays=True) + @timer(number=10) @record_mem_usage def test_polars_cfb_calendar(): sdv.cfb.espn_cfb_calendar(season=2022) + # @timer(number=10) # @record_mem_usage # def test_pandas_cfb_calendar(): @@ -39,6 +45,7 @@ def test_polars_cfb_calendar(): def test_polars_load_cfb_pbp(): sdv.cfb.load_cfb_pbp(seasons=2021) + def main(): test_polars_cfb_schedule() # test_pandas_cfb_schedule() @@ -47,7 +54,8 @@ def main(): test_polars_cfb_calendar_ondays() # test_pandas_cfb_calendar_ondays() test_polars_load_cfb_pbp() - sdv.cfb.espn_cfb_schedule(dates=20241010, logger = logger) + sdv.cfb.espn_cfb_schedule(dates=20241010, logger=logger) + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5ea5ef7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,26 @@ +[tool.black] +line-length = 119 +py38 = true +py39 = true +target-version = ['py39', 'py38'] +include = '\.pyi?$' +exclude = ''' +/( + \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist + + # The following are specific to Black, you probably don't want those. + | blib2to3 + | tests/data +)/ +''' + +[tool.isort] +profile = 'black' diff --git a/setup.py b/setup.py index 96ea9d3..91961e1 100755 --- a/setup.py +++ b/setup.py @@ -12,23 +12,32 @@ long_description = f.read() extras = { - "tests": ["pytest>=6.0.2", - "mypy>=0.782", - "pytest-cov>=2.10.1", - "pytest-xdist>=2.1.0",], - "docs": ["sphinx"], - "models": ["beautifulsoup4>=4.4.0", - "inflection>=0.5.1", - "requests>=2.18.1", - "lxml>=4.2.1", - "pyarrow>=1.0.1", - "pyjanitor>=0.23.1", - "pyreadr>=0.4.0", - "scipy>=1.4.0", - "matplotlib>=2.0.0", - "tqdm>=4.50.0", - "attrs>=20.3.0", - "xgboost>=1.2.0",] + "tests": [ + "black>=22.3.0", + "flake8>=5.0.0", + "isort>=5.10.1", + "mypy>=0.782", + "pycln>=2.1.6", + "pydocstyle>=6.3.0", + "pytest>=6.0.2", + "pytest-cov>=2.10.1", + "pytest-xdist>=2.1.0", + ], + "docs": ["sphinx"], + "models": [ + "beautifulsoup4>=4.4.0", + "inflection>=0.5.1", + "requests>=2.18.1", + "lxml>=4.2.1", + "pyarrow>=1.0.1", + "pyjanitor>=0.23.1", + "pyreadr>=0.4.0", + "scipy>=1.4.0", + "matplotlib>=2.0.0", + "tqdm>=4.50.0", + "attrs>=20.3.0", + "xgboost>=1.2.0", + ], } extras["all"] = extras["tests"] + extras["docs"] + extras["models"] diff --git a/sportsdataverse.egg-info/SOURCES.txt b/sportsdataverse.egg-info/SOURCES.txt index 9a30348..c318593 100755 --- a/sportsdataverse.egg-info/SOURCES.txt +++ b/sportsdataverse.egg-info/SOURCES.txt @@ -253,4 +253,4 @@ sportsdataverse/wnba/__init__.py sportsdataverse/wnba/wnba_loaders.py sportsdataverse/wnba/wnba_pbp.py sportsdataverse/wnba/wnba_schedule.py -sportsdataverse/wnba/wnba_teams.py \ No newline at end of file +sportsdataverse/wnba/wnba_teams.py diff --git a/sportsdataverse.egg-info/dependency_links.txt b/sportsdataverse.egg-info/dependency_links.txt index 8b13789..e69de29 100755 --- a/sportsdataverse.egg-info/dependency_links.txt +++ b/sportsdataverse.egg-info/dependency_links.txt @@ -1 +0,0 @@ - diff --git a/sportsdataverse/cfb/__init__.py b/sportsdataverse/cfb/__init__.py index 9a0afcd..2ee385d 100755 --- a/sportsdataverse/cfb/__init__.py +++ b/sportsdataverse/cfb/__init__.py @@ -2,4 +2,4 @@ from sportsdataverse.cfb.cfb_loaders import * from sportsdataverse.cfb.cfb_pbp import * from sportsdataverse.cfb.cfb_schedule import * -from sportsdataverse.cfb.cfb_teams import * \ No newline at end of file +from sportsdataverse.cfb.cfb_teams import * diff --git a/sportsdataverse/cfb/cfb_game_rosters.py b/sportsdataverse/cfb/cfb_game_rosters.py index 951511b..69af954 100644 --- a/sportsdataverse/cfb/cfb_game_rosters.py +++ b/sportsdataverse/cfb/cfb_game_rosters.py @@ -1,5 +1,6 @@ import pandas as pd import polars as pl + from sportsdataverse.dl_utils import download, underscore diff --git a/sportsdataverse/cfb/cfb_pbp.py b/sportsdataverse/cfb/cfb_pbp.py index 8913dfa..7b5ee0d 100755 --- a/sportsdataverse/cfb/cfb_pbp.py +++ b/sportsdataverse/cfb/cfb_pbp.py @@ -1,33 +1,41 @@ -import pandas as pd -import polars as pl -import numpy as np -import re -import os import json +import os +import re import time +from functools import reduce + +import numpy as np +import pandas as pd import pkg_resources +import polars as pl from xgboost import Booster, DMatrix -from numpy.core.fromnumeric import mean -from functools import reduce, partial -from sportsdataverse.dl_utils import download, key_check -from sportsdataverse.cfb.model_vars import * - -# "td" : float(p[0]), -# "opp_td" : float(p[1]), -# "fg" : float(p[2]), -# "opp_fg" : float(p[3]), -# "safety" : float(p[4]), -# "opp_safety" : float(p[5]), -# "no_score" : float(p[6]) -ep_model_file = pkg_resources.resource_filename( - "sportsdataverse", "cfb/models/ep_model.model" -) -wp_spread_file = pkg_resources.resource_filename( - "sportsdataverse", "cfb/models/wp_spread.model" -) -qbr_model_file = pkg_resources.resource_filename( - "sportsdataverse", "cfb/models/qbr_model.model" + +from sportsdataverse.cfb.model_vars import ( + defense_score_vec, + end_change_vec, + ep_class_to_score_mapping, + ep_end_columns, + ep_start_columns, + ep_start_touchback_columns, + int_vec, + kickoff_turnovers, + kickoff_vec, + normalplay, + offense_score_vec, + penalty, + punt_vec, + qbr_vars, + scores_vec, + turnover_vec, + wp_end_columns, + wp_start_columns, + wp_start_touchback_columns, ) +from sportsdataverse.dl_utils import download, key_check + +ep_model_file = pkg_resources.resource_filename("sportsdataverse", "cfb/models/ep_model.model") +wp_spread_file = pkg_resources.resource_filename("sportsdataverse", "cfb/models/wp_spread.model") +qbr_model_file = pkg_resources.resource_filename("sportsdataverse", "cfb/models/qbr_model.model") ep_model = Booster({"nthread": 4}) # init model ep_model.load_model(ep_model_file) @@ -38,16 +46,17 @@ qbr_model = Booster({"nthread": 4}) # init model qbr_model.load_model(qbr_model_file) -class CFBPlayProcess(object): +class CFBPlayProcess(object): gameId = 0 # logger = None ran_pipeline = False ran_cleaning_pipeline = False raw = False - path_to_json = '/' + path_to_json = "/" return_keys = None - def __init__(self, gameId=0, raw=False, path_to_json='/', return_keys=None, **kwargs): + + def __init__(self, gameId=0, raw=False, path_to_json="/", return_keys=None, **kwargs): self.gameId = int(gameId) # self.logger = logger self.ran_pipeline = False @@ -57,11 +66,13 @@ def __init__(self, gameId=0, raw=False, path_to_json='/', return_keys=None, **kw self.return_keys = return_keys def espn_cfb_pbp(self, **kwargs): - """espn_cfb_pbp() - Pull the game by id. Data from API endpoints: `college-football/playbyplay`, `college-football/summary` + """espn_cfb_pbp() - Pull the game by id. Data from API endpoints: `college-football/playbyplay`, + `college-football/summary` Args: game_id (int): Unique game_id, can be obtained from cfb_schedule(). - raw (bool): If True, returns the raw json from the API endpoint. If False, returns a cleaned dictionary of datasets. + raw (bool): If True, returns the raw json from the API endpoint. If False, returns a + cleaned dictionary of datasets. Returns: Dict: Dictionary of game data with keys - "gameId", "plays", "boxscore", "header", "broadcasts", @@ -76,20 +87,35 @@ def espn_cfb_pbp(self, **kwargs): pbp_txt = {"timeouts": {}} # summary endpoint for pickcenter array summary_url = f"http://site.api.espn.com/apis/site/v2/sports/football/college-football/summary?event={self.gameId}&{cache_buster}" - summary_resp = download(url = summary_url, **kwargs) + summary_resp = download(url=summary_url, **kwargs) summary = summary_resp.json() incoming_keys_expected = [ - 'boxscore', 'format', 'gameInfo', 'drives', 'leaders', 'broadcasts', - 'predictor', 'pickcenter', 'againstTheSpread', 'odds', 'winprobability', - 'header', 'scoringPlays', 'videos', 'standings' - ] - dict_keys_expected = [ - 'boxscore', 'format', 'gameInfo', 'drives', 'predictor', - 'header', 'standings' + "boxscore", + "format", + "gameInfo", + "drives", + "leaders", + "broadcasts", + "predictor", + "pickcenter", + "againstTheSpread", + "odds", + "winprobability", + "header", + "scoringPlays", + "videos", + "standings", ] + dict_keys_expected = ["boxscore", "format", "gameInfo", "drives", "predictor", "header", "standings"] array_keys_expected = [ - 'leaders', 'broadcasts', 'pickcenter','againstTheSpread', - 'odds', 'winprobability', 'scoringPlays', 'videos' + "leaders", + "broadcasts", + "pickcenter", + "againstTheSpread", + "odds", + "winprobability", + "scoringPlays", + "videos", ] if self.raw == True: # reorder keys in raw format, appending empty keys which are defined later to the end @@ -122,8 +148,8 @@ def espn_cfb_pbp(self, **kwargs): "leaders", ]: pbp_txt[k] = key_check(obj=summary, key=k) - for k in ['news','shop']: - pbp_txt.pop(f'{k}', None) + for k in ["news", "shop"]: + pbp_txt.pop(f"{k}", None) self.json = pbp_txt return self.json @@ -140,7 +166,10 @@ def __helper_cfb_pbp_drives(self, pbp_txt): pbp_txt["plays"] = pl.DataFrame() # negotiating the drive meta keys into columns after unnesting drive plays # concatenating the previous and current drives categories when necessary - if "drives" in pbp_txt.keys() and pbp_txt.get('header').get('competitions')[0].get('playByPlaySource') != 'none': + if ( + "drives" in pbp_txt.keys() + and pbp_txt.get("header").get("competitions")[0].get("playByPlaySource") != "none" + ): pbp_txt = self.__helper_cfb_pbp_features(pbp_txt, init) else: pbp_txt["drives"] = {} @@ -150,204 +179,227 @@ def __helper_cfb_pbp_features(self, pbp_txt, init): pbp_txt["plays"] = pl.DataFrame() for key in pbp_txt.get("drives").keys(): prev_drives = pd.json_normalize( - data=pbp_txt.get("drives").get("{}".format(key)), - record_path="plays", - meta=[ - "id", - "displayResult", - "isScore", - ["team", "shortDisplayName"], - ["team", "displayName"], - ["team", "name"], - ["team", "abbreviation"], - "yards", - "offensivePlays", - "result", - "description", - "shortDisplayResult", - ["timeElapsed", "displayValue"], - ["start", "period", "number"], - ["start", "period", "type"], - ["start", "yardLine"], - ["start", "clock", "displayValue"], - ["start", "text"], - ["end", "period", "number"], - ["end", "period", "type"], - ["end", "yardLine"], - ["end", "clock", "displayValue"], - ], - meta_prefix="drive.", - errors="ignore", - ) - pbp_txt["plays"] = pl.concat( - [pbp_txt["plays"], pl.from_pandas(prev_drives)], how='vertical' + data=pbp_txt.get("drives").get("{}".format(key)), + record_path="plays", + meta=[ + "id", + "displayResult", + "isScore", + ["team", "shortDisplayName"], + ["team", "displayName"], + ["team", "name"], + ["team", "abbreviation"], + "yards", + "offensivePlays", + "result", + "description", + "shortDisplayResult", + ["timeElapsed", "displayValue"], + ["start", "period", "number"], + ["start", "period", "type"], + ["start", "yardLine"], + ["start", "clock", "displayValue"], + ["start", "text"], + ["end", "period", "number"], + ["end", "period", "type"], + ["end", "yardLine"], + ["end", "clock", "displayValue"], + ], + meta_prefix="drive.", + errors="ignore", ) + pbp_txt["plays"] = pl.concat([pbp_txt["plays"], pl.from_pandas(prev_drives)], how="vertical") # pbp_txt["plays"] = pbp_txt["plays"].to_dict(orient="records") # pbp_txt["plays"] = pd.DataFrame(pbp_txt["plays"]) - pbp_txt["plays"] = pbp_txt["plays"].with_columns( - game_id = pl.lit(int(self.gameId)), - season = pbp_txt.get("header").get("season").get("year"), - seasonType = pbp_txt.get("header").get("season").get("type"), - week = pbp_txt.get("header").get("week"), - status_type_completed = pbp_txt.get("header").get("competitions")[0].get("status").get("type").get("completed"), - homeTeamId = pl.lit(init["homeTeamId"]), - awayTeamId = pl.lit(init["awayTeamId"]), - homeTeamName = pl.lit(str(init["homeTeamName"])), - awayTeamName = pl.lit(str(init["awayTeamName"])), - homeTeamMascot = pl.lit(str(init["homeTeamMascot"])), - awayTeamMascot = pl.lit(str(init["awayTeamMascot"])), - homeTeamAbbrev = pl.lit(str(init["homeTeamAbbrev"])), - awayTeamAbbrev = pl.lit(str(init["awayTeamAbbrev"])), - homeTeamNameAlt = pl.lit(str(init["homeTeamNameAlt"])), - awayTeamNameAlt = pl.lit(str(init["awayTeamNameAlt"])), - gameSpread = pl.lit(init["gameSpread"]).abs(), - homeFavorite = pl.lit(init["homeFavorite"]), - gameSpreadAvailable = pl.lit(init["gameSpreadAvailable"]), - overUnder = pl.lit(float(init["overUnder"])), - ).with_columns( - homeTeamSpread = pl.when(pl.col('homeFavorite') == True) - .then(pl.col('gameSpread')) - .otherwise(-1*pl.col('gameSpread')), - ).with_columns( - pl.col("period.number").cast(pl.Int32), - pl.col("clock.displayValue").str.split(':').list.to_struct(n_field_strategy="max_width").alias("clock.mm") - ).with_columns( - pl.col("clock.mm").struct.rename_fields(["clock.minutes", "clock.seconds"]) - ).unnest( - "clock.mm" - ).with_columns( - pl.col("clock.minutes").cast(pl.Int32), - pl.col("clock.seconds").cast(pl.Int32), - half = pl.when(pl.col("period.number") <= 2).then(1).otherwise(2) - ).with_columns( - lag_half = pl.col("half").shift(1), - lead_half = pl.col("half").shift(-1) - ).with_columns( - pl.when(pl.col("period.number").is_in([1, 3])) + pbp_txt["plays"] = ( + pbp_txt["plays"] + .with_columns( + game_id=pl.lit(int(self.gameId)), + season=pbp_txt.get("header").get("season").get("year"), + seasonType=pbp_txt.get("header").get("season").get("type"), + week=pbp_txt.get("header").get("week"), + status_type_completed=pbp_txt.get("header") + .get("competitions")[0] + .get("status") + .get("type") + .get("completed"), + homeTeamId=pl.lit(init["homeTeamId"]), + awayTeamId=pl.lit(init["awayTeamId"]), + homeTeamName=pl.lit(str(init["homeTeamName"])), + awayTeamName=pl.lit(str(init["awayTeamName"])), + homeTeamMascot=pl.lit(str(init["homeTeamMascot"])), + awayTeamMascot=pl.lit(str(init["awayTeamMascot"])), + homeTeamAbbrev=pl.lit(str(init["homeTeamAbbrev"])), + awayTeamAbbrev=pl.lit(str(init["awayTeamAbbrev"])), + homeTeamNameAlt=pl.lit(str(init["homeTeamNameAlt"])), + awayTeamNameAlt=pl.lit(str(init["awayTeamNameAlt"])), + gameSpread=pl.lit(init["gameSpread"]).abs(), + homeFavorite=pl.lit(init["homeFavorite"]), + gameSpreadAvailable=pl.lit(init["gameSpreadAvailable"]), + overUnder=pl.lit(float(init["overUnder"])), + ) + .with_columns( + homeTeamSpread=pl.when(pl.col("homeFavorite") == True) + .then(pl.col("gameSpread")) + .otherwise(-1 * pl.col("gameSpread")), + ) + .with_columns( + pl.col("period.number").cast(pl.Int32), + pl.col("clock.displayValue") + .str.split(":") + .list.to_struct(n_field_strategy="max_width") + .alias("clock.mm"), + ) + .with_columns(pl.col("clock.mm").struct.rename_fields(["clock.minutes", "clock.seconds"])) + .unnest("clock.mm") + .with_columns( + pl.col("clock.minutes").cast(pl.Int32), + pl.col("clock.seconds").cast(pl.Int32), + half=pl.when(pl.col("period.number") <= 2).then(1).otherwise(2), + ) + .with_columns(lag_half=pl.col("half").shift(1), lead_half=pl.col("half").shift(-1)) + .with_columns( + pl.when(pl.col("period.number").is_in([1, 3])) .then(900 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) - .otherwise(60 * pl.col("clock.minutes")+ pl.col("clock.seconds")) + .otherwise(60 * pl.col("clock.minutes") + pl.col("clock.seconds")) .alias("start.TimeSecsRem"), - pl.when(pl.col("period.number") == 1) + pl.when(pl.col("period.number") == 1) .then(2700 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) .when(pl.col("period.number") == 2) .then(1800 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) .when(pl.col("period.number") == 3) - .then( 900 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) + .then(900 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) .otherwise(60 * pl.col("clock.minutes") + pl.col("clock.seconds")) .alias("start.adj_TimeSecsRem"), - pl.col("id").cast(pl.Int64) + pl.col("id").cast(pl.Int64), + ) ) pbp_txt["plays"] = pbp_txt["plays"].sort(by=["id", "start.adj_TimeSecsRem"]) # drop play text dupes intelligently, even if they have different play_id values - pbp_txt["plays"] = pbp_txt["plays"].with_columns( - pl.col("text").cast(str), - orig_play_type = pl.col("type.text"), - lead_text = pl.col("text").shift(-1), - lead_start_team = pl.col("start.team.id").shift(-1), - lead_start_yardsToEndzone = pl.col("start.yardsToEndzone").shift(-1), - lead_start_down = pl.col("start.down").shift(-1), - lead_start_distance = pl.col("start.distance").shift(-1), - lead_scoringPlay = pl.col("scoringPlay").shift(-1), - text_dupe = pl.lit(False) - ).with_columns( - text_dupe = pl.when((pl.col("start.team.id") == pl.col("lead_start_team")) + pbp_txt["plays"] = ( + pbp_txt["plays"] + .with_columns( + pl.col("text").cast(str), + orig_play_type=pl.col("type.text"), + lead_text=pl.col("text").shift(-1), + lead_start_team=pl.col("start.team.id").shift(-1), + lead_start_yardsToEndzone=pl.col("start.yardsToEndzone").shift(-1), + lead_start_down=pl.col("start.down").shift(-1), + lead_start_distance=pl.col("start.distance").shift(-1), + lead_scoringPlay=pl.col("scoringPlay").shift(-1), + text_dupe=pl.lit(False), + ) + .with_columns( + text_dupe=pl.when( + (pl.col("start.team.id") == pl.col("lead_start_team")) .and_(pl.col("start.down") == pl.col("lead_start_down")) .and_(pl.col("start.yardsToEndzone") == pl.col("lead_start_yardsToEndzone")) .and_(pl.col("start.distance") == pl.col("lead_start_distance")) - .and_(pl.col("text") == pl.col("lead_text"))) + .and_(pl.col("text") == pl.col("lead_text")) + ) .then(pl.lit(True)) - .when((pl.col("start.team.id") == pl.col("lead_start_team")) + .when( + (pl.col("start.team.id") == pl.col("lead_start_team")) .and_(pl.col("start.down") == pl.col("lead_start_down")) .and_(pl.col("start.yardsToEndzone") == pl.col("lead_start_yardsToEndzone")) .and_(pl.col("start.distance") == pl.col("lead_start_distance")) - .and_(pl.col("text").is_in(pl.col("lead_text")))) + .and_(pl.col("text").is_in(pl.col("lead_text"))) + ) .then(pl.lit(True)) .otherwise(pl.lit(False)) + ) ) pbp_txt["plays"] = pbp_txt["plays"].filter(pl.col("text_dupe") == False) pbp_txt["plays"] = pbp_txt["plays"].with_row_count("game_play_number", 1) - pbp_txt["plays"] = pbp_txt["plays"].with_columns( - pl.col("start.team.id").fill_null(strategy="forward") - .fill_null(strategy="backward").cast(pl.Int32) - ).with_columns( - pl.col("end.team.id").fill_null(value=pl.col("start.team.id")).cast(pl.Int32) - ).with_columns( - pl.col("start.team.id").cast(pl.Int32), - pl.col("end.team.id").cast(pl.Int32), - pl.col("homeTeamId").cast(pl.Int32), - pl.col("awayTeamId").cast(pl.Int32), - pl.when(pl.col("type.text").is_in(kickoff_vec) - .and_(pl.col("start.team.id") == init["homeTeamId"])) + pbp_txt["plays"] = ( + pbp_txt["plays"] + .with_columns( + pl.col("start.team.id").fill_null(strategy="forward").fill_null(strategy="backward").cast(pl.Int32) + ) + .with_columns(pl.col("end.team.id").fill_null(value=pl.col("start.team.id")).cast(pl.Int32)) + .with_columns( + pl.col("start.team.id").cast(pl.Int32), + pl.col("end.team.id").cast(pl.Int32), + pl.col("homeTeamId").cast(pl.Int32), + pl.col("awayTeamId").cast(pl.Int32), + pl.when(pl.col("type.text").is_in(kickoff_vec).and_(pl.col("start.team.id") == init["homeTeamId"])) .then(pl.col("awayTeamId")) - .when(pl.col("type.text").is_in(kickoff_vec) - .and_(pl.col("start.team.id") == init["awayTeamId"])) + .when(pl.col("type.text").is_in(kickoff_vec).and_(pl.col("start.team.id") == init["awayTeamId"])) .then(pl.col("homeTeamId")) .otherwise(pl.col("start.team.id")) - .alias("start.pos_team.id") - ).with_columns( - pl.when(pl.col("start.pos_team.id") == init["homeTeamId"]) + .alias("start.pos_team.id"), + ) + .with_columns( + pl.when(pl.col("start.pos_team.id") == init["homeTeamId"]) .then(init["awayTeamId"]) .otherwise(init["homeTeamId"]) .alias("start.def_pos_team.id"), - pl.when(pl.col("end.team.id") == init["homeTeamId"]) + pl.when(pl.col("end.team.id") == init["homeTeamId"]) .then(init["awayTeamId"]) .otherwise(init["homeTeamId"]) .alias("end.def_pos_team.id"), - pl.col("end.team.id") - .alias("end.pos_team.id") - ).with_columns( - pl.when(pl.col("start.pos_team.id") == init["homeTeamId"]) + pl.col("end.team.id").alias("end.pos_team.id"), + ) + .with_columns( + pl.when(pl.col("start.pos_team.id") == init["homeTeamId"]) .then(pl.col("homeTeamName")) .otherwise(pl.col("awayTeamName")) .alias("start.pos_team.name"), - pl.when(pl.col("start.pos_team.id") == init["homeTeamId"]) + pl.when(pl.col("start.pos_team.id") == init["homeTeamId"]) .then(pl.col("awayTeamName")) .otherwise(pl.col("homeTeamName")) .alias("start.def_pos_team.name"), - pl.when(pl.col("end.pos_team.id") == init["homeTeamId"]) + pl.when(pl.col("end.pos_team.id") == init["homeTeamId"]) .then(pl.col("homeTeamName")) .otherwise(pl.col("awayTeamName")) .alias("end.pos_team.name"), - pl.when(pl.col("end.pos_team.id") == init["homeTeamId"]) + pl.when(pl.col("end.pos_team.id") == init["homeTeamId"]) .then(pl.col("awayTeamName")) .otherwise(pl.col("homeTeamName")) .alias("end.def_pos_team.name"), - pl.when(pl.col("start.pos_team.id") == init["homeTeamId"]) + pl.when(pl.col("start.pos_team.id") == init["homeTeamId"]) .then(True) .otherwise(False) .alias("start.is_home"), - pl.when(pl.col("end.pos_team.id") == init["homeTeamId"]) + pl.when(pl.col("end.pos_team.id") == init["homeTeamId"]) .then(True) .otherwise(False) .alias("end.is_home"), - pl.when((pl.col("type.text") == "Timeout") - .and_( - pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamAbbrev"]).lower()) - .or_( - pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamAbbrev"]).lower()), - pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamName"]).lower()), - pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamMascot"]).lower()), - pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamNameAlt"]).lower()) + pl.when( + (pl.col("type.text") == "Timeout").and_( + pl.col("text") + .str.to_lowercase() + .str.contains(str(init["homeTeamAbbrev"]).lower()) + .or_( + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamAbbrev"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamName"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamMascot"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamNameAlt"]).lower()), + ) ) - )) + ) .then(True) .otherwise(False) .alias("homeTimeoutCalled"), - pl.when((pl.col("type.text") == "Timeout") - .and_( - pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamAbbrev"]).lower()) - .or_( - pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamAbbrev"]).lower()), - pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamName"]).lower()), - pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamMascot"]).lower()), - pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamNameAlt"]).lower()) + pl.when( + (pl.col("type.text") == "Timeout").and_( + pl.col("text") + .str.to_lowercase() + .str.contains(str(init["awayTeamAbbrev"]).lower()) + .or_( + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamAbbrev"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamName"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamMascot"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamNameAlt"]).lower()), + ) ) - )) + ) .then(True) .otherwise(False) - .alias("awayTimeoutCalled") + .alias("awayTimeoutCalled"), + ) ) pbp_txt["timeouts"] = { @@ -355,247 +407,282 @@ def __helper_cfb_pbp_features(self, pbp_txt, init): init["awayTeamId"]: {"1": [], "2": []}, } pbp_txt["timeouts"][init["homeTeamId"]]["1"] = ( - pbp_txt["plays"] - .filter( - (pl.col("homeTimeoutCalled") == True) - .and_(pl.col("period.number") <= 2) - ) - .get_column("id") - .to_list() - ) + pbp_txt["plays"] + .filter((pl.col("homeTimeoutCalled") == True).and_(pl.col("period.number") <= 2)) + .get_column("id") + .to_list() + ) pbp_txt["timeouts"][init["homeTeamId"]]["2"] = ( - pbp_txt["plays"] - .filter( - (pl.col("homeTimeoutCalled") == True) - .and_(pl.col("period.number") > 2) - ) - .get_column("id") - .to_list() - ) + pbp_txt["plays"] + .filter((pl.col("homeTimeoutCalled") == True).and_(pl.col("period.number") > 2)) + .get_column("id") + .to_list() + ) pbp_txt["timeouts"][init["awayTeamId"]]["1"] = ( - pbp_txt["plays"] - .filter( - (pl.col("awayTimeoutCalled") == True) - .and_(pl.col("period.number") <= 2) + pbp_txt["plays"] + .filter((pl.col("awayTimeoutCalled") == True).and_(pl.col("period.number") <= 2)) + .get_column("id") + .to_list() + ) + pbp_txt["timeouts"][init["awayTeamId"]]["2"] = ( + pbp_txt["plays"] + .filter((pl.col("awayTimeoutCalled") == True).and_(pl.col("period.number") > 2)) + .get_column("id") + .to_list() + ) + pbp_txt["plays"] = ( + pbp_txt["plays"] + .with_columns( + pl.when( + ( + (pbp_txt["timeouts"][init["homeTeamId"]]["1"] <= pl.col("id")).and_( + pl.col("period.number") <= 2, len(pbp_txt["timeouts"][init["homeTeamId"]]["1"]) == 1 + ) + ).or_( + (pbp_txt["timeouts"][init["homeTeamId"]]["2"] <= pl.col("id")).and_( + pl.col("period.number") > 2, len(pbp_txt["timeouts"][init["homeTeamId"]]["2"]) == 1 + ) + ) + ) + .then(2) + .when( + ( + (pbp_txt["timeouts"][init["homeTeamId"]]["1"] <= pl.col("id")).and_( + pl.col("period.number") <= 2, len(pbp_txt["timeouts"][init["homeTeamId"]]["1"]) == 2 + ) + ).or_( + (pbp_txt["timeouts"][init["homeTeamId"]]["2"] <= pl.col("id")).and_( + pl.col("period.number") > 2, len(pbp_txt["timeouts"][init["homeTeamId"]]["2"]) == 2 + ) + ) + ) + .then(1) + .when( + ( + (pbp_txt["timeouts"][init["homeTeamId"]]["1"] <= pl.col("id")).and_( + pl.col("period.number") <= 2, len(pbp_txt["timeouts"][init["homeTeamId"]]["1"]) == 3 + ) + ).or_( + (pbp_txt["timeouts"][init["homeTeamId"]]["2"] <= pl.col("id")).and_( + pl.col("period.number") > 2, len(pbp_txt["timeouts"][init["homeTeamId"]]["2"]) == 3 + ) + ) + ) + .then(0) + .otherwise(3) + .alias("end.homeTeamTimeouts"), + pl.when( + ( + (pbp_txt["timeouts"][init["awayTeamId"]]["1"] <= pl.col("id")).and_( + pl.col("period.number") <= 2, len(pbp_txt["timeouts"][init["awayTeamId"]]["1"]) == 1 + ) + ).or_( + (pbp_txt["timeouts"][init["awayTeamId"]]["2"] <= pl.col("id")).and_( + pl.col("period.number") > 2, len(pbp_txt["timeouts"][init["awayTeamId"]]["2"]) == 1 + ) + ) + ) + .then(2) + .when( + ( + (pbp_txt["timeouts"][init["awayTeamId"]]["1"] <= pl.col("id")).and_( + pl.col("period.number") <= 2, len(pbp_txt["timeouts"][init["awayTeamId"]]["1"]) == 2 + ) + ).or_( + (pbp_txt["timeouts"][init["awayTeamId"]]["2"] <= pl.col("id")).and_( + pl.col("period.number") > 2, len(pbp_txt["timeouts"][init["awayTeamId"]]["2"]) == 2 + ) + ) + ) + .then(1) + .when( + ( + (pbp_txt["timeouts"][init["awayTeamId"]]["1"] <= pl.col("id")).and_( + pl.col("period.number") <= 2, len(pbp_txt["timeouts"][init["awayTeamId"]]["1"]) == 3 + ) + ).or_( + (pbp_txt["timeouts"][init["awayTeamId"]]["2"] <= pl.col("id")).and_( + pl.col("period.number") > 2, len(pbp_txt["timeouts"][init["awayTeamId"]]["2"]) == 3 + ) + ) ) - .get_column("id") - .to_list() + .then(0) + .otherwise(3) + .alias("end.awayTeamTimeouts"), ) - pbp_txt["timeouts"][init["awayTeamId"]]["2"] = ( - pbp_txt["plays"] - .filter( - (pl.col("awayTimeoutCalled") == True) - .and_(pl.col("period.number") > 2) - ) - .get_column("id") - .to_list() - ) - pbp_txt["plays"] = pbp_txt["plays"].with_columns( - pl.when(( - (pbp_txt["timeouts"][init["homeTeamId"]]["1"] <= pl.col("id")) - .and_(pl.col("period.number") <= 2, - len(pbp_txt["timeouts"][init["homeTeamId"]]["1"]) == 1) - ).or_( - (pbp_txt["timeouts"][init["homeTeamId"]]["2"] <= pl.col("id")) - .and_(pl.col("period.number") > 2, - len(pbp_txt["timeouts"][init["homeTeamId"]]["2"]) == 1) - )) - .then(2) - .when(( - (pbp_txt["timeouts"][init["homeTeamId"]]["1"] <= pl.col("id")) - .and_(pl.col("period.number") <= 2, - len(pbp_txt["timeouts"][init["homeTeamId"]]["1"]) == 2) - ).or_( - (pbp_txt["timeouts"][init["homeTeamId"]]["2"] <= pl.col("id")) - .and_(pl.col("period.number") > 2, - len(pbp_txt["timeouts"][init["homeTeamId"]]["2"]) == 2) - )) - .then(1) - .when(( - (pbp_txt["timeouts"][init["homeTeamId"]]["1"] <= pl.col("id")) - .and_(pl.col("period.number") <= 2, - len(pbp_txt["timeouts"][init["homeTeamId"]]["1"]) == 3) - ).or_( - (pbp_txt["timeouts"][init["homeTeamId"]]["2"] <= pl.col("id")) - .and_(pl.col("period.number") > 2, - len(pbp_txt["timeouts"][init["homeTeamId"]]["2"]) == 3) - )) - .then(0) - .otherwise(3) - .alias("end.homeTeamTimeouts"), - pl.when(( - (pbp_txt["timeouts"][init["awayTeamId"]]["1"] <= pl.col("id")) - .and_(pl.col("period.number") <= 2, - len(pbp_txt["timeouts"][init["awayTeamId"]]["1"]) == 1) - ).or_( - (pbp_txt["timeouts"][init["awayTeamId"]]["2"] <= pl.col("id")) - .and_(pl.col("period.number") > 2, - len(pbp_txt["timeouts"][init["awayTeamId"]]["2"]) == 1) - )) - .then(2) - .when(( - (pbp_txt["timeouts"][init["awayTeamId"]]["1"] <= pl.col("id")) - .and_(pl.col("period.number") <= 2, - len(pbp_txt["timeouts"][init["awayTeamId"]]["1"]) == 2) - ).or_( - (pbp_txt["timeouts"][init["awayTeamId"]]["2"] <= pl.col("id")) - .and_(pl.col("period.number") > 2, - len(pbp_txt["timeouts"][init["awayTeamId"]]["2"]) == 2) - )) - .then(1) - .when(( - (pbp_txt["timeouts"][init["awayTeamId"]]["1"] <= pl.col("id")) - .and_(pl.col("period.number") <= 2, - len(pbp_txt["timeouts"][init["awayTeamId"]]["1"]) == 3) - ).or_( - (pbp_txt["timeouts"][init["awayTeamId"]]["2"] <= pl.col("id")) - .and_(pl.col("period.number") > 2, - len(pbp_txt["timeouts"][init["awayTeamId"]]["2"]) == 3) - )) - .then(0) - .otherwise(3) - .alias("end.awayTeamTimeouts") - ).with_columns( - pl.col("end.homeTeamTimeouts").shift_and_fill(periods = 1, fill_value = 3) - .alias("start.homeTeamTimeouts"), - pl.col("end.awayTeamTimeouts").shift_and_fill(periods = 1, fill_value = 3) - .alias("start.awayTeamTimeouts"), - pl.col("start.TimeSecsRem").shift(periods = 1) - .alias("end.TimeSecsRem"), - pl.col("start.adj_TimeSecsRem").shift(periods = 1) - .alias("end.adj_TimeSecsRem") - ).with_columns( - pl.when(pl.col("game_play_number") == 1) + .with_columns( + pl.col("end.homeTeamTimeouts").shift_and_fill(periods=1, fill_value=3).alias("start.homeTeamTimeouts"), + pl.col("end.awayTeamTimeouts").shift_and_fill(periods=1, fill_value=3).alias("start.awayTeamTimeouts"), + pl.col("start.TimeSecsRem").shift(periods=1).alias("end.TimeSecsRem"), + pl.col("start.adj_TimeSecsRem").shift(periods=1).alias("end.adj_TimeSecsRem"), + ) + .with_columns( + pl.when(pl.col("game_play_number") == 1) .then(pl.lit(1800)) .when((pl.col("half") == 2) & (pl.col("lag_half") == 1)) .then(pl.lit(1800)) .otherwise(pl.col("end.TimeSecsRem")) .alias("end.TimeSecsRem"), - pl.when(pl.col("game_play_number") == 1) + pl.when(pl.col("game_play_number") == 1) .then(pl.lit(3600)) .when((pl.col("half") == 2) & (pl.col("lag_half") == 1)) .then(pl.lit(1800)) .otherwise(pl.col("end.adj_TimeSecsRem")) .alias("end.adj_TimeSecsRem"), - pl.when(pl.col("start.pos_team.id") == pl.col("homeTeamId")) + pl.when(pl.col("start.pos_team.id") == pl.col("homeTeamId")) .then(pl.col("start.homeTeamTimeouts")) .otherwise(pl.col("start.awayTeamTimeouts")) .alias("start.posTeamTimeouts"), - pl.when(pl.col("start.pos_team.id") == pl.col("homeTeamId")) + pl.when(pl.col("start.pos_team.id") == pl.col("homeTeamId")) .then(pl.col("start.awayTeamTimeouts")) .otherwise(pl.col("start.homeTeamTimeouts")) .alias("start.defPosTeamTimeouts"), - pl.when(pl.col("end.pos_team.id") == pl.col("homeTeamId")) + pl.when(pl.col("end.pos_team.id") == pl.col("homeTeamId")) .then(pl.col("end.homeTeamTimeouts")) .otherwise(pl.col("end.awayTeamTimeouts")) .alias("end.posTeamTimeouts"), - pl.when(pl.col("end.pos_team.id") == pl.col("homeTeamId")) + pl.when(pl.col("end.pos_team.id") == pl.col("homeTeamId")) .then(pl.col("end.awayTeamTimeouts")) .otherwise(pl.col("end.homeTeamTimeouts")) .alias("end.defPosTeamTimeouts"), - pl.when((pl.col("game_play_number") == 1) - .and_(pl.col("type.text").is_in(kickoff_vec), - pl.col("start.pos_team.id") == pl.col("homeTeamId"))) + pl.when( + (pl.col("game_play_number") == 1).and_( + pl.col("type.text").is_in(kickoff_vec), pl.col("start.pos_team.id") == pl.col("homeTeamId") + ) + ) .then(pl.col("homeTeamId")) .otherwise(pl.col("awayTeamId")) .alias("firstHalfKickoffTeamId"), - pl.col("period.number").alias("period"), - pl.when(pl.col("start.team.id") == pl.col("homeTeamId")) + pl.col("period.number").alias("period"), + pl.when(pl.col("start.team.id") == pl.col("homeTeamId")) .then(pl.lit(100) - pl.col("start.yardLine")) .otherwise(pl.col("start.yardLine")) .alias("start.yard"), - ).with_columns( - pl.when(pl.col("start.yardLine").is_null() == False) + ) + .with_columns( + pl.when(pl.col("start.yardLine").is_null() == False) .then(pl.col("start.yardLine")) .otherwise(pl.col("start.yard")) .alias("start.yardLine"), - ).with_columns( - pl.when(pl.col("start.yardLine").is_null() == False) + ) + .with_columns( + pl.when(pl.col("start.yardLine").is_null() == False) .then(pl.col("start.yardsToEndzone")) .otherwise(pl.col("start.yardLine")) .alias("start.yardsToEndzone") - ).with_columns( - pl.when(pl.col("start.yardsToEndzone") == 0) + ) + .with_columns( + pl.when(pl.col("start.yardsToEndzone") == 0) .then(pl.col("start.yard")) .otherwise(pl.col("start.yardsToEndzone")) .alias("start.yardsToEndzone"), - pl.when(pl.col("end.team.id") == pl.col("homeTeamId")) + pl.when(pl.col("end.team.id") == pl.col("homeTeamId")) .then(pl.lit(100) - pl.col("end.yardLine")) .otherwise(pl.col("end.yardLine")) - .alias("end.yard") - ).with_columns( - pl.when((pl.col("type.text") == "Penalty") - .and_(pl.col("text").str.contains(r"(?i)declined"))) + .alias("end.yard"), + ) + .with_columns( + pl.when((pl.col("type.text") == "Penalty").and_(pl.col("text").str.contains(r"(?i)declined"))) .then(pl.col("start.yard")) .otherwise(pl.col("end.yard")) .alias("end.yard"), - ).with_columns( - pl.when(pl.col("end.yardLine").is_null() == False) + ) + .with_columns( + pl.when(pl.col("end.yardLine").is_null() == False) .then(pl.col("end.yardsToEndzone")) .otherwise(pl.col("end.yard")) .alias("end.yardsToEndzone"), - pl.when((pl.col("start.distance") == 0) - .and_(pl.col("start.downDistanceText").str.contains(r"(?i)goal"))) + pl.when( + (pl.col("start.distance") == 0).and_(pl.col("start.downDistanceText").str.contains(r"(?i)goal")) + ) .then(pl.col("start.yardsToEndzone")) .otherwise(pl.col("start.distance")) .alias("start.distance"), - ).with_columns( - pl.when((pl.col("type.text") == "Penalty") - .and_(pl.col("text").str.contains(r"(?i)declined"))) + ) + .with_columns( + pl.when((pl.col("type.text") == "Penalty").and_(pl.col("text").str.contains(r"(?i)declined"))) .then(pl.col("start.yardsToEndzone")) .otherwise(pl.col("end.yardsToEndzone")) .alias("end.yardsToEndzone"), + ) ) pbp_txt["firstHalfKickoffTeamId"] = np.where( - (pbp_txt["plays"]["game_play_number"] == 1) - & (pbp_txt["plays"]["type.text"].is_in(kickoff_vec)) - & (pbp_txt["plays"]["start.team.id"] == init["homeTeamId"]), - init["homeTeamId"], - init["awayTeamId"] - ) + (pbp_txt["plays"]["game_play_number"] == 1) + & (pbp_txt["plays"]["type.text"].is_in(kickoff_vec)) + & (pbp_txt["plays"]["start.team.id"] == init["homeTeamId"]), + init["homeTeamId"], + init["awayTeamId"], + ) pbp_txt["firstHalfKickoffTeamId"] = pbp_txt["firstHalfKickoffTeamId"][0] if "scoringType.displayName" in pbp_txt["plays"].columns: - pbp_txt["plays"] = pbp_txt["plays"].with_columns( - pl.when(pl.col("scoringType.displayName") == "Field Goal") + pbp_txt["plays"] = ( + pbp_txt["plays"] + .with_columns( + pl.when(pl.col("scoringType.displayName") == "Field Goal") .then("Field Goal Good") .otherwise(pl.col("type.text")) .alias("type.text") - ).with_columns( - pl.when(pl.col("scoringType.displayName") == "Extra Point") + ) + .with_columns( + pl.when(pl.col("scoringType.displayName") == "Extra Point") .then("Extra Point Good") .otherwise(pl.col("type.text")) .alias("type.text") + ) ) - pbp_txt["plays"] = pbp_txt["plays"].with_columns( - pl.when(pl.col("type.text").is_null()) + pbp_txt["plays"] = ( + pbp_txt["plays"] + .with_columns( + pl.when(pl.col("type.text").is_null()) .then("Unknown") .otherwise(pl.col("type.text")) .alias("type.text") - ).with_columns( - pl.when(pl.col("type.text").str.to_lowercase().str.contains("(?i)extra point") - .and_(pl.col("type.text").str.to_lowercase().str.contains("(?i)no good"))) + ) + .with_columns( + pl.when( + pl.col("type.text") + .str.to_lowercase() + .str.contains("(?i)extra point") + .and_(pl.col("type.text").str.to_lowercase().str.contains("(?i)no good")) + ) .then("Extra Point Missed") .otherwise(pl.col("type.text")) .alias("type.text") - ).with_columns( - pl.when(pl.col("type.text").str.to_lowercase().str.contains("(?i)extra point") - .and_(pl.col("type.text").str.to_lowercase().str.contains("(?i)blocked"))) + ) + .with_columns( + pl.when( + pl.col("type.text") + .str.to_lowercase() + .str.contains("(?i)extra point") + .and_(pl.col("type.text").str.to_lowercase().str.contains("(?i)blocked")) + ) .then("Extra Point Missed") .otherwise(pl.col("type.text")) .alias("type.text") - ).with_columns( - pl.when(pl.col("type.text").str.to_lowercase().str.contains("(?i)field goal") - .and_(pl.col("type.text").str.to_lowercase().str.contains("(?i)blocked"))) + ) + .with_columns( + pl.when( + pl.col("type.text") + .str.to_lowercase() + .str.contains("(?i)field goal") + .and_(pl.col("type.text").str.to_lowercase().str.contains("(?i)blocked")) + ) .then("Extra Point Missed") .otherwise(pl.col("type.text")) .alias("type.text") - ).with_columns( - pl.when(pl.col("type.text").str.to_lowercase().str.contains("(?i)field goal") - .and_(pl.col("type.text").str.to_lowercase().str.contains("(?i)no good"))) + ) + .with_columns( + pl.when( + pl.col("type.text") + .str.to_lowercase() + .str.contains("(?i)field goal") + .and_(pl.col("type.text").str.to_lowercase().str.contains("(?i)no good")) + ) .then("Extra Point Missed") .otherwise(pl.col("type.text")) .alias("type.text") + ) ) return pbp_txt @@ -606,16 +693,16 @@ def __helper_cfb_pbp(self, pbp_txt): def __helper_cfb_pickcenter(self, pbp_txt): # Spread definition - if len(pbp_txt.get("pickcenter",[])) > 1: - homeFavorite = pbp_txt.get("pickcenter",{})[0].get("homeTeamOdds",{}).get("favorite","") - if "spread" in pbp_txt.get("pickcenter",{})[1].keys(): - gameSpread = pbp_txt.get("pickcenter",{})[1].get("spread","") - overUnder = pbp_txt.get("pickcenter",{})[1].get("overUnder","") + if len(pbp_txt.get("pickcenter", [])) > 1: + homeFavorite = pbp_txt.get("pickcenter", {})[0].get("homeTeamOdds", {}).get("favorite", "") + if "spread" in pbp_txt.get("pickcenter", {})[1].keys(): + gameSpread = pbp_txt.get("pickcenter", {})[1].get("spread", "") + overUnder = pbp_txt.get("pickcenter", {})[1].get("overUnder", "") else: - gameSpread = pbp_txt.get("pickcenter",{})[0].get("spread","") - overUnder = pbp_txt.get("pickcenter",{})[0].get("overUnder","") + gameSpread = pbp_txt.get("pickcenter", {})[0].get("spread", "") + overUnder = pbp_txt.get("pickcenter", {})[0].get("overUnder", "") gameSpreadAvailable = True - # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") + # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") else: gameSpread = 2.5 overUnder = 140.5 @@ -626,48 +713,56 @@ def __helper_cfb_pickcenter(self, pbp_txt): "gameSpread": gameSpread, "overUnder": overUnder, "homeFavorite": homeFavorite, - "gameSpreadAvailable": gameSpreadAvailable + "gameSpreadAvailable": gameSpreadAvailable, } def __helper_cfb_game_data(self, pbp_txt, init): - pbp_txt['timeouts'] = {} - pbp_txt['teamInfo'] = pbp_txt['header']['competitions'][0] - pbp_txt['season'] = pbp_txt['header']['season'] - pbp_txt['playByPlaySource'] = pbp_txt['header']['competitions'][0]['playByPlaySource'] - pbp_txt['boxscoreSource'] = pbp_txt['header']['competitions'][0]['boxscoreSource'] - pbp_txt['gameSpreadAvailable'] = init["gameSpreadAvailable"] - pbp_txt['gameSpread'] = init["gameSpread"] + pbp_txt["timeouts"] = {} + pbp_txt["teamInfo"] = pbp_txt["header"]["competitions"][0] + pbp_txt["season"] = pbp_txt["header"]["season"] + pbp_txt["playByPlaySource"] = pbp_txt["header"]["competitions"][0]["playByPlaySource"] + pbp_txt["boxscoreSource"] = pbp_txt["header"]["competitions"][0]["boxscoreSource"] + pbp_txt["gameSpreadAvailable"] = init["gameSpreadAvailable"] + pbp_txt["gameSpread"] = init["gameSpread"] pbp_txt["homeFavorite"] = init["homeFavorite"] pbp_txt["homeTeamSpread"] = np.where( init["homeFavorite"] == True, abs(init["gameSpread"]), -1 * abs(init["gameSpread"]) ) pbp_txt["overUnder"] = init["overUnder"] # Home and Away identification variables - if pbp_txt['header']['competitions'][0]['competitors'][0]['homeAway']=='home': - pbp_txt['header']['competitions'][0]['home'] = pbp_txt['header']['competitions'][0]['competitors'][0]['team'] - homeTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['id']) - homeTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['name']) - homeTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['location']) - homeTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['abbreviation']) + if pbp_txt["header"]["competitions"][0]["competitors"][0]["homeAway"] == "home": + pbp_txt["header"]["competitions"][0]["home"] = pbp_txt["header"]["competitions"][0]["competitors"][0][ + "team" + ] + homeTeamId = int(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["id"]) + homeTeamMascot = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["name"]) + homeTeamName = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["location"]) + homeTeamAbbrev = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["abbreviation"]) homeTeamNameAlt = re.sub("Stat(.+)", "St", homeTeamName) - pbp_txt['header']['competitions'][0]['away'] = pbp_txt['header']['competitions'][0]['competitors'][1]['team'] - awayTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['id']) - awayTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['name']) - awayTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['location']) - awayTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['abbreviation']) + pbp_txt["header"]["competitions"][0]["away"] = pbp_txt["header"]["competitions"][0]["competitors"][1][ + "team" + ] + awayTeamId = int(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["id"]) + awayTeamMascot = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["name"]) + awayTeamName = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["location"]) + awayTeamAbbrev = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["abbreviation"]) awayTeamNameAlt = re.sub("Stat(.+)", "St", awayTeamName) else: - pbp_txt['header']['competitions'][0]['away'] = pbp_txt['header']['competitions'][0]['competitors'][0]['team'] - awayTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['id']) - awayTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['name']) - awayTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['location']) - awayTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['abbreviation']) + pbp_txt["header"]["competitions"][0]["away"] = pbp_txt["header"]["competitions"][0]["competitors"][0][ + "team" + ] + awayTeamId = int(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["id"]) + awayTeamMascot = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["name"]) + awayTeamName = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["location"]) + awayTeamAbbrev = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["abbreviation"]) awayTeamNameAlt = re.sub("Stat(.+)", "St", awayTeamName) - pbp_txt['header']['competitions'][0]['home'] = pbp_txt['header']['competitions'][0]['competitors'][1]['team'] - homeTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['id']) - homeTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['name']) - homeTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['location']) - homeTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['abbreviation']) + pbp_txt["header"]["competitions"][0]["home"] = pbp_txt["header"]["competitions"][0]["competitors"][1][ + "team" + ] + homeTeamId = int(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["id"]) + homeTeamMascot = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["name"]) + homeTeamName = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["location"]) + homeTeamAbbrev = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["abbreviation"]) homeTeamNameAlt = re.sub("Stat(.+)", "St", homeTeamName) init["homeTeamId"] = homeTeamId init["homeTeamMascot"] = homeTeamMascot @@ -689,33 +784,33 @@ def __add_downs_data(self, play_df): """ play_df = play_df.sort(by=["id", "start.adj_TimeSecsRem"]) - play_df = play_df.unique( subset=["text", "id", "type.text", "start.down", "sequenceNumber"], keep="last", maintain_order=True ) play_df = play_df.filter( - pl.col("type.text").str.contains( - "(?i)end of|(?i)coin toss|(?i)end period|(?i)wins toss" - ) == False + pl.col("type.text").str.contains("(?i)end of|(?i)coin toss|(?i)end period|(?i)wins toss") == False ) - play_df = play_df.with_columns( - period = pl.col("period.number"), - half = pl.when(pl.col("period.number") <= 2).then(1).otherwise(2), - ).with_columns( - lead_half = pl.col("half").shift(-1), - lag_scoringPlay = pl.col("scoringPlay").shift(1), - - ).with_columns( - pl.when(pl.col("lead_half").is_null()).then(2).otherwise(pl.col("lead_half")).alias("lead_half"), - end_of_half = pl.col("half") != pl.col("lead_half"), - down_1 = pl.col("start.down") == 1, - down_2 = pl.col("start.down") == 2, - down_3 = pl.col("start.down") == 3, - down_4 = pl.col("start.down") == 4, - down_1_end = pl.col("end.down") == 1, - down_2_end = pl.col("end.down") == 2, - down_3_end = pl.col("end.down") == 3, - down_4_end = pl.col("end.down") == 4 + play_df = ( + play_df.with_columns( + period=pl.col("period.number"), + half=pl.when(pl.col("period.number") <= 2).then(1).otherwise(2), + ) + .with_columns( + lead_half=pl.col("half").shift(-1), + lag_scoringPlay=pl.col("scoringPlay").shift(1), + ) + .with_columns( + pl.when(pl.col("lead_half").is_null()).then(2).otherwise(pl.col("lead_half")).alias("lead_half"), + end_of_half=pl.col("half") != pl.col("lead_half"), + down_1=pl.col("start.down") == 1, + down_2=pl.col("start.down") == 2, + down_3=pl.col("start.down") == 3, + down_4=pl.col("start.down") == 4, + down_1_end=pl.col("end.down") == 1, + down_2_end=pl.col("end.down") == 2, + down_3_end=pl.col("end.down") == 3, + down_4_end=pl.col("end.down") == 4, + ) ) return play_df @@ -726,92 +821,104 @@ def __add_play_type_flags(self, play_df): * Flags for fumbles, scores, kickoffs, punts, field goals """ # --- Touchdown, Fumble, Special Teams flags ----------------- - play_df = play_df.with_columns( - scoring_play = pl.when(pl.col("type.text").is_in(scores_vec)).then(True).otherwise(False), - td_play = pl.col("text").str.contains("(?i)touchdown|(?i)for a TD"), - touchdown = pl.col("type.text").str.contains("(?i)touchdown"), - ## Portion of touchdown check for plays where touchdown is not listed in the play_type-- - td_check = pl.col("text").str.contains("(?i)touchdown"), - safety = pl.col("text").str.contains("(?i)safety"), - fumble_vec = pl.when(pl.col("text").str.contains("(?i)fumble")) - .then(True) - .when((pl.col("text").str.contains("(?i)fumble")) - .and_(pl.col("type.text") == "Rush", - pl.col("start.pos_team.id") != pl.col("end.pos_team.id"))) - .then(True) - .when((pl.col("text").str.contains("(?i)fumble")) - .and_(pl.col("type.text") == "Sack", - pl.col("start.pos_team.id") != pl.col("end.pos_team.id"))) - .then(True) - .otherwise(False), - forced_fumble = pl.when(pl.col("text").str.contains("(?i)forced by")) - .then(True) - .otherwise(False), - # --- Kicks---- - kickoff_play = pl.col("type.text").is_in(kickoff_vec), - ).with_columns( - kickoff_tb = pl.when((pl.col("text").str.contains("(?i)touchback")) - .and_(pl.col("kickoff_play") == True)) + play_df = ( + play_df.with_columns( + scoring_play=pl.when(pl.col("type.text").is_in(scores_vec)).then(True).otherwise(False), + td_play=pl.col("text").str.contains("(?i)touchdown|(?i)for a TD"), + touchdown=pl.col("type.text").str.contains("(?i)touchdown"), + ## Portion of touchdown check for plays where touchdown is not listed in the play_type-- + td_check=pl.col("text").str.contains("(?i)touchdown"), + safety=pl.col("text").str.contains("(?i)safety"), + fumble_vec=pl.when(pl.col("text").str.contains("(?i)fumble")) + .then(True) + .when( + (pl.col("text").str.contains("(?i)fumble")).and_( + pl.col("type.text") == "Rush", pl.col("start.pos_team.id") != pl.col("end.pos_team.id") + ) + ) + .then(True) + .when( + (pl.col("text").str.contains("(?i)fumble")).and_( + pl.col("type.text") == "Sack", pl.col("start.pos_team.id") != pl.col("end.pos_team.id") + ) + ) + .then(True) + .otherwise(False), + forced_fumble=pl.when(pl.col("text").str.contains("(?i)forced by")).then(True).otherwise(False), + # --- Kicks---- + kickoff_play=pl.col("type.text").is_in(kickoff_vec), + ) + .with_columns( + kickoff_tb=pl.when((pl.col("text").str.contains("(?i)touchback")).and_(pl.col("kickoff_play") == True)) .then(True) - .when((pl.col("text").str.contains("(?i)kickoff$")) - .and_(pl.col("kickoff_play") == True)) + .when((pl.col("text").str.contains("(?i)kickoff$")).and_(pl.col("kickoff_play") == True)) .then(True) .otherwise(False), - kickoff_onside = pl.when((pl.col("text").str.contains("(?i)on-side|(?i)onside|(?i)on side")) - .and_(pl.col("kickoff_play") == True)) + kickoff_onside=pl.when( + (pl.col("text").str.contains("(?i)on-side|(?i)onside|(?i)on side")).and_( + pl.col("kickoff_play") == True + ) + ) .then(True) .otherwise(False), - kickoff_oob = pl.when((pl.col("text").str.contains("(?i)out-of-bounds|(?i)out of bounds")) - .and_(pl.col("kickoff_play") == True)) + kickoff_oob=pl.when( + (pl.col("text").str.contains("(?i)out-of-bounds|(?i)out of bounds")).and_( + pl.col("kickoff_play") == True + ) + ) .then(True) .otherwise(False), - kickoff_fair_catch = pl.when((pl.col("text").str.contains("(?i)fair catch|(?i)fair caught")) - .and_(pl.col("kickoff_play") == True)) + kickoff_fair_catch=pl.when( + (pl.col("text").str.contains("(?i)fair catch|(?i)fair caught")).and_( + pl.col("kickoff_play") == True + ) + ) .then(True) .otherwise(False), - kickoff_downed = pl.when((pl.col("text").str.contains("(?i)downed")) - .and_(pl.col("kickoff_play") == True)) + kickoff_downed=pl.when( + (pl.col("text").str.contains("(?i)downed")).and_(pl.col("kickoff_play") == True) + ) .then(True) .otherwise(False), - kick_play = pl.col("text").str.contains("(?i)kick|(?i)kickoff"), - kickoff_safety = pl.when((pl.col("text").str.contains("(?i)kickoff")) - .and_(pl.col("safety") == True, - pl.col("type.text").is_in(["Blocked Punt", "Penalty"]) == False)) + kick_play=pl.col("text").str.contains("(?i)kick|(?i)kickoff"), + kickoff_safety=pl.when( + (pl.col("text").str.contains("(?i)kickoff")).and_( + pl.col("safety") == True, pl.col("type.text").is_in(["Blocked Punt", "Penalty"]) == False + ) + ) .then(True) .otherwise(False), - # --- Punts---- - punt = pl.col("type.text").is_in(punt_vec), - punt_play = pl.col("text").str.contains("(?i)punt") - ).with_columns( - punt_tb = pl.when((pl.col("text").str.contains("(?i)touchback")) - .and_(pl.col("punt") == True)) + # --- Punts---- + punt=pl.col("type.text").is_in(punt_vec), + punt_play=pl.col("text").str.contains("(?i)punt"), + ) + .with_columns( + punt_tb=pl.when((pl.col("text").str.contains("(?i)touchback")).and_(pl.col("punt") == True)) .then(True) .otherwise(False), - punt_oob = pl.when((pl.col("text").str.contains("(?i)out-of-bounds|(?i)out of bounds")) - .and_(pl.col("punt") == True)) + punt_oob=pl.when( + (pl.col("text").str.contains("(?i)out-of-bounds|(?i)out of bounds")).and_(pl.col("punt") == True) + ) .then(True) .otherwise(False), - punt_fair_catch = pl.when((pl.col("text").str.contains("(?i)fair catch|(?i)fair caught")) - .and_(pl.col("punt") == True)) + punt_fair_catch=pl.when( + (pl.col("text").str.contains("(?i)fair catch|(?i)fair caught")).and_(pl.col("punt") == True) + ) .then(True) .otherwise(False), - punt_downed = pl.when((pl.col("text").str.contains("(?i)downed")) - .and_(pl.col("punt") == True)) + punt_downed=pl.when((pl.col("text").str.contains("(?i)downed")).and_(pl.col("punt") == True)) .then(True) .otherwise(False), - punt_safety = pl.when((pl.col("text").str.contains("(?i)punt")) - .and_(pl.col("safety") == True)) + punt_safety=pl.when((pl.col("text").str.contains("(?i)punt")).and_(pl.col("safety") == True)) .then(True) .otherwise(False), - punt_blocked = pl.when((pl.col("text").str.contains("(?i)blocked")) - .and_(pl.col("punt") == True)) + punt_blocked=pl.when((pl.col("text").str.contains("(?i)blocked")).and_(pl.col("punt") == True)) .then(True) .otherwise(False), - penalty_safety = pl.when((pl.col("type.text").is_in(["Penalty"])) - .and_(pl.col("safety") == True)) + penalty_safety=pl.when((pl.col("type.text").is_in(["Penalty"])).and_(pl.col("safety") == True)) .then(True) .otherwise(False), - + ) ) return play_df @@ -822,66 +929,117 @@ def __add_rush_pass_flags(self, play_df): * Rush, Pass, Sacks """ - play_df = play_df.with_columns( - # --- Pass/Rush---- - pl.when((pl.col("type.text") == "Rush") + play_df = ( + play_df.with_columns( + # --- Pass/Rush---- + pl.when( + (pl.col("type.text") == "Rush") .or_(pl.col("type.text") == "Rushing Touchdown") - .or_((pl.col("type.text").is_in(["Safety", "Fumble Recovery (Opponent)", - "Fumble Recovery (Opponent) Touchdown", - "Fumble Recovery (Own)", - "Fumble Recovery (Own) Touchdown", - "Fumble Return Touchdown"])) - .and_(pl.col("text").str.contains("run for")))) - .then(True) - .otherwise(False) - .alias("rush"), - pl.when((pl.col("type.text").is_in(["Pass Reception", "Pass Completion", - "Passing Touchdown", "Sack", "Pass", - "Interception", "Pass Interception Return", - "Interception Return Touchdown", - "Pass Incompletion", "Sack Touchdown", - "Interception Return"])) - .or_((pl.col("type.text") == "Safety") - .and_(pl.col("text").str.contains("sacked"))) - .or_((pl.col("type.text") == "Safety") - .and_(pl.col("text").str.contains("pass complete"))) - .or_((pl.col("type.text") == "Fumble Recovery (Own)") - .and_(pl.col("text").str.contains(r"pass complete|pass incomplete|pass intercepted"))) - .or_((pl.col("type.text") == "Fumble Recovery (Own)") - .and_(pl.col("text").str.contains("sacked"))) - .or_((pl.col("type.text") == "Fumble Recovery (Own) Touchdown") - .and_(pl.col("text").str.contains(r"pass complete|pass incomplete|pass intercepted"))) - .or_((pl.col("type.text") == "Fumble Recovery (Own) Touchdown") - .and_(pl.col("text").str.contains("sacked"))) - .or_((pl.col("type.text") == "Fumble Recovery (Opponent)") - .and_(pl.col("text").str.contains(r"pass complete|pass incomplete|pass intercepted"))) - .or_((pl.col("type.text") == "Fumble Recovery (Opponent)") - .and_(pl.col("text").str.contains("sacked"))) - .or_((pl.col("type.text") == "Fumble Recovery (Opponent) Touchdown") - .and_(pl.col("text").str.contains(r"pass complete|pass incomplete"))) - .or_((pl.col("type.text") == "Fumble Return Touchdown") - .and_(pl.col("text").str.contains(r"pass complete|pass incomplete"))) - .or_((pl.col("type.text") == "Fumble Return Touchdown") - .and_(pl.col("text").str.contains("sacked")))) - .then(True) - .otherwise(False) - .alias("pass") - ).with_columns( - # --- Sacks---- - sack_vec = pl.when((pl.col("type.text").is_in(["Sack", "Sack Touchdown"])) - .or_((pl.col("type.text").is_in([ - "Fumble Recovery (Own)", - "Fumble Recovery (Own) Touchdown", - "Fumble Recovery (Opponent)", - "Fumble Recovery (Opponent) Touchdown", - "Fumble Return Touchdown", - ])) - .and_(pl.col("text").str.contains("(?i)sacked"), - pl.col("pass") == True))) - .then(True) - .otherwise(False), - ).with_columns( - pl.when(pl.col("sack_vec") == True).then(True).otherwise(pl.col("pass")).alias("pass"), + .or_( + ( + pl.col("type.text").is_in( + [ + "Safety", + "Fumble Recovery (Opponent)", + "Fumble Recovery (Opponent) Touchdown", + "Fumble Recovery (Own)", + "Fumble Recovery (Own) Touchdown", + "Fumble Return Touchdown", + ] + ) + ).and_(pl.col("text").str.contains("run for")) + ) + ) + .then(True) + .otherwise(False) + .alias("rush"), + pl.when( + ( + pl.col("type.text").is_in( + [ + "Pass Reception", + "Pass Completion", + "Passing Touchdown", + "Sack", + "Pass", + "Interception", + "Pass Interception Return", + "Interception Return Touchdown", + "Pass Incompletion", + "Sack Touchdown", + "Interception Return", + ] + ) + ) + .or_((pl.col("type.text") == "Safety").and_(pl.col("text").str.contains("sacked"))) + .or_((pl.col("type.text") == "Safety").and_(pl.col("text").str.contains("pass complete"))) + .or_( + (pl.col("type.text") == "Fumble Recovery (Own)").and_( + pl.col("text").str.contains(r"pass complete|pass incomplete|pass intercepted") + ) + ) + .or_((pl.col("type.text") == "Fumble Recovery (Own)").and_(pl.col("text").str.contains("sacked"))) + .or_( + (pl.col("type.text") == "Fumble Recovery (Own) Touchdown").and_( + pl.col("text").str.contains(r"pass complete|pass incomplete|pass intercepted") + ) + ) + .or_( + (pl.col("type.text") == "Fumble Recovery (Own) Touchdown").and_( + pl.col("text").str.contains("sacked") + ) + ) + .or_( + (pl.col("type.text") == "Fumble Recovery (Opponent)").and_( + pl.col("text").str.contains(r"pass complete|pass incomplete|pass intercepted") + ) + ) + .or_( + (pl.col("type.text") == "Fumble Recovery (Opponent)").and_( + pl.col("text").str.contains("sacked") + ) + ) + .or_( + (pl.col("type.text") == "Fumble Recovery (Opponent) Touchdown").and_( + pl.col("text").str.contains(r"pass complete|pass incomplete") + ) + ) + .or_( + (pl.col("type.text") == "Fumble Return Touchdown").and_( + pl.col("text").str.contains(r"pass complete|pass incomplete") + ) + ) + .or_( + (pl.col("type.text") == "Fumble Return Touchdown").and_(pl.col("text").str.contains("sacked")) + ) + ) + .then(True) + .otherwise(False) + .alias("pass"), + ) + .with_columns( + # --- Sacks---- + sack_vec=pl.when( + (pl.col("type.text").is_in(["Sack", "Sack Touchdown"])).or_( + ( + pl.col("type.text").is_in( + [ + "Fumble Recovery (Own)", + "Fumble Recovery (Own) Touchdown", + "Fumble Recovery (Opponent)", + "Fumble Recovery (Opponent) Touchdown", + "Fumble Return Touchdown", + ] + ) + ).and_(pl.col("text").str.contains("(?i)sacked"), pl.col("pass") == True) + ) + ) + .then(True) + .otherwise(False), + ) + .with_columns( + pl.when(pl.col("sack_vec") == True).then(True).otherwise(pl.col("pass")).alias("pass"), + ) ) return play_df @@ -892,149 +1050,178 @@ def __add_team_score_variables(self, play_df): * Team Score variables * Fix change of poss variables """ - play_df = play_df.with_columns( - pos_team = pl.col("start.pos_team.id"), - def_pos_team = pl.col("start.def_pos_team.id"), - ).with_columns( - is_home = pl.col("pos_team") == pl.col("homeTeamId"), - - # --- Team Score variables ------ - lag_homeScore = pl.col("homeScore").shift(1), - lag_awayScore = pl.col("awayScore").shift(1), - ).with_columns( - lag_HA_score_diff = pl.col("lag_homeScore") - pl.col("lag_awayScore"), - HA_score_diff = pl.col("homeScore") - pl.col("awayScore"), - ).with_columns( - net_HA_score_pts = pl.col("HA_score_diff") - pl.col("lag_HA_score_diff"), - H_score_diff = pl.col("homeScore") - pl.col("lag_homeScore"), - A_score_diff = pl.col("awayScore") - pl.col("lag_awayScore"), - ).with_columns( - homeScore = pl.when( - (pl.col("scoringPlay") == False) - & (pl.col("game_play_number") != 1) - & (pl.col("H_score_diff") >= 9)) + play_df = ( + play_df.with_columns( + pos_team=pl.col("start.pos_team.id"), + def_pos_team=pl.col("start.def_pos_team.id"), + ) + .with_columns( + is_home=pl.col("pos_team") == pl.col("homeTeamId"), + # --- Team Score variables ------ + lag_homeScore=pl.col("homeScore").shift(1), + lag_awayScore=pl.col("awayScore").shift(1), + ) + .with_columns( + lag_HA_score_diff=pl.col("lag_homeScore") - pl.col("lag_awayScore"), + HA_score_diff=pl.col("homeScore") - pl.col("awayScore"), + ) + .with_columns( + net_HA_score_pts=pl.col("HA_score_diff") - pl.col("lag_HA_score_diff"), + H_score_diff=pl.col("homeScore") - pl.col("lag_homeScore"), + A_score_diff=pl.col("awayScore") - pl.col("lag_awayScore"), + ) + .with_columns( + homeScore=pl.when( + (pl.col("scoringPlay") == False) + & (pl.col("game_play_number") != 1) + & (pl.col("H_score_diff") >= 9) + ) .then(pl.col("lag_homeScore")) .when( (pl.col("scoringPlay") == False) & (pl.col("game_play_number") != 1) & (pl.col("H_score_diff") < 9) - & (pl.col("H_score_diff") > 1)) + & (pl.col("H_score_diff") > 1) + ) .then(pl.col("lag_homeScore")) .when( (pl.col("scoringPlay") == False) & (pl.col("game_play_number") != 1) & (pl.col("H_score_diff") >= -9) - & (pl.col("H_score_diff") < -1)) + & (pl.col("H_score_diff") < -1) + ) .then(pl.col("homeScore")) .otherwise(pl.col("homeScore")), - awayScore = pl.when( - (pl.col("scoringPlay") == False) - & (pl.col("game_play_number") != 1) - & (pl.col("A_score_diff") >= 9)) + awayScore=pl.when( + (pl.col("scoringPlay") == False) + & (pl.col("game_play_number") != 1) + & (pl.col("A_score_diff") >= 9) + ) .then(pl.col("lag_awayScore")) .when( (pl.col("scoringPlay") == False) & (pl.col("game_play_number") != 1) & (pl.col("A_score_diff") < 9) - & (pl.col("A_score_diff") > 1)) + & (pl.col("A_score_diff") > 1) + ) .then(pl.col("lag_awayScore")) .when( (pl.col("scoringPlay") == False) & (pl.col("game_play_number") != 1) & (pl.col("A_score_diff") >= -9) - & (pl.col("A_score_diff") < -1)) + & (pl.col("A_score_diff") < -1) + ) .then(pl.col("awayScore")) .otherwise(pl.col("awayScore")), - - - ).drop( - ["lag_homeScore", "lag_awayScore"] - ).with_columns( - lag_homeScore = pl.col("homeScore").shift(1), - lag_awayScore = pl.col("awayScore").shift(1), - ).with_columns( - lag_homeScore = pl.when(pl.col("lag_homeScore").is_null()).then(0).otherwise(pl.col("lag_homeScore")), - lag_awayScore = pl.when(pl.col("lag_awayScore").is_null()).then(0).otherwise(pl.col("lag_awayScore")), - ).with_columns( - pl.when(pl.col("game_play_number") == 1).then(0).otherwise(pl.col("lag_homeScore")).alias("start.homeScore"), - pl.when(pl.col("game_play_number") == 1).then(0).otherwise(pl.col("lag_awayScore")).alias("start.awayScore"), - pl.col("homeScore").alias("end.homeScore"), - pl.col("awayScore").alias("end.awayScore"), - pl.when(pl.col("pos_team") == pl.col("homeTeamId")) + ) + .drop(["lag_homeScore", "lag_awayScore"]) + .with_columns( + lag_homeScore=pl.col("homeScore").shift(1), + lag_awayScore=pl.col("awayScore").shift(1), + ) + .with_columns( + lag_homeScore=pl.when(pl.col("lag_homeScore").is_null()).then(0).otherwise(pl.col("lag_homeScore")), + lag_awayScore=pl.when(pl.col("lag_awayScore").is_null()).then(0).otherwise(pl.col("lag_awayScore")), + ) + .with_columns( + pl.when(pl.col("game_play_number") == 1) + .then(0) + .otherwise(pl.col("lag_homeScore")) + .alias("start.homeScore"), + pl.when(pl.col("game_play_number") == 1) + .then(0) + .otherwise(pl.col("lag_awayScore")) + .alias("start.awayScore"), + pl.col("homeScore").alias("end.homeScore"), + pl.col("awayScore").alias("end.awayScore"), + pl.when(pl.col("pos_team") == pl.col("homeTeamId")) .then(pl.col("homeScore")) .otherwise(pl.col("awayScore")) .alias("pos_team_score"), - pl.when(pl.col("pos_team") == pl.col("homeTeamId")) + pl.when(pl.col("pos_team") == pl.col("homeTeamId")) .then(pl.col("awayScore")) .otherwise(pl.col("homeScore")) .alias("def_pos_team_score"), - - ).with_columns( - pl.when(pl.col("start.pos_team.id") == pl.col("homeTeamId")) + ) + .with_columns( + pl.when(pl.col("start.pos_team.id") == pl.col("homeTeamId")) .then(pl.col("start.homeScore")) .otherwise(pl.col("start.awayScore")) .alias("start.pos_team_score"), - pl.when(pl.col("start.pos_team.id") == pl.col("homeTeamId")) + pl.when(pl.col("start.pos_team.id") == pl.col("homeTeamId")) .then(pl.col("start.awayScore")) .otherwise(pl.col("start.homeScore")) .alias("start.def_pos_team_score"), - ).with_columns( - (pl.col("start.pos_team_score") - pl.col("start.def_pos_team_score")).alias("start.pos_score_diff"), - pl.when(pl.col("pos_team") == pl.col("homeTeamId")) + ) + .with_columns( + (pl.col("start.pos_team_score") - pl.col("start.def_pos_team_score")).alias("start.pos_score_diff"), + pl.when(pl.col("pos_team") == pl.col("homeTeamId")) .then(pl.col("end.homeScore")) .otherwise(pl.col("end.awayScore")) .alias("end.pos_team_score"), - pl.when(pl.col("pos_team") == pl.col("homeTeamId")) + pl.when(pl.col("pos_team") == pl.col("homeTeamId")) .then(pl.col("end.awayScore")) .otherwise(pl.col("end.homeScore")) .alias("end.def_pos_team_score"), - ).with_columns( - (pl.col("end.pos_team_score") - pl.col("end.def_pos_team_score")).alias("end.pos_score_diff"), - pl.col("pos_team").shift(1).alias("lag_pos_team") - ).with_columns( - pl.when(pl.col("lag_pos_team").is_null()).then(pl.col("pos_team")).otherwise(pl.col("lag_pos_team")).alias("lag_pos_team"), - pl.col("pos_team").shift(-1).alias("lead_pos_team"), - pl.col("pos_team").shift(-2).alias("lead_pos_team2"), - (pl.col("pos_team_score") - pl.col("def_pos_team_score")).alias("pos_score_diff"), - ).with_columns( - pl.col("pos_score_diff").shift(1).alias("lag_pos_score_diff"), - ).with_columns( - pl.when(pl.col("lag_pos_score_diff").is_null()).then(0).otherwise(pl.col("lag_pos_score_diff")).alias("lag_pos_score_diff"), - ).with_columns( - pl.when(pl.col("lag_pos_team") == pl.col("pos_team")) + ) + .with_columns( + (pl.col("end.pos_team_score") - pl.col("end.def_pos_team_score")).alias("end.pos_score_diff"), + pl.col("pos_team").shift(1).alias("lag_pos_team"), + ) + .with_columns( + pl.when(pl.col("lag_pos_team").is_null()) + .then(pl.col("pos_team")) + .otherwise(pl.col("lag_pos_team")) + .alias("lag_pos_team"), + pl.col("pos_team").shift(-1).alias("lead_pos_team"), + pl.col("pos_team").shift(-2).alias("lead_pos_team2"), + (pl.col("pos_team_score") - pl.col("def_pos_team_score")).alias("pos_score_diff"), + ) + .with_columns( + pl.col("pos_score_diff").shift(1).alias("lag_pos_score_diff"), + ) + .with_columns( + pl.when(pl.col("lag_pos_score_diff").is_null()) + .then(0) + .otherwise(pl.col("lag_pos_score_diff")) + .alias("lag_pos_score_diff"), + ) + .with_columns( + pl.when(pl.col("lag_pos_team") == pl.col("pos_team")) .then(pl.col("pos_score_diff") - pl.col("lag_pos_score_diff")) .otherwise(pl.col("pos_score_diff") + pl.col("lag_pos_score_diff")) .alias("pos_score_pts"), - pl.when((pl.col("kickoff_play") == True) - .and_(pl.col("lag_pos_team") == pl.col("pos_team"))) + pl.when((pl.col("kickoff_play") == True).and_(pl.col("lag_pos_team") == pl.col("pos_team"))) .then(pl.col("lag_pos_score_diff")) - .when((pl.col("kickoff_play") == True) - .or_(pl.col("lag_pos_team") != pl.col("pos_team"))) + .when((pl.col("kickoff_play") == True).or_(pl.col("lag_pos_team") != pl.col("pos_team"))) .then(-1 * pl.col("lag_pos_score_diff")) .otherwise(pl.col("lag_pos_score_diff")) .alias("pos_score_diff_start"), - ).with_columns( - pl.when(pl.col("pos_score_diff_start").is_null() == True) + ) + .with_columns( + pl.when(pl.col("pos_score_diff_start").is_null() == True) .then(pl.col("pos_score_diff")) .otherwise(pl.col("pos_score_diff_start")) .alias("pos_score_diff_start"), - pl.when(pl.col("start.pos_team.id") == pl.col("firstHalfKickoffTeamId")) + pl.when(pl.col("start.pos_team.id") == pl.col("firstHalfKickoffTeamId")) .then(True) .otherwise(False) .alias("start.pos_team_receives_2H_kickoff"), - pl.when(pl.col("end.pos_team.id") == pl.col("firstHalfKickoffTeamId")) + pl.when(pl.col("end.pos_team.id") == pl.col("firstHalfKickoffTeamId")) .then(True) .otherwise(False) .alias("end.pos_team_receives_2H_kickoff"), - pl.when(pl.col("start.pos_team.id") == pl.col("end.pos_team.id")) + pl.when(pl.col("start.pos_team.id") == pl.col("end.pos_team.id")) .then(False) .otherwise(True) .alias("change_of_poss"), - ).with_columns( - pl.when(pl.col("change_of_poss").is_null() == True) + ) + .with_columns( + pl.when(pl.col("change_of_poss").is_null() == True) .then(False) .otherwise(pl.col("change_of_poss")) .alias("change_of_poss"), + ) ) return play_df @@ -1045,269 +1232,343 @@ def __add_new_play_types(self, play_df): * Fix play types """ # -------------------------------------------------- - play_df = play_df.with_columns( - # --- Fix Strip Sacks to Fumbles ---- - pl.when((pl.col("fumble_vec") == True) - .and_(pl.col("pass") == True) - .and_(pl.col("change_of_poss") == 1) - .and_(pl.col("td_play") == False) - .and_(pl.col("start.down") != 4) - .and_(pl.col("type.text").is_in(defense_score_vec) == False)) + play_df = ( + play_df.with_columns( + # --- Fix Strip Sacks to Fumbles ---- + pl.when( + (pl.col("fumble_vec") == True) + .and_(pl.col("pass") == True) + .and_(pl.col("change_of_poss") == 1) + .and_(pl.col("td_play") == False) + .and_(pl.col("start.down") != 4) + .and_(pl.col("type.text").is_in(defense_score_vec) == False) + ) .then("Fumble Recovery (Opponent)") .otherwise(pl.col("type.text")) .alias("type.text"), - ).with_columns( - pl.when((pl.col("fumble_vec") == True) - .and_(pl.col("pass") == True) - .and_(pl.col("change_of_poss") == 1) - .and_(pl.col("td_play") == True)) + ) + .with_columns( + pl.when( + (pl.col("fumble_vec") == True) + .and_(pl.col("pass") == True) + .and_(pl.col("change_of_poss") == 1) + .and_(pl.col("td_play") == True) + ) .then("Fumble Recovery (Opponent) Touchdown") .otherwise(pl.col("type.text")) .alias("type.text"), - ).with_columns( - # --- Fix rushes with fumbles and a change of possession to fumbles---- - pl.when((pl.col("fumble_vec") == True) - .and_(pl.col("rush") == True) - .and_(pl.col("change_of_poss") == 1) - .and_(pl.col("td_play") == False) - .and_(pl.col("start.down") != 4) - .and_(pl.col("type.text").is_in(defense_score_vec) == False)) + ) + .with_columns( + # --- Fix rushes with fumbles and a change of possession to fumbles---- + pl.when( + (pl.col("fumble_vec") == True) + .and_(pl.col("rush") == True) + .and_(pl.col("change_of_poss") == 1) + .and_(pl.col("td_play") == False) + .and_(pl.col("start.down") != 4) + .and_(pl.col("type.text").is_in(defense_score_vec) == False) + ) .then("Fumble Recovery (Opponent)") .otherwise(pl.col("type.text")) .alias("type.text"), - ).with_columns( - pl.when((pl.col("fumble_vec") == True) - .and_(pl.col("rush") == True) - .and_(pl.col("change_of_poss") == 1) - .and_(pl.col("td_play") == True)) + ) + .with_columns( + pl.when( + (pl.col("fumble_vec") == True) + .and_(pl.col("rush") == True) + .and_(pl.col("change_of_poss") == 1) + .and_(pl.col("td_play") == True) + ) .then("Fumble Recovery (Opponent) Touchdown") .otherwise(pl.col("type.text")) .alias("type.text"), - ).with_columns( - # -- Fix kickoff fumble return TDs ---- - pl.when((pl.col("kickoff_play") == True) - .and_(pl.col("change_of_poss") == 1) - .and_(pl.col("td_play") == True) - .and_(pl.col("td_check") == True)) + ) + .with_columns( + # -- Fix kickoff fumble return TDs ---- + pl.when( + (pl.col("kickoff_play") == True) + .and_(pl.col("change_of_poss") == 1) + .and_(pl.col("td_play") == True) + .and_(pl.col("td_check") == True) + ) .then("Kickoff Return Touchdown") .otherwise(pl.col("type.text")) .alias("type.text"), - ).with_columns( - # -- Fix punt return TDs ---- - pl.when((pl.col("punt_play") == True) - .and_(pl.col("td_play") == True) - .and_(pl.col("td_check") == True)) + ) + .with_columns( + # -- Fix punt return TDs ---- + pl.when((pl.col("punt_play") == True).and_(pl.col("td_play") == True).and_(pl.col("td_check") == True)) .then("Punt Return Touchdown") .otherwise(pl.col("type.text")) .alias("type.text"), - ).with_columns( - # -- Fix kick return TDs ---- - pl.when((pl.col("kickoff_play") == True) - .and_(pl.col("fumble_vec") == False) - .and_(pl.col("td_play") == True) - .and_(pl.col("td_check") == True)) + ) + .with_columns( + # -- Fix kick return TDs ---- + pl.when( + (pl.col("kickoff_play") == True) + .and_(pl.col("fumble_vec") == False) + .and_(pl.col("td_play") == True) + .and_(pl.col("td_check") == True) + ) .then("Kickoff Return Touchdown") .otherwise(pl.col("type.text")) .alias("type.text"), - ).with_columns( - # -- Fix rush/pass tds that aren't explicit---- - pl.when((pl.col("td_play") == True) - .and_(pl.col("rush") == True) - .and_(pl.col("fumble_vec") == False) - .and_(pl.col("td_check") == True)) + ) + .with_columns( + # -- Fix rush/pass tds that aren't explicit---- + pl.when( + (pl.col("td_play") == True) + .and_(pl.col("rush") == True) + .and_(pl.col("fumble_vec") == False) + .and_(pl.col("td_check") == True) + ) .then("Rushing Touchdown") .otherwise(pl.col("type.text")) .alias("type.text"), - ).with_columns( - pl.when((pl.col("td_play") == True) - .and_(pl.col("pass") == True) - .and_(pl.col("fumble_vec") == False) - .and_(pl.col("td_check") == True) - .and_(pl.col("type.text").is_in(int_vec) == False)) + ) + .with_columns( + pl.when( + (pl.col("td_play") == True) + .and_(pl.col("pass") == True) + .and_(pl.col("fumble_vec") == False) + .and_(pl.col("td_check") == True) + .and_(pl.col("type.text").is_in(int_vec) == False) + ) .then("Passing Touchdown") .otherwise(pl.col("type.text")) .alias("type.text"), - ).with_columns( - pl.when((pl.col("pass") == True) - .and_(pl.col("type.text").is_in(["Pass Reception", "Pass Completion", "Pass"])) - .and_(pl.col("statYardage") == pl.col("start.yardsToEndzone")) - .and_(pl.col("fumble_vec") == False) - .and_(pl.col("type.text").is_in(int_vec) == False)) + ) + .with_columns( + pl.when( + (pl.col("pass") == True) + .and_(pl.col("type.text").is_in(["Pass Reception", "Pass Completion", "Pass"])) + .and_(pl.col("statYardage") == pl.col("start.yardsToEndzone")) + .and_(pl.col("fumble_vec") == False) + .and_(pl.col("type.text").is_in(int_vec) == False) + ) .then("Passing Touchdown") .otherwise(pl.col("type.text")) .alias("type.text"), - ).with_columns( - pl.when((pl.col("type.text").is_in(["Blocked Field Goal"])) - .and_(pl.col("text").str.contains("(?i)for a TD"))) + ) + .with_columns( + pl.when( + (pl.col("type.text").is_in(["Blocked Field Goal"])).and_( + pl.col("text").str.contains("(?i)for a TD") + ) + ) .then("Blocked Field Goal Touchdown") .otherwise(pl.col("type.text")) .alias("type.text"), - ).with_columns( - pl.when((pl.col("type.text").is_in(["Blocked Punt"])) - .and_(pl.col("text").str.contains("(?i)for a TD"))) + ) + .with_columns( + pl.when( + (pl.col("type.text").is_in(["Blocked Punt"])).and_(pl.col("text").str.contains("(?i)for a TD")) + ) .then("Blocked Punt Touchdown") .otherwise(pl.col("type.text")) .alias("type.text"), - ).with_columns( - # -- Fix duplicated TD play_type labels---- - pl.col("type.text").str.replace(r"(?i)Touchdown Touchdown", "Touchdown") + ) + .with_columns( + # -- Fix duplicated TD play_type labels---- + pl.col("type.text") + .str.replace(r"(?i)Touchdown Touchdown", "Touchdown") .alias("type.text") - ).with_columns( - #-- Fix Pass Interception Return TD play_type labels---- - pl.when(pl.col("text").str.contains("(?i)pass intercepted for a TD")) + ) + .with_columns( + # -- Fix Pass Interception Return TD play_type labels---- + pl.when(pl.col("text").str.contains("(?i)pass intercepted for a TD")) .then("Interception Return Touchdown") .otherwise(pl.col("type.text")) .alias("type.text"), - ).with_columns( - #-- Fix Sack/Fumbles Touchdown play_type labels---- - pl.when((pl.col("text").str.contains("(?i)sacked")) - .and_(pl.col("text").str.contains("(?i)fumbled")) - .and_(pl.col("text").str.contains("(?i)TD"))) + ) + .with_columns( + # -- Fix Sack/Fumbles Touchdown play_type labels---- + pl.when( + (pl.col("text").str.contains("(?i)sacked")) + .and_(pl.col("text").str.contains("(?i)fumbled")) + .and_(pl.col("text").str.contains("(?i)TD")) + ) .then("Fumble Recovery (Opponent) Touchdown") .otherwise(pl.col("type.text")) .alias("type.text"), - ).with_columns( - #-- Fix generic pass plays ---- - ##-- first one looks for complete pass - pl.when((pl.col("type.text") == "Pass") - .and_(pl.col("text").str.contains("(?i)pass complete"))) + ) + .with_columns( + # -- Fix generic pass plays ---- + ##-- first one looks for complete pass + pl.when((pl.col("type.text") == "Pass").and_(pl.col("text").str.contains("(?i)pass complete"))) .then("Pass Completion") .otherwise(pl.col("type.text")) .alias("type.text"), - ).with_columns( - ##-- second one looks for incomplete pass - pl.when((pl.col("type.text") == "Pass") - .and_(pl.col("text").str.contains("(?i)pass incomplete"))) + ) + .with_columns( + ##-- second one looks for incomplete pass + pl.when((pl.col("type.text") == "Pass").and_(pl.col("text").str.contains("(?i)pass incomplete"))) .then("Pass Incompletion") .otherwise(pl.col("type.text")) .alias("type.text"), - ).with_columns( - ##-- third one looks for interceptions - pl.when((pl.col("type.text") == "Pass") - .and_(pl.col("text").str.contains("(?i)pass intercepted"))) + ) + .with_columns( + ##-- third one looks for interceptions + pl.when((pl.col("type.text") == "Pass").and_(pl.col("text").str.contains("(?i)pass intercepted"))) .then("Pass Interception") .otherwise(pl.col("type.text")) .alias("type.text"), - ).with_columns( - ##-- fourth one looks for sacked - pl.when((pl.col("type.text") == "Pass") - .and_(pl.col("text").str.contains("(?i)sacked"))) + ) + .with_columns( + ##-- fourth one looks for sacked + pl.when((pl.col("type.text") == "Pass").and_(pl.col("text").str.contains("(?i)sacked"))) .then("Sack") .otherwise(pl.col("type.text")) .alias("type.text"), - ).with_columns( - ##-- fifth one play type is Passing Touchdown, but its intercepted - pl.when((pl.col("type.text") == "Passing Touchdown") - .and_(pl.col("text").str.contains("(?i)pass intercepted for a TD"))) + ) + .with_columns( + ##-- fifth one play type is Passing Touchdown, but its intercepted + pl.when( + (pl.col("type.text") == "Passing Touchdown").and_( + pl.col("text").str.contains("(?i)pass intercepted for a TD") + ) + ) .then("Interception Return Touchdown") .otherwise(pl.col("type.text")) .alias("type.text"), - ).with_columns( - # --- Moving non-Touchdown pass interceptions to one play_type: "Interception Return" ----- - pl.when(pl.col("type.text").is_in(["Interception", "Pass Interception", "Pass Interception Return"])) + ) + .with_columns( + # --- Moving non-Touchdown pass interceptions to one play_type: "Interception Return" ----- + pl.when(pl.col("type.text").is_in(["Interception", "Pass Interception", "Pass Interception Return"])) .then("Interception Return") .otherwise(pl.col("type.text")) .alias("type.text"), - ).with_columns( - # --- Moving Kickoff/Punt Touchdowns without fumbles to Kickoff/Punt Return Touchdown - pl.when((pl.col("type.text") == "Kickoff Touchdown") - .and_(pl.col("fumble_vec") == False)) + ) + .with_columns( + # --- Moving Kickoff/Punt Touchdowns without fumbles to Kickoff/Punt Return Touchdown + pl.when((pl.col("type.text") == "Kickoff Touchdown").and_(pl.col("fumble_vec") == False)) .then("Kickoff Return Touchdown") .otherwise(pl.col("type.text")) .alias("type.text"), - ).with_columns( - pl.when((pl.col("type.text") == "Kickoff") + ) + .with_columns( + pl.when( + (pl.col("type.text") == "Kickoff") .and_(pl.col("td_play") == True) - .and_(pl.col("fumble_vec") == False)) + .and_(pl.col("fumble_vec") == False) + ) .then("Kickoff Return Touchdown") - .when((pl.col("type.text") == "Kickoff") + .when( + (pl.col("type.text") == "Kickoff") .and_(pl.col("text").str.contains("(?i)for a TD")) - .and_(pl.col("fumble_vec") == False)) + .and_(pl.col("fumble_vec") == False) + ) .then("Kickoff Return Touchdown") .otherwise(pl.col("type.text")) .alias("type.text"), - ).with_columns( - pl.when((pl.col("type.text").is_in(["Kickoff", "Kickoff Return (Offense)"])) + ) + .with_columns( + pl.when( + (pl.col("type.text").is_in(["Kickoff", "Kickoff Return (Offense)"])) .and_(pl.col("fumble_vec") == True) - .and_(pl.col("change_of_poss") == 1)) + .and_(pl.col("change_of_poss") == 1) + ) .then("Kickoff Team Fumble Recovery") .otherwise(pl.col("type.text")) .alias("type.text"), - ).with_columns( - pl.when((pl.col("type.text") == "Punt Touchdown") + ) + .with_columns( + pl.when( + (pl.col("type.text") == "Punt Touchdown") .and_(pl.col("fumble_vec") == False) - .and_(pl.col("change_of_poss") == 1)) + .and_(pl.col("change_of_poss") == 1) + ) .then("Punt Return Touchdown") - .when((pl.col("type.text") == "Punt") + .when( + (pl.col("type.text") == "Punt") .and_(pl.col("text").str.contains("(?i)for a TD")) - .and_(pl.col("change_of_poss") == 1)) + .and_(pl.col("change_of_poss") == 1) + ) .then("Punt Return Touchdown") .otherwise(pl.col("type.text")) .alias("type.text"), - ).with_columns( - pl.when((pl.col("type.text") == "Punt") + ) + .with_columns( + pl.when( + (pl.col("type.text") == "Punt") .and_(pl.col("fumble_vec") == True) - .and_(pl.col("change_of_poss") == 0)) + .and_(pl.col("change_of_poss") == 0) + ) .then("Punt Team Fumble Recovery") .otherwise(pl.col("type.text")) .alias("type.text"), - ).with_columns( - pl.when(pl.col("type.text").is_in(["Punt Touchdown"])) + ) + .with_columns( + pl.when(pl.col("type.text").is_in(["Punt Touchdown"])) .then("Punt Team Fumble Recovery Touchdown") - .when((pl.col("scoringPlay") == True) + .when( + (pl.col("scoringPlay") == True) .and_(pl.col("punt_play") == True) - .and_(pl.col("change_of_poss") == 0)) + .and_(pl.col("change_of_poss") == 0) + ) .then("Punt Team Fumble Recovery Touchdown") .otherwise(pl.col("type.text")) .alias("type.text"), - ).with_columns( - pl.when(pl.col("type.text").is_in(["Kickoff Touchdown"])) + ) + .with_columns( + pl.when(pl.col("type.text").is_in(["Kickoff Touchdown"])) .then("Kickoff Team Fumble Recovery Touchdown") .otherwise(pl.col("type.text")) .alias("type.text"), - ).with_columns( - pl.when((pl.col("type.text").is_in(["Fumble Return Touchdown"])) - .and_((pl.col("pass") == True) - .or_(pl.col("rush") == True))) + ) + .with_columns( + pl.when( + (pl.col("type.text").is_in(["Fumble Return Touchdown"])).and_( + (pl.col("pass") == True).or_(pl.col("rush") == True) + ) + ) .then("Fumble Recovery (Opponent) Touchdown") .otherwise(pl.col("type.text")) .alias("type.text"), - ).with_columns( - # --- Safeties (kickoff, punt, penalty) ---- - pl.when((pl.col("type.text").is_in(["Pass Reception", "Rush", "Rushing Touchdown"])) - .and_((pl.col("pass") == True) - .or_(pl.col("rush") == True)) - .and_(pl.col("safety") == True)) + ) + .with_columns( + # --- Safeties (kickoff, punt, penalty) ---- + pl.when( + (pl.col("type.text").is_in(["Pass Reception", "Rush", "Rushing Touchdown"])) + .and_((pl.col("pass") == True).or_(pl.col("rush") == True)) + .and_(pl.col("safety") == True) + ) .then("Safety") .otherwise(pl.col("type.text")) .alias("type.text"), - ).with_columns( - pl.when(pl.col("kickoff_safety") == True) + ) + .with_columns( + pl.when(pl.col("kickoff_safety") == True) .then("Kickoff (Safety)") .otherwise(pl.col("type.text")) .alias("type.text"), - ).with_columns( - pl.when(pl.col("punt_safety") == True) + ) + .with_columns( + pl.when(pl.col("punt_safety") == True) .then("Punt (Safety)") .otherwise(pl.col("type.text")) .alias("type.text"), - ).with_columns( - pl.when(pl.col("penalty_safety") == True) + ) + .with_columns( + pl.when(pl.col("penalty_safety") == True) .then("Penalty (Safety)") .otherwise(pl.col("type.text")) .alias("type.text"), - ).with_columns( - pl.when((pl.col("type.text") == "Extra Point Good") - .and_(pl.col("text").str.contains("(?i)Two-Point"))) + ) + .with_columns( + pl.when((pl.col("type.text") == "Extra Point Good").and_(pl.col("text").str.contains("(?i)Two-Point"))) .then("Two-Point Conversion Good") .otherwise(pl.col("type.text")) .alias("type.text"), - ).with_columns( - pl.when((pl.col("type.text") == "Extra Point Missed") - .and_(pl.col("text").str.contains("(?i)Two-Point"))) + ) + .with_columns( + pl.when( + (pl.col("type.text") == "Extra Point Missed").and_(pl.col("text").str.contains("(?i)Two-Point")) + ) .then("Two-Point Conversion Missed") .otherwise(pl.col("type.text")) .alias("type.text"), + ) ) return play_df @@ -1324,50 +1585,58 @@ def __setup_penalty_data(self, play_df): * Yds Penalty """ ##-- 'Penalty' in play text ---- - play_df = play_df.with_columns( - # -- T/F flag conditions penalty_flag - penalty_flag = pl.when((pl.col("type.text") == "Penalty") - .or_(pl.col("text").str.contains("(?i)penalty"))) + play_df = ( + play_df.with_columns( + # -- T/F flag conditions penalty_flag + penalty_flag=pl.when( + (pl.col("type.text") == "Penalty").or_(pl.col("text").str.contains("(?i)penalty")) + ) .then(True) .otherwise(False), - # -- T/F flag conditions penalty_declined - penalty_declined = pl.when((pl.col("type.text") == "Penalty") - .and_(pl.col("text").str.contains("(?i)declined"))) + # -- T/F flag conditions penalty_declined + penalty_declined=pl.when( + (pl.col("type.text") == "Penalty").and_(pl.col("text").str.contains("(?i)declined")) + ) .then(True) .otherwise(False), - # -- T/F flag conditions penalty_no_play - penalty_no_play = pl.when((pl.col("type.text") == "Penalty") - .and_(pl.col("text").str.contains("(?i)no play"))) + # -- T/F flag conditions penalty_no_play + penalty_no_play=pl.when( + (pl.col("type.text") == "Penalty").and_(pl.col("text").str.contains("(?i)no play")) + ) .then(True) .otherwise(False), - - # -- T/F flag conditions penalty_offset - penalty_offset = pl.when((pl.col("type.text") == "Penalty") - .and_(pl.col("text").str.contains("(?i)off-setting"))) + # -- T/F flag conditions penalty_offset + penalty_offset=pl.when( + (pl.col("type.text") == "Penalty").and_(pl.col("text").str.contains("(?i)off-setting")) + ) .then(True) - .when((pl.col("text").str.contains("(?i)penalty")) - .and_(pl.col("text").str.contains("(?i)off-setting"))) + .when( + (pl.col("text").str.contains("(?i)penalty")).and_(pl.col("text").str.contains("(?i)off-setting")) + ) .then(True) .otherwise(False), - # -- T/F flag conditions penalty_1st_conv - penalty_1st_conv = pl.when((pl.col("type.text") == "Penalty") - .and_(pl.col("text").str.contains("(?i)1st down"))) + # -- T/F flag conditions penalty_1st_conv + penalty_1st_conv=pl.when( + (pl.col("type.text") == "Penalty").and_(pl.col("text").str.contains("(?i)1st down")) + ) .then(True) - .when((pl.col("text").str.contains("(?i)penalty")) - .and_(pl.col("text").str.contains("(?i)1st down"))) + .when((pl.col("text").str.contains("(?i)penalty")).and_(pl.col("text").str.contains("(?i)1st down"))) .then(True) .otherwise(False), - # -- T/F flag for penalty text but not penalty play type -- - penalty_in_text = pl.when((pl.col("text").str.contains("(?i)penalty")) - .and_(pl.col("type.text") != "Penalty", + # -- T/F flag for penalty text but not penalty play type -- + penalty_in_text=pl.when( + (pl.col("text").str.contains("(?i)penalty")).and_( + pl.col("type.text") != "Penalty", pl.col("text").str.contains("(?i)declined") == False, pl.col("text").str.contains("(?i)off-setting") == False, - pl.col("text").str.contains("(?i)no play") == False)) + pl.col("text").str.contains("(?i)no play") == False, + ) + ) .then(True) .otherwise(False), - - ).with_columns( - penalty_detail = pl.when(pl.col("penalty_offset") == 1) + ) + .with_columns( + penalty_detail=pl.when(pl.col("penalty_offset") == 1) .then("Offsetting") .when(pl.col("penalty_declined") == 1) .then("Declined") @@ -1483,140 +1752,203 @@ def __setup_penalty_data(self, play_df): .then("Player Disqualification") .when(pl.col("penalty_flag") == True) .then("Missing") - ).with_columns( - penalty_text = pl.when(pl.col("penalty_flag") == True) + ) + .with_columns( + penalty_text=pl.when(pl.col("penalty_flag") == True) .then(pl.col("text").str.extract(r"(?i)Penalty(.+)", 1)) .otherwise(None), - ).with_columns( - yds_penalty = pl.when(pl.col("penalty_flag") == True) - .then(pl.col("penalty_text").str.extract(r"(?i)(.{0,3}) yards|(?i)yds|(?i)yd to the", 1) - .str.replace(" yards to the | yds to the | yd to the ", "")) + ) + .with_columns( + yds_penalty=pl.when(pl.col("penalty_flag") == True) + .then( + pl.col("penalty_text") + .str.extract(r"(?i)(.{0,3}) yards|(?i)yds|(?i)yd to the", 1) + .str.replace(" yards to the | yds to the | yd to the ", "") + ) .otherwise(None), - - ).with_columns( - yds_penalty = pl.when((pl.col("penalty_flag") == True) - .and_(pl.col("yds_penalty").is_null(), - pl.col("text").str.contains(r"(?i)ards\)"))) - .then(pl.col("text").str.extract(r"(.{0,4})yards\)|Yards\)|yds\)|Yds\)", 1) - .str.replace("yards\\)|Yards\\)|yds\\)|Yds\\)", "").str.replace("\\(", "")) + ) + .with_columns( + yds_penalty=pl.when( + (pl.col("penalty_flag") == True).and_( + pl.col("yds_penalty").is_null(), pl.col("text").str.contains(r"(?i)ards\)") + ) + ) + .then( + pl.col("text") + .str.extract(r"(.{0,4})yards\)|Yards\)|yds\)|Yds\)", 1) + .str.replace("yards\\)|Yards\\)|yds\\)|Yds\\)", "") + .str.replace("\\(", "") + ) .otherwise(pl.col("yds_penalty")), + ) ) return play_df def __add_play_category_flags(self, play_df): - play_df = play_df.with_columns( - # --- Sacks ----- - sack = pl.when(pl.col("type.text").is_in(["Sack"])) + play_df = ( + play_df.with_columns( + # --- Sacks ----- + sack=pl.when(pl.col("type.text").is_in(["Sack"])) .then(True) - .when((pl.col("type.text").is_in(["Fumble Recovery (Opponent)", - "Fumble Recovery (Opponent) Touchdown", - "Fumble Recovery (Own)", - "Fumble Recovery (Own) Touchdown"])) - .and_(pl.col("pass") == True) - .and_(pl.col("text").str.contains("(?i)sacked"))) + .when( + ( + pl.col("type.text").is_in( + [ + "Fumble Recovery (Opponent)", + "Fumble Recovery (Opponent) Touchdown", + "Fumble Recovery (Own)", + "Fumble Recovery (Own) Touchdown", + ] + ) + ) + .and_(pl.col("pass") == True) + .and_(pl.col("text").str.contains("(?i)sacked")) + ) .then(True) - .when((pl.col("type.text").is_in(["Safety"])) - .and_(pl.col("text").str.contains("(?i)sacked"))) + .when((pl.col("type.text").is_in(["Safety"])).and_(pl.col("text").str.contains("(?i)sacked"))) .then(True) .otherwise(False), - # --- Interceptions ------ - int = pl.col("type.text").is_in(["Interception Return", "Interception Return Touchdown"]), - int_td = pl.col("type.text").is_in(["Interception Return Touchdown"]), - # --- Pass Completions, Attempts and Targets ------- - completion = pl.when(pl.col("type.text").is_in(["Pass Reception", "Pass Completion", "Passing Touchdown"])) + # --- Interceptions ------ + int=pl.col("type.text").is_in(["Interception Return", "Interception Return Touchdown"]), + int_td=pl.col("type.text").is_in(["Interception Return Touchdown"]), + # --- Pass Completions, Attempts and Targets ------- + completion=pl.when( + pl.col("type.text").is_in(["Pass Reception", "Pass Completion", "Passing Touchdown"]) + ) .then(True) - .when((pl.col("type.text").is_in(["Fumble Recovery (Opponent)", - "Fumble Recovery (Opponent) Touchdown", - "Fumble Recovery (Own)", - "Fumble Recovery (Own) Touchdown"])) - .and_(pl.col("pass") == True) - .and_(pl.col("text").str.contains("(?i)sacked") == False)) + .when( + ( + pl.col("type.text").is_in( + [ + "Fumble Recovery (Opponent)", + "Fumble Recovery (Opponent) Touchdown", + "Fumble Recovery (Own)", + "Fumble Recovery (Own) Touchdown", + ] + ) + ) + .and_(pl.col("pass") == True) + .and_(pl.col("text").str.contains("(?i)sacked") == False) + ) .then(True) .otherwise(False), - pass_attempt = pl.when(pl.col("type.text").is_in(["Pass Reception", "Pass Completion", "Passing Touchdown", "Pass Incompletion"])) + pass_attempt=pl.when( + pl.col("type.text").is_in( + ["Pass Reception", "Pass Completion", "Passing Touchdown", "Pass Incompletion"] + ) + ) .then(True) - .when((pl.col("type.text").is_in(["Fumble Recovery (Opponent)", - "Fumble Recovery (Opponent) Touchdown", - "Fumble Recovery (Own)", - "Fumble Recovery (Own) Touchdown"])) - .and_(pl.col("pass") == True) - .and_(pl.col("text").str.contains("(?i)sacked") == False)) + .when( + ( + pl.col("type.text").is_in( + [ + "Fumble Recovery (Opponent)", + "Fumble Recovery (Opponent) Touchdown", + "Fumble Recovery (Own)", + "Fumble Recovery (Own) Touchdown", + ] + ) + ) + .and_(pl.col("pass") == True) + .and_(pl.col("text").str.contains("(?i)sacked") == False) + ) .then(True) - .when((pl.col("pass") == True) - .and_(pl.col("text").str.contains("(?i)sacked") == False)) + .when((pl.col("pass") == True).and_(pl.col("text").str.contains("(?i)sacked") == False)) .then(True) .otherwise(False), - target = pl.when(pl.col("type.text").is_in(["Pass Reception", "Pass Completion", "Passing Touchdown", "Pass Incompletion"])) - .then(True) - .when((pl.col("type.text").is_in(["Fumble Recovery (Opponent)", - "Fumble Recovery (Opponent) Touchdown", - "Fumble Recovery (Own)", - "Fumble Recovery (Own) Touchdown"])) - .and_(pl.col("pass") == True) - .and_(pl.col("text").str.contains("(?i)sacked") == False)) + target=pl.when( + pl.col("type.text").is_in( + ["Pass Reception", "Pass Completion", "Passing Touchdown", "Pass Incompletion"] + ) + ) .then(True) - .when((pl.col("pass") == True) - .and_(pl.col("text").str.contains("(?i)sacked") == False)) + .when( + ( + pl.col("type.text").is_in( + [ + "Fumble Recovery (Opponent)", + "Fumble Recovery (Opponent) Touchdown", + "Fumble Recovery (Own)", + "Fumble Recovery (Own) Touchdown", + ] + ) + ) + .and_(pl.col("pass") == True) + .and_(pl.col("text").str.contains("(?i)sacked") == False) + ) .then(True) - .otherwise(False), - pass_breakup = pl.when(pl.col("text").str.contains("(?i)broken up by")) + .when((pl.col("pass") == True).and_(pl.col("text").str.contains("(?i)sacked") == False)) .then(True) .otherwise(False), - # --- Pass/Rush TDs ------ - pass_td = pl.when(pl.col("type.text").is_in(["Passing Touchdown"])) + pass_breakup=pl.when(pl.col("text").str.contains("(?i)broken up by")).then(True).otherwise(False), + # --- Pass/Rush TDs ------ + pass_td=pl.when(pl.col("type.text").is_in(["Passing Touchdown"])) .then(True) - .when((pl.col("pass") == True) - .and_(pl.col("td_play") == True)) + .when((pl.col("pass") == True).and_(pl.col("td_play") == True)) .then(True) .otherwise(False), - rush_td = pl.when(pl.col("type.text").is_in(["Rushing Touchdown"])) + rush_td=pl.when(pl.col("type.text").is_in(["Rushing Touchdown"])) .then(True) - .when((pl.col("rush") == True) - .and_(pl.col("td_play") == True)) + .when((pl.col("rush") == True).and_(pl.col("td_play") == True)) .then(True) .otherwise(False), - # --- Change of possession via turnover - turnover_vec = pl.col("type.text").is_in(turnover_vec), - offense_score_play = pl.col("type.text").is_in(offense_score_vec), - defense_score_play = pl.col("type.text").is_in(defense_score_vec), - downs_turnover = pl.when((pl.col("type.text").is_in(normalplay)) - .and_(pl.col("statYardage") < pl.col("start.distance")) - .and_(pl.col("start.down") == 4) - .and_(pl.col("penalty_1st_conv") == False)) + # --- Change of possession via turnover + turnover_vec=pl.col("type.text").is_in(turnover_vec), + offense_score_play=pl.col("type.text").is_in(offense_score_vec), + defense_score_play=pl.col("type.text").is_in(defense_score_vec), + downs_turnover=pl.when( + (pl.col("type.text").is_in(normalplay)) + .and_(pl.col("statYardage") < pl.col("start.distance")) + .and_(pl.col("start.down") == 4) + .and_(pl.col("penalty_1st_conv") == False) + ) .then(True) .otherwise(False), - # --- Touchdowns ---- - scoring_play = pl.col("type.text").is_in(scores_vec), - yds_punted = pl.col("text").str.extract(r"(?i)(punt for \d+)").str.extract(r"(\d+)").cast(pl.Int32), - yds_punt_gained = pl.when(pl.col("punt") == True) - .then(pl.col("statYardage")) - .otherwise(None), - fg_attempt = pl.when((pl.col("type.text").str.contains(r"(?i)Field Goal")) - .or_(pl.col("text").str.contains(r"(?i)Field Goal"))) + # --- Touchdowns ---- + scoring_play=pl.col("type.text").is_in(scores_vec), + yds_punted=pl.col("text").str.extract(r"(?i)(punt for \d+)").str.extract(r"(\d+)").cast(pl.Int32), + yds_punt_gained=pl.when(pl.col("punt") == True).then(pl.col("statYardage")).otherwise(None), + fg_attempt=pl.when( + (pl.col("type.text").str.contains(r"(?i)Field Goal")).or_( + pl.col("text").str.contains(r"(?i)Field Goal") + ) + ) .then(True) .otherwise(False), - fg_made = pl.col("type.text") == "Field Goal Good", - yds_fg = pl.col("text").str.extract(r"(?i)(\d+)\s?Yd Field|(?i)(\d+)\s?YD FG|(?i)(\d+)\s?Yard FG|(?i)(\d+)\s?Field|(?i)(\d+)\s?Yard Field", 0).str.extract(r"(\d+)").cast(pl.Int32), - - ).with_columns( - pl.when(pl.col("fg_attempt") == True) + fg_made=pl.col("type.text") == "Field Goal Good", + yds_fg=pl.col("text") + .str.extract( + r"(?i)(\d+)\s?Yd Field|(?i)(\d+)\s?YD FG|(?i)(\d+)\s?Yard FG|(?i)(\d+)\s?Field|(?i)(\d+)\s?Yard Field", + 0, + ) + .str.extract(r"(\d+)") + .cast(pl.Int32), + ) + .with_columns( + pl.when(pl.col("fg_attempt") == True) .then(pl.col("yds_fg") - 17) .otherwise(pl.col("start.yardsToEndzone")) .alias("start.yardsToEndzone"), - ).with_columns( - pl.when((pl.col("start.yardsToEndzone").is_null()) + ) + .with_columns( + pl.when( + (pl.col("start.yardsToEndzone").is_null()) .and_(pl.col("type.text").is_in(kickoff_vec) == False) - .and_(pl.col("start.pos_team.id") == pl.col("homeTeamId"))) + .and_(pl.col("start.pos_team.id") == pl.col("homeTeamId")) + ) .then(100 - pl.col("start.yardLine").cast(pl.Int32)) - .when((pl.col("start.yardsToEndzone").is_null()) + .when( + (pl.col("start.yardsToEndzone").is_null()) .and_(pl.col("type.text").is_in(kickoff_vec) == False) - .and_(pl.col("start.pos_team.id") == pl.col("awayTeamId"))) + .and_(pl.col("start.pos_team.id") == pl.col("awayTeamId")) + ) .then(pl.col("start.yardLine").cast(pl.Int32)) .otherwise(pl.col("start.yardsToEndzone")) .alias("start.yardsToEndzone"), - ).with_columns( - pos_unit = pl.when(pl.col("punt") == True) + ) + .with_columns( + pos_unit=pl.when(pl.col("punt") == True) .then("Punt Offense") .when(pl.col("kickoff_play") == True) .then("Kickoff Return") @@ -1625,7 +1957,7 @@ def __add_play_category_flags(self, play_df): .when(pl.col("type.text") == "Defensive 2pt Conversion") .then("Offense") .otherwise("Offense"), - def_pos_unit = pl.when(pl.col("punt") == True) + def_pos_unit=pl.when(pl.col("punt") == True) .then("Punt Return") .when(pl.col("kickoff_play") == True) .then("Kickoff Defense") @@ -1634,895 +1966,1067 @@ def __add_play_category_flags(self, play_df): .when(pl.col("type.text") == "Defensive 2pt Conversion") .then("Defense") .otherwise("Defense"), - # --- Lags/Leads play type ---- - lead_play_type = pl.col("type.text").shift(-1), - sp = pl.when((pl.col("fg_attempt") == True) - .or_(pl.col("punt") == True) - .or_(pl.col("kickoff_play") == True)) + # --- Lags/Leads play type ---- + lead_play_type=pl.col("type.text").shift(-1), + sp=pl.when( + (pl.col("fg_attempt") == True).or_(pl.col("punt") == True).or_(pl.col("kickoff_play") == True) + ) .then(True) .otherwise(False), - play = pl.when(pl.col("type.text").is_in([ - "Timeout", "End Period", "End of Half", "Penalty"]) == False) + play=pl.when(pl.col("type.text").is_in(["Timeout", "End Period", "End of Half", "Penalty"]) == False) .then(True) .otherwise(False), - ).with_columns( - scrimmage_play = pl.when((pl.col("sp") == False) - .and_(pl.col("type.text").is_in([ - "Timeout", "Extra Point Good", "Extra Point Missed", - "Two-Point Pass", "Two-Point Rush", "Penalty"]) == False)) + ) + .with_columns( + scrimmage_play=pl.when( + (pl.col("sp") == False).and_( + pl.col("type.text").is_in( + [ + "Timeout", + "Extra Point Good", + "Extra Point Missed", + "Two-Point Pass", + "Two-Point Rush", + "Penalty", + ] + ) + == False + ) + ) .then(True) .otherwise(False), - # --- Change of pos_team by lead('pos_team', 1)---- - change_of_pos_team = pl.when((pl.col("pos_team") == pl.col("lead_pos_team")) - .and_(((pl.col("lead_play_type").is_in(["End Period", "End of Half"])) == False) - .or_(pl.col("lead_play_type").is_null()))) + # --- Change of pos_team by lead('pos_team', 1)---- + change_of_pos_team=pl.when( + (pl.col("pos_team") == pl.col("lead_pos_team")).and_( + ((pl.col("lead_play_type").is_in(["End Period", "End of Half"])) == False).or_( + pl.col("lead_play_type").is_null() + ) + ) + ) .then(False) - .when((pl.col("pos_team") == pl.col("lead_pos_team2")) - .and_((pl.col("lead_play_type").is_in(["End Period", "End of Half"])) - .or_(pl.col("lead_play_type").is_null()))) + .when( + (pl.col("pos_team") == pl.col("lead_pos_team2")).and_( + (pl.col("lead_play_type").is_in(["End Period", "End of Half"])).or_( + pl.col("lead_play_type").is_null() + ) + ) + ) .then(False) .otherwise(True), - - ).with_columns( - change_of_pos_team = pl.when(pl.col("change_of_poss").is_null()) + ) + .with_columns( + change_of_pos_team=pl.when(pl.col("change_of_poss").is_null()) .then(False) .otherwise(pl.col("change_of_pos_team")), - pos_score_diff_end = pl.when(((pl.col("type.text").is_in(end_change_vec)) - .and_(pl.col("start.pos_team.id") != pl.col("end.pos_team.id"))) - .or_(pl.col("downs_turnover") == True)) + pos_score_diff_end=pl.when( + ( + (pl.col("type.text").is_in(end_change_vec)).and_( + pl.col("start.pos_team.id") != pl.col("end.pos_team.id") + ) + ).or_(pl.col("downs_turnover") == True) + ) .then(-1 * pl.col("pos_score_diff")) .otherwise(pl.col("pos_score_diff")), - ).with_columns( - pos_score_diff_end = pl.when((pl.col("pos_score_pts").abs() >= 8) - .and_(pl.col("scoring_play") == False) - .and_(pl.col("change_of_pos_team") == False)) + ) + .with_columns( + pos_score_diff_end=pl.when( + (pl.col("pos_score_pts").abs() >= 8) + .and_(pl.col("scoring_play") == False) + .and_(pl.col("change_of_pos_team") == False) + ) .then(pl.col("pos_score_diff_start")) - .when((pl.col("pos_score_pts").abs() >= 8) - .and_(pl.col("scoring_play") == False) - .and_(pl.col("change_of_pos_team") == True)) + .when( + (pl.col("pos_score_pts").abs() >= 8) + .and_(pl.col("scoring_play") == False) + .and_(pl.col("change_of_pos_team") == True) + ) .then(-1 * pl.col("pos_score_diff_start")) .otherwise(pl.col("pos_score_diff_end")), - fumble_lost = pl.when((pl.col("fumble_vec") == True) - .and_(pl.col("change_of_pos_team") == True)) + fumble_lost=pl.when((pl.col("fumble_vec") == True).and_(pl.col("change_of_pos_team") == True)) .then(True) .otherwise(False), - fumble_recovered = pl.when((pl.col("fumble_vec") == True) - .and_(pl.col("change_of_pos_team") == False)) + fumble_recovered=pl.when((pl.col("fumble_vec") == True).and_(pl.col("change_of_pos_team") == False)) .then(True) - .otherwise(False) + .otherwise(False), + ) ) return play_df def __add_yardage_cols(self, play_df): play_df = play_df.with_columns( - yds_rushed = pl.when((pl.col("rush") == True) - .and_(pl.col("text").str.contains("(?i)run for no gain"))) - .then(0) - .when((pl.col("rush") == True) - .and_(pl.col("text").str.contains("(?i)for no gain"))) - .then(0) - .when((pl.col("rush") == True) - .and_(pl.col("text").str.contains("(?i)run for a loss of"))) - .then(-1 * pl.col("text").str.extract(r"(?i)run for a loss of (\d+)").cast(pl.Int32)) - .when((pl.col("rush") == True) - .and_(pl.col("text").str.contains("(?i)rush for a loss of"))) - .then(-1 * pl.col("text").str.extract(r"(?i)rush for a loss of (\d+)").cast(pl.Int32)) - .when((pl.col("rush") == True) - .and_(pl.col("text").str.contains("(?i)run for"))) - .then(pl.col("text").str.extract(r"(?i)run for (\d+)").cast(pl.Int32)) - .when((pl.col("rush") == True) - .and_(pl.col("text").str.contains("(?i)rush for"))) - .then(pl.col("text").str.extract(r"(?i)rush for (\d+)").cast(pl.Int32)) - .when((pl.col("rush") == True) - .and_(pl.col("text").str.contains("(?i)Yd Run"))) - .then(pl.col("text").str.extract(r"(?i)(\d+) Yd Run").cast(pl.Int32)) - .when((pl.col("rush") == True) - .and_(pl.col("text").str.contains("(?i)Yd Rush"))) - .then(pl.col("text").str.extract(r"(?i)(\d+) Yd Rush").cast(pl.Int32)) - .when((pl.col("rush") == True) - .and_(pl.col("text").str.contains("(?i)Yard Rush"))) - .then(pl.col("text").str.extract(r"(?i)(\d+) Yard Rush").cast(pl.Int32)) - .when((pl.col("rush") == True) - .and_(pl.col("text").str.contains("(?i)rushed")) - .and_(pl.col("text").str.contains("(?i)touchdown") == False)) - .then(pl.col("text").str.extract(r"(?i)for (\d+) yards").cast(pl.Int32)) - .when((pl.col("rush") == True) - .and_(pl.col("text").str.contains("(?i)rushed")) - .and_(pl.col("text").str.contains("(?i)touchdown") == True)) - .then(pl.col("text").str.extract(r"(?i)for a (\d+) yard").cast(pl.Int32)) - .otherwise(None), - yds_receiving = pl.when((pl.col("pass") == True) - .and_(pl.col("text").str.contains(r"(?i)complete to")) - .and_(pl.col("text").str.contains(r"(?i)for no gain"))) - .then(0) - .when((pl.col("pass") == True) - .and_(pl.col("text").str.contains(r"(?i)complete to")) - .and_(pl.col("text").str.contains(r"(?i)for a loss of"))) - .then(-1 * pl.col("text").str.extract(r"(?i)for a loss of (\d+)").cast(pl.Int32)) - .when((pl.col("pass") == True) - .and_(pl.col("text").str.contains(r"(?i)complete to"))) - .then(pl.col("text").str.extract(r"(?i)for (\d+)").cast(pl.Int32)) - .when((pl.col("pass") == True) - .and_(pl.col("text").str.contains(r"(?i)incomplete|(?i) sacked|(?i)intercepted|(?i)pass defensed"))) - .then(0) - .when((pl.col("pass") == True) - .and_(pl.col("text").str.contains(r"(?i)incompletion"))) - .then(0) - .when((pl.col("pass") == True) - .and_(pl.col("text").str.contains(r"(?i)Yd pass"))) - .then(pl.col("text").str.extract(r"(?i)(\d+) Yd pass").cast(pl.Int32)) - .otherwise(None), - yds_int_return = pl.when((pl.col("pass") == True) - .and_(pl.col("int_td") == True) - .and_(pl.col("text").str.contains(r"(?i)Yd Interception Return"))) - .then(pl.col("text").str.extract(r"(?i)(.+)Yd Interception Return") - .str.extract(r"(\d+)").cast(pl.Int32)) - .when((pl.col("pass") == True) - .and_(pl.col("int") == True) - .and_(pl.col("text").str.contains(r"(?i)for no gain"))) - .then(0) - .when((pl.col("pass") == True) - .and_(pl.col("int") == True) - .and_(pl.col("text").str.contains(r"(?i)for a loss of"))) - .then(-1 * pl.col("text").str.extract(r"(?i)for a loss of (\d+)").cast(pl.Int32)) - .when((pl.col("pass") == True) - .and_(pl.col("int") == True) - .and_(pl.col("text").str.contains(r"(?i)for a TD"))) - .then(pl.col("text").str.extract(r"(?i)return for (.+)") - .str.extract(r"(\d+)").cast(pl.Int32)) - .when((pl.col("pass") == True) - .and_(pl.col("int") == True)) - .then(pl.col("text").str.replace("for a 1st", "") - .str.extract(r"(?i)for (.+)") - .str.extract(r"(\d+)").cast(pl.Int32)) - .otherwise(None), - yds_kickoff = pl.when(pl.col("kickoff_play") == True) - .then(pl.col("text").str.extract(r"(?i)kickoff for (.+)") - .str.extract(r"(\d+)").cast(pl.Int32)) - .otherwise(None), - yds_kickoff_return = pl.when((pl.col("kickoff_play") == True) - .and_(pl.col("kickoff_tb") == True) - .and_(pl.col("season") > 2013)) - .then(25) - .when((pl.col("kickoff_play") == True) - .and_(pl.col("kickoff_tb") == True) - .and_(pl.col("season") <= 2013)) - .then(20) - .when((pl.col("kickoff_play") == True) - .and_(pl.col("fumble_vec") == False) - .and_(pl.col("text").str.contains(r"(?i)for no gain|fair catch|fair caught"))) - .then(0) - .when((pl.col("kickoff_play") == True) - .and_(pl.col("fumble_vec") == False) - .and_(pl.col("text").str.contains(r"(?i)out-of-bounds|out of bounds"))) - .then(40) - .when((pl.col("kickoff_downed") == True) - .or_(pl.col("kickoff_fair_catch") == True)) - .then(0) - .when((pl.col("kickoff_play") == True) - .and_(pl.col("text").str.contains(r"(?i)returned by"))) - .then(pl.col("text").str.extract(r"(?i)returned by (.+)") - .str.extract(r"(\d+)").cast(pl.Int32)) - .when((pl.col("kickoff_play") == True) - .and_(pl.col("text").str.contains(r"(?i)return for"))) - .then(pl.col("text").str.extract(r"(?i)return for (.+)") - .str.extract(r"(\d+)").cast(pl.Int32)) - .otherwise(None), - yds_punted = pl.when((pl.col("punt") == True) - .and_(pl.col("punt_blocked") == True)) - .then(0) - .when(pl.col("punt") == True) - .then(pl.col("text").str.extract(r"(?i)punt for (.+)") - .str.extract(r"(\d+)").cast(pl.Int32)) - .otherwise(None), - yds_punt_return = pl.when((pl.col("punt") == True) - .and_(pl.col("punt_tb") == True)) - .then(20) - .when((pl.col("punt") == True) - .and_(pl.col("text").str.contains(r"(?i)fair catch|fair caught"))) - .then(0) - .when((pl.col("punt") == True) - .and_((pl.col("punt_downed") == True) - .or_(pl.col("punt_oob") == True) - .or_(pl.col("punt_fair_catch") == True))) - .then(0) - .when((pl.col("punt") == True) - .and_(pl.col("text").str.contains(r"(?i)no return|no gain"))) - .then(0) - .when((pl.col("punt") == True) - .and_(pl.col("text").str.contains(r"(?i)returned \d+ yards"))) - .then(pl.col("text").str.extract(r"(?i)returned (.+)") - .str.extract(r"(\d+)").cast(pl.Int32)) - .when((pl.col("punt") == True) - .and_(pl.col("punt_blocked") == False)) - .then(pl.col("text").str.extract(r"(?i)returns for (.+)") - .str.extract(r"(\d+)").cast(pl.Int32)) - .when((pl.col("punt") == True) - .and_(pl.col("punt_blocked") == True)) - .then(pl.col("text").str.extract(r"(?i)return for (.+)") - .str.extract(r"(\d+)").cast(pl.Int32)) - .otherwise(None), - yds_fumble_return = pl.when((pl.col("fumble_vec") == True) - .and_(pl.col("kickoff_play") == False)) - .then(pl.col("text").str.extract(r"(?i)return for (.+)") - .str.extract(r"(\d+)").cast(pl.Int32)) - .otherwise(None), - yds_sacked = pl.when(pl.col("sack") == True) - .then(-1 * pl.col("text").str.extract(r"(?i)sacked (.+)") - .str.extract(r"(\d+)").cast(pl.Int32)) - .otherwise(None), - ).with_columns( - yds_penalty = pl.when(pl.col("penalty_detail").is_in(["Penalty Declined", "Penalty Offset"])) - .then(0) - .when(pl.col("yds_penalty").is_not_null()) - .then(pl.col("yds_penalty")) - .when((pl.col("penalty_detail").is_not_null()) - .and_(pl.col("yds_penalty").is_null()) - .and_(pl.col("rush") == True)) - .then(pl.col("statYardage") - pl.col("yds_rushed")) - .when((pl.col("penalty_detail").is_not_null()) - .and_(pl.col("yds_penalty").is_null()) - .and_(pl.col("int") == True)) - .then(pl.col("statYardage") - pl.col("yds_int_return")) - .when((pl.col("penalty_detail").is_not_null()) - .and_(pl.col("yds_penalty").is_null()) - .and_(pl.col("pass") == True) - .and_(pl.col("sack") == False) - .and_(pl.col("type.text") != "Pass Incompletion")) - .then(pl.col("statYardage") - pl.col("yds_receiving")) - .when((pl.col("penalty_detail").is_not_null()) - .and_(pl.col("yds_penalty").is_null()) - .and_(pl.col("pass") == True) - .and_(pl.col("sack") == False) - .and_(pl.col("type.text") == "Pass Incompletion")) - .then(pl.col("statYardage")) - .when((pl.col("penalty_detail").is_not_null()) - .and_(pl.col("yds_penalty").is_null()) - .and_(pl.col("pass") == True) - .and_(pl.col("sack") == True)) - .then(pl.col("statYardage") - pl.col("yds_sacked")) - .when(pl.col("type.text") == "Penalty") - .then(pl.col("statYardage")) - .otherwise(None), + yds_rushed=pl.when((pl.col("rush") == True).and_(pl.col("text").str.contains("(?i)run for no gain"))) + .then(0) + .when((pl.col("rush") == True).and_(pl.col("text").str.contains("(?i)for no gain"))) + .then(0) + .when((pl.col("rush") == True).and_(pl.col("text").str.contains("(?i)run for a loss of"))) + .then(-1 * pl.col("text").str.extract(r"(?i)run for a loss of (\d+)").cast(pl.Int32)) + .when((pl.col("rush") == True).and_(pl.col("text").str.contains("(?i)rush for a loss of"))) + .then(-1 * pl.col("text").str.extract(r"(?i)rush for a loss of (\d+)").cast(pl.Int32)) + .when((pl.col("rush") == True).and_(pl.col("text").str.contains("(?i)run for"))) + .then(pl.col("text").str.extract(r"(?i)run for (\d+)").cast(pl.Int32)) + .when((pl.col("rush") == True).and_(pl.col("text").str.contains("(?i)rush for"))) + .then(pl.col("text").str.extract(r"(?i)rush for (\d+)").cast(pl.Int32)) + .when((pl.col("rush") == True).and_(pl.col("text").str.contains("(?i)Yd Run"))) + .then(pl.col("text").str.extract(r"(?i)(\d+) Yd Run").cast(pl.Int32)) + .when((pl.col("rush") == True).and_(pl.col("text").str.contains("(?i)Yd Rush"))) + .then(pl.col("text").str.extract(r"(?i)(\d+) Yd Rush").cast(pl.Int32)) + .when((pl.col("rush") == True).and_(pl.col("text").str.contains("(?i)Yard Rush"))) + .then(pl.col("text").str.extract(r"(?i)(\d+) Yard Rush").cast(pl.Int32)) + .when( + (pl.col("rush") == True) + .and_(pl.col("text").str.contains("(?i)rushed")) + .and_(pl.col("text").str.contains("(?i)touchdown") == False) + ) + .then(pl.col("text").str.extract(r"(?i)for (\d+) yards").cast(pl.Int32)) + .when( + (pl.col("rush") == True) + .and_(pl.col("text").str.contains("(?i)rushed")) + .and_(pl.col("text").str.contains("(?i)touchdown") == True) + ) + .then(pl.col("text").str.extract(r"(?i)for a (\d+) yard").cast(pl.Int32)) + .otherwise(None), + yds_receiving=pl.when( + (pl.col("pass") == True) + .and_(pl.col("text").str.contains(r"(?i)complete to")) + .and_(pl.col("text").str.contains(r"(?i)for no gain")) + ) + .then(0) + .when( + (pl.col("pass") == True) + .and_(pl.col("text").str.contains(r"(?i)complete to")) + .and_(pl.col("text").str.contains(r"(?i)for a loss of")) + ) + .then(-1 * pl.col("text").str.extract(r"(?i)for a loss of (\d+)").cast(pl.Int32)) + .when((pl.col("pass") == True).and_(pl.col("text").str.contains(r"(?i)complete to"))) + .then(pl.col("text").str.extract(r"(?i)for (\d+)").cast(pl.Int32)) + .when( + (pl.col("pass") == True).and_( + pl.col("text").str.contains(r"(?i)incomplete|(?i) sacked|(?i)intercepted|(?i)pass defensed") + ) + ) + .then(0) + .when((pl.col("pass") == True).and_(pl.col("text").str.contains(r"(?i)incompletion"))) + .then(0) + .when((pl.col("pass") == True).and_(pl.col("text").str.contains(r"(?i)Yd pass"))) + .then(pl.col("text").str.extract(r"(?i)(\d+) Yd pass").cast(pl.Int32)) + .otherwise(None), + yds_int_return=pl.when( + (pl.col("pass") == True) + .and_(pl.col("int_td") == True) + .and_(pl.col("text").str.contains(r"(?i)Yd Interception Return")) + ) + .then(pl.col("text").str.extract(r"(?i)(.+)Yd Interception Return").str.extract(r"(\d+)").cast(pl.Int32)) + .when( + (pl.col("pass") == True) + .and_(pl.col("int") == True) + .and_(pl.col("text").str.contains(r"(?i)for no gain")) + ) + .then(0) + .when( + (pl.col("pass") == True) + .and_(pl.col("int") == True) + .and_(pl.col("text").str.contains(r"(?i)for a loss of")) + ) + .then(-1 * pl.col("text").str.extract(r"(?i)for a loss of (\d+)").cast(pl.Int32)) + .when( + (pl.col("pass") == True).and_(pl.col("int") == True).and_(pl.col("text").str.contains(r"(?i)for a TD")) + ) + .then(pl.col("text").str.extract(r"(?i)return for (.+)").str.extract(r"(\d+)").cast(pl.Int32)) + .when((pl.col("pass") == True).and_(pl.col("int") == True)) + .then( + pl.col("text") + .str.replace("for a 1st", "") + .str.extract(r"(?i)for (.+)") + .str.extract(r"(\d+)") + .cast(pl.Int32) + ) + .otherwise(None), + yds_kickoff=pl.when(pl.col("kickoff_play") == True) + .then(pl.col("text").str.extract(r"(?i)kickoff for (.+)").str.extract(r"(\d+)").cast(pl.Int32)) + .otherwise(None), + yds_kickoff_return=pl.when( + (pl.col("kickoff_play") == True).and_(pl.col("kickoff_tb") == True).and_(pl.col("season") > 2013) + ) + .then(25) + .when((pl.col("kickoff_play") == True).and_(pl.col("kickoff_tb") == True).and_(pl.col("season") <= 2013)) + .then(20) + .when( + (pl.col("kickoff_play") == True) + .and_(pl.col("fumble_vec") == False) + .and_(pl.col("text").str.contains(r"(?i)for no gain|fair catch|fair caught")) + ) + .then(0) + .when( + (pl.col("kickoff_play") == True) + .and_(pl.col("fumble_vec") == False) + .and_(pl.col("text").str.contains(r"(?i)out-of-bounds|out of bounds")) + ) + .then(40) + .when((pl.col("kickoff_downed") == True).or_(pl.col("kickoff_fair_catch") == True)) + .then(0) + .when((pl.col("kickoff_play") == True).and_(pl.col("text").str.contains(r"(?i)returned by"))) + .then(pl.col("text").str.extract(r"(?i)returned by (.+)").str.extract(r"(\d+)").cast(pl.Int32)) + .when((pl.col("kickoff_play") == True).and_(pl.col("text").str.contains(r"(?i)return for"))) + .then(pl.col("text").str.extract(r"(?i)return for (.+)").str.extract(r"(\d+)").cast(pl.Int32)) + .otherwise(None), + yds_punted=pl.when((pl.col("punt") == True).and_(pl.col("punt_blocked") == True)) + .then(0) + .when(pl.col("punt") == True) + .then(pl.col("text").str.extract(r"(?i)punt for (.+)").str.extract(r"(\d+)").cast(pl.Int32)) + .otherwise(None), + yds_punt_return=pl.when((pl.col("punt") == True).and_(pl.col("punt_tb") == True)) + .then(20) + .when((pl.col("punt") == True).and_(pl.col("text").str.contains(r"(?i)fair catch|fair caught"))) + .then(0) + .when( + (pl.col("punt") == True).and_( + (pl.col("punt_downed") == True) + .or_(pl.col("punt_oob") == True) + .or_(pl.col("punt_fair_catch") == True) + ) + ) + .then(0) + .when((pl.col("punt") == True).and_(pl.col("text").str.contains(r"(?i)no return|no gain"))) + .then(0) + .when((pl.col("punt") == True).and_(pl.col("text").str.contains(r"(?i)returned \d+ yards"))) + .then(pl.col("text").str.extract(r"(?i)returned (.+)").str.extract(r"(\d+)").cast(pl.Int32)) + .when((pl.col("punt") == True).and_(pl.col("punt_blocked") == False)) + .then(pl.col("text").str.extract(r"(?i)returns for (.+)").str.extract(r"(\d+)").cast(pl.Int32)) + .when((pl.col("punt") == True).and_(pl.col("punt_blocked") == True)) + .then(pl.col("text").str.extract(r"(?i)return for (.+)").str.extract(r"(\d+)").cast(pl.Int32)) + .otherwise(None), + yds_fumble_return=pl.when((pl.col("fumble_vec") == True).and_(pl.col("kickoff_play") == False)) + .then(pl.col("text").str.extract(r"(?i)return for (.+)").str.extract(r"(\d+)").cast(pl.Int32)) + .otherwise(None), + yds_sacked=pl.when(pl.col("sack") == True) + .then(-1 * pl.col("text").str.extract(r"(?i)sacked (.+)").str.extract(r"(\d+)").cast(pl.Int32)) + .otherwise(None), + ).with_columns( + yds_penalty=pl.when(pl.col("penalty_detail").is_in(["Penalty Declined", "Penalty Offset"])) + .then(0) + .when(pl.col("yds_penalty").is_not_null()) + .then(pl.col("yds_penalty")) + .when( + (pl.col("penalty_detail").is_not_null()) + .and_(pl.col("yds_penalty").is_null()) + .and_(pl.col("rush") == True) + ) + .then(pl.col("statYardage") - pl.col("yds_rushed")) + .when( + (pl.col("penalty_detail").is_not_null()) + .and_(pl.col("yds_penalty").is_null()) + .and_(pl.col("int") == True) + ) + .then(pl.col("statYardage") - pl.col("yds_int_return")) + .when( + (pl.col("penalty_detail").is_not_null()) + .and_(pl.col("yds_penalty").is_null()) + .and_(pl.col("pass") == True) + .and_(pl.col("sack") == False) + .and_(pl.col("type.text") != "Pass Incompletion") + ) + .then(pl.col("statYardage") - pl.col("yds_receiving")) + .when( + (pl.col("penalty_detail").is_not_null()) + .and_(pl.col("yds_penalty").is_null()) + .and_(pl.col("pass") == True) + .and_(pl.col("sack") == False) + .and_(pl.col("type.text") == "Pass Incompletion") + ) + .then(pl.col("statYardage")) + .when( + (pl.col("penalty_detail").is_not_null()) + .and_(pl.col("yds_penalty").is_null()) + .and_(pl.col("pass") == True) + .and_(pl.col("sack") == True) + ) + .then(pl.col("statYardage") - pl.col("yds_sacked")) + .when(pl.col("type.text") == "Penalty") + .then(pl.col("statYardage")) + .otherwise(None), ) return play_df def __add_player_cols(self, play_df): - play_df = play_df.with_columns( - # --- RB Names ----- - rush_player = pl.when(pl.col("rush") == True) - .then(pl.col("text").str.extract(r"(?i)(.{0,25} )run |(?i)(.{0,25} )\d{0,2} Yd Run|(?i)(.{0,25} )rush |(?i)(.{0,25} )rushed ") - .str.replace(r"(?i) run |(?i) \d+ Yd Run|(?i) rush ", "") - .str.replace(r" \((.+)\)", "")) + play_df = ( + play_df.with_columns( + # --- RB Names ----- + rush_player=pl.when(pl.col("rush") == True) + .then( + pl.col("text") + .str.extract( + r"(?i)(.{0,25} )run |(?i)(.{0,25} )\d{0,2} Yd Run|(?i)(.{0,25} )rush |(?i)(.{0,25} )rushed " + ) + .str.replace(r"(?i) run |(?i) \d+ Yd Run|(?i) rush ", "") + .str.replace(r" \((.+)\)", "") + ) .otherwise(None), - # --- QB Names ----- - pass_player = pl.when((pl.col("pass") == True) - .and_(pl.col("sack_vec") == False) - .and_(pl.col("type.text") != "Passing Touchdown")) - .then(pl.col("text").str.extract(r"(?i)(.{0,30} )pass |(?i)(.{0,30} )sacked by|(?i)(.{0,30} )sacked for|(?i)(.{0,30} )incomplete|(?i)pass from (.{0,30} ) \( ") - .str.replace(r"(?i)pass |(?i) sacked by|(?i) sacked for|(?i) incomplete", "")) - .when((pl.col("pass") == True) - .and_(pl.col("sack_vec") == True) - .and_(pl.col("type.text") != "Passing Touchdown")) - .then(pl.col("text").str.extract(r"(?i)(.{0,30} )sacked by|(?i)(.{0,30} )sacked for") - .str.replace(r"(?i)pass |(?i) sacked by|(?i) sacked for|(?i) incomplete", "")) - .when((pl.col("pass") == True) - .and_(pl.col("type.text") == "Passing Touchdown")) - .then(pl.col("text").str.extract(r"(?i)pass from(.+)") - .str.replace(r"pass from", "") - # .str.replace(r"\((.+)\)", "") - .str.replace(r" \,", "")) - .otherwise(None) - ).with_columns( - pass_player = pl.when((pl.col("type.text") == "Passing Touchdown") - .and_(pl.col("pass_player").is_null())) - .then(pl.col("text").str.extract(r"(.+)pass(.+)? complete to") - .str.replace(r" pass complete to(.+)", "") - .str.replace(r" pass complete to", "")) + # --- QB Names ----- + pass_player=pl.when( + (pl.col("pass") == True) + .and_(pl.col("sack_vec") == False) + .and_(pl.col("type.text") != "Passing Touchdown") + ) + .then( + pl.col("text") + .str.extract( + r"(?i)(.{0,30} )pass |(?i)(.{0,30} )sacked by|(?i)(.{0,30} )sacked for|(?i)(.{0,30} )incomplete|(?i)pass from (.{0,30} ) \( " + ) + .str.replace(r"(?i)pass |(?i) sacked by|(?i) sacked for|(?i) incomplete", "") + ) + .when( + (pl.col("pass") == True) + .and_(pl.col("sack_vec") == True) + .and_(pl.col("type.text") != "Passing Touchdown") + ) + .then( + pl.col("text") + .str.extract(r"(?i)(.{0,30} )sacked by|(?i)(.{0,30} )sacked for") + .str.replace(r"(?i)pass |(?i) sacked by|(?i) sacked for|(?i) incomplete", "") + ) + .when((pl.col("pass") == True).and_(pl.col("type.text") == "Passing Touchdown")) + .then( + pl.col("text") + .str.extract(r"(?i)pass from(.+)") + .str.replace(r"pass from", "") + # .str.replace(r"\((.+)\)", "") + .str.replace(r" \,", "") + ) + .otherwise(None), + ) + .with_columns( + pass_player=pl.when((pl.col("type.text") == "Passing Touchdown").and_(pl.col("pass_player").is_null())) + .then( + pl.col("text") + .str.extract(r"(.+)pass(.+)? complete to") + .str.replace(r" pass complete to(.+)", "") + .str.replace(r" pass complete to", "") + ) .otherwise(pl.col("pass_player")) - ).with_columns( - pass_player = pl.when((pl.col("type.text") == "Passing Touchdown") - .and_(pl.col("pass_player").is_null())) - .then(pl.col("text").str.extract(r"(.+)pass,to") - .str.replace(r" pass,to(.+)", "") - .str.replace(r" pass,to", "") - .str.replace(r" \((.+)\)", "")) + ) + .with_columns( + pass_player=pl.when((pl.col("type.text") == "Passing Touchdown").and_(pl.col("pass_player").is_null())) + .then( + pl.col("text") + .str.extract(r"(.+)pass,to") + .str.replace(r" pass,to(.+)", "") + .str.replace(r" pass,to", "") + .str.replace(r" \((.+)\)", "") + ) .otherwise(pl.col("pass_player")) - ).with_columns( - pass_player = pl.when((pl.col("pass") == True) - .and_(((pl.col("pass_player").str.strip().str.n_chars() == 0) - .or_(pl.col("pass_player").is_null())))) + ) + .with_columns( + pass_player=pl.when( + (pl.col("pass") == True).and_( + ((pl.col("pass_player").str.strip().str.n_chars() == 0).or_(pl.col("pass_player").is_null())) + ) + ) .then("TEAM") .otherwise(pl.col("pass_player")), - - # --- WR Names ----- - receiver_player = pl.when((pl.col("pass") == True) - .and_(pl.col("text").str.contains(r"(?i)sacked") == False)) + # --- WR Names ----- + receiver_player=pl.when( + (pl.col("pass") == True).and_(pl.col("text").str.contains(r"(?i)sacked") == False) + ) .then(pl.col("text").str.extract(r"(?i)to (.+)")) .when(pl.col("text").str.contains(r"(?i)Yd pass")) .then(pl.col("text").str.extract(r"(?i)(.{0,25} )\d{0,2} Yd pass")) .when(pl.col("text").str.contains(r"(?i)Yd TD pass")) .then(pl.col("text").str.extract(r"(?i)(.{0,25} )\d{0,2} Yd TD pass")) - .otherwise(None) - ).with_columns( - receiver_player = pl.when((pl.col("type.text") == "Sack") - .or_(pl.col("type.text") == "Interception Return") - .or_(pl.col("type.text") == "Interception Return Touchdown") - .or_((pl.col("type.text").is_in(["Fumble Recovery (Opponent) Touchdown", "Fumble Recovery (Opponent)"])) - .and_(pl.col("text").str.contains(r"(?i)sacked")))) + .otherwise(None), + ) + .with_columns( + receiver_player=pl.when( + (pl.col("type.text") == "Sack") + .or_(pl.col("type.text") == "Interception Return") + .or_(pl.col("type.text") == "Interception Return Touchdown") + .or_( + ( + pl.col("type.text").is_in( + ["Fumble Recovery (Opponent) Touchdown", "Fumble Recovery (Opponent)"] + ) + ).and_(pl.col("text").str.contains(r"(?i)sacked")) + ) + ) .then(None) - .otherwise(pl.col("receiver_player").str.replace(r"to ", "") - .str.replace(r"(?i)\\,.+", "") - .str.replace(r"(?i)for (.+)", "") - .str.replace(r"(?i) (\d{1,2})", "") - .str.replace(r"(?i) Yd pass", "") - .str.replace(r"(?i) Yd TD pass", "") - .str.replace(r"(?i)pass complete to", "") - .str.replace(r"(?i)penalty", "") - .str.replace(r'(?i) "', "")) - ).with_columns( - receiver_player = pl.when(pl.col("receiver_player").str.contains(r"(?i)III") == True) + .otherwise( + pl.col("receiver_player") + .str.replace(r"to ", "") + .str.replace(r"(?i)\\,.+", "") + .str.replace(r"(?i)for (.+)", "") + .str.replace(r"(?i) (\d{1,2})", "") + .str.replace(r"(?i) Yd pass", "") + .str.replace(r"(?i) Yd TD pass", "") + .str.replace(r"(?i)pass complete to", "") + .str.replace(r"(?i)penalty", "") + .str.replace(r'(?i) "', "") + ) + ) + .with_columns( + receiver_player=pl.when(pl.col("receiver_player").str.contains(r"(?i)III") == True) .then(pl.col("receiver_player").str.replace(r"(?i)[A-Z]{3,}", "")) .otherwise(pl.col("receiver_player")) - ).with_columns( - receiver_player = pl.col("receiver_player").str.replace(r"(?i) &", "") - .str.replace(r"(?i)A&M", "") - .str.replace(r"(?i) ST", "") - .str.replace(r"(?i) GA", "") - .str.replace(r"(?i) UL", "") - .str.replace(r"(?i) FL", "") - .str.replace(r"(?i) OH", "") - .str.replace(r"(?i) NC", "") - .str.replace(r'(?i) "', "") - .str.replace(r"(?i) \\u00c9", "") - .str.replace(r"(?i) fumbled,", "") - .str.replace(r"(?i)the (.+)", "") - .str.replace(r"(?i)pass incomplete to", "") - .str.replace(r"(?i)(.+)pass incomplete", "") - .str.replace(r"(?i)pass incomplete", "") - .str.replace(r"(?i) \((.+)\)", ""), - # --- Sack Names ----- - sack_players = pl.when((pl.col("sack") == True) - .or_((pl.col("fumble_vec") == True) - .and_(pl.col("pass") == True))) - .then(pl.col("text").str.extract(r"(?i)sacked by(.+)") - .str.replace(r"for (.+)", "") - .str.replace(r"(.+) by ", "") - .str.replace(r" at the (.+)", "")) - .otherwise(None) - ).with_columns( - sack_player1 = pl.col("sack_players").str.replace(r"and (.+)", ""), - sack_player2 = pl.when(pl.col("sack_players").str.contains(r"and (.+)")) + ) + .with_columns( + receiver_player=pl.col("receiver_player") + .str.replace(r"(?i) &", "") + .str.replace(r"(?i)A&M", "") + .str.replace(r"(?i) ST", "") + .str.replace(r"(?i) GA", "") + .str.replace(r"(?i) UL", "") + .str.replace(r"(?i) FL", "") + .str.replace(r"(?i) OH", "") + .str.replace(r"(?i) NC", "") + .str.replace(r'(?i) "', "") + .str.replace(r"(?i) \\u00c9", "") + .str.replace(r"(?i) fumbled,", "") + .str.replace(r"(?i)the (.+)", "") + .str.replace(r"(?i)pass incomplete to", "") + .str.replace(r"(?i)(.+)pass incomplete", "") + .str.replace(r"(?i)pass incomplete", "") + .str.replace(r"(?i) \((.+)\)", ""), + # --- Sack Names ----- + sack_players=pl.when( + (pl.col("sack") == True).or_((pl.col("fumble_vec") == True).and_(pl.col("pass") == True)) + ) + .then( + pl.col("text") + .str.extract(r"(?i)sacked by(.+)") + .str.replace(r"for (.+)", "") + .str.replace(r"(.+) by ", "") + .str.replace(r" at the (.+)", "") + ) + .otherwise(None), + ) + .with_columns( + sack_player1=pl.col("sack_players").str.replace(r"and (.+)", ""), + sack_player2=pl.when(pl.col("sack_players").str.contains(r"and (.+)")) .then(pl.col("sack_players").str.replace(r"(.+) and", "")) .otherwise(None), - # --- Interception Names ----- - interception_player = pl.when(((pl.col("type.text") == "Interception Return") - .or_(pl.col("type.text") == "Interception Return Touchdown")) - .and_(pl.col("pass") == True)) + # --- Interception Names ----- + interception_player=pl.when( + ( + (pl.col("type.text") == "Interception Return").or_( + pl.col("type.text") == "Interception Return Touchdown" + ) + ).and_(pl.col("pass") == True) + ) .then(pl.col("text").str.extract(r"(?i)intercepted (.+)")) .when(pl.col("text").str.contains(r"Yd Interception Return")) - .then(pl.col("text").str.extract(r"(?i)(.{0,25} )\\d{0,2} Yd Interception Return|(?i)(.{0,25} )\\d{0,2} yd interception return") - .str.replace(r"return (.+)", "") - .str.replace(r"(.+) intercepted", "") - .str.replace(r"intercepted", "") - .str.replace(r"Yd Interception Return", "") - .str.replace(r"for a 1st down", "") - .str.replace(r"(\\d{1,2})", "") - .str.replace(r"for a TD", "") - .str.replace(r"at the (.+)", "") - .str.replace(r" by ", "")) + .then( + pl.col("text") + .str.extract( + r"(?i)(.{0,25} )\\d{0,2} Yd Interception Return|(?i)(.{0,25} )\\d{0,2} yd interception return" + ) + .str.replace(r"return (.+)", "") + .str.replace(r"(.+) intercepted", "") + .str.replace(r"intercepted", "") + .str.replace(r"Yd Interception Return", "") + .str.replace(r"for a 1st down", "") + .str.replace(r"(\\d{1,2})", "") + .str.replace(r"for a TD", "") + .str.replace(r"at the (.+)", "") + .str.replace(r" by ", "") + ) .otherwise(None), - # --- Pass Breakup Players ---- - pass_breakup_player = pl.when(pl.col("pass") == True) - .then(pl.col("text").str.extract(r"(?i)broken up by (.+)") - .str.replace(r"(.+) broken up by", "") - .str.replace(r"broken up by", "") - .str.replace(r"Penalty(.+)", "") - .str.replace(r"SOUTH FLORIDA", "") - .str.replace(r"WEST VIRGINIA", "") - .str.replace(r"MISSISSIPPI ST", "") - .str.replace(r"CAMPBELL", "") - .str.replace(r"COASTL CAROLINA", "")) + # --- Pass Breakup Players ---- + pass_breakup_player=pl.when(pl.col("pass") == True) + .then( + pl.col("text") + .str.extract(r"(?i)broken up by (.+)") + .str.replace(r"(.+) broken up by", "") + .str.replace(r"broken up by", "") + .str.replace(r"Penalty(.+)", "") + .str.replace(r"SOUTH FLORIDA", "") + .str.replace(r"WEST VIRGINIA", "") + .str.replace(r"MISSISSIPPI ST", "") + .str.replace(r"CAMPBELL", "") + .str.replace(r"COASTL CAROLINA", "") + ) .otherwise(None), - # --- Punter Names ---- - punter_player = pl.when(pl.col("type.text").str.contains("Punt")) - .then(pl.col("text").str.extract(r"(?i)(.{0,30}) punt|(?i)Punt by (.{0,30})") - .str.replace(r"(?i) punt", "") - .str.replace(r"(?i) for(.+)", "") - .str.replace(r"(?i)Punt by ", "") - .str.replace(r"(?i)\((.+)\)", "") - .str.replace(r"(?i) returned \d+", "") - .str.replace(r"(?i) returned", "") - .str.replace(r"(?i) no return", "")) + # --- Punter Names ---- + punter_player=pl.when(pl.col("type.text").str.contains("Punt")) + .then( + pl.col("text") + .str.extract(r"(?i)(.{0,30}) punt|(?i)Punt by (.{0,30})") + .str.replace(r"(?i) punt", "") + .str.replace(r"(?i) for(.+)", "") + .str.replace(r"(?i)Punt by ", "") + .str.replace(r"(?i)\((.+)\)", "") + .str.replace(r"(?i) returned \d+", "") + .str.replace(r"(?i) returned", "") + .str.replace(r"(?i) no return", "") + ) .otherwise(None), - # --- Punt Returner Names ---- - punt_return_player = pl.when(pl.col("type.text").str.contains("Punt")) - .then(pl.col("text").str.extract(r"(?i), (.{0,25}) returns|(?i)fair catch by (.{0,25})|(?i), returned by (.{0,25})|(?i)yards by (.{0,30})|(?i) return by (.{0,25})") - .str.replace(r"(?i), ", "") - .str.replace(r"(?i) returns", "") - .str.replace(r"(?i) returned", "") - .str.replace(r"(?i) return", "") - .str.replace(r"(?i)fair catch by", "") - .str.replace(r"(?i) at (.+)", "") - .str.replace(r"(?i) for (.+)", "") - .str.replace(r"(?i)(.+) by ", "") - .str.replace(r"(?i) to (.+)", "") - .str.replace(r"(?i)\((.+)\)", "")) + # --- Punt Returner Names ---- + punt_return_player=pl.when(pl.col("type.text").str.contains("Punt")) + .then( + pl.col("text") + .str.extract( + r"(?i), (.{0,25}) returns|(?i)fair catch by (.{0,25})|(?i), returned by (.{0,25})|(?i)yards by (.{0,30})|(?i) return by (.{0,25})" + ) + .str.replace(r"(?i), ", "") + .str.replace(r"(?i) returns", "") + .str.replace(r"(?i) returned", "") + .str.replace(r"(?i) return", "") + .str.replace(r"(?i)fair catch by", "") + .str.replace(r"(?i) at (.+)", "") + .str.replace(r"(?i) for (.+)", "") + .str.replace(r"(?i)(.+) by ", "") + .str.replace(r"(?i) to (.+)", "") + .str.replace(r"(?i)\((.+)\)", "") + ) .otherwise(None), - # --- Punt Blocker Names ---- - punt_block_player = pl.when(pl.col("type.text").str.contains("Punt")) - .then(pl.col("text").str.extract(r"(?i)punt blocked by (.{0,25})|(?i)blocked by(.+)") - .str.replace(r"punt blocked by |for a(.+)", "") - .str.replace(r"blocked by(.+)", "") - .str.replace(r"blocked(.+)", "") - .str.replace(r" for(.+)", "") - .str.replace(r",(.+)", "") - .str.replace(r"punt blocked by |for a(.+)", "")) + # --- Punt Blocker Names ---- + punt_block_player=pl.when(pl.col("type.text").str.contains("Punt")) + .then( + pl.col("text") + .str.extract(r"(?i)punt blocked by (.{0,25})|(?i)blocked by(.+)") + .str.replace(r"punt blocked by |for a(.+)", "") + .str.replace(r"blocked by(.+)", "") + .str.replace(r"blocked(.+)", "") + .str.replace(r" for(.+)", "") + .str.replace(r",(.+)", "") + .str.replace(r"punt blocked by |for a(.+)", "") + ) .otherwise(None), - ).with_columns( - punt_block_player = pl.when((pl.col("type.text").str.contains(r"(?i)yd return of blocked punt"))) - .then(pl.col("text").str.extract(r"(?i)(.+) yd return of blocked") - .str.replace(r"(?i)blocked|(?i)Blocked", "") - .str.replace(r"(?i)\\d+", "") - .str.replace(r"(?i)yd return of", "")) + ) + .with_columns( + punt_block_player=pl.when((pl.col("type.text").str.contains(r"(?i)yd return of blocked punt"))) + .then( + pl.col("text") + .str.extract(r"(?i)(.+) yd return of blocked") + .str.replace(r"(?i)blocked|(?i)Blocked", "") + .str.replace(r"(?i)\\d+", "") + .str.replace(r"(?i)yd return of", "") + ) .otherwise(pl.col("punt_block_player")), - - # --- Punt Block Returner Names ---- - punt_block_return_player = pl.when((pl.col("type.text").str.contains(r"Punt")) - .and_(pl.col("text").str.contains(r"(?i)blocked")) - .and_(pl.col("text").str.contains(r"(?i)return"))) + # --- Punt Block Returner Names ---- + punt_block_return_player=pl.when( + (pl.col("type.text").str.contains(r"Punt")) + .and_(pl.col("text").str.contains(r"(?i)blocked")) + .and_(pl.col("text").str.contains(r"(?i)return")) + ) .then(pl.col("text").str.extract(r"(?i)(.+) return")) - .otherwise(None) - ).with_columns( - punt_block_return_player = pl.struct(['punt_block_player', 'punt_block_return_player']) - .apply(lambda cols: cols['punt_block_return_player'].str.replace(r"(?i)(.+)blocked by", "") - .str.replace(pl.format(r"(?i)blocked by {}", cols['punt_block_player']), ""), - return_dtype = pl.Utf8) - ).with_columns( - punt_block_return_player = pl.col("punt_block_return_player").str.replace(r"(?i)return(.+)", "") + .otherwise(None), + ) + .with_columns( + punt_block_return_player=pl.struct(["punt_block_player", "punt_block_return_player"]).apply( + lambda cols: cols["punt_block_return_player"] + .str.replace(r"(?i)(.+)blocked by", "") + .str.replace(pl.format(r"(?i)blocked by {}", cols["punt_block_player"]), ""), + return_dtype=pl.Utf8, + ) + ) + .with_columns( + punt_block_return_player=pl.col("punt_block_return_player") + .str.replace(r"(?i)return(.+)", "") .str.replace(r"(?i)return", "") .str.replace(r"for a TD(.+)|for a SAFETY(.+)", "") .str.replace(r"(?i)blocked by ", "") .str.replace(r", ", ""), - # --- Kickoff Names ---- - kickoff_player = pl.when(pl.col("type.text").str.contains(r"(?i)kickoff")) - .then(pl.col("text").str.extract(r"(?i)(.{0,25}) kickoff|(.{0,25}) on-side") - .str.replace(r"(?i) on-side| kickoff", "")) + # --- Kickoff Names ---- + kickoff_player=pl.when(pl.col("type.text").str.contains(r"(?i)kickoff")) + .then( + pl.col("text") + .str.extract(r"(?i)(.{0,25}) kickoff|(.{0,25}) on-side") + .str.replace(r"(?i) on-side| kickoff", "") + ) .otherwise(None), - # --- Kickoff Returner Names ---- - kickoff_return_player = pl.when(pl.col("type.text").str.contains(r"(?i)ickoff")) - .then(pl.col("text").str.extract(r"(?i), (.{0,25}) return|(?i), (.{0,25}) fumble|(?i)returned by (.{0,25})|(?i)touchback by (.{0,25})") - .str.replace(r", ", "") - .str.replace(r"(?i) return|(?i) fumble|(?i) returned by|(?i) for |(?i)touchback by ", "") - .str.replace(r"\((.+)\)(.+)", "")) + # --- Kickoff Returner Names ---- + kickoff_return_player=pl.when(pl.col("type.text").str.contains(r"(?i)ickoff")) + .then( + pl.col("text") + .str.extract( + r"(?i), (.{0,25}) return|(?i), (.{0,25}) fumble|(?i)returned by (.{0,25})|(?i)touchback by (.{0,25})" + ) + .str.replace(r", ", "") + .str.replace(r"(?i) return|(?i) fumble|(?i) returned by|(?i) for |(?i)touchback by ", "") + .str.replace(r"\((.+)\)(.+)", "") + ) .otherwise(None), - # --- Field Goal Kicker Names ---- - fg_kicker_player = pl.when(pl.col("type.text").str.contains(r"(?i)Field Goal")) - .then(pl.col("text").str.extract(r"(?i)(.{0,25} )\\d{0,2} yd field goal|(?i)(.{0,25} )\\d{0,2} yd fg|(?i)(.{0,25} )\\d{0,2} yard field goal") - .str.replace(r"(?i) Yd Field Goal|(?i)Yd FG |(?i)yd FG|(?i) yd FG", "") - .str.replace(r"(\\d{1,2})", "")) + # --- Field Goal Kicker Names ---- + fg_kicker_player=pl.when(pl.col("type.text").str.contains(r"(?i)Field Goal")) + .then( + pl.col("text") + .str.extract( + r"(?i)(.{0,25} )\\d{0,2} yd field goal|(?i)(.{0,25} )\\d{0,2} yd fg|(?i)(.{0,25} )\\d{0,2} yard field goal" + ) + .str.replace(r"(?i) Yd Field Goal|(?i)Yd FG |(?i)yd FG|(?i) yd FG", "") + .str.replace(r"(\\d{1,2})", "") + ) .otherwise(None), - # --- Field Goal Blocker Names ---- - fg_block_player = pl.when(pl.col("type.text").str.contains(r"(?i)Field Goal")) - .then(pl.col("text").str.extract(r"(?i)blocked by (.{0,25})") - .str.replace(r",(.+)", "") - .str.replace(r"blocked by ", "") - .str.replace(r" (.)+", "")) + # --- Field Goal Blocker Names ---- + fg_block_player=pl.when(pl.col("type.text").str.contains(r"(?i)Field Goal")) + .then( + pl.col("text") + .str.extract(r"(?i)blocked by (.{0,25})") + .str.replace(r",(.+)", "") + .str.replace(r"blocked by ", "") + .str.replace(r" (.)+", "") + ) .otherwise(None), - # --- Field Goal Returner Names ---- - fg_return_player = pl.when((pl.col("type.text").str.contains(r"(?i)Field Goal")) - .and_(pl.col("text").str.contains(r"(?i)blocked by|missed")) - .and_(pl.col("text").str.contains(r"(?i)return"))) - .then(pl.col("text").str.extract(r"(?i) (.+)") - .str.replace(r"(?i),(.+)", "") - .str.replace(r"(?i)return ", "") - .str.replace(r"(?i)returned ", "") - .str.replace(r"(?i) for (.+)", "") - .str.replace(r"(?i) for (.+)", "")) + # --- Field Goal Returner Names ---- + fg_return_player=pl.when( + (pl.col("type.text").str.contains(r"(?i)Field Goal")) + .and_(pl.col("text").str.contains(r"(?i)blocked by|missed")) + .and_(pl.col("text").str.contains(r"(?i)return")) + ) + .then( + pl.col("text") + .str.extract(r"(?i) (.+)") + .str.replace(r"(?i),(.+)", "") + .str.replace(r"(?i)return ", "") + .str.replace(r"(?i)returned ", "") + .str.replace(r"(?i) for (.+)", "") + .str.replace(r"(?i) for (.+)", "") + ) .otherwise(None), - ).with_columns( - fg_return_player = pl.when((pl.col("type.text").is_in(["Missed Field Goal Return", "Missed Field Goal Return Touchdown"]))) - .then(pl.col("text").str.extract(r"(?i)(.+)return") - .str.replace(r"(?i) return", "") - .str.replace(r"(?i)(.+),", "")) + ) + .with_columns( + fg_return_player=pl.when( + (pl.col("type.text").is_in(["Missed Field Goal Return", "Missed Field Goal Return Touchdown"])) + ) + .then( + pl.col("text") + .str.extract(r"(?i)(.+)return") + .str.replace(r"(?i) return", "") + .str.replace(r"(?i)(.+),", "") + ) .otherwise(pl.col("fg_return_player")), - # --- Fumble Recovery Names ---- - fumble_player = pl.when(pl.col("text").str.contains(r"(?i)fumble")) - .then(pl.col("text").str.extract(r"(?i)(.{0,25} )fumble|(?i)(.{0,25} )fumble") - .str.replace(r"(?i) fumble(.+)", "") - .str.replace(r"(?i)fumble", "") - .str.replace(r"(?i) yds", "") - .str.replace(r"(?i) yd", "") - .str.replace(r"(?i)yardline", "") - .str.replace(r"(?i) yards|(?i) yard|(?i)for a TD|(?i)or a safety", "") - .str.replace(r"(?i) for ", "") - .str.replace(r"(?i) a safety", "") - .str.replace(r"(?i)r no gain", "") - .str.replace(r"(?i)(.+)(\\d{1,2})", "") - .str.replace(r"(?i)(\\d{1,2})", "") - .str.replace(r", ", "")) + # --- Fumble Recovery Names ---- + fumble_player=pl.when(pl.col("text").str.contains(r"(?i)fumble")) + .then( + pl.col("text") + .str.extract(r"(?i)(.{0,25} )fumble|(?i)(.{0,25} )fumble") + .str.replace(r"(?i) fumble(.+)", "") + .str.replace(r"(?i)fumble", "") + .str.replace(r"(?i) yds", "") + .str.replace(r"(?i) yd", "") + .str.replace(r"(?i)yardline", "") + .str.replace(r"(?i) yards|(?i) yard|(?i)for a TD|(?i)or a safety", "") + .str.replace(r"(?i) for ", "") + .str.replace(r"(?i) a safety", "") + .str.replace(r"(?i)r no gain", "") + .str.replace(r"(?i)(.+)(\\d{1,2})", "") + .str.replace(r"(?i)(\\d{1,2})", "") + .str.replace(r", ", "") + ) .otherwise(None), - ).with_columns( - fumble_player = pl.when(pl.col("type.text") == "Penalty") - .then(None) - .otherwise(pl.col("fumble_player")), - # --- Forced Fumble Names ---- - fumble_forced_player = pl.when((pl.col("text").str.contains(r"(?i)fumble")) - .and_(pl.col("text").str.contains(r"(?i)forced by"))) - .then(pl.col("text").str.extract(r"(?i)forced by(.{0,25})") - .str.replace(r"(?i)(.+)forced by", "") - .str.replace(r"(?i)forced by", "") - .str.replace(r"(?i), recove(.+)", "") - .str.replace(r"(?i), re(.+)", "") - .str.replace(r"(?i), fo(.+)", "") - .str.replace(r"(?i), r", "") - .str.replace(r"(?i), ", "")) + ) + .with_columns( + fumble_player=pl.when(pl.col("type.text") == "Penalty").then(None).otherwise(pl.col("fumble_player")), + # --- Forced Fumble Names ---- + fumble_forced_player=pl.when( + (pl.col("text").str.contains(r"(?i)fumble")).and_(pl.col("text").str.contains(r"(?i)forced by")) + ) + .then( + pl.col("text") + .str.extract(r"(?i)forced by(.{0,25})") + .str.replace(r"(?i)(.+)forced by", "") + .str.replace(r"(?i)forced by", "") + .str.replace(r"(?i), recove(.+)", "") + .str.replace(r"(?i), re(.+)", "") + .str.replace(r"(?i), fo(.+)", "") + .str.replace(r"(?i), r", "") + .str.replace(r"(?i), ", "") + ) .otherwise(None), - ).with_columns( - fumble_forced_player = pl.when(pl.col("type.text") == "Penalty") + ) + .with_columns( + fumble_forced_player=pl.when(pl.col("type.text") == "Penalty") .then(None) .otherwise(pl.col("fumble_forced_player")), - # --- Fumble Recovered Names ---- - fumble_recovered_player = pl.when((pl.col("text").str.contains(r"(?i)fumble")) - .and_(pl.col("text").str.contains(r"(?i)recovered by"))) - .then(pl.col("text").str.extract(r"(?i)recovered by(.{0,30})") - .str.replace(r"(?i)for a 1ST down", "") - .str.replace(r"(?i)for a 1st down", "") - .str.replace(r"(?i)(.+)recovered", "") - .str.replace(r"(?i)(.+) by", "") - .str.replace(r"(?i), recove(.+)", "") - .str.replace(r"(?i), re(.+)", "") - .str.replace(r"(?i)a 1st down", "") - .str.replace(r"(?i) a 1st down", "") - .str.replace(r"(?i), for(.+)", "") - .str.replace(r"(?i) for a", "") - .str.replace(r"(?i) fo", "") - .str.replace(r"(?i) , r", "") - .str.replace(r"(?i), r", "") - .str.replace(r"(?i) (.+)", "") - .str.replace(r"(?i) ,", "") - .str.replace(r"(?i)penalty(.+)", "") - .str.replace(r"(?i)for a 1ST down", "")) + # --- Fumble Recovered Names ---- + fumble_recovered_player=pl.when( + (pl.col("text").str.contains(r"(?i)fumble")).and_(pl.col("text").str.contains(r"(?i)recovered by")) + ) + .then( + pl.col("text") + .str.extract(r"(?i)recovered by(.{0,30})") + .str.replace(r"(?i)for a 1ST down", "") + .str.replace(r"(?i)for a 1st down", "") + .str.replace(r"(?i)(.+)recovered", "") + .str.replace(r"(?i)(.+) by", "") + .str.replace(r"(?i), recove(.+)", "") + .str.replace(r"(?i), re(.+)", "") + .str.replace(r"(?i)a 1st down", "") + .str.replace(r"(?i) a 1st down", "") + .str.replace(r"(?i), for(.+)", "") + .str.replace(r"(?i) for a", "") + .str.replace(r"(?i) fo", "") + .str.replace(r"(?i) , r", "") + .str.replace(r"(?i), r", "") + .str.replace(r"(?i) (.+)", "") + .str.replace(r"(?i) ,", "") + .str.replace(r"(?i)penalty(.+)", "") + .str.replace(r"(?i)for a 1ST down", "") + ) .otherwise(None), - ).with_columns( - fumble_recovered_player = pl.when(pl.col("type.text") == "Penalty") + ) + .with_columns( + fumble_recovered_player=pl.when(pl.col("type.text") == "Penalty") .then(None) .otherwise(pl.col("fumble_recovered_player")), - - ).with_columns( - ## Extract player names - passer_player_name = pl.col("pass_player").str.strip(), - rusher_player_name = pl.col("rush_player").str.strip(), - receiver_player_name = pl.col("receiver_player").str.strip(), - sack_player_name = pl.col("sack_player1").str.strip(), - sack_player_name2 = pl.col("sack_player2").str.strip(), - pass_breakup_player_name = pl.col("pass_breakup_player").str.strip(), - interception_player_name = pl.col("interception_player").str.strip(), - fg_kicker_player_name = pl.col("fg_kicker_player").str.strip(), - fg_block_player_name = pl.col("fg_block_player").str.strip(), - fg_return_player_name = pl.col("fg_return_player").str.strip(), - kickoff_player_name = pl.col("kickoff_player").str.strip(), - kickoff_return_player_name = pl.col("kickoff_return_player").str.strip(), - punter_player_name = pl.col("punter_player").str.strip(), - punt_block_player_name = pl.col("punt_block_player").str.strip(), - punt_return_player_name = pl.col("punt_return_player").str.strip(), - punt_block_return_player_name = pl.col("punt_block_return_player").str.strip(), - fumble_player_name = pl.col("fumble_player").str.strip(), - fumble_forced_player_name = pl.col("fumble_forced_player").str.strip(), - fumble_recovered_player_name = pl.col("fumble_recovered_player").str.strip(), - ).drop([ - "rush_player", - "receiver_player", - "pass_player", - "sack_player1", - "sack_player2", - "pass_breakup_player", - "interception_player", - "punter_player", - "fg_kicker_player", - "fg_block_player", - "fg_return_player", - "kickoff_player", - "kickoff_return_player", - "punt_return_player", - "punt_block_player", - "punt_block_return_player", - "fumble_player", - "fumble_forced_player", - "fumble_recovered_player" - ]) + ) + .with_columns( + ## Extract player names + passer_player_name=pl.col("pass_player").str.strip(), + rusher_player_name=pl.col("rush_player").str.strip(), + receiver_player_name=pl.col("receiver_player").str.strip(), + sack_player_name=pl.col("sack_player1").str.strip(), + sack_player_name2=pl.col("sack_player2").str.strip(), + pass_breakup_player_name=pl.col("pass_breakup_player").str.strip(), + interception_player_name=pl.col("interception_player").str.strip(), + fg_kicker_player_name=pl.col("fg_kicker_player").str.strip(), + fg_block_player_name=pl.col("fg_block_player").str.strip(), + fg_return_player_name=pl.col("fg_return_player").str.strip(), + kickoff_player_name=pl.col("kickoff_player").str.strip(), + kickoff_return_player_name=pl.col("kickoff_return_player").str.strip(), + punter_player_name=pl.col("punter_player").str.strip(), + punt_block_player_name=pl.col("punt_block_player").str.strip(), + punt_return_player_name=pl.col("punt_return_player").str.strip(), + punt_block_return_player_name=pl.col("punt_block_return_player").str.strip(), + fumble_player_name=pl.col("fumble_player").str.strip(), + fumble_forced_player_name=pl.col("fumble_forced_player").str.strip(), + fumble_recovered_player_name=pl.col("fumble_recovered_player").str.strip(), + ) + .drop( + [ + "rush_player", + "receiver_player", + "pass_player", + "sack_player1", + "sack_player2", + "pass_breakup_player", + "interception_player", + "punter_player", + "fg_kicker_player", + "fg_block_player", + "fg_return_player", + "kickoff_player", + "kickoff_return_player", + "punt_return_player", + "punt_block_player", + "punt_block_return_player", + "fumble_player", + "fumble_forced_player", + "fumble_recovered_player", + ] + ) + ) return play_df def __after_cols(self, play_df): - play_df = play_df.with_columns( - new_down = pl.when(pl.col("type.text") == "Timeout") + play_df = ( + play_df.with_columns( + new_down=pl.when(pl.col("type.text") == "Timeout") .then(pl.col("start.down")) - .when((pl.col("type.text").is_in(penalty)) - .and_(pl.col("penalty_1st_conv") == True)) + .when((pl.col("type.text").is_in(penalty)).and_(pl.col("penalty_1st_conv") == True)) .then(1) - .when((pl.col("type.text").is_in(penalty)) - .and_(pl.col("penalty_1st_conv") == False) - .and_(pl.col("penalty_offset") == True) - .and_(pl.col("penalty_declined") == False)) + .when( + (pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == True) + .and_(pl.col("penalty_declined") == False) + ) .then(pl.col("start.down")) - .when((pl.col("type.text").is_in(penalty)) - .and_(pl.col("penalty_1st_conv") == False) - .and_(pl.col("penalty_offset") == True) - .and_(pl.col("penalty_declined") == True) - .and_(pl.col("statYardage") < pl.col("start.distance")) - .and_(pl.col("start.down") <= 3)) + .when( + (pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == True) + .and_(pl.col("penalty_declined") == True) + .and_(pl.col("statYardage") < pl.col("start.distance")) + .and_(pl.col("start.down") <= 3) + ) .then(pl.col("start.down") + 1) - .when((pl.col("type.text").is_in(penalty)) - .and_(pl.col("penalty_1st_conv") == False) - .and_(pl.col("penalty_offset") == True) - .and_(pl.col("penalty_declined") == True) - .and_(pl.col("statYardage") < pl.col("start.distance")) - .and_(pl.col("start.down") == 4)) + .when( + (pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == True) + .and_(pl.col("penalty_declined") == True) + .and_(pl.col("statYardage") < pl.col("start.distance")) + .and_(pl.col("start.down") == 4) + ) .then(1) - .when((pl.col("type.text").is_in(penalty)) - .and_(pl.col("penalty_1st_conv") == False) - .and_(pl.col("penalty_offset") == True) - .and_(pl.col("penalty_declined") == True) - .and_(pl.col("statYardage") >= pl.col("start.distance"))) + .when( + (pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == True) + .and_(pl.col("penalty_declined") == True) + .and_(pl.col("statYardage") >= pl.col("start.distance")) + ) .then(1) - .when((pl.col("type.text").is_in(penalty)) - .and_(pl.col("penalty_1st_conv") == False) - .and_(pl.col("penalty_offset") == False) - .and_(pl.col("penalty_declined") == True) - .and_(pl.col("statYardage") < pl.col("start.distance")) - .and_(pl.col("start.down") <= 3)) + .when( + (pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == False) + .and_(pl.col("penalty_declined") == True) + .and_(pl.col("statYardage") < pl.col("start.distance")) + .and_(pl.col("start.down") <= 3) + ) .then(pl.col("start.down") + 1) - .when((pl.col("type.text").is_in(penalty)) - .and_(pl.col("penalty_1st_conv") == False) - .and_(pl.col("penalty_offset") == False) - .and_(pl.col("penalty_declined") == True) - .and_(pl.col("statYardage") < pl.col("start.distance")) - .and_(pl.col("start.down") == 4)) + .when( + (pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == False) + .and_(pl.col("penalty_declined") == True) + .and_(pl.col("statYardage") < pl.col("start.distance")) + .and_(pl.col("start.down") == 4) + ) .then(1) - .when((pl.col("type.text").is_in(penalty)) - .and_(pl.col("penalty_1st_conv") == False) - .and_(pl.col("penalty_offset") == False) - .and_(pl.col("penalty_declined") == True) - .and_(pl.col("statYardage") >= pl.col("start.distance"))) + .when( + (pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == False) + .and_(pl.col("penalty_declined") == True) + .and_(pl.col("statYardage") >= pl.col("start.distance")) + ) .then(1) .otherwise(pl.col("start.down")), - new_distance = pl.when(pl.col("type.text") == "Timeout") + new_distance=pl.when(pl.col("type.text") == "Timeout") .then(pl.col("start.distance")) - .when((pl.col("type.text").is_in(penalty)) - .and_(pl.col("penalty_1st_conv") == True)) + .when((pl.col("type.text").is_in(penalty)).and_(pl.col("penalty_1st_conv") == True)) .then(10) - .when((pl.col("type.text").is_in(penalty)) - .and_(pl.col("penalty_1st_conv") == False) - .and_(pl.col("penalty_offset") == True) - .and_(pl.col("penalty_declined") == False)) + .when( + (pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == True) + .and_(pl.col("penalty_declined") == False) + ) .then(pl.col("start.distance")) - .when((pl.col("type.text").is_in(penalty)) - .and_(pl.col("penalty_1st_conv") == False) - .and_(pl.col("penalty_offset") == True) - .and_(pl.col("penalty_declined") == True) - .and_(pl.col("statYardage") < pl.col("start.distance")) - .and_(pl.col("start.down") <= 3)) + .when( + (pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == True) + .and_(pl.col("penalty_declined") == True) + .and_(pl.col("statYardage") < pl.col("start.distance")) + .and_(pl.col("start.down") <= 3) + ) .then(pl.col("start.distance")) - .when((pl.col("type.text").is_in(penalty)) - .and_(pl.col("penalty_1st_conv") == False) - .and_(pl.col("penalty_offset") == True) - .and_(pl.col("penalty_declined") == True) - .and_(pl.col("statYardage") < pl.col("start.distance")) - .and_(pl.col("start.down") == 4)) + .when( + (pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == True) + .and_(pl.col("penalty_declined") == True) + .and_(pl.col("statYardage") < pl.col("start.distance")) + .and_(pl.col("start.down") == 4) + ) .then(pl.col("start.distance")) - .when((pl.col("type.text").is_in(penalty)) - .and_(pl.col("penalty_1st_conv") == False) - .and_(pl.col("penalty_offset") == True) - .and_(pl.col("penalty_declined") == True) - .and_(pl.col("statYardage") >= pl.col("start.distance"))) + .when( + (pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == True) + .and_(pl.col("penalty_declined") == True) + .and_(pl.col("statYardage") >= pl.col("start.distance")) + ) .then(pl.col("start.distance")) - .when((pl.col("type.text").is_in(penalty)) - .and_(pl.col("penalty_1st_conv") == False) - .and_(pl.col("penalty_offset") == False) - .and_(pl.col("penalty_declined") == True) - .and_(pl.col("statYardage") < pl.col("start.distance")) - .and_(pl.col("start.down") <= 3)) + .when( + (pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == False) + .and_(pl.col("penalty_declined") == True) + .and_(pl.col("statYardage") < pl.col("start.distance")) + .and_(pl.col("start.down") <= 3) + ) .then(pl.col("start.distance")) - .when((pl.col("type.text").is_in(penalty)) - .and_(pl.col("penalty_1st_conv") == False) - .and_(pl.col("penalty_offset") == False) - .and_(pl.col("penalty_declined") == True) - .and_(pl.col("statYardage") < pl.col("start.distance")) - .and_(pl.col("start.down") == 4)) + .when( + (pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == False) + .and_(pl.col("penalty_declined") == True) + .and_(pl.col("statYardage") < pl.col("start.distance")) + .and_(pl.col("start.down") == 4) + ) .then(pl.col("start.distance")) - .when((pl.col("type.text").is_in(penalty)) - .and_(pl.col("penalty_1st_conv") == False) - .and_(pl.col("penalty_offset") == False) - .and_(pl.col("penalty_declined") == True) - .and_(pl.col("statYardage") >= pl.col("start.distance"))) + .when( + (pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == False) + .and_(pl.col("penalty_declined") == True) + .and_(pl.col("statYardage") >= pl.col("start.distance")) + ) .then(pl.col("start.distance")) .otherwise(pl.col("start.distance")), - middle_8 = pl.when((pl.col("start.adj_TimeSecsRem") >= 1560) - .and_(pl.col("start.adj_TimeSecsRem") <= 2040)) - .then(True) - .otherwise(False), - rz_play = pl.when(pl.col("start.yardLine") <= 20) - .then(True) - .otherwise(False), - under_2 = pl.when(pl.col("start.TimeSecsRem") <= 120) - .then(True) - .otherwise(False), - goal_to_go = pl.when(pl.col("start.yardLine") <= 10) - .then(True) - .otherwise(False), - scoring_opp = pl.when(pl.col("start.yardLine") <= 40) + middle_8=pl.when( + (pl.col("start.adj_TimeSecsRem") >= 1560).and_(pl.col("start.adj_TimeSecsRem") <= 2040) + ) .then(True) .otherwise(False), - stuffed_run = pl.when((pl.col("type.text") == "Rush") - .and_(pl.col("yds_rushed") <= 0)) + rz_play=pl.when(pl.col("start.yardLine") <= 20).then(True).otherwise(False), + under_2=pl.when(pl.col("start.TimeSecsRem") <= 120).then(True).otherwise(False), + goal_to_go=pl.when(pl.col("start.yardLine") <= 10).then(True).otherwise(False), + scoring_opp=pl.when(pl.col("start.yardLine") <= 40).then(True).otherwise(False), + stuffed_run=pl.when((pl.col("type.text") == "Rush").and_(pl.col("yds_rushed") <= 0)) .then(True) .otherwise(False), - stopped_run = pl.when((pl.col("type.text") == "Rush") - .and_(pl.col("yds_rushed") <= 2)) + stopped_run=pl.when((pl.col("type.text") == "Rush").and_(pl.col("yds_rushed") <= 2)) .then(True) .otherwise(False), - opportunity_run = pl.when((pl.col("type.text") == "Rush") - .and_(pl.col("yds_rushed") <= 4)) + opportunity_run=pl.when((pl.col("type.text") == "Rush").and_(pl.col("yds_rushed") <= 4)) .then(True) .otherwise(False), - highlight_run = pl.when((pl.col("type.text") == "Rush") - .and_(pl.col("yds_rushed") >= 8)) + highlight_run=pl.when((pl.col("type.text") == "Rush").and_(pl.col("yds_rushed") >= 8)) .then(True) .otherwise(False), - adj_rush_yardage = pl.when((pl.col("type.text") == "Rush") - .and_(pl.col("yds_rushed") > 8)) + adj_rush_yardage=pl.when((pl.col("type.text") == "Rush").and_(pl.col("yds_rushed") > 8)) .then(8) - .when((pl.col("type.text") == "Rush") - .and_(pl.col("yds_rushed") <= 8)) + .when((pl.col("type.text") == "Rush").and_(pl.col("yds_rushed") <= 8)) .then(pl.col("yds_rushed")) .otherwise(None), - ).with_columns( - line_yards = pl.when((pl.col("rush") == True) - .and_(pl.col("yds_rushed") < 0)) + ) + .with_columns( + line_yards=pl.when((pl.col("rush") == True).and_(pl.col("yds_rushed") < 0)) .then(1.2 * pl.col("adj_rush_yardage")) - .when((pl.col("rush") == True) - .and_(pl.col("yds_rushed") >= 0) - .and_(pl.col("yds_rushed") <= 3)) + .when((pl.col("rush") == True).and_(pl.col("yds_rushed") >= 0).and_(pl.col("yds_rushed") <= 3)) .then(pl.col("adj_rush_yardage")) - .when((pl.col("rush") == True) - .and_(pl.col("yds_rushed") >= 4) - .and_(pl.col("yds_rushed") <= 8)) + .when((pl.col("rush") == True).and_(pl.col("yds_rushed") >= 4).and_(pl.col("yds_rushed") <= 8)) .then(3 + 0.5 * (pl.col("adj_rush_yardage") - 3)) - .when((pl.col("rush") == True) - .and_(pl.col("yds_rushed") >= 8)) + .when((pl.col("rush") == True).and_(pl.col("yds_rushed") >= 8)) .then(5.5) .otherwise(None), - second_level_yards = pl.when((pl.col("rush") == True) - .and_(pl.col("yds_rushed") >= 4)) + second_level_yards=pl.when((pl.col("rush") == True).and_(pl.col("yds_rushed") >= 4)) .then(0.5 * (pl.col("adj_rush_yardage") - 4)) .when(pl.col("rush") == True) .then(0) .otherwise(None), - open_field_yards = pl.when((pl.col("rush") == True) - .and_(pl.col("yds_rushed") > 8)) + open_field_yards=pl.when((pl.col("rush") == True).and_(pl.col("yds_rushed") > 8)) .then(pl.col("yds_rushed") - pl.col("adj_rush_yardage")) .when(pl.col("rush") == True) .then(0) .otherwise(None), - ).with_columns( - highlight_yards = pl.col("second_level_yards") + pl.col("open_field_yards"), - ).with_columns( - opp_highlight_yards = pl.when(pl.col("opportunity_run") == True) + ) + .with_columns( + highlight_yards=pl.col("second_level_yards") + pl.col("open_field_yards"), + ) + .with_columns( + opp_highlight_yards=pl.when(pl.col("opportunity_run") == True) .then(pl.col("highlight_yards")) - .when((pl.col("opportunity_run") == False) - .and_(pl.col("rush") == True)) + .when((pl.col("opportunity_run") == False).and_(pl.col("rush") == True)) .then(0) .otherwise(None), - short_rush_success = pl.when((pl.col("start.distance") < 2) - .and_(pl.col("rush") == True) - .and_(pl.col("statYardage") >= pl.col("start.distance"))) + short_rush_success=pl.when( + (pl.col("start.distance") < 2) + .and_(pl.col("rush") == True) + .and_(pl.col("statYardage") >= pl.col("start.distance")) + ) .then(True) - .when((pl.col("start.distance") < 2) - .and_(pl.col("rush") == True) - .and_(pl.col("statYardage") < pl.col("start.distance"))) + .when( + (pl.col("start.distance") < 2) + .and_(pl.col("rush") == True) + .and_(pl.col("statYardage") < pl.col("start.distance")) + ) .then(False) .otherwise(None), - short_rush_attempt = pl.when((pl.col("start.distance") < 2) - .and_(pl.col("rush") == True)) + short_rush_attempt=pl.when((pl.col("start.distance") < 2).and_(pl.col("rush") == True)) .then(True) - .when((pl.col("start.distance") >= 2) - .and_(pl.col("rush") == True)) + .when((pl.col("start.distance") >= 2).and_(pl.col("rush") == True)) .then(False) .otherwise(None), - power_rush_success = pl.when((pl.col("start.distance") < 2) - .and_(pl.col("rush") == True) - .and_(pl.col("start.down").is_in([3, 4])) - .and_(pl.col("statYardage") >= pl.col("start.distance"))) - .then(True) - .when((pl.col("start.distance") < 2) - .and_(pl.col("rush") == True) - .and_(pl.col("start.down").is_in([3, 4])) - .and_(pl.col("statYardage") < pl.col("start.distance"))) + power_rush_success=pl.when( + (pl.col("start.distance") < 2) + .and_(pl.col("rush") == True) + .and_(pl.col("start.down").is_in([3, 4])) + .and_(pl.col("statYardage") >= pl.col("start.distance")) + ) + .then(True) + .when( + (pl.col("start.distance") < 2) + .and_(pl.col("rush") == True) + .and_(pl.col("start.down").is_in([3, 4])) + .and_(pl.col("statYardage") < pl.col("start.distance")) + ) .then(False) .otherwise(None), - power_rush_attempt = pl.when((pl.col("start.distance") < 2) - .and_(pl.col("rush") == True) - .and_(pl.col("start.down").is_in([3, 4]))) + power_rush_attempt=pl.when( + (pl.col("start.distance") < 2) + .and_(pl.col("rush") == True) + .and_(pl.col("start.down").is_in([3, 4])) + ) .then(True) - .when((pl.col("start.distance") < 2) - .and_(pl.col("rush") == True) - .and_(pl.col("start.down").is_in([3, 4]))) + .when( + (pl.col("start.distance") < 2) + .and_(pl.col("rush") == True) + .and_(pl.col("start.down").is_in([3, 4])) + ) .then(False) .otherwise(None), - early_down = pl.when(((pl.col("down_1") == True) - .or_(pl.col("down_2") == True)) - .and_(pl.col("scrimmage_play") == True)) + early_down=pl.when( + ((pl.col("down_1") == True).or_(pl.col("down_2") == True)).and_(pl.col("scrimmage_play") == True) + ) .then(True) .otherwise(False), - late_down = pl.when(((pl.col("down_3") == True) - .or_(pl.col("down_4"))) - .and_(pl.col("scrimmage_play") == True)) + late_down=pl.when( + ((pl.col("down_3") == True).or_(pl.col("down_4"))).and_(pl.col("scrimmage_play") == True) + ) .then(True) .otherwise(False), - ).with_columns( - early_down_pass = pl.when((pl.col("pass") == True) - .and_(pl.col("early_down") == True)) + ) + .with_columns( + early_down_pass=pl.when((pl.col("pass") == True).and_(pl.col("early_down") == True)) .then(True) .otherwise(False), - early_down_rush = pl.when((pl.col("rush") == True) - .and_(pl.col("early_down") == True)) + early_down_rush=pl.when((pl.col("rush") == True).and_(pl.col("early_down") == True)) .then(True) .otherwise(False), - late_down_pass = pl.when((pl.col("pass") == True) - .and_(pl.col("late_down") == True)) + late_down_pass=pl.when((pl.col("pass") == True).and_(pl.col("late_down") == True)) .then(True) .otherwise(False), - late_down_rush = pl.when((pl.col("rush") == True) - .and_(pl.col("late_down") == True)) + late_down_rush=pl.when((pl.col("rush") == True).and_(pl.col("late_down") == True)) .then(True) .otherwise(False), - standard_down = pl.when((pl.col("scrimmage_play") == True) - .and_(pl.col("down_1") == True)) + standard_down=pl.when((pl.col("scrimmage_play") == True).and_(pl.col("down_1") == True)) .then(True) - .when((pl.col("scrimmage_play") == True) - .and_(pl.col("down_2") == True) - .and_(pl.col("start.distance") < 8)) + .when( + (pl.col("scrimmage_play") == True) + .and_(pl.col("down_2") == True) + .and_(pl.col("start.distance") < 8) + ) .then(True) - .when((pl.col("scrimmage_play") == True) - .and_(pl.col("down_3") == True) - .and_(pl.col("start.distance") < 5)) + .when( + (pl.col("scrimmage_play") == True) + .and_(pl.col("down_3") == True) + .and_(pl.col("start.distance") < 5) + ) .then(True) - .when((pl.col("scrimmage_play") == True) - .and_(pl.col("down_4") == True) - .and_(pl.col("start.distance") < 5)) + .when( + (pl.col("scrimmage_play") == True) + .and_(pl.col("down_4") == True) + .and_(pl.col("start.distance") < 5) + ) .then(True) .otherwise(False), - passing_down = pl.when((pl.col("scrimmage_play") == True) - .and_(pl.col("down_2") == True) - .and_(pl.col("start.distance") >= 8)) + passing_down=pl.when( + (pl.col("scrimmage_play") == True) + .and_(pl.col("down_2") == True) + .and_(pl.col("start.distance") >= 8) + ) .then(True) - .when((pl.col("scrimmage_play") == True) - .and_(pl.col("down_3") == True) - .and_(pl.col("start.distance") >= 5)) + .when( + (pl.col("scrimmage_play") == True) + .and_(pl.col("down_3") == True) + .and_(pl.col("start.distance") >= 5) + ) .then(True) - .when((pl.col("scrimmage_play") == True) - .and_(pl.col("down_4") == True) - .and_(pl.col("start.distance") >= 5)) + .when( + (pl.col("scrimmage_play") == True) + .and_(pl.col("down_4") == True) + .and_(pl.col("start.distance") >= 5) + ) .then(True) .otherwise(False), - TFL = pl.when((pl.col("type.text") != "Penalty") - .and_(pl.col("sp") == False) - .and_(pl.col("statYardage") < 0)) + TFL=pl.when( + (pl.col("type.text") != "Penalty").and_(pl.col("sp") == False).and_(pl.col("statYardage") < 0) + ) .then(True) .when(pl.col("sack_vec") == True) .then(True) .otherwise(False), - ).with_columns( - TFL_pass = pl.when((pl.col("TFL") == True) - .and_(pl.col("pass") == True)) - .then(True) - .otherwise(False), - TFL_rush = pl.when((pl.col("TFL") == True) - .and_(pl.col("rush") == True)) - .then(True) - .otherwise(False), - havoc = pl.when(pl.col("pass_breakup") == True) + ) + .with_columns( + TFL_pass=pl.when((pl.col("TFL") == True).and_(pl.col("pass") == True)).then(True).otherwise(False), + TFL_rush=pl.when((pl.col("TFL") == True).and_(pl.col("rush") == True)).then(True).otherwise(False), + havoc=pl.when(pl.col("pass_breakup") == True) .then(True) .when(pl.col("TFL") == True) .then(True) @@ -2530,27 +3034,33 @@ def __after_cols(self, play_df): .then(True) .when(pl.col("forced_fumble") == True) .then(True) - .otherwise(False) + .otherwise(False), + ) ) return play_df def __add_spread_time(self, play_df): - play_df = play_df.with_columns( - pl.when(pl.col("start.pos_team.id") == pl.col("homeTeamId")) + play_df = ( + play_df.with_columns( + pl.when(pl.col("start.pos_team.id") == pl.col("homeTeamId")) .then(pl.col("homeTeamSpread")) .otherwise(-1 * pl.col("homeTeamSpread")) .alias("start.pos_team_spread"), - ((3600 - pl.col("start.adj_TimeSecsRem")) / 3600).clip(0, 3600).alias("start.elapsed_share") - ).with_columns( - (pl.col("start.pos_team_spread") * np.exp(-4 * pl.col("start.elapsed_share"))).alias("start.spread_time"), - pl.when(pl.col("end.pos_team.id") == pl.col("homeTeamId")) - .then(pl.col("homeTeamSpread")) - .otherwise(-1 * pl.col("homeTeamSpread")) - .alias("end.pos_team_spread"), - ((3600 - pl.col("end.adj_TimeSecsRem")) / 3600).clip(0, 3600).alias("end.elapsed_share"), - ).with_columns( - (pl.col("end.pos_team_spread") * np.exp(-4 * pl.col("end.elapsed_share"))).alias("end.spread_time"), - + ((3600 - pl.col("start.adj_TimeSecsRem")) / 3600).clip(0, 3600).alias("start.elapsed_share"), + ) + .with_columns( + (pl.col("start.pos_team_spread") * np.exp(-4 * pl.col("start.elapsed_share"))).alias( + "start.spread_time" + ), + pl.when(pl.col("end.pos_team.id") == pl.col("homeTeamId")) + .then(pl.col("homeTeamSpread")) + .otherwise(-1 * pl.col("homeTeamSpread")) + .alias("end.pos_team_spread"), + ((3600 - pl.col("end.adj_TimeSecsRem")) / 3600).clip(0, 3600).alias("end.elapsed_share"), + ) + .with_columns( + (pl.col("end.pos_team_spread") * np.exp(-4 * pl.col("end.elapsed_share"))).alias("end.spread_time"), + ) ) return play_df @@ -2566,44 +3076,34 @@ def __calculate_ep_exp_val(self, matrix): ) def __process_epa(self, play_df): - play_df = play_df.with_columns( - down = pl.when(pl.col("type.text").is_in(kickoff_vec)) - .then(1) - .otherwise(pl.col("start.down")), - down_1 = pl.when(pl.col("type.text").is_in(kickoff_vec)) - .then(True) - .otherwise(pl.col("down_1")), - down_2 = pl.when(pl.col("type.text").is_in(kickoff_vec)) - .then(False) - .otherwise(pl.col("down_2")), - down_3 = pl.when(pl.col("type.text").is_in(kickoff_vec)) - .then(False) - .otherwise(pl.col("down_3")), - down_4 = pl.when(pl.col("type.text").is_in(kickoff_vec)) - .then(False) - .otherwise(pl.col("down_4")), - distance = pl.when(pl.col("type.text").is_in(kickoff_vec)) - .then(10) - .otherwise(pl.col("start.distance")), - ).with_columns( - pl.when(pl.col("type.text").is_in(kickoff_vec)) + play_df = ( + play_df.with_columns( + down=pl.when(pl.col("type.text").is_in(kickoff_vec)).then(1).otherwise(pl.col("start.down")), + down_1=pl.when(pl.col("type.text").is_in(kickoff_vec)).then(True).otherwise(pl.col("down_1")), + down_2=pl.when(pl.col("type.text").is_in(kickoff_vec)).then(False).otherwise(pl.col("down_2")), + down_3=pl.when(pl.col("type.text").is_in(kickoff_vec)).then(False).otherwise(pl.col("down_3")), + down_4=pl.when(pl.col("type.text").is_in(kickoff_vec)).then(False).otherwise(pl.col("down_4")), + distance=pl.when(pl.col("type.text").is_in(kickoff_vec)).then(10).otherwise(pl.col("start.distance")), + ) + .with_columns( + pl.when(pl.col("type.text").is_in(kickoff_vec)) .then(1) .otherwise(pl.col("start.down")) .alias("start.down"), - pl.when(pl.col("type.text").is_in(kickoff_vec)) + pl.when(pl.col("type.text").is_in(kickoff_vec)) .then(10) .otherwise(pl.col("start.distance")) .alias("start.distance"), - pl.lit(99).alias("start.yardsToEndzone.touchback") - ).with_columns( - pl.when((pl.col("type.text").is_in(kickoff_vec)) - .and_(pl.col("season") > 2013)) + pl.lit(99).alias("start.yardsToEndzone.touchback"), + ) + .with_columns( + pl.when((pl.col("type.text").is_in(kickoff_vec)).and_(pl.col("season") > 2013)) .then(75) - .when((pl.col("type.text").is_in(kickoff_vec)) - .and_(pl.col("season") <= 2013)) + .when((pl.col("type.text").is_in(kickoff_vec)).and_(pl.col("season") <= 2013)) .then(80) .otherwise(pl.col("start.yardsToEndzone")) .alias("start.yardsToEndzone.touchback"), + ) ) start_touchback_data = play_df[ep_start_touchback_columns] @@ -2623,74 +3123,63 @@ def __process_epa(self, play_df): EP_start_parts = ep_model.predict(dtest_start) EP_start = self.__calculate_ep_exp_val(EP_start_parts) - - play_df = play_df.with_columns( + play_df = ( + play_df.with_columns( pl.when(pl.col("end.TimeSecsRem") <= 0) - .then(0) - .otherwise(pl.col("end.TimeSecsRem")) - .alias("end.TimeSecsRem"), - ).with_columns( - pl.when((pl.col("end.TimeSecsRem") <= 0) - .and_(pl.col("period") < 5)) - .then(99) - .otherwise(pl.col("end.yardsToEndzone")) - .alias("end.yardsToEndzone"), - pl.when((pl.col("end.TimeSecsRem") <= 0) - .and_(pl.col("period") < 5)) - .then(True) - .otherwise(pl.col("down_1_end")) - .alias("down_1_end"), - pl.when((pl.col("end.TimeSecsRem") <= 0) - .and_(pl.col("period") < 5)) - .then(False) - .otherwise(pl.col("down_2_end")) - .alias("down_2_end"), - pl.when((pl.col("end.TimeSecsRem") <= 0) - .and_(pl.col("period") < 5)) - .then(False) - .otherwise(pl.col("down_3_end")) - .alias("down_3_end"), - pl.when((pl.col("end.TimeSecsRem") <= 0) - .and_(pl.col("period") < 5)) - .then(False) - .otherwise(pl.col("down_4_end")) - .alias("down_4_end"), - ).with_columns( + .then(0) + .otherwise(pl.col("end.TimeSecsRem")) + .alias("end.TimeSecsRem"), + ) + .with_columns( + pl.when((pl.col("end.TimeSecsRem") <= 0).and_(pl.col("period") < 5)) + .then(99) + .otherwise(pl.col("end.yardsToEndzone")) + .alias("end.yardsToEndzone"), + pl.when((pl.col("end.TimeSecsRem") <= 0).and_(pl.col("period") < 5)) + .then(True) + .otherwise(pl.col("down_1_end")) + .alias("down_1_end"), + pl.when((pl.col("end.TimeSecsRem") <= 0).and_(pl.col("period") < 5)) + .then(False) + .otherwise(pl.col("down_2_end")) + .alias("down_2_end"), + pl.when((pl.col("end.TimeSecsRem") <= 0).and_(pl.col("period") < 5)) + .then(False) + .otherwise(pl.col("down_3_end")) + .alias("down_3_end"), + pl.when((pl.col("end.TimeSecsRem") <= 0).and_(pl.col("period") < 5)) + .then(False) + .otherwise(pl.col("down_4_end")) + .alias("down_4_end"), + ) + .with_columns( pl.when(pl.col("end.yardsToEndzone") >= 100) - .then(99) - .otherwise(pl.col("end.yardsToEndzone")) - .alias("end.yardsToEndzone"), - ).with_columns( + .then(99) + .otherwise(pl.col("end.yardsToEndzone")) + .alias("end.yardsToEndzone"), + ) + .with_columns( pl.when(pl.col("end.yardsToEndzone") <= 0) - .then(99) - .otherwise(pl.col("end.yardsToEndzone")) - .alias("end.yardsToEndzone"), - ).with_columns( - pl.when(pl.col("kickoff_tb") == True) - .then(75) - .otherwise(pl.col("end.yardsToEndzone")) - .alias("end.yardsToEndzone"), - pl.when(pl.col("kickoff_tb") == True) - .then(1) - .otherwise(pl.col("end.down")) - .alias("end.down"), + .then(99) + .otherwise(pl.col("end.yardsToEndzone")) + .alias("end.yardsToEndzone"), + ) + .with_columns( pl.when(pl.col("kickoff_tb") == True) - .then(10) - .otherwise(pl.col("end.distance")) - .alias("end.distance"), - ).with_columns( + .then(75) + .otherwise(pl.col("end.yardsToEndzone")) + .alias("end.yardsToEndzone"), + pl.when(pl.col("kickoff_tb") == True).then(1).otherwise(pl.col("end.down")).alias("end.down"), + pl.when(pl.col("kickoff_tb") == True).then(10).otherwise(pl.col("end.distance")).alias("end.distance"), + ) + .with_columns( + pl.when(pl.col("punt_tb") == True).then(1).otherwise(pl.col("end.down")).alias("end.down"), + pl.when(pl.col("punt_tb") == True).then(10).otherwise(pl.col("end.distance")).alias("end.distance"), pl.when(pl.col("punt_tb") == True) - .then(1) - .otherwise(pl.col("end.down")) - .alias("end.down"), - pl.when(pl.col("punt_tb") == True) - .then(10) - .otherwise(pl.col("end.distance")) - .alias("end.distance"), - pl.when(pl.col("punt_tb") == True) - .then(80) - .otherwise(pl.col("end.yardsToEndzone")) - .alias("end.yardsToEndzone"), + .then(80) + .otherwise(pl.col("end.yardsToEndzone")) + .alias("end.yardsToEndzone"), + ) ) end_data = play_df[ep_end_columns] @@ -2702,419 +3191,421 @@ def __process_epa(self, play_df): EP_end = self.__calculate_ep_exp_val(EP_end_parts) play_df = play_df.with_columns( - EP_start_touchback = pl.lit(EP_start_touchback), - EP_start = pl.lit(EP_start), - EP_end = pl.lit(EP_end), + EP_start_touchback=pl.lit(EP_start_touchback), + EP_start=pl.lit(EP_start), + EP_end=pl.lit(EP_end), ) kick = "kick)" - play_df = play_df.with_columns( - EP_start = pl.when(pl.col("type.text").is_in([ - "Extra Point Good", - "Extra Point Missed", - "Two-Point Conversion Good", - "Two-Point Conversion Missed", - "Two Point Pass", - "Two Point Rush", - "Blocked PAT" - ])) - .then(0.92) - .otherwise(pl.col("EP_start")), - ).with_columns( - # End of Half - EP_end = pl.when((pl.col("type.text").str.to_lowercase().str.contains(r"end of game")) - .or_(pl.col("type.text").str.to_lowercase().str.contains(r"end of half"))) - .then(0) - # Defensive 2pt Conversion - .when(pl.col("type.text").is_in(["Defensive 2pt Conversion"])) - .then(-2) - # Safeties - .when((pl.col("type.text").is_in(defense_score_vec)) - .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)safety"))) - .then(-2) - # Defense TD + Successful Two-Point Conversion - .when((pl.col("type.text").is_in(defense_score_vec)) - .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)conversion")) - .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)failed") == False)) - .then(-8) - # Defense TD + Failed Two-Point Conversion - .when((pl.col("type.text").is_in(defense_score_vec)) - .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)conversion")) - .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)failed"))) - .then(-6) - # Defense TD + Kick/PAT Missed - .when((pl.col("type.text").is_in(defense_score_vec)) - .and_(pl.col("text").str.to_lowercase().str.contains(r"PAT")) - .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)missed"))) - .then(-6) - # Defense TD + Kick/PAT Good - .when((pl.col("type.text").is_in(defense_score_vec)) - .and_(pl.col("text").str.to_lowercase().str.contains(r"kick\)"))) - .then(-7) - # Defense TD - .when(pl.col("type.text").is_in(defense_score_vec)) - .then(-6.92) - # Offense TD + Failed Two-Point Conversion - .when((pl.col("type.text").is_in(offense_score_vec)) - .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)conversion")) - .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)failed"))) - .then(6) - # Offense TD + Successful Two-Point Conversion - .when((pl.col("type.text").is_in(offense_score_vec)) - .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)conversion")) - .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)failed") == False)) - .then(8) - # Offense Made FG - .when((pl.col("type.text").is_in(offense_score_vec)) - .and_(pl.col("type.text").str.to_lowercase().str.contains(r"(?i)field goal")) - .and_(pl.col("type.text").str.to_lowercase().str.contains(r"(?i)good"))) - .then(3) - # Offense TD + Kick/PAT Missed - .when((pl.col("type.text").is_in(offense_score_vec)) - .and_(pl.col("text").str.to_lowercase().str.contains(r"PAT")) - .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)missed"))) - .then(6) - # Offense TD + Kick/PAT Good - .when((pl.col("type.text").is_in(offense_score_vec)) - .and_(pl.col("text").str.to_lowercase().str.contains(r"kick\)"))) - .then(7) - # Offense TD - .when(pl.col("type.text").is_in(offense_score_vec)) - .then(6.92) - # Extra Point Good - .when(pl.col("type.text").is_in(["Extra Point Good"])) - .then(1) - # Extra Point Missed - .when(pl.col("type.text").is_in(["Extra Point Missed"])) - .then(0) - # Two-Point Conversion Good - .when(pl.col("type.text").is_in(["Two-Point Conversion Good"])) - .then(2) - # Two-Point Conversion Missed - .when(pl.col("type.text").is_in(["Two-Point Conversion Missed"])) - .then(0) - # Two Point Pass/Rush Missed (Pre-2014 Data) - .when((pl.col("type.text").is_in(["Two Point Pass", "Two Point Rush"])) - .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)no good"))) - .then(0) - # Two Point Pass/Rush Good (Pre-2014 Data) - .when((pl.col("type.text").is_in(["Two Point Pass", "Two Point Rush"])) - .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)no good") == False)) - .then(2) - # Blocked PAT - .when(pl.col("type.text").is_in(["Blocked PAT"])) - .then(0) - # Flips for Turnovers that aren't kickoffs - .when(((pl.col("type.text").is_in(end_change_vec)) - .or_(pl.col("downs_turnover") == True)) - .and_(pl.col("type.text").is_in(kickoff_vec) == False)) - .then(pl.col("EP_end") * -1) - # Flips for Turnovers that are kickoffs - .when(pl.col("type.text").is_in(kickoff_turnovers)) - .then(pl.col("EP_end") * -1) - # Onside kicks - .when((pl.col("kickoff_onside") == True) - .and_(pl.col("change_of_pos_team") == True)) - .then(pl.col("EP_end") * -1) - .otherwise(pl.col("EP_end")) - ).with_columns( - lag_EP_end = pl.col("EP_end").shift(1), - lag_change_of_pos_team = pl.col("change_of_pos_team").shift(1), - - ).with_columns( - lag_change_of_pos_team = pl.when(pl.col("lag_change_of_pos_team").is_null()) - .then(False) - .otherwise(pl.col("lag_change_of_pos_team")), - - ).with_columns( - EP_between = pl.when(pl.col("lag_change_of_pos_team") == True) - .then(pl.col("EP_start") + pl.col("lag_EP_end")) - .otherwise(pl.col("EP_start") - pl.col("lag_EP_end")), - EP_start = pl.when((pl.col("type.text").is_in(["Timeout", "End Period"])) - .and_(pl.col("lag_change_of_pos_team") == False)) - .then(pl.col("lag_EP_end")) - .otherwise(pl.col("EP_start")), - - ).with_columns( - EP_start = pl.when(pl.col("type.text").is_in(kickoff_vec)) - .then(pl.col("EP_start_touchback")) - .otherwise(pl.col("EP_start")), - - ).with_columns( - EP_end = pl.when(pl.col("type.text").is_in(["Timeout"])) - .then(pl.col("EP_start")) - .otherwise(pl.col("EP_end")), - - ).with_columns( - EPA = pl.when(pl.col("type.text").is_in(["Timeout"])) - .then(0) - .when((pl.col("scoring_play") == False) - .and_(pl.col("end_of_half") == True)) - .then(-1 * pl.col("EP_start")) - .when((pl.col("type.text").is_in(kickoff_vec)) - .and_(pl.col("penalty_in_text") == True)) - .then(pl.col("EP_end") - pl.col("EP_start")) - .when((pl.col("penalty_in_text") == True) - .and_(pl.col("type.text").is_in(["Penalty"]) == False) - .and_(pl.col("type.text").is_in(kickoff_vec) == False)) - .then(pl.col("EP_end") - pl.col("EP_start") + pl.col("EP_between")) - .otherwise(pl.col("EP_end") - pl.col("EP_start")), - ).with_columns( - def_EPA = pl.col("EPA") * -1, - # --- EPA Summary flags ---- - EPA_scrimmage = pl.when(pl.col("scrimmage_play") == True) - .then(pl.col("EPA")) - .otherwise(None), - EPA_rush = pl.when((pl.col("rush") == True) - .and_(pl.col("penalty_in_text") == True)) - .then(pl.col("EPA")) - .when((pl.col("rush") == True) - .and_(pl.col("penalty_in_text") == False)) - .then(pl.col("EPA")) - .otherwise(None), - EPA_pass = pl.when(pl.col("pass") == True) - .then(pl.col("EPA")) - .otherwise(None), - EPA_explosive = pl.when((pl.col("pass") == True) - .and_(pl.col("EPA") >= 2.4)) - .then(True) - .when(((pl.col("rush") == True) - .and_(pl.col("EPA") >= 1.8))) - .then(True) - .otherwise(False), - ).with_columns( - EPA_non_explosive = pl.when(pl.col("EPA_explosive") == False) - .then(pl.col("EPA")) - .otherwise(None), - EPA_explosive_pass = pl.when((pl.col("pass") == True) - .and_(pl.col("EPA") >= 2.4)) - .then(True) - .otherwise(False), - EPA_explosive_rush = pl.when((pl.col("rush") == True) - .and_(pl.col("EPA") >= 1.8)) - .then(True) - .otherwise(False), - first_down_created = pl.when((pl.col("scrimmage_play") == True) - .and_(pl.col("end.down") == 1) - .and_(pl.col("start.pos_team.id") == pl.col("end.pos_team.id"))) - .then(True) - .otherwise(False), - EPA_success = pl.when(pl.col("EPA") > 0) - .then(True) - .otherwise(False), - EPA_success_early_down = pl.when((pl.col("EPA") > 0) - .and_(pl.col("early_down") == True)) - .then(True) - .otherwise(False), - EPA_success_early_down_pass = pl.when((pl.col("pass") == True) - .and_(pl.col("EPA") > 0) - .and_(pl.col("early_down") == True)) - .then(True) - .otherwise(False), - EPA_success_early_down_rush = pl.when((pl.col("rush") == True) - .and_(pl.col("EPA") > 0) - .and_(pl.col("early_down") == True)) - .then(True) - .otherwise(False), - EPA_success_late_down = pl.when((pl.col("EPA") > 0) - .and_(pl.col("late_down") == True)) - .then(True) - .otherwise(False), - EPA_success_late_down_pass = pl.when((pl.col("pass") == True) - .and_(pl.col("EPA") > 0) - .and_(pl.col("late_down") == True)) - .then(True) - .otherwise(False), - EPA_success_late_down_rush = pl.when((pl.col("rush") == True) - .and_(pl.col("EPA") > 0) - .and_(pl.col("late_down") == True)) - .then(True) - .otherwise(False), - EPA_success_standard_down = pl.when((pl.col("EPA") > 0) - .and_(pl.col("standard_down") == True)) - .then(True) - .otherwise(False), - EPA_success_passing_down = pl.when((pl.col("EPA") > 0) - .and_(pl.col("passing_down") == True)) - .then(True) - .otherwise(False), - EPA_success_pass = pl.when((pl.col("EPA") > 0) - .and_(pl.col("pass") == True)) - .then(True) - .otherwise(False), - EPA_success_rush = pl.when((pl.col("EPA") > 0) - .and_(pl.col("rush") == True)) - .then(True) - .otherwise(False), - EPA_success_EPA = pl.when(pl.col("EPA") > 0) - .then(pl.col("EPA")) - .otherwise(None), - EPA_success_standard_down_EPA = pl.when((pl.col("EPA") > 0) - .and_(pl.col("standard_down") == True)) - .then(pl.col("EPA")) - .otherwise(None), - EPA_success_passing_down_EPA = pl.when((pl.col("EPA") > 0) - .and_(pl.col("passing_down") == True)) - .then(pl.col("EPA")) - .otherwise(None), - EPA_success_pass_EPA = pl.when((pl.col("EPA") > 0) - .and_(pl.col("pass") == True)) - .then(pl.col("EPA")) - .otherwise(None), - EPA_success_rush_EPA = pl.when((pl.col("EPA") > 0) - .and_(pl.col("rush") == True)) - .then(pl.col("EPA")) - .otherwise(None), - EPA_middle_8_success = pl.when((pl.col("EPA") > 0) - .and_(pl.col("middle_8") == True)) - .then(True) - .otherwise(False), - EPA_middle_8_success_pass = pl.when((pl.col("pass") == True) - .and_(pl.col("EPA") > 0) - .and_(pl.col("middle_8") == True)) - .then(True) - .otherwise(False), - EPA_middle_8_success_rush = pl.when((pl.col("rush") == True) - .and_(pl.col("EPA") > 0) - .and_(pl.col("middle_8") == True)) - .then(True) - .otherwise(False), - EPA_penalty = pl.when(pl.col("type.text").is_in(["Penalty", "Penalty (Kickoff)"])) - .then(pl.col("EPA")) - .when(pl.col("penalty_in_text") == True) - .then(pl.col("EP_end") - pl.col("EP_start")) - .otherwise(None), - EPA_sp = pl.when((pl.col("fg_attempt") == True) - .or_(pl.col("punt") == True) - .or_(pl.col("kickoff_play") == True)) - .then(pl.col("EPA")) - .otherwise(False), - EPA_fg = pl.when(pl.col("fg_attempt") == True) - .then(pl.col("EPA")) - .otherwise(None), - EPA_punt = pl.when(pl.col("punt") == True) - .then(pl.col("EPA")) - .otherwise(None), - EPA_kickoff = pl.when(pl.col("kickoff_play") == True) - .then(pl.col("EPA")) - .otherwise(None), + play_df = ( + play_df.with_columns( + EP_start=pl.when( + pl.col("type.text").is_in( + [ + "Extra Point Good", + "Extra Point Missed", + "Two-Point Conversion Good", + "Two-Point Conversion Missed", + "Two Point Pass", + "Two Point Rush", + "Blocked PAT", + ] + ) + ) + .then(0.92) + .otherwise(pl.col("EP_start")), + ) + .with_columns( + # End of Half + EP_end=pl.when( + (pl.col("type.text").str.to_lowercase().str.contains(r"end of game")).or_( + pl.col("type.text").str.to_lowercase().str.contains(r"end of half") + ) + ) + .then(0) + # Defensive 2pt Conversion + .when(pl.col("type.text").is_in(["Defensive 2pt Conversion"])) + .then(-2) + # Safeties + .when( + (pl.col("type.text").is_in(defense_score_vec)).and_( + pl.col("text").str.to_lowercase().str.contains(r"(?i)safety") + ) + ) + .then(-2) + # Defense TD + Successful Two-Point Conversion + .when( + (pl.col("type.text").is_in(defense_score_vec)) + .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)conversion")) + .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)failed") == False) + ) + .then(-8) + # Defense TD + Failed Two-Point Conversion + .when( + (pl.col("type.text").is_in(defense_score_vec)) + .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)conversion")) + .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)failed")) + ) + .then(-6) + # Defense TD + Kick/PAT Missed + .when( + (pl.col("type.text").is_in(defense_score_vec)) + .and_(pl.col("text").str.to_lowercase().str.contains(r"PAT")) + .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)missed")) + ) + .then(-6) + # Defense TD + Kick/PAT Good + .when( + (pl.col("type.text").is_in(defense_score_vec)).and_( + pl.col("text").str.to_lowercase().str.contains(r"kick\)") + ) + ) + .then(-7) + # Defense TD + .when(pl.col("type.text").is_in(defense_score_vec)) + .then(-6.92) + # Offense TD + Failed Two-Point Conversion + .when( + (pl.col("type.text").is_in(offense_score_vec)) + .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)conversion")) + .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)failed")) + ) + .then(6) + # Offense TD + Successful Two-Point Conversion + .when( + (pl.col("type.text").is_in(offense_score_vec)) + .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)conversion")) + .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)failed") == False) + ) + .then(8) + # Offense Made FG + .when( + (pl.col("type.text").is_in(offense_score_vec)) + .and_(pl.col("type.text").str.to_lowercase().str.contains(r"(?i)field goal")) + .and_(pl.col("type.text").str.to_lowercase().str.contains(r"(?i)good")) + ) + .then(3) + # Offense TD + Kick/PAT Missed + .when( + (pl.col("type.text").is_in(offense_score_vec)) + .and_(pl.col("text").str.to_lowercase().str.contains(r"PAT")) + .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)missed")) + ) + .then(6) + # Offense TD + Kick/PAT Good + .when( + (pl.col("type.text").is_in(offense_score_vec)).and_( + pl.col("text").str.to_lowercase().str.contains(r"kick\)") + ) + ) + .then(7) + # Offense TD + .when(pl.col("type.text").is_in(offense_score_vec)) + .then(6.92) + # Extra Point Good + .when(pl.col("type.text").is_in(["Extra Point Good"])) + .then(1) + # Extra Point Missed + .when(pl.col("type.text").is_in(["Extra Point Missed"])) + .then(0) + # Two-Point Conversion Good + .when(pl.col("type.text").is_in(["Two-Point Conversion Good"])) + .then(2) + # Two-Point Conversion Missed + .when(pl.col("type.text").is_in(["Two-Point Conversion Missed"])) + .then(0) + # Two Point Pass/Rush Missed (Pre-2014 Data) + .when( + (pl.col("type.text").is_in(["Two Point Pass", "Two Point Rush"])).and_( + pl.col("text").str.to_lowercase().str.contains(r"(?i)no good") + ) + ) + .then(0) + # Two Point Pass/Rush Good (Pre-2014 Data) + .when( + (pl.col("type.text").is_in(["Two Point Pass", "Two Point Rush"])).and_( + pl.col("text").str.to_lowercase().str.contains(r"(?i)no good") == False + ) + ) + .then(2) + # Blocked PAT + .when(pl.col("type.text").is_in(["Blocked PAT"])) + .then(0) + # Flips for Turnovers that aren't kickoffs + .when( + ((pl.col("type.text").is_in(end_change_vec)).or_(pl.col("downs_turnover") == True)).and_( + pl.col("type.text").is_in(kickoff_vec) == False + ) + ) + .then(pl.col("EP_end") * -1) + # Flips for Turnovers that are kickoffs + .when(pl.col("type.text").is_in(kickoff_turnovers)) + .then(pl.col("EP_end") * -1) + # Onside kicks + .when((pl.col("kickoff_onside") == True).and_(pl.col("change_of_pos_team") == True)) + .then(pl.col("EP_end") * -1) + .otherwise(pl.col("EP_end")) + ) + .with_columns( + lag_EP_end=pl.col("EP_end").shift(1), + lag_change_of_pos_team=pl.col("change_of_pos_team").shift(1), + ) + .with_columns( + lag_change_of_pos_team=pl.when(pl.col("lag_change_of_pos_team").is_null()) + .then(False) + .otherwise(pl.col("lag_change_of_pos_team")), + ) + .with_columns( + EP_between=pl.when(pl.col("lag_change_of_pos_team") == True) + .then(pl.col("EP_start") + pl.col("lag_EP_end")) + .otherwise(pl.col("EP_start") - pl.col("lag_EP_end")), + EP_start=pl.when( + (pl.col("type.text").is_in(["Timeout", "End Period"])).and_( + pl.col("lag_change_of_pos_team") == False + ) + ) + .then(pl.col("lag_EP_end")) + .otherwise(pl.col("EP_start")), + ) + .with_columns( + EP_start=pl.when(pl.col("type.text").is_in(kickoff_vec)) + .then(pl.col("EP_start_touchback")) + .otherwise(pl.col("EP_start")), + ) + .with_columns( + EP_end=pl.when(pl.col("type.text").is_in(["Timeout"])) + .then(pl.col("EP_start")) + .otherwise(pl.col("EP_end")), + ) + .with_columns( + EPA=pl.when(pl.col("type.text").is_in(["Timeout"])) + .then(0) + .when((pl.col("scoring_play") == False).and_(pl.col("end_of_half") == True)) + .then(-1 * pl.col("EP_start")) + .when((pl.col("type.text").is_in(kickoff_vec)).and_(pl.col("penalty_in_text") == True)) + .then(pl.col("EP_end") - pl.col("EP_start")) + .when( + (pl.col("penalty_in_text") == True) + .and_(pl.col("type.text").is_in(["Penalty"]) == False) + .and_(pl.col("type.text").is_in(kickoff_vec) == False) + ) + .then(pl.col("EP_end") - pl.col("EP_start") + pl.col("EP_between")) + .otherwise(pl.col("EP_end") - pl.col("EP_start")), + ) + .with_columns( + def_EPA=pl.col("EPA") * -1, + # --- EPA Summary flags ---- + EPA_scrimmage=pl.when(pl.col("scrimmage_play") == True).then(pl.col("EPA")).otherwise(None), + EPA_rush=pl.when((pl.col("rush") == True).and_(pl.col("penalty_in_text") == True)) + .then(pl.col("EPA")) + .when((pl.col("rush") == True).and_(pl.col("penalty_in_text") == False)) + .then(pl.col("EPA")) + .otherwise(None), + EPA_pass=pl.when(pl.col("pass") == True).then(pl.col("EPA")).otherwise(None), + EPA_explosive=pl.when((pl.col("pass") == True).and_(pl.col("EPA") >= 2.4)) + .then(True) + .when(((pl.col("rush") == True).and_(pl.col("EPA") >= 1.8))) + .then(True) + .otherwise(False), + ) + .with_columns( + EPA_non_explosive=pl.when(pl.col("EPA_explosive") == False).then(pl.col("EPA")).otherwise(None), + EPA_explosive_pass=pl.when((pl.col("pass") == True).and_(pl.col("EPA") >= 2.4)) + .then(True) + .otherwise(False), + EPA_explosive_rush=pl.when((pl.col("rush") == True).and_(pl.col("EPA") >= 1.8)) + .then(True) + .otherwise(False), + first_down_created=pl.when( + (pl.col("scrimmage_play") == True) + .and_(pl.col("end.down") == 1) + .and_(pl.col("start.pos_team.id") == pl.col("end.pos_team.id")) + ) + .then(True) + .otherwise(False), + EPA_success=pl.when(pl.col("EPA") > 0).then(True).otherwise(False), + EPA_success_early_down=pl.when((pl.col("EPA") > 0).and_(pl.col("early_down") == True)) + .then(True) + .otherwise(False), + EPA_success_early_down_pass=pl.when( + (pl.col("pass") == True).and_(pl.col("EPA") > 0).and_(pl.col("early_down") == True) + ) + .then(True) + .otherwise(False), + EPA_success_early_down_rush=pl.when( + (pl.col("rush") == True).and_(pl.col("EPA") > 0).and_(pl.col("early_down") == True) + ) + .then(True) + .otherwise(False), + EPA_success_late_down=pl.when((pl.col("EPA") > 0).and_(pl.col("late_down") == True)) + .then(True) + .otherwise(False), + EPA_success_late_down_pass=pl.when( + (pl.col("pass") == True).and_(pl.col("EPA") > 0).and_(pl.col("late_down") == True) + ) + .then(True) + .otherwise(False), + EPA_success_late_down_rush=pl.when( + (pl.col("rush") == True).and_(pl.col("EPA") > 0).and_(pl.col("late_down") == True) + ) + .then(True) + .otherwise(False), + EPA_success_standard_down=pl.when((pl.col("EPA") > 0).and_(pl.col("standard_down") == True)) + .then(True) + .otherwise(False), + EPA_success_passing_down=pl.when((pl.col("EPA") > 0).and_(pl.col("passing_down") == True)) + .then(True) + .otherwise(False), + EPA_success_pass=pl.when((pl.col("EPA") > 0).and_(pl.col("pass") == True)).then(True).otherwise(False), + EPA_success_rush=pl.when((pl.col("EPA") > 0).and_(pl.col("rush") == True)).then(True).otherwise(False), + EPA_success_EPA=pl.when(pl.col("EPA") > 0).then(pl.col("EPA")).otherwise(None), + EPA_success_standard_down_EPA=pl.when((pl.col("EPA") > 0).and_(pl.col("standard_down") == True)) + .then(pl.col("EPA")) + .otherwise(None), + EPA_success_passing_down_EPA=pl.when((pl.col("EPA") > 0).and_(pl.col("passing_down") == True)) + .then(pl.col("EPA")) + .otherwise(None), + EPA_success_pass_EPA=pl.when((pl.col("EPA") > 0).and_(pl.col("pass") == True)) + .then(pl.col("EPA")) + .otherwise(None), + EPA_success_rush_EPA=pl.when((pl.col("EPA") > 0).and_(pl.col("rush") == True)) + .then(pl.col("EPA")) + .otherwise(None), + EPA_middle_8_success=pl.when((pl.col("EPA") > 0).and_(pl.col("middle_8") == True)) + .then(True) + .otherwise(False), + EPA_middle_8_success_pass=pl.when( + (pl.col("pass") == True).and_(pl.col("EPA") > 0).and_(pl.col("middle_8") == True) + ) + .then(True) + .otherwise(False), + EPA_middle_8_success_rush=pl.when( + (pl.col("rush") == True).and_(pl.col("EPA") > 0).and_(pl.col("middle_8") == True) + ) + .then(True) + .otherwise(False), + EPA_penalty=pl.when(pl.col("type.text").is_in(["Penalty", "Penalty (Kickoff)"])) + .then(pl.col("EPA")) + .when(pl.col("penalty_in_text") == True) + .then(pl.col("EP_end") - pl.col("EP_start")) + .otherwise(None), + EPA_sp=pl.when( + (pl.col("fg_attempt") == True).or_(pl.col("punt") == True).or_(pl.col("kickoff_play") == True) + ) + .then(pl.col("EPA")) + .otherwise(False), + EPA_fg=pl.when(pl.col("fg_attempt") == True).then(pl.col("EPA")).otherwise(None), + EPA_punt=pl.when(pl.col("punt") == True).then(pl.col("EPA")).otherwise(None), + EPA_kickoff=pl.when(pl.col("kickoff_play") == True).then(pl.col("EPA")).otherwise(None), + ) ) return play_df def __process_qbr(self, play_df): - play_df = play_df.with_columns( - qbr_epa = pl.when(pl.col("EPA") < -5.0) - .then(-5.0) - .when(pl.col("fumble_vec") == True) - .then(-3.5) - .otherwise(pl.col("EPA")), - weight = pl.when(pl.col("home_wp_before") < 0.1) - .then(0.6) - .when((pl.col("home_wp_before") >= 0.1) - .and_(pl.col("home_wp_before") < 0.2)) - .then(0.9) - .when((pl.col("home_wp_before") >= 0.8) - .and_(pl.col("home_wp_before") < 0.9)) - .then(0.9) - .when(pl.col("home_wp_before") > 0.9) - .then(0.6) - .otherwise(1), - non_fumble_sack = pl.when((pl.col("sack_vec") == True) - .and_(pl.col("fumble_vec") == False)) - .then(True) - .otherwise(False), - ).with_columns( - sack_epa = pl.when(pl.col("non_fumble_sack") == True) - .then(pl.col("qbr_epa")) - .otherwise(None), - pass_epa = pl.when(pl.col("pass") == True) - .then(pl.col("qbr_epa")) - .otherwise(None), - rush_epa = pl.when(pl.col("rush") == True) - .then(pl.col("qbr_epa")) - .otherwise(None), - pen_epa = pl.when(pl.col("penalty_flag") == True) - .then(pl.col("qbr_epa")) - .otherwise(None), - ).with_columns( - sack_weight = pl.when(pl.col("non_fumble_sack") == True) - .then(pl.col("weight")) - .otherwise(None), - pass_weight = pl.when(pl.col("pass") == True) - .then(pl.col("weight")) - .otherwise(None), - rush_weight = pl.when(pl.col("rush") == True) - .then(pl.col("weight")) - .otherwise(None), - pen_weight = pl.when(pl.col("penalty_flag") == True) - .then(pl.col("weight")) - .otherwise(None), - ).with_columns( - action_play = pl.col("EPA") != 0, - athlete_name = pl.when(pl.col("passer_player_name").is_not_null()) - .then(pl.col("passer_player_name")) - .when(pl.col("rusher_player_name").is_not_null()) - .then(pl.col("rusher_player_name")) - .otherwise(None), + play_df = ( + play_df.with_columns( + qbr_epa=pl.when(pl.col("EPA") < -5.0) + .then(-5.0) + .when(pl.col("fumble_vec") == True) + .then(-3.5) + .otherwise(pl.col("EPA")), + weight=pl.when(pl.col("home_wp_before") < 0.1) + .then(0.6) + .when((pl.col("home_wp_before") >= 0.1).and_(pl.col("home_wp_before") < 0.2)) + .then(0.9) + .when((pl.col("home_wp_before") >= 0.8).and_(pl.col("home_wp_before") < 0.9)) + .then(0.9) + .when(pl.col("home_wp_before") > 0.9) + .then(0.6) + .otherwise(1), + non_fumble_sack=pl.when((pl.col("sack_vec") == True).and_(pl.col("fumble_vec") == False)) + .then(True) + .otherwise(False), + ) + .with_columns( + sack_epa=pl.when(pl.col("non_fumble_sack") == True).then(pl.col("qbr_epa")).otherwise(None), + pass_epa=pl.when(pl.col("pass") == True).then(pl.col("qbr_epa")).otherwise(None), + rush_epa=pl.when(pl.col("rush") == True).then(pl.col("qbr_epa")).otherwise(None), + pen_epa=pl.when(pl.col("penalty_flag") == True).then(pl.col("qbr_epa")).otherwise(None), + ) + .with_columns( + sack_weight=pl.when(pl.col("non_fumble_sack") == True).then(pl.col("weight")).otherwise(None), + pass_weight=pl.when(pl.col("pass") == True).then(pl.col("weight")).otherwise(None), + rush_weight=pl.when(pl.col("rush") == True).then(pl.col("weight")).otherwise(None), + pen_weight=pl.when(pl.col("penalty_flag") == True).then(pl.col("weight")).otherwise(None), + ) + .with_columns( + action_play=pl.col("EPA") != 0, + athlete_name=pl.when(pl.col("passer_player_name").is_not_null()) + .then(pl.col("passer_player_name")) + .when(pl.col("rusher_player_name").is_not_null()) + .then(pl.col("rusher_player_name")) + .otherwise(None), + ) ) return play_df def __process_wpa(self, play_df): # ---- prepare variables for wp_before calculations ---- - play_df = play_df.with_columns( - pl.when(pl.col("type.text").is_in(kickoff_vec)) - .then(pl.col("pos_score_diff_start") + pl.col("EP_start_touchback")) - .otherwise(0.000) - .alias("start.ExpScoreDiff_touchback"), - pl.when((pl.col("penalty_in_text") == True) - .and_(pl.col("type.text").is_in(["Penalty"]) == False)) - .then(pl.col("pos_score_diff_start") + pl.col("EP_start") - pl.col("EP_between")) - .when((pl.col("type.text") == "Timeout") - .and_(pl.col("lag_scoringPlay") == True)) - .then(pl.col("pos_score_diff_start") + 0.92) - .otherwise(pl.col("pos_score_diff_start") + pl.col("EP_start")) - .alias("start.ExpScoreDiff"), - ).with_columns( - (pl.col("start.ExpScoreDiff_touchback") / (pl.col("start.adj_TimeSecsRem") + 1)) - .alias("start.ExpScoreDiff_Time_Ratio_touchback"), - (pl.col("start.ExpScoreDiff") / (pl.col("start.adj_TimeSecsRem") + 1)) - .alias("start.ExpScoreDiff_Time_Ratio"), - # ---- prepare variables for wp_after calculations ---- - pl.when(((pl.col("type.text").is_in(end_change_vec)) - .or_(pl.col("downs_turnover") == True)) + play_df = ( + play_df.with_columns( + pl.when(pl.col("type.text").is_in(kickoff_vec)) + .then(pl.col("pos_score_diff_start") + pl.col("EP_start_touchback")) + .otherwise(0.000) + .alias("start.ExpScoreDiff_touchback"), + pl.when((pl.col("penalty_in_text") == True).and_(pl.col("type.text").is_in(["Penalty"]) == False)) + .then(pl.col("pos_score_diff_start") + pl.col("EP_start") - pl.col("EP_between")) + .when((pl.col("type.text") == "Timeout").and_(pl.col("lag_scoringPlay") == True)) + .then(pl.col("pos_score_diff_start") + 0.92) + .otherwise(pl.col("pos_score_diff_start") + pl.col("EP_start")) + .alias("start.ExpScoreDiff"), + ) + .with_columns( + (pl.col("start.ExpScoreDiff_touchback") / (pl.col("start.adj_TimeSecsRem") + 1)).alias( + "start.ExpScoreDiff_Time_Ratio_touchback" + ), + (pl.col("start.ExpScoreDiff") / (pl.col("start.adj_TimeSecsRem") + 1)).alias( + "start.ExpScoreDiff_Time_Ratio" + ), + # ---- prepare variables for wp_after calculations ---- + pl.when( + ((pl.col("type.text").is_in(end_change_vec)).or_(pl.col("downs_turnover") == True)) .and_(pl.col("kickoff_play") == False) - .and_(pl.col("scoringPlay") == False)) - .then(pl.col("pos_score_diff_end") - pl.col("EP_end")) - .when(pl.col("type.text").is_in(kickoff_turnovers) - .and_(pl.col("scoringPlay") == False)) - .then(pl.col("pos_score_diff_end") + pl.col("EP_end")) - .when((pl.col("scoringPlay") == False) - .and_(pl.col("type.text") != "Timeout")) - .then(pl.col("pos_score_diff_end") + pl.col("EP_end")) - .when((pl.col("scoringPlay") == False) - .and_(pl.col("type.text") == "Timeout")) - .then(pl.col("pos_score_diff_end") + pl.col("EP_end")) - .when((pl.col("scoringPlay") == True) + .and_(pl.col("scoringPlay") == False) + ) + .then(pl.col("pos_score_diff_end") - pl.col("EP_end")) + .when(pl.col("type.text").is_in(kickoff_turnovers).and_(pl.col("scoringPlay") == False)) + .then(pl.col("pos_score_diff_end") + pl.col("EP_end")) + .when((pl.col("scoringPlay") == False).and_(pl.col("type.text") != "Timeout")) + .then(pl.col("pos_score_diff_end") + pl.col("EP_end")) + .when((pl.col("scoringPlay") == False).and_(pl.col("type.text") == "Timeout")) + .then(pl.col("pos_score_diff_end") + pl.col("EP_end")) + .when( + (pl.col("scoringPlay") == True) .and_(pl.col("td_play") == True) .and_(pl.col("type.text").is_in(defense_score_vec)) - .and_(pl.col("season") <= 2013)) - .then(pl.col("pos_score_diff_end") - 0.92) - .when((pl.col("scoringPlay") == True) + .and_(pl.col("season") <= 2013) + ) + .then(pl.col("pos_score_diff_end") - 0.92) + .when( + (pl.col("scoringPlay") == True) .and_(pl.col("td_play") == True) .and_(pl.col("type.text").is_in(offense_score_vec)) - .and_(pl.col("season") <= 2013)) - .then(pl.col("pos_score_diff_end") + 0.92) - .when((pl.col("type.text") == "Timeout") + .and_(pl.col("season") <= 2013) + ) + .then(pl.col("pos_score_diff_end") + 0.92) + .when( + (pl.col("type.text") == "Timeout") .and_(pl.col("lag_scoringPlay") == True) - .and_(pl.col("season") <= 2013)) - .then(pl.col("pos_score_diff_end") + 0.92) - .otherwise(pl.col("pos_score_diff_end")) - .alias("end.ExpScoreDiff"), - - - ).with_columns( - (pl.col("end.ExpScoreDiff") / (pl.col("end.adj_TimeSecsRem") + 1)) - .alias("end.ExpScoreDiff_Time_Ratio") + .and_(pl.col("season") <= 2013) + ) + .then(pl.col("pos_score_diff_end") + 0.92) + .otherwise(pl.col("pos_score_diff_end")) + .alias("end.ExpScoreDiff"), + ) + .with_columns( + (pl.col("end.ExpScoreDiff") / (pl.col("end.adj_TimeSecsRem") + 1)).alias("end.ExpScoreDiff_Time_Ratio") + ) ) # ---- wp_before ---- @@ -3136,181 +3627,208 @@ def __process_wpa(self, play_df): dtest_end = DMatrix(end_data) WP_end = wp_model.predict(dtest_end) - play_df = play_df.with_columns( - wp_before = pl.lit(WP_start), - wp_touchback = pl.lit(WP_start_touchback), - wp_after = pl.lit(WP_end) - ).with_columns( - wp_before = pl.when(pl.col("type.text").is_in(kickoff_vec)) - .then(pl.col("wp_touchback")) - .otherwise(pl.col("wp_before")), - - ).with_columns( - def_wp_before = 1 - pl.col("wp_before"), - - ).with_columns( - home_wp_before = pl.when(pl.col("start.pos_team.id") == pl.col("homeTeamId")) - .then(pl.col("wp_before")) - .otherwise(pl.col("def_wp_before")), - away_wp_before = pl.when(pl.col("start.pos_team.id") != pl.col("homeTeamId")) - .then(pl.col("wp_before")) - .otherwise(pl.col("def_wp_before")), - ).with_columns( - lead_wp_before = pl.col("wp_before").shift(-1), - lead_wp_before2 = pl.col("wp_before").shift(-2), - ).with_columns( - wp_after = pl.when(pl.col("type.text").is_in(["Timeout"])) - .then(pl.col("wp_before")) - .when((pl.col("status_type_completed") == True) - .and_((pl.col("lead_play_type").is_null()) - .or_(pl.col("game_play_number") == pl.col("game_play_number").max())) - .and_(pl.col("pos_score_diff_end") > 0)) - .then(1.0) - .when((pl.col("status_type_completed") == True) - .and_((pl.col("lead_play_type").is_null()) - .or_(pl.col("game_play_number") == pl.col("game_play_number").max())) - .and_(pl.col("pos_score_diff_end") < 0)) - .then(0.0) - .when((pl.col("end_of_half") == True) - .and_(pl.col("start.pos_team.id") == pl.col("lead_pos_team")) - .and_(pl.col("type.text") != "Timeout")) - .then(pl.col("lead_wp_before")) - .when((pl.col("end_of_half") == True) - .and_(pl.col("start.pos_team.id") != pl.col("end.pos_team.id")) - .and_(pl.col("type.text") != "Timeout")) - .then(1 - pl.col("lead_wp_before")) - .when((pl.col("end_of_half") == True) - .and_(pl.col("start.pos_team_receives_2H_kickoff") == False) - .and_(pl.col("type.text") == "Timeout")) - .then(pl.col("wp_after")) - .when((pl.col("lead_play_type").is_in(["End Period", "End of Half"])) - .and_(pl.col("change_of_pos_team") == False)) - .then(pl.col("lead_wp_before")) - .when((pl.col("lead_play_type").is_in(["End Period", "End of Half"])) - .and_(pl.col("change_of_pos_team") == True)) - .then(1 - pl.col("lead_wp_before")) - .when((pl.col("kickoff_onside") == True) - .and_(pl.col("change_of_pos_team") == True)) - .then(pl.col("wp_after")) - .when(pl.col("start.pos_team.id") != pl.col("end.pos_team.id")) - .then(1 - pl.col("wp_after")) - .otherwise(pl.col("wp_after")), - ).with_columns( - def_wp_after = 1 - pl.col("wp_after"), - ).with_columns( - home_wp_after = pl.when(pl.col("end.pos_team.id") == pl.col("homeTeamId")) - .then(pl.col("wp_after")) - .otherwise(pl.col("def_wp_after")), - away_wp_after = pl.when(pl.col("end.pos_team.id") != pl.col("homeTeamId")) - .then(pl.col("wp_after")) - .otherwise(pl.col("def_wp_after")), - ).with_columns( - wpa = pl.col("wp_after") - pl.col("wp_before"), + play_df = ( + play_df.with_columns( + wp_before=pl.lit(WP_start), wp_touchback=pl.lit(WP_start_touchback), wp_after=pl.lit(WP_end) + ) + .with_columns( + wp_before=pl.when(pl.col("type.text").is_in(kickoff_vec)) + .then(pl.col("wp_touchback")) + .otherwise(pl.col("wp_before")), + ) + .with_columns( + def_wp_before=1 - pl.col("wp_before"), + ) + .with_columns( + home_wp_before=pl.when(pl.col("start.pos_team.id") == pl.col("homeTeamId")) + .then(pl.col("wp_before")) + .otherwise(pl.col("def_wp_before")), + away_wp_before=pl.when(pl.col("start.pos_team.id") != pl.col("homeTeamId")) + .then(pl.col("wp_before")) + .otherwise(pl.col("def_wp_before")), + ) + .with_columns( + lead_wp_before=pl.col("wp_before").shift(-1), + lead_wp_before2=pl.col("wp_before").shift(-2), + ) + .with_columns( + wp_after=pl.when(pl.col("type.text").is_in(["Timeout"])) + .then(pl.col("wp_before")) + .when( + (pl.col("status_type_completed") == True) + .and_( + (pl.col("lead_play_type").is_null()).or_( + pl.col("game_play_number") == pl.col("game_play_number").max() + ) + ) + .and_(pl.col("pos_score_diff_end") > 0) + ) + .then(1.0) + .when( + (pl.col("status_type_completed") == True) + .and_( + (pl.col("lead_play_type").is_null()).or_( + pl.col("game_play_number") == pl.col("game_play_number").max() + ) + ) + .and_(pl.col("pos_score_diff_end") < 0) + ) + .then(0.0) + .when( + (pl.col("end_of_half") == True) + .and_(pl.col("start.pos_team.id") == pl.col("lead_pos_team")) + .and_(pl.col("type.text") != "Timeout") + ) + .then(pl.col("lead_wp_before")) + .when( + (pl.col("end_of_half") == True) + .and_(pl.col("start.pos_team.id") != pl.col("end.pos_team.id")) + .and_(pl.col("type.text") != "Timeout") + ) + .then(1 - pl.col("lead_wp_before")) + .when( + (pl.col("end_of_half") == True) + .and_(pl.col("start.pos_team_receives_2H_kickoff") == False) + .and_(pl.col("type.text") == "Timeout") + ) + .then(pl.col("wp_after")) + .when( + (pl.col("lead_play_type").is_in(["End Period", "End of Half"])).and_( + pl.col("change_of_pos_team") == False + ) + ) + .then(pl.col("lead_wp_before")) + .when( + (pl.col("lead_play_type").is_in(["End Period", "End of Half"])).and_( + pl.col("change_of_pos_team") == True + ) + ) + .then(1 - pl.col("lead_wp_before")) + .when((pl.col("kickoff_onside") == True).and_(pl.col("change_of_pos_team") == True)) + .then(pl.col("wp_after")) + .when(pl.col("start.pos_team.id") != pl.col("end.pos_team.id")) + .then(1 - pl.col("wp_after")) + .otherwise(pl.col("wp_after")), + ) + .with_columns( + def_wp_after=1 - pl.col("wp_after"), + ) + .with_columns( + home_wp_after=pl.when(pl.col("end.pos_team.id") == pl.col("homeTeamId")) + .then(pl.col("wp_after")) + .otherwise(pl.col("def_wp_after")), + away_wp_after=pl.when(pl.col("end.pos_team.id") != pl.col("homeTeamId")) + .then(pl.col("wp_after")) + .otherwise(pl.col("def_wp_after")), + ) + .with_columns( + wpa=pl.col("wp_after") - pl.col("wp_before"), + ) ) return play_df def __add_drive_data(self, play_df): base_groups = play_df.groupby(["drive.id"]) - play_df = play_df.with_columns( - drive_start = pl.when(pl.col("start.pos_team.id") == pl.col("homeTeamId")) - .then(100 - pl.col("drive.start.yardLine")) - .otherwise(pl.col("drive.start.yardLine")), - drive_stopped = pl.when(pl.col("drive.result").is_null()) - .then(False) - .otherwise(pl.col("drive.result").str.to_lowercase().str.contains(r"(?i)punt|fumble|interception|downs")), - ).with_columns( - drive_start = pl.col("drive_start").cast(pl.Float32), - ).with_columns( - drive_play_index = pl.col("scrimmage_play").cumsum().over("drive.id"), - ).with_columns( - drive_offense_plays = pl.when((pl.col("sp") == False) - .and_(pl.col("scrimmage_play") == True)) - .then(pl.col("play").cast(pl.Int32)) - .otherwise(0), - prog_drive_EPA = pl.col("EPA_scrimmage").cumsum().over("drive.id"), - prog_drive_WPA = pl.col("wpa").cumsum().over("drive.id"), - drive_offense_yards = pl.when((pl.col("sp") == False) - .and_(pl.col("scrimmage_play") == True)) - .then(pl.col("statYardage")) - .otherwise(0), - ).with_columns( - drive_total_yards = pl.col("drive_offense_yards").cumsum().over("drive.id"), + play_df = ( + play_df.with_columns( + drive_start=pl.when(pl.col("start.pos_team.id") == pl.col("homeTeamId")) + .then(100 - pl.col("drive.start.yardLine")) + .otherwise(pl.col("drive.start.yardLine")), + drive_stopped=pl.when(pl.col("drive.result").is_null()) + .then(False) + .otherwise( + pl.col("drive.result").str.to_lowercase().str.contains(r"(?i)punt|fumble|interception|downs") + ), + ) + .with_columns( + drive_start=pl.col("drive_start").cast(pl.Float32), + ) + .with_columns( + drive_play_index=pl.col("scrimmage_play").cumsum().over("drive.id"), + ) + .with_columns( + drive_offense_plays=pl.when((pl.col("sp") == False).and_(pl.col("scrimmage_play") == True)) + .then(pl.col("play").cast(pl.Int32)) + .otherwise(0), + prog_drive_EPA=pl.col("EPA_scrimmage").cumsum().over("drive.id"), + prog_drive_WPA=pl.col("wpa").cumsum().over("drive.id"), + drive_offense_yards=pl.when((pl.col("sp") == False).and_(pl.col("scrimmage_play") == True)) + .then(pl.col("statYardage")) + .otherwise(0), + ) + .with_columns( + drive_total_yards=pl.col("drive_offense_yards").cumsum().over("drive.id"), + ) ) return play_df def __cast_box_score_column(self, play_df, column, target_type): - if (column in play_df.columns): - play_df = play_df.with_columns( - pl.col(column) - .cast(target_type) - .alias(column) - ) + if column in play_df.columns: + play_df = play_df.with_columns(pl.col(column).cast(target_type).alias(column)) else: - play_df = play_df.with_columns( - (pl.Null).alias(column) - ) + play_df = play_df.with_columns((pl.Null).alias(column)) return play_df def create_box_score(self, play_df): # have to run the pipeline before pulling this in - if (self.ran_pipeline == False): + if self.ran_pipeline == False: self.run_processing_pipeline() box_score_columns = [ - 'completion', - 'target', - 'yds_receiving', - 'yds_rushed', - 'rush', - 'rush_td', - 'pass', - 'pass_td', - 'EPA', - 'wpa', - 'int', - 'int_td', - 'def_EPA', - 'EPA_rush', - 'EPA_pass', - 'EPA_success', - 'EPA_success_pass', - 'EPA_success_rush', - 'EPA_success_standard_down', - 'EPA_success_passing_down', - 'middle_8', - 'rz_play', - 'scoring_opp', - 'stuffed_run', - 'stopped_run', - 'opportunity_run', - 'highlight_run', - 'short_rush_success', - 'short_rush_attempt', - 'power_rush_success', - 'power_rush_attempt', - 'EPA_explosive', - 'EPA_explosive_pass', - 'EPA_explosive_rush', - 'standard_down', - 'passing_down', - 'fumble_vec', - 'sack', - 'penalty_flag', - 'play', - 'scrimmage_play', - 'sp', - 'kickoff_play', - 'punt', - 'fg_attempt', - 'EPA_penalty', - 'EPA_sp', - 'EPA_fg', - 'EPA_punt', - 'EPA_kickoff', - 'TFL', - 'TFL_pass', - 'TFL_rush', - 'havoc', + "completion", + "target", + "yds_receiving", + "yds_rushed", + "rush", + "rush_td", + "pass", + "pass_td", + "EPA", + "wpa", + "int", + "int_td", + "def_EPA", + "EPA_rush", + "EPA_pass", + "EPA_success", + "EPA_success_pass", + "EPA_success_rush", + "EPA_success_standard_down", + "EPA_success_passing_down", + "middle_8", + "rz_play", + "scoring_opp", + "stuffed_run", + "stopped_run", + "opportunity_run", + "highlight_run", + "short_rush_success", + "short_rush_attempt", + "power_rush_success", + "power_rush_attempt", + "EPA_explosive", + "EPA_explosive_pass", + "EPA_explosive_rush", + "standard_down", + "passing_down", + "fumble_vec", + "sack", + "penalty_flag", + "play", + "scrimmage_play", + "sp", + "kickoff_play", + "punt", + "fg_attempt", + "EPA_penalty", + "EPA_sp", + "EPA_fg", + "EPA_punt", + "EPA_kickoff", + "TFL", + "TFL_pass", + "TFL_rush", + "havoc", ] for item in box_score_columns: self.__cast_box_score_column(play_df, item, pl.Float32) @@ -3318,492 +3836,607 @@ def create_box_score(self, play_df): pass_box = play_df.filter((pl.col("pass") == True) & (pl.col("scrimmage_play") == True)) rush_box = play_df.filter((pl.col("rush") == True) & (pl.col("scrimmage_play") == True)) # pass_box.yds_receiving.fillna(0.0, inplace=True) - passer_box = pass_box.fill_null(0.0).groupby(by=["pos_team","passer_player_name"]).agg( - Comp = pl.col('completion').sum(), - Att = pl.col('pass_attempt').sum(), - Yds = pl.col('yds_receiving').sum(), - Pass_TD = pl.col('pass_td').sum(), - Int = pl.col('int').sum(), - YPA = pl.col('yds_receiving').mean(), - EPA = pl.col('EPA').sum(), - EPA_per_Play = pl.col('EPA').mean(), - WPA = pl.col('wpa').sum(), - SR = pl.col('EPA_success').mean(), - Sck = pl.col('sack_vec').sum() - ).with_columns(pl.col(pl.Float32).round(2) - ).with_columns( - pos_team = pl.col("pos_team").cast(pl.Int32) + passer_box = ( + pass_box.fill_null(0.0) + .groupby(by=["pos_team", "passer_player_name"]) + .agg( + Comp=pl.col("completion").sum(), + Att=pl.col("pass_attempt").sum(), + Yds=pl.col("yds_receiving").sum(), + Pass_TD=pl.col("pass_td").sum(), + Int=pl.col("int").sum(), + YPA=pl.col("yds_receiving").mean(), + EPA=pl.col("EPA").sum(), + EPA_per_Play=pl.col("EPA").mean(), + WPA=pl.col("wpa").sum(), + SR=pl.col("EPA_success").mean(), + Sck=pl.col("sack_vec").sum(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns(pos_team=pl.col("pos_team").cast(pl.Int32)) ) # passer_box = passer_box.replace(pl.all(), pl.Null) qbs_list = passer_box["passer_player_name"].to_list() pass_qbr_box = play_df.filter( - (pl.col("athlete_name").is_not_null() == True) & - (pl.col("scrimmage_play") == True) & - (pl.col("athlete_name").is_in(qbs_list)) - ) - pass_qbr_box_df = pass_qbr_box.groupby(by=["pos_team","athlete_name"]) - pass_qbr = pass_qbr_box.groupby(by=["pos_team","athlete_name"]).agg( - qbr_epa = (pl.col("qbr_epa") * pl.col("weight")).sum() / pl.col("weight").sum(), - sack_epa = (pl.col("sack_epa") * pl.col("sack_weight")).sum() / pl.col("sack_weight").sum(), - pass_epa = (pl.col("pass_epa") * pl.col("pass_weight")).sum() / pl.col("pass_weight").sum(), - rush_epa = (pl.col("rush_epa") * pl.col("rush_weight")).sum() / pl.col("rush_weight").sum(), - pen_epa = (pl.col("pen_epa") * pl.col("pen_weight")).sum() / pl.col("pen_weight").sum(), - spread = (pl.col('start.pos_team_spread').first()) - ).fill_null(0.0).with_columns( - pos_team = pl.col("pos_team").cast(pl.Int32) + (pl.col("athlete_name").is_not_null() == True) + & (pl.col("scrimmage_play") == True) + & (pl.col("athlete_name").is_in(qbs_list)) + ) + pass_qbr_box_df = pass_qbr_box.groupby(by=["pos_team", "athlete_name"]) + pass_qbr = ( + pass_qbr_box.groupby(by=["pos_team", "athlete_name"]) + .agg( + qbr_epa=(pl.col("qbr_epa") * pl.col("weight")).sum() / pl.col("weight").sum(), + sack_epa=(pl.col("sack_epa") * pl.col("sack_weight")).sum() / pl.col("sack_weight").sum(), + pass_epa=(pl.col("pass_epa") * pl.col("pass_weight")).sum() / pl.col("pass_weight").sum(), + rush_epa=(pl.col("rush_epa") * pl.col("rush_weight")).sum() / pl.col("rush_weight").sum(), + pen_epa=(pl.col("pen_epa") * pl.col("pen_weight")).sum() / pl.col("pen_weight").sum(), + spread=(pl.col("start.pos_team_spread").first()), + ) + .fill_null(0.0) + .with_columns(pos_team=pl.col("pos_team").cast(pl.Int32)) ) # # self.logger.info(pass_qbr) dtest_qbr = DMatrix(pass_qbr[qbr_vars]) qbr_result = qbr_model.predict(dtest_qbr) - pass_qbr = pass_qbr.with_columns( - exp_qbr = pl.lit(qbr_result) - ) - passer_box = passer_box.join(pass_qbr, - left_on=["passer_player_name", "pos_team"], - right_on=["athlete_name", "pos_team"]) - - rusher_box = rush_box.fill_null(0.0).groupby(by=["pos_team","rusher_player_name"]).agg( - Car= pl.col('rush').sum(), - Yds= pl.col('yds_rushed').sum(), - Rush_TD = pl.col('rush_td').sum(), - YPC= pl.col('yds_rushed').mean(), - EPA= pl.col('EPA').sum(), - EPA_per_Play= pl.col('EPA').mean(), - WPA= pl.col('wpa').sum(), - SR = pl.col('EPA_success').mean(), - Fum = pl.col('fumble_vec').sum(), - Fum_Lost = pl.col('fumble_lost').sum() - ).with_columns(pl.col(pl.Float32).round(2) - ).with_columns( - pos_team = pl.col("pos_team").cast(pl.Int32) + pass_qbr = pass_qbr.with_columns(exp_qbr=pl.lit(qbr_result)) + passer_box = passer_box.join( + pass_qbr, left_on=["passer_player_name", "pos_team"], right_on=["athlete_name", "pos_team"] + ) + + rusher_box = ( + rush_box.fill_null(0.0) + .groupby(by=["pos_team", "rusher_player_name"]) + .agg( + Car=pl.col("rush").sum(), + Yds=pl.col("yds_rushed").sum(), + Rush_TD=pl.col("rush_td").sum(), + YPC=pl.col("yds_rushed").mean(), + EPA=pl.col("EPA").sum(), + EPA_per_Play=pl.col("EPA").mean(), + WPA=pl.col("wpa").sum(), + SR=pl.col("EPA_success").mean(), + Fum=pl.col("fumble_vec").sum(), + Fum_Lost=pl.col("fumble_lost").sum(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns(pos_team=pl.col("pos_team").cast(pl.Int32)) ) # rusher_box = rusher_box.replace({np.nan: None}) - receiver_box = pass_box.fill_null(0.0).groupby(by=["pos_team","receiver_player_name"]).agg( - Rec= pl.col('completion').sum(), - Tar= pl.col('target').sum(), - Yds= pl.col('yds_receiving').sum(), - Rec_TD = pl.col('pass_td').sum(), - YPT= pl.col('yds_receiving').mean(), - EPA= pl.col('EPA').sum(), - EPA_per_Play= pl.col('EPA').mean(), - WPA= pl.col('wpa').sum(), - SR = pl.col('EPA_success').mean(), - Fum = pl.col('fumble_vec').sum(), - Fum_Lost = pl.col('fumble_lost').sum() - ).with_columns(pl.col(pl.Float32).round(2) - ).with_columns( - pos_team = pl.col("pos_team").cast(pl.Int32) + receiver_box = ( + pass_box.fill_null(0.0) + .groupby(by=["pos_team", "receiver_player_name"]) + .agg( + Rec=pl.col("completion").sum(), + Tar=pl.col("target").sum(), + Yds=pl.col("yds_receiving").sum(), + Rec_TD=pl.col("pass_td").sum(), + YPT=pl.col("yds_receiving").mean(), + EPA=pl.col("EPA").sum(), + EPA_per_Play=pl.col("EPA").mean(), + WPA=pl.col("wpa").sum(), + SR=pl.col("EPA_success").mean(), + Fum=pl.col("fumble_vec").sum(), + Fum_Lost=pl.col("fumble_lost").sum(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns(pos_team=pl.col("pos_team").cast(pl.Int32)) ) - team_base_box = play_df.groupby(by=["pos_team"]).agg( - EPA_plays = pl.col('play').sum(), - total_yards = pl.col('statYardage').sum(), - EPA_overall_total = pl.col('EPA').sum(), - ).with_columns(pl.col(pl.Float32).round(2) - ).with_columns( - pos_team = pl.col("pos_team").cast(pl.Int32) + team_base_box = ( + play_df.groupby(by=["pos_team"]) + .agg( + EPA_plays=pl.col("play").sum(), + total_yards=pl.col("statYardage").sum(), + EPA_overall_total=pl.col("EPA").sum(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns(pos_team=pl.col("pos_team").cast(pl.Int32)) ) - team_pen_box = play_df.filter(pl.col("penalty_flag") == True).groupby(by=["pos_team"]).agg( - total_pen_yards = pl.col('statYardage').sum(), - EPA_penalty = pl.col('EPA_penalty').sum(), - penalty_first_downs_created = pl.col("penalty_1st_conv").sum(), - penalty_first_downs_created_rate = pl.col("penalty_1st_conv").mean(), - ).with_columns(pl.col(pl.Float32).round(2) - ).with_columns( - pos_team = pl.col("pos_team").cast(pl.Int32) - ) - - team_scrimmage_box = play_df.filter(pl.col("scrimmage_play") == True).groupby(by=["pos_team"]).agg( - scrimmage_plays = pl.col('scrimmage_play').sum(), - EPA_overall_off = pl.col('EPA').sum(), - EPA_overall_offense = pl.col('EPA').sum(), - EPA_per_play = pl.col('EPA').mean(), - EPA_non_explosive = pl.col('EPA_non_explosive').sum(), - EPA_non_explosive_per_play = pl.col('EPA_non_explosive').mean(), - EPA_explosive = pl.col('EPA_explosive').sum(), - EPA_explosive_rate = pl.col('EPA_explosive').mean(), - passes_rate = pl.col('pass').mean(), - off_yards = pl.col('statYardage').sum(), - total_off_yards = pl.col('statYardage').sum(), - yards_per_play = pl.col('statYardage').mean() - ).with_columns(pl.col(pl.Float32).round(2) - ).with_columns( - pos_team = pl.col("pos_team").cast(pl.Int32) - ) - - team_sp_box = play_df.filter(pl.col("sp") == True).groupby(by=["pos_team"]).agg( - special_teams_plays = pl.col('sp').sum(), - EPA_sp = pl.col('EPA_sp').sum(), - EPA_special_teams = pl.col('EPA_sp').sum(), - field_goals = pl.col("fg_attempt").sum(), - EPA_fg = pl.col('EPA_fg').sum(), - punt_plays = pl.col("punt_play").sum(), - EPA_punt = pl.col('EPA_punt').sum(), - kickoff_plays = pl.col('kickoff_play').sum(), - EPA_kickoff = pl.col('EPA_kickoff').sum() - ).with_columns(pl.col(pl.Float32).round(2) - ).with_columns( - pos_team = pl.col("pos_team").cast(pl.Int32) - ) - - team_scrimmage_box_pass = play_df.filter( - (pl.col("pass") == True) & (pl.col("scrimmage_play") == True) - ).fill_null(0.0).groupby(by=["pos_team"]).agg( - passes = pl.col('pass').sum(), - pass_yards = pl.col('yds_receiving').sum(), - yards_per_pass = pl.col('yds_receiving').mean(), - passing_first_downs_created = pl.col("first_down_created").sum(), - passing_first_downs_created_rate = pl.col("first_down_created").mean(), - EPA_passing_overall = pl.col('EPA').sum(), - EPA_passing_per_play = pl.col('EPA').mean(), - EPA_explosive_passing = pl.col('EPA_explosive').sum(), - EPA_explosive_passing_rate = pl.col('EPA_explosive').mean(), - EPA_non_explosive_passing = pl.col('EPA_non_explosive').sum(), - EPA_non_explosive_passing_per_play = pl.col('EPA_non_explosive').mean(), - ).with_columns(pl.col(pl.Float32).round(2) - ).with_columns( - pos_team = pl.col("pos_team").cast(pl.Int32) - ) - - team_scrimmage_box_rush = play_df.filter( - (pl.col("rush") == True) & (pl.col("scrimmage_play") == True) - ).fill_null(0.0).groupby(by=["pos_team"]).agg( - rushes = pl.col('rush').sum(), - rush_yards = pl.col('yds_rushed').sum(), - yards_per_rush = pl.col('yds_rushed').mean(), - rushing_power_rate = pl.col('power_rush_attempt').mean(), - rushing_first_downs_created = pl.col("first_down_created").sum(), - rushing_first_downs_created_rate = pl.col("first_down_created").mean(), - EPA_rushing_overall = pl.col('EPA').sum(), - EPA_rushing_per_play = pl.col('EPA').mean(), - EPA_explosive_rushing = pl.col('EPA_explosive').sum(), - EPA_explosive_rushing_rate = pl.col('EPA_explosive').mean(), - EPA_non_explosive_rushing = pl.col('EPA_non_explosive').sum(), - EPA_non_explosive_rushing_per_play = pl.col('EPA_non_explosive').mean(), - ).with_columns(pl.col(pl.Float32).round(2) - ).with_columns( - pos_team = pl.col("pos_team").cast(pl.Int32) + team_pen_box = ( + play_df.filter(pl.col("penalty_flag") == True) + .groupby(by=["pos_team"]) + .agg( + total_pen_yards=pl.col("statYardage").sum(), + EPA_penalty=pl.col("EPA_penalty").sum(), + penalty_first_downs_created=pl.col("penalty_1st_conv").sum(), + penalty_first_downs_created_rate=pl.col("penalty_1st_conv").mean(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns(pos_team=pl.col("pos_team").cast(pl.Int32)) ) - team_rush_base_box = play_df.filter( - (pl.col("scrimmage_play") == True) - ).fill_null(0.0).groupby(by=["pos_team"]).agg( - rushes_rate = pl.col('rush').mean(), - first_downs_created = pl.col("first_down_created").sum(), - first_downs_created_rate = pl.col("first_down_created").mean() - ).with_columns(pl.col(pl.Float32).round(2) - ).with_columns( - pos_team = pl.col("pos_team").cast(pl.Int32) - ) - - team_rush_power_box = play_df.filter( - (pl.col("power_rush_attempt") == True) & (pl.col("scrimmage_play") == True) - ).fill_null(0.0).groupby(by=["pos_team"]).agg( - EPA_rushing_power = pl.col('EPA').sum(), - EPA_rushing_power_per_play = pl.col('EPA').mean(), - rushing_power_success = pl.col('power_rush_success').sum(), - rushing_power_success_rate = pl.col('power_rush_success').mean(), - rushing_power = pl.col('power_rush_attempt').sum(), - ).with_columns(pl.col(pl.Float32).round(2) - ).with_columns( - pos_team = pl.col("pos_team").cast(pl.Int32) + team_scrimmage_box = ( + play_df.filter(pl.col("scrimmage_play") == True) + .groupby(by=["pos_team"]) + .agg( + scrimmage_plays=pl.col("scrimmage_play").sum(), + EPA_overall_off=pl.col("EPA").sum(), + EPA_overall_offense=pl.col("EPA").sum(), + EPA_per_play=pl.col("EPA").mean(), + EPA_non_explosive=pl.col("EPA_non_explosive").sum(), + EPA_non_explosive_per_play=pl.col("EPA_non_explosive").mean(), + EPA_explosive=pl.col("EPA_explosive").sum(), + EPA_explosive_rate=pl.col("EPA_explosive").mean(), + passes_rate=pl.col("pass").mean(), + off_yards=pl.col("statYardage").sum(), + total_off_yards=pl.col("statYardage").sum(), + yards_per_play=pl.col("statYardage").mean(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns(pos_team=pl.col("pos_team").cast(pl.Int32)) + ) + + team_sp_box = ( + play_df.filter(pl.col("sp") == True) + .groupby(by=["pos_team"]) + .agg( + special_teams_plays=pl.col("sp").sum(), + EPA_sp=pl.col("EPA_sp").sum(), + EPA_special_teams=pl.col("EPA_sp").sum(), + field_goals=pl.col("fg_attempt").sum(), + EPA_fg=pl.col("EPA_fg").sum(), + punt_plays=pl.col("punt_play").sum(), + EPA_punt=pl.col("EPA_punt").sum(), + kickoff_plays=pl.col("kickoff_play").sum(), + EPA_kickoff=pl.col("EPA_kickoff").sum(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns(pos_team=pl.col("pos_team").cast(pl.Int32)) + ) + + team_scrimmage_box_pass = ( + play_df.filter((pl.col("pass") == True) & (pl.col("scrimmage_play") == True)) + .fill_null(0.0) + .groupby(by=["pos_team"]) + .agg( + passes=pl.col("pass").sum(), + pass_yards=pl.col("yds_receiving").sum(), + yards_per_pass=pl.col("yds_receiving").mean(), + passing_first_downs_created=pl.col("first_down_created").sum(), + passing_first_downs_created_rate=pl.col("first_down_created").mean(), + EPA_passing_overall=pl.col("EPA").sum(), + EPA_passing_per_play=pl.col("EPA").mean(), + EPA_explosive_passing=pl.col("EPA_explosive").sum(), + EPA_explosive_passing_rate=pl.col("EPA_explosive").mean(), + EPA_non_explosive_passing=pl.col("EPA_non_explosive").sum(), + EPA_non_explosive_passing_per_play=pl.col("EPA_non_explosive").mean(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns(pos_team=pl.col("pos_team").cast(pl.Int32)) + ) + + team_scrimmage_box_rush = ( + play_df.filter((pl.col("rush") == True) & (pl.col("scrimmage_play") == True)) + .fill_null(0.0) + .groupby(by=["pos_team"]) + .agg( + rushes=pl.col("rush").sum(), + rush_yards=pl.col("yds_rushed").sum(), + yards_per_rush=pl.col("yds_rushed").mean(), + rushing_power_rate=pl.col("power_rush_attempt").mean(), + rushing_first_downs_created=pl.col("first_down_created").sum(), + rushing_first_downs_created_rate=pl.col("first_down_created").mean(), + EPA_rushing_overall=pl.col("EPA").sum(), + EPA_rushing_per_play=pl.col("EPA").mean(), + EPA_explosive_rushing=pl.col("EPA_explosive").sum(), + EPA_explosive_rushing_rate=pl.col("EPA_explosive").mean(), + EPA_non_explosive_rushing=pl.col("EPA_non_explosive").sum(), + EPA_non_explosive_rushing_per_play=pl.col("EPA_non_explosive").mean(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns(pos_team=pl.col("pos_team").cast(pl.Int32)) + ) + + team_rush_base_box = ( + play_df.filter((pl.col("scrimmage_play") == True)) + .fill_null(0.0) + .groupby(by=["pos_team"]) + .agg( + rushes_rate=pl.col("rush").mean(), + first_downs_created=pl.col("first_down_created").sum(), + first_downs_created_rate=pl.col("first_down_created").mean(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns(pos_team=pl.col("pos_team").cast(pl.Int32)) + ) + + team_rush_power_box = ( + play_df.filter((pl.col("power_rush_attempt") == True) & (pl.col("scrimmage_play") == True)) + .fill_null(0.0) + .groupby(by=["pos_team"]) + .agg( + EPA_rushing_power=pl.col("EPA").sum(), + EPA_rushing_power_per_play=pl.col("EPA").mean(), + rushing_power_success=pl.col("power_rush_success").sum(), + rushing_power_success_rate=pl.col("power_rush_success").mean(), + rushing_power=pl.col("power_rush_attempt").sum(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns(pos_team=pl.col("pos_team").cast(pl.Int32)) ) play_df = play_df.with_columns( - opp_highlight_yards = pl.col("opp_highlight_yards").cast(pl.Float32), - highlight_yards = pl.col("highlight_yards").cast(pl.Float32), - line_yards = pl.col("line_yards").cast(pl.Float32), - second_level_yards = pl.col("second_level_yards").cast(pl.Float32), - open_field_yards = pl.col("open_field_yards").cast(pl.Float32), - ) - - team_rush_box = play_df.filter( - (pl.col("rush") == True) & (pl.col("scrimmage_play") == True) - ).fill_null(0.0).groupby(by=["pos_team"]).agg( - rushing_stuff = pl.col('stuffed_run').sum(), - rushing_stuff_rate = pl.col('stuffed_run').mean(), - rushing_stopped = pl.col('stopped_run').sum(), - rushing_stopped_rate = pl.col('stopped_run').mean(), - rushing_opportunity = pl.col('opportunity_run').sum(), - rushing_opportunity_rate = pl.col('opportunity_run').mean(), - rushing_highlight = pl.col('highlight_run').sum(), - rushing_highlight_rate = pl.col('highlight_run').mean(), - rushing_highlight_yards = pl.col('highlight_yards').sum(), - line_yards = pl.col('line_yards').sum(), - line_yards_per_carry = pl.col('line_yards').mean(), - second_level_yards = pl.col('second_level_yards').sum(), - open_field_yards = pl.col('open_field_yards').sum(), - ).with_columns(pl.col(pl.Float32).round(2) - ).with_columns( - pos_team = pl.col("pos_team").cast(pl.Int32) + opp_highlight_yards=pl.col("opp_highlight_yards").cast(pl.Float32), + highlight_yards=pl.col("highlight_yards").cast(pl.Float32), + line_yards=pl.col("line_yards").cast(pl.Float32), + second_level_yards=pl.col("second_level_yards").cast(pl.Float32), + open_field_yards=pl.col("open_field_yards").cast(pl.Float32), ) - team_rush_opp_box = play_df.filter( - (pl.col("rush") == True) & (pl.col("scrimmage_play") == True) & (pl.col("opportunity_run") == True) - ).fill_null(0.0).groupby(by=["pos_team"]).agg( - rushing_highlight_yards_per_opp = pl.col('opp_highlight_yards').mean(), - ).with_columns(pl.col(pl.Float32).round(2) - ).with_columns( - pos_team = pl.col("pos_team").cast(pl.Int32) + team_rush_box = ( + play_df.filter((pl.col("rush") == True) & (pl.col("scrimmage_play") == True)) + .fill_null(0.0) + .groupby(by=["pos_team"]) + .agg( + rushing_stuff=pl.col("stuffed_run").sum(), + rushing_stuff_rate=pl.col("stuffed_run").mean(), + rushing_stopped=pl.col("stopped_run").sum(), + rushing_stopped_rate=pl.col("stopped_run").mean(), + rushing_opportunity=pl.col("opportunity_run").sum(), + rushing_opportunity_rate=pl.col("opportunity_run").mean(), + rushing_highlight=pl.col("highlight_run").sum(), + rushing_highlight_rate=pl.col("highlight_run").mean(), + rushing_highlight_yards=pl.col("highlight_yards").sum(), + line_yards=pl.col("line_yards").sum(), + line_yards_per_carry=pl.col("line_yards").mean(), + second_level_yards=pl.col("second_level_yards").sum(), + open_field_yards=pl.col("open_field_yards").sum(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns(pos_team=pl.col("pos_team").cast(pl.Int32)) + ) + + team_rush_opp_box = ( + play_df.filter( + (pl.col("rush") == True) & (pl.col("scrimmage_play") == True) & (pl.col("opportunity_run") == True) + ) + .fill_null(0.0) + .groupby(by=["pos_team"]) + .agg( + rushing_highlight_yards_per_opp=pl.col("opp_highlight_yards").mean(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns(pos_team=pl.col("pos_team").cast(pl.Int32)) ) - team_data_frames = [team_rush_opp_box, team_pen_box, team_sp_box, team_scrimmage_box_rush, team_scrimmage_box_pass, - team_scrimmage_box, team_base_box, team_rush_base_box, team_rush_power_box, team_rush_box] + team_data_frames = [ + team_rush_opp_box, + team_pen_box, + team_sp_box, + team_scrimmage_box_rush, + team_scrimmage_box_pass, + team_scrimmage_box, + team_base_box, + team_rush_base_box, + team_rush_power_box, + team_rush_box, + ] team_box = reduce(lambda left, right: left.join(right, on=["pos_team"], how="outer"), team_data_frames) - situation_box_normal = play_df.filter(pl.col("scrimmage_play") == True).groupby(by=["pos_team"]).agg( - EPA_success = pl.col('EPA_success').sum(), - EPA_success_rate = pl.col('EPA_success').mean(), - ).with_columns(pl.col(pl.Float32).round(2) - ).with_columns( - pos_team = pl.col("pos_team").cast(pl.Int32) + situation_box_normal = ( + play_df.filter(pl.col("scrimmage_play") == True) + .groupby(by=["pos_team"]) + .agg( + EPA_success=pl.col("EPA_success").sum(), + EPA_success_rate=pl.col("EPA_success").mean(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns(pos_team=pl.col("pos_team").cast(pl.Int32)) ) - situation_box_rz = play_df.filter( - (pl.col("rz_play") == True) & (pl.col("scrimmage_play") == True) - ).groupby(by=["pos_team"]).agg( - EPA_success_rz = pl.col('EPA_success').sum(), - EPA_success_rate_rz = pl.col('EPA_success').mean(), - ).with_columns(pl.col(pl.Float32).round(2) - ).with_columns( - pos_team = pl.col("pos_team").cast(pl.Int32) + situation_box_rz = ( + play_df.filter((pl.col("rz_play") == True) & (pl.col("scrimmage_play") == True)) + .groupby(by=["pos_team"]) + .agg( + EPA_success_rz=pl.col("EPA_success").sum(), + EPA_success_rate_rz=pl.col("EPA_success").mean(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns(pos_team=pl.col("pos_team").cast(pl.Int32)) ) - situation_box_third = play_df.filter( - (pl.col("start.down") == 3) & (pl.col("scrimmage_play") == True) - ).groupby(by=["pos_team"]).agg( - EPA_success_third = pl.col('EPA_success').sum(), - EPA_success_rate_third = pl.col('EPA_success').mean(), - ).with_columns(pl.col(pl.Float32).round(2) - ).with_columns( - pos_team = pl.col("pos_team").cast(pl.Int32) + situation_box_third = ( + play_df.filter((pl.col("start.down") == 3) & (pl.col("scrimmage_play") == True)) + .groupby(by=["pos_team"]) + .agg( + EPA_success_third=pl.col("EPA_success").sum(), + EPA_success_rate_third=pl.col("EPA_success").mean(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns(pos_team=pl.col("pos_team").cast(pl.Int32)) ) - situation_box_pass = play_df.filter( - (pl.col("pass") == True) & (pl.col("scrimmage_play") == True) - ).groupby(by=["pos_team"]).agg( - EPA_success_pass = pl.col('EPA_success').sum(), - EPA_success_pass_rate = pl.col('EPA_success').mean(), - ).with_columns(pl.col(pl.Float32).round(2) - ).with_columns( - pos_team = pl.col("pos_team").cast(pl.Int32) + situation_box_pass = ( + play_df.filter((pl.col("pass") == True) & (pl.col("scrimmage_play") == True)) + .groupby(by=["pos_team"]) + .agg( + EPA_success_pass=pl.col("EPA_success").sum(), + EPA_success_pass_rate=pl.col("EPA_success").mean(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns(pos_team=pl.col("pos_team").cast(pl.Int32)) ) - situation_box_rush = play_df.filter( - (pl.col("rush") == True) & (pl.col("scrimmage_play") == True) - ).groupby(by=["pos_team"]).agg( - EPA_success_rush = pl.col('EPA_success').sum(), - EPA_success_rush_rate = pl.col('EPA_success').mean(), - ).with_columns(pl.col(pl.Float32).round(2) - ).with_columns( - pos_team = pl.col("pos_team").cast(pl.Int32) - ) - - situation_box_middle8 = play_df.filter( - (pl.col("middle_8") == True) & (pl.col("scrimmage_play") == True) - ).groupby(by=["pos_team"]).agg( - middle_8 = pl.col('middle_8').sum(), - middle_8_pass_rate = pl.col('pass').mean(), - middle_8_rush_rate = pl.col('rush').mean(), - EPA_middle_8 = pl.col('EPA').sum(), - EPA_middle_8_per_play = pl.col('EPA').mean(), - EPA_middle_8_success = pl.col('EPA_success').sum(), - EPA_middle_8_success_rate = pl.col('EPA_success').mean(), - ).with_columns(pl.col(pl.Float32).round(2) - ).with_columns( - pos_team = pl.col("pos_team").cast(pl.Int32), - ) - - situation_box_middle8_pass = play_df.filter( - (pl.col("pass") == True) & (pl.col("middle_8") == True) & (pl.col("scrimmage_play") == True) - ).groupby(by=["pos_team"]).agg( - middle_8_pass = pl.col('pass').sum(), - EPA_middle_8_pass = pl.col('EPA').sum(), - EPA_middle_8_pass_per_play = pl.col('EPA').mean(), - EPA_middle_8_success_pass = pl.col('EPA_success').sum(), - EPA_middle_8_success_pass_rate = pl.col('EPA_success').mean(), - ).with_columns(pl.col(pl.Float32).round(2) - ).with_columns( - pos_team = pl.col("pos_team").cast(pl.Int32), - ) - - situation_box_middle8_rush = play_df.filter( - (pl.col("rush") == True) & (pl.col("middle_8") == True) & (pl.col("scrimmage_play") == True) - ).groupby(by=["pos_team"]).agg( - middle_8_rush = pl.col('rush').sum(), - EPA_middle_8_rush = pl.col('EPA').sum(), - EPA_middle_8_rush_per_play = pl.col('EPA').mean(), - EPA_middle_8_success_rush = pl.col('EPA_success').sum(), - EPA_middle_8_success_rush_rate = pl.col('EPA_success').mean(), - ).with_columns(pl.col(pl.Float32).round(2) - ).with_columns( - pos_team = pl.col("pos_team").cast(pl.Int32), - ) - - situation_box_early = play_df.filter( - (pl.col("early_down") == True) & (pl.col("scrimmage_play") == True) - ).groupby(by=["pos_team"]).agg( - EPA_success_early_down = pl.col('EPA_success').sum(), - EPA_success_early_down_rate = pl.col('EPA_success').mean(), - early_downs = pl.col('early_down').sum(), - early_down_pass_rate = pl.col('pass').mean(), - early_down_rush_rate = pl.col('rush').mean(), - EPA_early_down = pl.col('EPA').sum(), - EPA_early_down_per_play = pl.col('EPA').mean(), - early_down_first_down = pl.col('first_down_created').sum(), - early_down_first_down_rate = pl.col('first_down_created').mean(), - ).with_columns(pl.col(pl.Float32).round(2) - ).with_columns( - pos_team = pl.col("pos_team").cast(pl.Int32), - ) - - situation_box_early_pass = play_df.filter( - (pl.col("pass") == True) & (pl.col("early_down") == True) & (pl.col("scrimmage_play") == True) - ).groupby(by=["pos_team"]).agg( - early_down_pass = pl.col('pass').sum(), - EPA_early_down_pass = pl.col('EPA').sum(), - EPA_early_down_pass_per_play = pl.col('EPA').mean(), - EPA_success_early_down_pass = pl.col('EPA_success').sum(), - EPA_success_early_down_pass_rate = pl.col('EPA_success').mean(), - ).with_columns(pl.col(pl.Float32).round(2) - ).with_columns( - pos_team = pl.col("pos_team").cast(pl.Int32), - ) - - situation_box_early_rush = play_df.filter( - (pl.col("rush") == True) & (pl.col("early_down") == True) & (pl.col("scrimmage_play") == True) - ).groupby(by=["pos_team"]).agg( - early_down_rush = pl.col('rush').sum(), - EPA_early_down_rush = pl.col('EPA').sum(), - EPA_early_down_rush_per_play = pl.col('EPA').mean(), - EPA_success_early_down_rush = pl.col('EPA_success').sum(), - EPA_success_early_down_rush_rate = pl.col('EPA_success').mean(), - ).with_columns(pl.col(pl.Float32).round(2) - ).with_columns( - pos_team = pl.col("pos_team").cast(pl.Int32), - ) - - situation_box_late = play_df.filter( - (pl.col("late_down") == True) & (pl.col("scrimmage_play") == True) - ).groupby(by=["pos_team"]).agg( - EPA_success_late_down = pl.col('EPA_success_late_down').sum(), - EPA_success_late_down_pass = pl.col('EPA_success_late_down_pass').sum(), - EPA_success_late_down_rush = pl.col('EPA_success_late_down_rush').sum(), - late_downs = pl.col('late_down').sum(), - late_down_pass = pl.col('late_down_pass').sum(), - late_down_rush = pl.col('late_down_rush').sum(), - EPA_late_down = pl.col('EPA').sum(), - EPA_late_down_per_play = pl.col('EPA').mean(), - EPA_success_late_down_rate = pl.col('EPA_success_late_down').mean(), - EPA_success_late_down_pass_rate = pl.col('EPA_success_late_down_pass').mean(), - EPA_success_late_down_rush_rate = pl.col('EPA_success_late_down_rush').mean(), - late_down_pass_rate = pl.col('late_down_pass').mean(), - late_down_rush_rate = pl.col('late_down_rush').mean(), - ).with_columns(pl.col(pl.Float32).round(2) - ).with_columns( - pos_team = pl.col("pos_team").cast(pl.Int32), - ) - - situation_box_standard = play_df.filter( - (pl.col("standard_down") == True) & (pl.col("scrimmage_play") == True) - ).groupby(by=["pos_team"]).agg( - EPA_success_standard_down = pl.col('EPA_success').sum(), - EPA_success_standard_down_rate = pl.col('EPA_success').mean(), - EPA_standard_down = pl.col('EPA').sum(), - EPA_standard_down_per_play = pl.col('EPA').mean(), - standard_downs = pl.col('standard_down').sum() - ).with_columns(pl.col(pl.Float32).round(2) - ).with_columns( - pos_team = pl.col("pos_team").cast(pl.Int32), - ) - - situation_box_passing = play_df.filter( - (pl.col("passing_down") == True) & (pl.col("scrimmage_play") == True) - ).groupby(by=["pos_team"]).agg( - EPA_success_passing_down = pl.col('EPA_success').sum(), - EPA_success_passing_down_rate = pl.col('EPA_success').mean(), - EPA_passing_down = pl.col('EPA').sum(), - EPA_passing_down_per_play = pl.col('EPA').mean(), - passing_downs = pl.col('passing_down').sum() - ).with_columns(pl.col(pl.Float32).round(2) - ).with_columns( - pos_team = pl.col("pos_team").cast(pl.Int32), + situation_box_rush = ( + play_df.filter((pl.col("rush") == True) & (pl.col("scrimmage_play") == True)) + .groupby(by=["pos_team"]) + .agg( + EPA_success_rush=pl.col("EPA_success").sum(), + EPA_success_rush_rate=pl.col("EPA_success").mean(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns(pos_team=pl.col("pos_team").cast(pl.Int32)) + ) + + situation_box_middle8 = ( + play_df.filter((pl.col("middle_8") == True) & (pl.col("scrimmage_play") == True)) + .groupby(by=["pos_team"]) + .agg( + middle_8=pl.col("middle_8").sum(), + middle_8_pass_rate=pl.col("pass").mean(), + middle_8_rush_rate=pl.col("rush").mean(), + EPA_middle_8=pl.col("EPA").sum(), + EPA_middle_8_per_play=pl.col("EPA").mean(), + EPA_middle_8_success=pl.col("EPA_success").sum(), + EPA_middle_8_success_rate=pl.col("EPA_success").mean(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns( + pos_team=pl.col("pos_team").cast(pl.Int32), + ) + ) + + situation_box_middle8_pass = ( + play_df.filter( + (pl.col("pass") == True) & (pl.col("middle_8") == True) & (pl.col("scrimmage_play") == True) + ) + .groupby(by=["pos_team"]) + .agg( + middle_8_pass=pl.col("pass").sum(), + EPA_middle_8_pass=pl.col("EPA").sum(), + EPA_middle_8_pass_per_play=pl.col("EPA").mean(), + EPA_middle_8_success_pass=pl.col("EPA_success").sum(), + EPA_middle_8_success_pass_rate=pl.col("EPA_success").mean(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns( + pos_team=pl.col("pos_team").cast(pl.Int32), + ) + ) + + situation_box_middle8_rush = ( + play_df.filter( + (pl.col("rush") == True) & (pl.col("middle_8") == True) & (pl.col("scrimmage_play") == True) + ) + .groupby(by=["pos_team"]) + .agg( + middle_8_rush=pl.col("rush").sum(), + EPA_middle_8_rush=pl.col("EPA").sum(), + EPA_middle_8_rush_per_play=pl.col("EPA").mean(), + EPA_middle_8_success_rush=pl.col("EPA_success").sum(), + EPA_middle_8_success_rush_rate=pl.col("EPA_success").mean(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns( + pos_team=pl.col("pos_team").cast(pl.Int32), + ) + ) + + situation_box_early = ( + play_df.filter((pl.col("early_down") == True) & (pl.col("scrimmage_play") == True)) + .groupby(by=["pos_team"]) + .agg( + EPA_success_early_down=pl.col("EPA_success").sum(), + EPA_success_early_down_rate=pl.col("EPA_success").mean(), + early_downs=pl.col("early_down").sum(), + early_down_pass_rate=pl.col("pass").mean(), + early_down_rush_rate=pl.col("rush").mean(), + EPA_early_down=pl.col("EPA").sum(), + EPA_early_down_per_play=pl.col("EPA").mean(), + early_down_first_down=pl.col("first_down_created").sum(), + early_down_first_down_rate=pl.col("first_down_created").mean(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns( + pos_team=pl.col("pos_team").cast(pl.Int32), + ) + ) + + situation_box_early_pass = ( + play_df.filter( + (pl.col("pass") == True) & (pl.col("early_down") == True) & (pl.col("scrimmage_play") == True) + ) + .groupby(by=["pos_team"]) + .agg( + early_down_pass=pl.col("pass").sum(), + EPA_early_down_pass=pl.col("EPA").sum(), + EPA_early_down_pass_per_play=pl.col("EPA").mean(), + EPA_success_early_down_pass=pl.col("EPA_success").sum(), + EPA_success_early_down_pass_rate=pl.col("EPA_success").mean(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns( + pos_team=pl.col("pos_team").cast(pl.Int32), + ) + ) + + situation_box_early_rush = ( + play_df.filter( + (pl.col("rush") == True) & (pl.col("early_down") == True) & (pl.col("scrimmage_play") == True) + ) + .groupby(by=["pos_team"]) + .agg( + early_down_rush=pl.col("rush").sum(), + EPA_early_down_rush=pl.col("EPA").sum(), + EPA_early_down_rush_per_play=pl.col("EPA").mean(), + EPA_success_early_down_rush=pl.col("EPA_success").sum(), + EPA_success_early_down_rush_rate=pl.col("EPA_success").mean(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns( + pos_team=pl.col("pos_team").cast(pl.Int32), + ) + ) + + situation_box_late = ( + play_df.filter((pl.col("late_down") == True) & (pl.col("scrimmage_play") == True)) + .groupby(by=["pos_team"]) + .agg( + EPA_success_late_down=pl.col("EPA_success_late_down").sum(), + EPA_success_late_down_pass=pl.col("EPA_success_late_down_pass").sum(), + EPA_success_late_down_rush=pl.col("EPA_success_late_down_rush").sum(), + late_downs=pl.col("late_down").sum(), + late_down_pass=pl.col("late_down_pass").sum(), + late_down_rush=pl.col("late_down_rush").sum(), + EPA_late_down=pl.col("EPA").sum(), + EPA_late_down_per_play=pl.col("EPA").mean(), + EPA_success_late_down_rate=pl.col("EPA_success_late_down").mean(), + EPA_success_late_down_pass_rate=pl.col("EPA_success_late_down_pass").mean(), + EPA_success_late_down_rush_rate=pl.col("EPA_success_late_down_rush").mean(), + late_down_pass_rate=pl.col("late_down_pass").mean(), + late_down_rush_rate=pl.col("late_down_rush").mean(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns( + pos_team=pl.col("pos_team").cast(pl.Int32), + ) + ) + + situation_box_standard = ( + play_df.filter((pl.col("standard_down") == True) & (pl.col("scrimmage_play") == True)) + .groupby(by=["pos_team"]) + .agg( + EPA_success_standard_down=pl.col("EPA_success").sum(), + EPA_success_standard_down_rate=pl.col("EPA_success").mean(), + EPA_standard_down=pl.col("EPA").sum(), + EPA_standard_down_per_play=pl.col("EPA").mean(), + standard_downs=pl.col("standard_down").sum(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns( + pos_team=pl.col("pos_team").cast(pl.Int32), + ) + ) + + situation_box_passing = ( + play_df.filter((pl.col("passing_down") == True) & (pl.col("scrimmage_play") == True)) + .groupby(by=["pos_team"]) + .agg( + EPA_success_passing_down=pl.col("EPA_success").sum(), + EPA_success_passing_down_rate=pl.col("EPA_success").mean(), + EPA_passing_down=pl.col("EPA").sum(), + EPA_passing_down_per_play=pl.col("EPA").mean(), + passing_downs=pl.col("passing_down").sum(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns( + pos_team=pl.col("pos_team").cast(pl.Int32), + ) ) situation_data_frames = [ - situation_box_normal, situation_box_pass, situation_box_rush, - situation_box_rz, situation_box_third, situation_box_early, situation_box_early_pass, - situation_box_early_rush, situation_box_middle8, situation_box_middle8_pass, - situation_box_middle8_rush, situation_box_late, situation_box_standard, situation_box_passing + situation_box_normal, + situation_box_pass, + situation_box_rush, + situation_box_rz, + situation_box_third, + situation_box_early, + situation_box_early_pass, + situation_box_early_rush, + situation_box_middle8, + situation_box_middle8_pass, + situation_box_middle8_rush, + situation_box_late, + situation_box_standard, + situation_box_passing, ] - situation_box = reduce(lambda left, right: left.join(right, on=["pos_team"], how="outer"), situation_data_frames) + situation_box = reduce( + lambda left, right: left.join(right, on=["pos_team"], how="outer"), situation_data_frames + ) play_df = play_df.with_columns( - drive_stopped = pl.col("drive_stopped").cast(pl.Float32), - drive_start = pl.col("drive_start").cast(pl.Float32), - ) - - def_base_box = play_df.filter(pl.col("scrimmage_play") == True).groupby(by=["def_pos_team"]).agg( - scrimmage_plays = pl.col('scrimmage_play').sum(), - TFL = pl.col('TFL').sum(), - TFL_pass = pl.col('TFL_pass').sum(), - TFL_rush = pl.col('TFL_rush').sum(), - havoc_total = pl.col('havoc').sum(), - havoc_total_rate = pl.col('havoc').mean(), - fumbles = pl.col('forced_fumble').sum(), - def_int = pl.col('int').sum(), - drive_stopped_rate = 100 * pl.col('drive_stopped').mean(), - ).with_columns(pl.col(pl.Float32).round(2) - ).with_columns( - def_pos_team = pl.col("def_pos_team").cast(pl.Int32), - ) - - def_box_havoc_pass = play_df.filter( - (pl.col("pass") == True) & (pl.col("scrimmage_play") == True) - ).groupby(by=["def_pos_team"]).agg( - num_pass_plays = pl.col('pass').sum(), - havoc_total_pass = pl.col('havoc').sum(), - havoc_total_pass_rate = pl.col('havoc').mean(), - sacks = pl.col('sack_vec').sum(), - sacks_rate = pl.col('sack_vec').mean(), - pass_breakups = pl.col('pass_breakup').sum() - ).with_columns(pl.col(pl.Float32).round(2) - ).with_columns( - def_pos_team = pl.col("def_pos_team").cast(pl.Int32), + drive_stopped=pl.col("drive_stopped").cast(pl.Float32), + drive_start=pl.col("drive_start").cast(pl.Float32), ) - def_box_havoc_rush = play_df.filter( - (pl.col("rush") == True) & (pl.col("scrimmage_play") == True) - ).groupby(by=["def_pos_team"]).agg( - havoc_total_rush = pl.col('havoc').sum(), - havoc_total_rush_rate = pl.col('havoc').mean(), - ).with_columns(pl.col(pl.Float32).round(2) - ).with_columns( - def_pos_team = pl.col("def_pos_team").cast(pl.Int32), + def_base_box = ( + play_df.filter(pl.col("scrimmage_play") == True) + .groupby(by=["def_pos_team"]) + .agg( + scrimmage_plays=pl.col("scrimmage_play").sum(), + TFL=pl.col("TFL").sum(), + TFL_pass=pl.col("TFL_pass").sum(), + TFL_rush=pl.col("TFL_rush").sum(), + havoc_total=pl.col("havoc").sum(), + havoc_total_rate=pl.col("havoc").mean(), + fumbles=pl.col("forced_fumble").sum(), + def_int=pl.col("int").sum(), + drive_stopped_rate=100 * pl.col("drive_stopped").mean(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns( + def_pos_team=pl.col("def_pos_team").cast(pl.Int32), + ) + ) + + def_box_havoc_pass = ( + play_df.filter((pl.col("pass") == True) & (pl.col("scrimmage_play") == True)) + .groupby(by=["def_pos_team"]) + .agg( + num_pass_plays=pl.col("pass").sum(), + havoc_total_pass=pl.col("havoc").sum(), + havoc_total_pass_rate=pl.col("havoc").mean(), + sacks=pl.col("sack_vec").sum(), + sacks_rate=pl.col("sack_vec").mean(), + pass_breakups=pl.col("pass_breakup").sum(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns( + def_pos_team=pl.col("def_pos_team").cast(pl.Int32), + ) ) - def_data_frames = [def_base_box,def_box_havoc_pass,def_box_havoc_rush] - def_box = reduce(lambda left,right: left.join(right, on=["def_pos_team"], how="outer"), def_data_frames) + def_box_havoc_rush = ( + play_df.filter((pl.col("rush") == True) & (pl.col("scrimmage_play") == True)) + .groupby(by=["def_pos_team"]) + .agg( + havoc_total_rush=pl.col("havoc").sum(), + havoc_total_rush_rate=pl.col("havoc").mean(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns( + def_pos_team=pl.col("def_pos_team").cast(pl.Int32), + ) + ) + + def_data_frames = [def_base_box, def_box_havoc_pass, def_box_havoc_rush] + def_box = reduce(lambda left, right: left.join(right, on=["def_pos_team"], how="outer"), def_data_frames) def_box_json = json.loads(def_box.write_json(row_oriented=True)) - turnover_box = play_df.filter(pl.col("scrimmage_play") == True).groupby(by=["pos_team"]).agg( - pass_breakups = pl.col('pass_breakup').sum(), - fumbles_lost = pl.col('fumble_lost').sum(), - fumbles_recovered = pl.col('fumble_recovered').sum(), - total_fumbles = pl.col('fumble_vec').sum(), - Int = pl.col('int').sum(), - ).with_columns(pl.col(pl.Float32).round(2) - ).with_columns( - pos_team = pl.col("pos_team").cast(pl.Int32), + turnover_box = ( + play_df.filter(pl.col("scrimmage_play") == True) + .groupby(by=["pos_team"]) + .agg( + pass_breakups=pl.col("pass_breakup").sum(), + fumbles_lost=pl.col("fumble_lost").sum(), + fumbles_recovered=pl.col("fumble_recovered").sum(), + total_fumbles=pl.col("fumble_vec").sum(), + Int=pl.col("int").sum(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns( + pos_team=pl.col("pos_team").cast(pl.Int32), + ) ) turnover_box_json = json.loads(turnover_box.write_json(row_oriented=True)) - if (len(turnover_box_json) < 2): + if len(turnover_box_json) < 2: for i in range(len(turnover_box_json), 2): turnover_box_json.append({}) @@ -3812,16 +4445,24 @@ def create_box_score(self, play_df): away_passes_def = turnover_box_json[0].get("pass_breakups", 0) away_passes_int = turnover_box_json[0].get("Int", 0) - away_fumbles = turnover_box_json[0].get('total_fumbles', 0) - turnover_box_json[0]["expected_turnovers"] = (0.5 * away_fumbles) + (0.22 * (away_passes_def + away_passes_int)) + away_fumbles = turnover_box_json[0].get("total_fumbles", 0) + turnover_box_json[0]["expected_turnovers"] = (0.5 * away_fumbles) + ( + 0.22 * (away_passes_def + away_passes_int) + ) home_passes_def = turnover_box_json[1].get("pass_breakups", 0) home_passes_int = turnover_box_json[1].get("Int", 0) - home_fumbles = turnover_box_json[1].get('total_fumbles', 0) - turnover_box_json[1]["expected_turnovers"] = (0.5 * home_fumbles) + (0.22 * (home_passes_def + home_passes_int)) + home_fumbles = turnover_box_json[1].get("total_fumbles", 0) + turnover_box_json[1]["expected_turnovers"] = (0.5 * home_fumbles) + ( + 0.22 * (home_passes_def + home_passes_int) + ) - turnover_box_json[0]["expected_turnover_margin"] = turnover_box_json[1]["expected_turnovers"] - turnover_box_json[0]["expected_turnovers"] - turnover_box_json[1]["expected_turnover_margin"] = turnover_box_json[0]["expected_turnovers"] - turnover_box_json[1]["expected_turnovers"] + turnover_box_json[0]["expected_turnover_margin"] = ( + turnover_box_json[1]["expected_turnovers"] - turnover_box_json[0]["expected_turnovers"] + ) + turnover_box_json[1]["expected_turnover_margin"] = ( + turnover_box_json[0]["expected_turnovers"] - turnover_box_json[1]["expected_turnovers"] + ) away_to = turnover_box_json[0].get("fumbles_lost", 0) + turnover_box_json[0]["Int"] home_to = turnover_box_json[1].get("fumbles_lost", 0) + turnover_box_json[1]["Int"] @@ -3832,46 +4473,55 @@ def create_box_score(self, play_df): turnover_box_json[0]["turnover_margin"] = home_to - away_to turnover_box_json[1]["turnover_margin"] = away_to - home_to - turnover_box_json[0]["turnover_luck"] = 5.0 * (turnover_box_json[0]["turnover_margin"] - turnover_box_json[0]["expected_turnover_margin"]) - turnover_box_json[1]["turnover_luck"] = 5.0 * (turnover_box_json[1]["turnover_margin"] - turnover_box_json[1]["expected_turnover_margin"]) - - drives_data = play_df.filter(pl.col("scrimmage_play") == True).groupby(by=["pos_team"]).agg( - drive_total_available_yards = pl.col('drive_start').sum(), - drive_total_gained_yards = pl.col('drive.yards').sum(), - avg_field_position = pl.col('drive_start').mean(), - plays_per_drive = pl.col('drive.offensivePlays').mean(), - yards_per_drive = pl.col('drive.yards').mean(), - drives = pl.col('drive.id').n_unique(), - drive_total_gained_yards_rate = 100 * pl.col('drive.yards').sum() / pl.col('drive_start').sum() - ).with_columns(pl.col(pl.Float32).round(2) - ).with_columns( - pos_team = pl.col("pos_team").cast(pl.Int32), + turnover_box_json[0]["turnover_luck"] = 5.0 * ( + turnover_box_json[0]["turnover_margin"] - turnover_box_json[0]["expected_turnover_margin"] + ) + turnover_box_json[1]["turnover_luck"] = 5.0 * ( + turnover_box_json[1]["turnover_margin"] - turnover_box_json[1]["expected_turnover_margin"] + ) + + drives_data = ( + play_df.filter(pl.col("scrimmage_play") == True) + .groupby(by=["pos_team"]) + .agg( + drive_total_available_yards=pl.col("drive_start").sum(), + drive_total_gained_yards=pl.col("drive.yards").sum(), + avg_field_position=pl.col("drive_start").mean(), + plays_per_drive=pl.col("drive.offensivePlays").mean(), + yards_per_drive=pl.col("drive.yards").mean(), + drives=pl.col("drive.id").n_unique(), + drive_total_gained_yards_rate=100 * pl.col("drive.yards").sum() / pl.col("drive_start").sum(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns( + pos_team=pl.col("pos_team").cast(pl.Int32), + ) ) return { - "pass" : json.loads(passer_box.write_json(row_oriented=True)), - "rush" : json.loads(rusher_box.write_json(row_oriented=True)), - "receiver" : json.loads(receiver_box.write_json(row_oriented=True)), - "team" : json.loads(team_box.write_json(row_oriented=True)), - "situational" : json.loads(situation_box.write_json(row_oriented=True)), - "defensive" : def_box_json, - "turnover" : turnover_box_json, - "drives" : json.loads(drives_data.write_json(row_oriented=True)) + "pass": json.loads(passer_box.write_json(row_oriented=True)), + "rush": json.loads(rusher_box.write_json(row_oriented=True)), + "receiver": json.loads(receiver_box.write_json(row_oriented=True)), + "team": json.loads(team_box.write_json(row_oriented=True)), + "situational": json.loads(situation_box.write_json(row_oriented=True)), + "defensive": def_box_json, + "turnover": turnover_box_json, + "drives": json.loads(drives_data.write_json(row_oriented=True)), } def run_processing_pipeline(self): if self.ran_pipeline == False: pbp_txt = self.__helper_cfb_pbp_drives(self.json) - self.plays_json = pbp_txt['plays'] + self.plays_json = pbp_txt["plays"] pbp_json = { "gameId": int(self.gameId), - "plays":self.plays_json, + "plays": self.plays_json, "season": pbp_txt["season"], - "week": pbp_txt['header']['week'], + "week": pbp_txt["header"]["week"], "gameInfo": pbp_txt["gameInfo"], "teamInfo": pbp_txt["header"]["competitions"][0], - "playByPlaySource": pbp_txt.get('header').get('competitions')[0].get('playByPlaySource'), + "playByPlaySource": pbp_txt.get("header").get("competitions")[0].get("playByPlaySource"), "drives": pbp_txt["drives"], "boxscore": pbp_txt["boxscore"], "header": pbp_txt["header"], @@ -3889,38 +4539,24 @@ def run_processing_pipeline(self): "videos": np.array(pbp_txt["videos"]).tolist(), } self.json = pbp_json - self.plays_json = pbp_txt['plays'] - if pbp_json.get('header').get('competitions')[0].get('playByPlaySource') != 'none': - self.plays_json = self.plays_json.pipe( - self.__add_downs_data - ).pipe( - self.__add_play_type_flags - ).pipe( - self.__add_rush_pass_flags - ).pipe( - self.__add_team_score_variables - ).pipe( - self.__add_new_play_types - ).pipe( - self.__setup_penalty_data - ).pipe( - self.__add_play_category_flags - ).pipe( - self.__add_yardage_cols - ).pipe( - self.__add_player_cols - ).pipe( - self.__after_cols - ).pipe( - self.__add_spread_time - ).pipe( - self.__process_epa - ).pipe( - self.__process_wpa - ).pipe( - self.__add_drive_data - ).pipe( - self.__process_qbr + self.plays_json = pbp_txt["plays"] + if pbp_json.get("header").get("competitions")[0].get("playByPlaySource") != "none": + self.plays_json = ( + self.plays_json.pipe(self.__add_downs_data) + .pipe(self.__add_play_type_flags) + .pipe(self.__add_rush_pass_flags) + .pipe(self.__add_team_score_variables) + .pipe(self.__add_new_play_types) + .pipe(self.__setup_penalty_data) + .pipe(self.__add_play_category_flags) + .pipe(self.__add_yardage_cols) + .pipe(self.__add_player_cols) + .pipe(self.__after_cols) + .pipe(self.__add_spread_time) + .pipe(self.__process_epa) + .pipe(self.__process_wpa) + .pipe(self.__add_drive_data) + .pipe(self.__process_qbr) ) self.ran_pipeline = True advBoxScore = self.create_box_score(self.plays_json) @@ -3929,7 +4565,7 @@ def run_processing_pipeline(self): "gameId": int(self.gameId), "plays": self.plays_json, "season": pbp_txt["season"], - "week": pbp_txt['header']['week'], + "week": pbp_txt["header"]["week"], "gameInfo": pbp_txt["gameInfo"], "teamInfo": pbp_txt["header"]["competitions"][0], "playByPlaySource": pbp_txt["playByPlaySource"], @@ -3957,16 +4593,16 @@ def run_processing_pipeline(self): def run_cleaning_pipeline(self): if self.ran_cleaning_pipeline == False: pbp_txt = self.__helper_cfb_pbp_drives(self.json) - self.plays_json = pbp_txt['plays'] + self.plays_json = pbp_txt["plays"] pbp_json = { "gameId": int(self.gameId), "plays": self.plays_json, "season": pbp_txt["season"], - "week": pbp_txt['header']['week'], + "week": pbp_txt["header"]["week"], "gameInfo": pbp_txt["gameInfo"], "teamInfo": pbp_txt["header"]["competitions"][0], - "playByPlaySource": pbp_txt.get('header').get('competitions')[0].get('playByPlaySource'), + "playByPlaySource": pbp_txt.get("header").get("competitions")[0].get("playByPlaySource"), "drives": pbp_txt["drives"], "boxscore": pbp_txt["boxscore"], "header": pbp_txt["header"], @@ -3984,37 +4620,27 @@ def run_cleaning_pipeline(self): "videos": np.array(pbp_txt["videos"]).tolist(), } self.json = pbp_json - self.plays_json = pbp_txt['plays'] - if pbp_json.get('header').get('competitions')[0].get('playByPlaySource') != 'none': - self.plays_json = self.plays_json.pipe( - self.__add_downs_data - ).pipe( - self.__add_play_type_flags - ).pipe( - self.__add_rush_pass_flags - ).pipe( - self.__add_team_score_variables - ).pipe( - self.__add_new_play_types - ).pipe( - self.__setup_penalty_data - ).pipe( - self.__add_play_category_flags - ).pipe( - self.__add_yardage_cols - ).pipe( - self.__add_player_cols - ).pipe( - self.__after_cols - ).pipe( - self.__add_spread_time + self.plays_json = pbp_txt["plays"] + if pbp_json.get("header").get("competitions")[0].get("playByPlaySource") != "none": + self.plays_json = ( + self.plays_json.pipe(self.__add_downs_data) + .pipe(self.__add_play_type_flags) + .pipe(self.__add_rush_pass_flags) + .pipe(self.__add_team_score_variables) + .pipe(self.__add_new_play_types) + .pipe(self.__setup_penalty_data) + .pipe(self.__add_play_category_flags) + .pipe(self.__add_yardage_cols) + .pipe(self.__add_player_cols) + .pipe(self.__after_cols) + .pipe(self.__add_spread_time) ) self.plays_json = self.plays_json.to_dicts() pbp_json = { "gameId": int(self.gameId), "plays": self.plays_json, "season": pbp_txt["season"], - "week": pbp_txt['header']['week'], + "week": pbp_txt["header"]["week"], "gameInfo": pbp_txt["gameInfo"], "teamInfo": pbp_txt["header"]["competitions"][0], "playByPlaySource": pbp_txt["playByPlaySource"], @@ -4036,4 +4662,4 @@ def run_cleaning_pipeline(self): } self.json = pbp_json self.ran_cleaning_pipeline = True - return self.json \ No newline at end of file + return self.json diff --git a/sportsdataverse/cfb/cfb_teams.py b/sportsdataverse/cfb/cfb_teams.py index 3191807..5bfed0a 100755 --- a/sportsdataverse/cfb/cfb_teams.py +++ b/sportsdataverse/cfb/cfb_teams.py @@ -1,8 +1,12 @@ +from functools import lru_cache + import pandas as pd import polars as pl + from sportsdataverse.dl_utils import download, underscore +@lru_cache(maxsize=None) def espn_cfb_teams(groups=None, return_as_pandas=True, **kwargs) -> pd.DataFrame: """espn_cfb_teams - look up the college football teams diff --git a/sportsdataverse/config.py b/sportsdataverse/config.py index b1195eb..3da5428 100755 --- a/sportsdataverse/config.py +++ b/sportsdataverse/config.py @@ -1,73 +1,73 @@ SGITHUB = "https://raw.githubusercontent.com/sportsdataverse/" SDVRELEASES = "https://github.com/sportsdataverse/sportsdataverse-data/releases/download/" -CFB_BASE_URL = SGITHUB+"cfbfastR-data/main/pbp/parquet/play_by_play_{season}.parquet" -CFB_ROSTER_URL = SGITHUB+"cfbfastR-data/main/rosters/parquet/cfb_rosters_{season}.parquet" -CFB_TEAM_LOGO_URL = SGITHUB+"cfbfastR-data/main/teams/teams_colors_logos.parquet" -CFB_TEAM_SCHEDULE_URL = SGITHUB+"cfbfastR-data/main/schedules/parquet/cfb_schedules_{season}.parquet" -CFB_TEAM_INFO_URL = SGITHUB+"cfbfastR-data/main/team_info/parquet/cfb_team_info_{season}.parquet" +CFB_BASE_URL = SGITHUB + "cfbfastR-data/main/pbp/parquet/play_by_play_{season}.parquet" +CFB_ROSTER_URL = SGITHUB + "cfbfastR-data/main/rosters/parquet/cfb_rosters_{season}.parquet" +CFB_TEAM_LOGO_URL = SGITHUB + "cfbfastR-data/main/teams/teams_colors_logos.parquet" +CFB_TEAM_SCHEDULE_URL = SGITHUB + "cfbfastR-data/main/schedules/parquet/cfb_schedules_{season}.parquet" +CFB_TEAM_INFO_URL = SGITHUB + "cfbfastR-data/main/team_info/parquet/cfb_team_info_{season}.parquet" -NHL_BASE_URL = SGITHUB+"fastRhockey-data/main/nhl/pbp/parquet/play_by_play_{season}.parquet" -NHL_PLAYER_BOX_URL = SGITHUB+"fastRhockey-data/main/nhl/player_box/parquet/player_box_{season}.parquet" -NHL_TEAM_BOX_URL = SGITHUB+"fastRhockey-data/main/nhl/team_box/parquet/team_box_{season}.parquet" -NHL_TEAM_SCHEDULE_URL = SGITHUB+"fastRhockey-data/main/nhl/schedules/parquet/nhl_schedule_{season}.parquet" -NHL_TEAM_LOGO_URL = SGITHUB+"fastRhockey-data/main/nhl/nhl_teams_colors_logos.csv" +NHL_BASE_URL = SGITHUB + "fastRhockey-data/main/nhl/pbp/parquet/play_by_play_{season}.parquet" +NHL_PLAYER_BOX_URL = SGITHUB + "fastRhockey-data/main/nhl/player_box/parquet/player_box_{season}.parquet" +NHL_TEAM_BOX_URL = SGITHUB + "fastRhockey-data/main/nhl/team_box/parquet/team_box_{season}.parquet" +NHL_TEAM_SCHEDULE_URL = SGITHUB + "fastRhockey-data/main/nhl/schedules/parquet/nhl_schedule_{season}.parquet" +NHL_TEAM_LOGO_URL = SGITHUB + "fastRhockey-data/main/nhl/nhl_teams_colors_logos.csv" -PHF_BASE_URL = SGITHUB+"fastRhockey-data/main/phf/pbp/parquet/play_by_play_{season}.parquet" -PHF_PLAYER_BOX_URL = SGITHUB+"fastRhockey-data/main/phf/player_box/parquet/player_box_{season}.parquet" -PHF_TEAM_BOX_URL = SGITHUB+"fastRhockey-data/main/phf/team_box/parquet/team_box_{season}.parquet" -PHF_TEAM_SCHEDULE_URL = SGITHUB+"fastRhockey-data/main/phf/schedules/parquet/phf_schedule_{season}.parquet" +PHF_BASE_URL = SGITHUB + "fastRhockey-data/main/phf/pbp/parquet/play_by_play_{season}.parquet" +PHF_PLAYER_BOX_URL = SGITHUB + "fastRhockey-data/main/phf/player_box/parquet/player_box_{season}.parquet" +PHF_TEAM_BOX_URL = SGITHUB + "fastRhockey-data/main/phf/team_box/parquet/team_box_{season}.parquet" +PHF_TEAM_SCHEDULE_URL = SGITHUB + "fastRhockey-data/main/phf/schedules/parquet/phf_schedule_{season}.parquet" -MBB_BASE_URL = SDVRELEASES+"espn_mens_college_basketball_pbp/play_by_play_{season}.parquet" -MBB_TEAM_BOX_URL = SDVRELEASES+"espn_mens_college_basketball_team_boxscores/team_box_{season}.parquet" -MBB_PLAYER_BOX_URL = SDVRELEASES+"espn_mens_college_basketball_player_boxscores/player_box_{season}.parquet" -MBB_TEAM_LOGO_URL = SDVRELEASES+"hoopR-data/master/mbb/teams_colors_logos.csv" -MBB_TEAM_SCHEDULE_URL = SDVRELEASES+"espn_mens_college_basketball_schedules/mbb_schedule_{season}.parquet" +MBB_BASE_URL = SDVRELEASES + "espn_mens_college_basketball_pbp/play_by_play_{season}.parquet" +MBB_TEAM_BOX_URL = SDVRELEASES + "espn_mens_college_basketball_team_boxscores/team_box_{season}.parquet" +MBB_PLAYER_BOX_URL = SDVRELEASES + "espn_mens_college_basketball_player_boxscores/player_box_{season}.parquet" +MBB_TEAM_LOGO_URL = SDVRELEASES + "hoopR-data/master/mbb/teams_colors_logos.csv" +MBB_TEAM_SCHEDULE_URL = SDVRELEASES + "espn_mens_college_basketball_schedules/mbb_schedule_{season}.parquet" -NBA_BASE_URL = SDVRELEASES+"espn_nba_pbp/play_by_play_{season}.parquet" -NBA_TEAM_BOX_URL = SDVRELEASES+"espn_nba_team_boxscores/team_box_{season}.parquet" -NBA_PLAYER_BOX_URL = SDVRELEASES+"espn_nba_player_boxscores/player_box_{season}.parquet" -NBA_TEAM_SCHEDULE_URL = SDVRELEASES+"espn_nba_schedules/nba_schedule_{season}.parquet" +NBA_BASE_URL = SDVRELEASES + "espn_nba_pbp/play_by_play_{season}.parquet" +NBA_TEAM_BOX_URL = SDVRELEASES + "espn_nba_team_boxscores/team_box_{season}.parquet" +NBA_PLAYER_BOX_URL = SDVRELEASES + "espn_nba_player_boxscores/player_box_{season}.parquet" +NBA_TEAM_SCHEDULE_URL = SDVRELEASES + "espn_nba_schedules/nba_schedule_{season}.parquet" -WBB_BASE_URL = SDVRELEASES+"espn_womens_college_basketball_pbp/play_by_play_{season}.parquet" -WBB_TEAM_BOX_URL = SDVRELEASES+"espn_womens_college_basketball_team_boxscores/team_box_{season}.parquet" -WBB_PLAYER_BOX_URL = SDVRELEASES+"espn_womens_college_basketball_player_boxscores/player_box_{season}.parquet" -WBB_TEAM_LOGO_URL = SDVRELEASES+"wehoop-data/master/wbb/teams_colors_logos.csv" -WBB_TEAM_SCHEDULE_URL = SDVRELEASES+"espn_womens_college_basketball_schedules/wbb_schedule_{season}.parquet" +WBB_BASE_URL = SDVRELEASES + "espn_womens_college_basketball_pbp/play_by_play_{season}.parquet" +WBB_TEAM_BOX_URL = SDVRELEASES + "espn_womens_college_basketball_team_boxscores/team_box_{season}.parquet" +WBB_PLAYER_BOX_URL = SDVRELEASES + "espn_womens_college_basketball_player_boxscores/player_box_{season}.parquet" +WBB_TEAM_LOGO_URL = SDVRELEASES + "wehoop-data/master/wbb/teams_colors_logos.csv" +WBB_TEAM_SCHEDULE_URL = SDVRELEASES + "espn_womens_college_basketball_schedules/wbb_schedule_{season}.parquet" -WNBA_BASE_URL = SDVRELEASES+"espn_wnba_pbp/play_by_play_{season}.parquet" -WNBA_TEAM_BOX_URL = SDVRELEASES+"espn_wnba_team_boxscores/team_box_{season}.parquet" -WNBA_PLAYER_BOX_URL = SDVRELEASES+"espn_wnba_player_boxscores/player_box_{season}.parquet" -WNBA_TEAM_SCHEDULE_URL = SDVRELEASES+"espn_wnba_schedules/wnba_schedule_{season}.parquet" +WNBA_BASE_URL = SDVRELEASES + "espn_wnba_pbp/play_by_play_{season}.parquet" +WNBA_TEAM_BOX_URL = SDVRELEASES + "espn_wnba_team_boxscores/team_box_{season}.parquet" +WNBA_PLAYER_BOX_URL = SDVRELEASES + "espn_wnba_player_boxscores/player_box_{season}.parquet" +WNBA_TEAM_SCHEDULE_URL = SDVRELEASES + "espn_wnba_schedules/wnba_schedule_{season}.parquet" -NFLVERSEGITHUB="https://github.com/nflverse/nflverse-data/releases/download/" -NFLVERSEGITHUBPBP="https://raw.githubusercontent.com/nflverse/" -NFL_BASE_URL = NFLVERSEGITHUB+"pbp/play_by_play_{season}.parquet" #done -NFL_PLAYER_URL = NFLVERSEGITHUB+"players/players.parquet" #done -NFL_PLAYER_STATS_URL = NFLVERSEGITHUB+"player_stats/player_stats.parquet" #done -NFL_PLAYER_KICKING_STATS_URL = NFLVERSEGITHUB+"player_stats/player_stats_kicking.parquet" #done -NFL_PFR_SEASON_DEF_URL = NFLVERSEGITHUB+"pfr_advstats/advstats_season_def.parquet" -NFL_PFR_WEEK_DEF_URL = NFLVERSEGITHUB+"pfr_advstats/advstats_week_def_{season}.parquet" -NFL_PFR_SEASON_PASS_URL = NFLVERSEGITHUB+"pfr_advstats/advstats_season_pass.parquet" -NFL_PFR_WEEK_PASS_URL = NFLVERSEGITHUB+"pfr_advstats/advstats_week_pass_{season}.parquet" -NFL_PFR_SEASON_REC_URL = NFLVERSEGITHUB+"pfr_advstats/advstats_season_rec.parquet" -NFL_PFR_WEEK_REC_URL = NFLVERSEGITHUB+"pfr_advstats/advstats_week_rec_{season}.parquet" -NFL_PFR_SEASON_RUSH_URL = NFLVERSEGITHUB+"pfr_advstats/advstats_season_rush.parquet" -NFL_PFR_WEEK_RUSH_URL = NFLVERSEGITHUB+"pfr_advstats/advstats_week_rush_{season}.parquet" -NFL_NGS_RUSHING_URL = NFLVERSEGITHUB+"nextgen_stats/ngs_rushing.parquet" -NFL_NGS_PASSING_URL = NFLVERSEGITHUB+"nextgen_stats/ngs_passing.parquet" -NFL_NGS_RECEIVING_URL = NFLVERSEGITHUB+"nextgen_stats/ngs_receiving.parquet" -NFL_ROSTER_URL = NFLVERSEGITHUB+"rosters/roster_{season}.parquet" #done -NFL_WEEKLY_ROSTER_URL = NFLVERSEGITHUB+"weekly_rosters/roster_weekly_{season}.parquet" #done -NFL_SNAP_COUNTS_URL = NFLVERSEGITHUB+"snap_counts/snap_counts_{season}.parquet" -NFL_PBP_PARTICIPATION_URL = NFLVERSEGITHUB+"pbp_participation/pbp_participation_{season}.parquet" -NFL_CONTRACTS_URL = NFLVERSEGITHUB+"contracts/historical_contracts.parquet" -NFL_OTC_PLAYER_DETAILS_URL = NFLVERSEGITHUB+"contracts/otc_player_details.rds" -NFL_DRAFT_PICKS_URL = NFLVERSEGITHUB+"draft_picks/draft_picks.parquet" -NFL_COMBINE_URL = NFLVERSEGITHUB+"combine/combine.parquet" -NFL_INJURIES_URL = NFLVERSEGITHUB+"injuries/injuries_{season}.parquet" -NFL_DEPTH_CHARTS_URL = NFLVERSEGITHUB+"depth_charts/depth_charts_{season}.parquet" -NFL_OFFICIALS_URL = NFLVERSEGITHUB+"officials/officials.parquet" -NFL_TEAM_LOGO_URL = NFLVERSEGITHUBPBP+"nflverse-pbp/master/teams_colors_logos.csv" -NFL_TEAM_SCHEDULE_URL = NFLVERSEGITHUBPBP+"nflverse-pbp/master/schedules/sched_{season}.rds" \ No newline at end of file +NFLVERSEGITHUB = "https://github.com/nflverse/nflverse-data/releases/download/" +NFLVERSEGITHUBPBP = "https://raw.githubusercontent.com/nflverse/" +NFL_BASE_URL = NFLVERSEGITHUB + "pbp/play_by_play_{season}.parquet" # done +NFL_PLAYER_URL = NFLVERSEGITHUB + "players/players.parquet" # done +NFL_PLAYER_STATS_URL = NFLVERSEGITHUB + "player_stats/player_stats.parquet" # done +NFL_PLAYER_KICKING_STATS_URL = NFLVERSEGITHUB + "player_stats/player_stats_kicking.parquet" # done +NFL_PFR_SEASON_DEF_URL = NFLVERSEGITHUB + "pfr_advstats/advstats_season_def.parquet" +NFL_PFR_WEEK_DEF_URL = NFLVERSEGITHUB + "pfr_advstats/advstats_week_def_{season}.parquet" +NFL_PFR_SEASON_PASS_URL = NFLVERSEGITHUB + "pfr_advstats/advstats_season_pass.parquet" +NFL_PFR_WEEK_PASS_URL = NFLVERSEGITHUB + "pfr_advstats/advstats_week_pass_{season}.parquet" +NFL_PFR_SEASON_REC_URL = NFLVERSEGITHUB + "pfr_advstats/advstats_season_rec.parquet" +NFL_PFR_WEEK_REC_URL = NFLVERSEGITHUB + "pfr_advstats/advstats_week_rec_{season}.parquet" +NFL_PFR_SEASON_RUSH_URL = NFLVERSEGITHUB + "pfr_advstats/advstats_season_rush.parquet" +NFL_PFR_WEEK_RUSH_URL = NFLVERSEGITHUB + "pfr_advstats/advstats_week_rush_{season}.parquet" +NFL_NGS_RUSHING_URL = NFLVERSEGITHUB + "nextgen_stats/ngs_rushing.parquet" +NFL_NGS_PASSING_URL = NFLVERSEGITHUB + "nextgen_stats/ngs_passing.parquet" +NFL_NGS_RECEIVING_URL = NFLVERSEGITHUB + "nextgen_stats/ngs_receiving.parquet" +NFL_ROSTER_URL = NFLVERSEGITHUB + "rosters/roster_{season}.parquet" # done +NFL_WEEKLY_ROSTER_URL = NFLVERSEGITHUB + "weekly_rosters/roster_weekly_{season}.parquet" # done +NFL_SNAP_COUNTS_URL = NFLVERSEGITHUB + "snap_counts/snap_counts_{season}.parquet" +NFL_PBP_PARTICIPATION_URL = NFLVERSEGITHUB + "pbp_participation/pbp_participation_{season}.parquet" +NFL_CONTRACTS_URL = NFLVERSEGITHUB + "contracts/historical_contracts.parquet" +NFL_OTC_PLAYER_DETAILS_URL = NFLVERSEGITHUB + "contracts/otc_player_details.rds" +NFL_DRAFT_PICKS_URL = NFLVERSEGITHUB + "draft_picks/draft_picks.parquet" +NFL_COMBINE_URL = NFLVERSEGITHUB + "combine/combine.parquet" +NFL_INJURIES_URL = NFLVERSEGITHUB + "injuries/injuries_{season}.parquet" +NFL_DEPTH_CHARTS_URL = NFLVERSEGITHUB + "depth_charts/depth_charts_{season}.parquet" +NFL_OFFICIALS_URL = NFLVERSEGITHUB + "officials/officials.parquet" +NFL_TEAM_LOGO_URL = NFLVERSEGITHUBPBP + "nflverse-pbp/master/teams_colors_logos.csv" +NFL_TEAM_SCHEDULE_URL = NFLVERSEGITHUBPBP + "nflverse-pbp/master/schedules/sched_{season}.rds" diff --git a/sportsdataverse/decorators.py b/sportsdataverse/decorators.py index 15acb2e..0847512 100644 --- a/sportsdataverse/decorators.py +++ b/sportsdataverse/decorators.py @@ -1,9 +1,11 @@ -from functools import wraps -import os import functools +import os import time +from functools import wraps + import psutil + def timer(number=10): """Decorator that times the function it wraps over repeated executions @@ -13,6 +15,7 @@ def timer(number=10): Returns: func """ + def actual_wrapper(func): @functools.wraps(func) def wrapper_timer(*args, **kwargs): @@ -23,12 +26,14 @@ def wrapper_timer(*args, **kwargs): value = func(*args, **kwargs) toc = time.perf_counter() elapsed_time = toc - tic - print(f"Elapsed time of {func.__name__} for {number} runs:\n" - f" {elapsed_time:0.6f} seconds") + print(f"Elapsed time of {func.__name__} for {number} runs:\n" f" {elapsed_time:0.6f} seconds") return value + return wrapper_timer + return actual_wrapper + # this decorator is used to record memory usage of the decorated function def record_mem_usage(func): @wraps(func) @@ -38,11 +43,12 @@ def wrapper(*args, **kwargs): rt = func(*args, **kwargs) mem_end = process.memory_info()[0] diff_KB = (mem_end - mem_start) // 1000 - print(f'memory usage of {func.__name__}: {diff_KB} KB') + print(f"memory usage of {func.__name__}: {diff_KB} KB") return rt return wrapper + def record_time_usage(func): @wraps(func) def timeit_wrapper(*args, **kwargs): @@ -50,6 +56,7 @@ def timeit_wrapper(*args, **kwargs): result = func(*args, **kwargs) end_time = time.perf_counter() total_time = end_time - start_time - print(f'Function {func.__name__}{args} {kwargs} Took {total_time:.4f} seconds') + print(f"Function {func.__name__}{args} {kwargs} Took {total_time:.4f} seconds") return result + return timeit_wrapper diff --git a/sportsdataverse/dl_utils.py b/sportsdataverse/dl_utils.py index d5d3088..5156606 100755 --- a/sportsdataverse/dl_utils.py +++ b/sportsdataverse/dl_utils.py @@ -1,35 +1,43 @@ - -import numpy as np -import re -import time import http.client -import urllib.request -import logging -import requests import json -from urllib.error import URLError, HTTPError, ContentTooShortError -from datetime import datetime +import logging +import re +import time from itertools import chain, starmap +from urllib.error import ContentTooShortError, HTTPError, URLError -def download(url, params = None, headers = None, proxy = None, timeout = 30, num_retries = 15, logger = None): +import numpy as np +import requests + + +def download(url, params=None, headers=None, proxy=None, timeout=30, num_retries=15, logger=None): if params is None: params = {} if logger is None: logger = logging.getLogger(__name__) try: - response = requests.get(url, params = params, proxies = proxy, headers = headers, timeout = timeout) + response = requests.get(url, params=params, proxies=proxy, headers=headers, timeout=timeout) # print(response.url) except (URLError, HTTPError, ContentTooShortError, http.client.HTTPException, http.client.IncompleteRead) as e: logger.warn("Download error: %i - %s for url (%s)", response.status_code, response.reason, response.url) response = None - if num_retries > 0 and (hasattr(e, 'code') and 500 <= getattr(e, 'code') < 600): + if num_retries > 0 and (hasattr(e, "code") and 500 <= getattr(e, "code") < 600): time.sleep(2) - return download(url, params = params, proxies = proxy, headers = headers, timeout = timeout, num_retries = num_retries - 1, logger = logger) + return download( + url, + params=params, + proxies=proxy, + headers=headers, + timeout=timeout, + num_retries=num_retries - 1, + logger=logger, + ) if num_retries == 0: logger.error("Retry Limit Exceeded") return response -def flatten_json_iterative(dictionary, sep = '.', ind_start = 0): + +def flatten_json_iterative(dictionary, sep=".", ind_start=0): """Flattening a nested json file""" def unpack_one(parent_key, parent_value): @@ -42,24 +50,28 @@ def unpack_one(parent_key, parent_value): elif isinstance(parent_value, list): i = ind_start for value in parent_value: - t2 = parent_key + sep +str(i) + t2 = parent_key + sep + str(i) i += 1 yield t2, value else: yield parent_key, parent_value + # Continue iterating the unpack_one function until the terminating condition is satisfied while True: # Continue unpacking the json file until all values are atomic elements (aka neither a dictionary nor a list) dictionary = dict(chain.from_iterable(starmap(unpack_one, dictionary.items()))) # Terminating condition: none of the values in the json file are a dictionary or a list - if not any(isinstance(value, dict) for value in dictionary.values()) and \ - not any(isinstance(value, list) for value in dictionary.values()): + if not any(isinstance(value, dict) for value in dictionary.values()) and not any( + isinstance(value, list) for value in dictionary.values() + ): break return dictionary -def key_check(obj, key, replacement = np.array([])): + +def key_check(obj, key, replacement=np.array([])): return obj[key] if key in obj.keys() else replacement + def underscore(word): """ Make an underscored, lowercase form from the expression in the string. @@ -76,11 +88,12 @@ def underscore(word): 'IoError' """ - word = re.sub(r"([A-Z]+)([A-Z][a-z])", r'\1_\2', word) - word = re.sub(r"([a-z\d])([A-Z])", r'\1_\2', word) + word = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", word) + word = re.sub(r"([a-z\d])([A-Z])", r"\1_\2", word) word = word.replace("-", "_") return word.lower() + def camelize(string, uppercase_first_letter=True): """ Convert strings to CamelCase. @@ -107,6 +120,7 @@ def camelize(string, uppercase_first_letter=True): else: return string[0].lower() + camelize(string)[1:] + class ESPNResponse: def __init__(self, response, status_code, url): self._response = response @@ -132,8 +146,8 @@ def valid_json(self): def get_url(self): return self._url -class ESPNHTTP: +class ESPNHTTP: espn_response = ESPNResponse base_url = None @@ -145,9 +159,11 @@ class ESPNHTTP: def clean_contents(self, contents): return contents - def send_api_request(self, endpoint, parameters, referer=None, headers=None, timeout=None, raise_exception_on_error=False): + def send_api_request( + self, endpoint, parameters, referer=None, headers=None, timeout=None, raise_exception_on_error=False + ): if not self.base_url: - raise Exception('Cannot use send_api_request from _HTTP class.') + raise Exception("Cannot use send_api_request from _HTTP class.") base_url = self.base_url.format(endpoint=endpoint) endpoint = endpoint.lower() self.parameters = parameters @@ -158,7 +174,7 @@ def send_api_request(self, endpoint, parameters, referer=None, headers=None, tim request_headers = headers if referer: - request_headers['Referer'] = referer + request_headers["Referer"] = referer url = None status_code = None @@ -178,6 +194,6 @@ def send_api_request(self, endpoint, parameters, referer=None, headers=None, tim data = self.espn_response(response=contents, status_code=status_code, url=url) if raise_exception_on_error and not data.valid_json(): - raise Exception('InvalidResponse: Response is not in a valid JSON format.') + raise Exception("InvalidResponse: Response is not in a valid JSON format.") - return data \ No newline at end of file + return data diff --git a/sportsdataverse/errors.py b/sportsdataverse/errors.py index 39a21e8..b0bac33 100755 --- a/sportsdataverse/errors.py +++ b/sportsdataverse/errors.py @@ -2,6 +2,7 @@ Custom exceptions for sportsdataverse module """ + class SeasonNotFoundError(Exception): pass diff --git a/sportsdataverse/mbb/__init__.py b/sportsdataverse/mbb/__init__.py index bade5cd..ecc78b7 100755 --- a/sportsdataverse/mbb/__init__.py +++ b/sportsdataverse/mbb/__init__.py @@ -1,6 +1,5 @@ - from sportsdataverse.mbb.mbb_game_rosters import * from sportsdataverse.mbb.mbb_loaders import * from sportsdataverse.mbb.mbb_pbp import * from sportsdataverse.mbb.mbb_schedule import * -from sportsdataverse.mbb.mbb_teams import * \ No newline at end of file +from sportsdataverse.mbb.mbb_teams import * diff --git a/sportsdataverse/mbb/mbb_loaders.py b/sportsdataverse/mbb/mbb_loaders.py index 86c8cdf..ab7cba0 100755 --- a/sportsdataverse/mbb/mbb_loaders.py +++ b/sportsdataverse/mbb/mbb_loaders.py @@ -1,8 +1,15 @@ +from typing import List + import pandas as pd import polars as pl from tqdm import tqdm -from typing import List -from sportsdataverse.config import MBB_BASE_URL, MBB_TEAM_BOX_URL, MBB_PLAYER_BOX_URL, MBB_TEAM_SCHEDULE_URL + +from sportsdataverse.config import ( + MBB_BASE_URL, + MBB_PLAYER_BOX_URL, + MBB_TEAM_BOX_URL, + MBB_TEAM_SCHEDULE_URL, +) from sportsdataverse.errors import SeasonNotFoundError diff --git a/sportsdataverse/mbb/mbb_pbp.py b/sportsdataverse/mbb/mbb_pbp.py index 4b7f81e..c6a7aa3 100755 --- a/sportsdataverse/mbb/mbb_pbp.py +++ b/sportsdataverse/mbb/mbb_pbp.py @@ -1,10 +1,12 @@ -import pandas as pd -import polars as pl -import numpy as np -import os import json +import os import re from typing import Dict + +import numpy as np +import pandas as pd +import polars as pl + from sportsdataverse.dl_utils import download, flatten_json_iterative diff --git a/sportsdataverse/mbb/mbb_teams.py b/sportsdataverse/mbb/mbb_teams.py index aade45d..1824f2c 100755 --- a/sportsdataverse/mbb/mbb_teams.py +++ b/sportsdataverse/mbb/mbb_teams.py @@ -1,9 +1,12 @@ +from functools import lru_cache + import pandas as pd import polars as pl from sportsdataverse.dl_utils import download, underscore +@lru_cache(maxsize=None) def espn_mbb_teams(groups=None, return_as_pandas=True, **kwargs) -> pd.DataFrame: """espn_mbb_teams - look up the men's college basketball teams diff --git a/sportsdataverse/mlb/__init__.py b/sportsdataverse/mlb/__init__.py index 3351180..b03294a 100755 --- a/sportsdataverse/mlb/__init__.py +++ b/sportsdataverse/mlb/__init__.py @@ -1,8 +1,8 @@ from sportsdataverse.mlb.mlb_loaders import * -from sportsdataverse.mlb.retrosheet import * -from sportsdataverse.mlb.retrosplits import * from sportsdataverse.mlb.mlbam_games import * from sportsdataverse.mlb.mlbam_players import * from sportsdataverse.mlb.mlbam_reports import * from sportsdataverse.mlb.mlbam_stats import * from sportsdataverse.mlb.mlbam_teams import * +from sportsdataverse.mlb.retrosheet import * +from sportsdataverse.mlb.retrosplits import * diff --git a/sportsdataverse/mlb/mlb_loaders.py b/sportsdataverse/mlb/mlb_loaders.py index c481d2a..66074da 100755 --- a/sportsdataverse/mlb/mlb_loaders.py +++ b/sportsdataverse/mlb/mlb_loaders.py @@ -1,7 +1,7 @@ -from sportsdataverse.dl_utils import download - import os +from sportsdataverse.dl_utils import download + def mlbam_copyright_info(saveFile=False, returnFile=False, **kwargs): """ diff --git a/sportsdataverse/mlb/mlbam_games.py b/sportsdataverse/mlb/mlbam_games.py index 0664347..875fd66 100755 --- a/sportsdataverse/mlb/mlbam_games.py +++ b/sportsdataverse/mlb/mlbam_games.py @@ -1,74 +1,86 @@ ## Script: games.py ## Author: Joseph Armstrong (armstjc) -import pandas as pd -from pandas import json_normalize import json -from sportsdataverse.dl_utils import download from datetime import datetime +import pandas as pd +from pandas import json_normalize + +from sportsdataverse.dl_utils import download -def mlbam_schedule(season:int,gameType="R"): - """ - Retrieves the start and end date for games for every league, and the MLB,for a given season. - This function does not get individual games. - - Args: - season (int): - Required parameter. Indicates the season you are trying to find the games for. - - gameType (string) = "R": - Optional parameter. If there's no input, this function will get the info for the regular season. - - Other parts of the season are indicated as follows in the MLBAM API: - - 'S' - Spring Training - 'E' - Exhibition - 'A' - All Star Game - 'D' - Division Series - 'F' - First Round (Wild Card) - 'L' - League Championship - 'W' - World Series - - Returns: - A pandas dataframe containing MLB scheduled games. - """ - main_df = pd.DataFrame() - - searchURL = "http://lookup-service-prod.mlb.com/json/named.org_game_type_date_info.bam?current_sw='Y'&sport_code='mlb'&" - - if gameType == "R" or gameType == "S" or gameType == "E" or gameType == "A" or gameType == "D" or gameType == "F" or gameType == "L" or gameType == "W": - searchURL = searchURL + f'game_type=\'{gameType}\'&' - else: - print('Check your input for seasonType. Searching for regular season stats instead.') - gameType = "R" - searchURL = searchURL + f'game_type=\'{gameType}\'&' - - now = datetime.now() - if season < 1860: - print('Please input a proper year. The search will continue with the current year instead.') - season = int(now.year) - searchURL = searchURL + f'season=\'{season}\'' - elif int(now.year) < season: - print('Please input a proper year. The search will continue with the current year instead.') - season = int(now.year) - searchURL = searchURL + f'season=\'{season}\'' - else: - searchURL = searchURL + f'season=\'{season}\'' - - resp = download(searchURL) - - resp_str = str(resp.json(), 'UTF-8') - - resp_json = json.loads(resp_str) - try: - result_count = int(resp_json['org_game_type_date_info']['queryResults']['totalSize']) - except: - result_count = 0 - - if result_count > 0: - main_df = json_normalize(resp_json['org_game_type_date_info']['queryResults']['row']) - else: - print(f'No results found for the provided playerID. \nTry a diffrient search for better results.') - return main_df +def mlbam_schedule(season: int, gameType="R"): + """ + Retrieves the start and end date for games for every league, and the MLB,for a given season. + This function does not get individual games. + + Args: + season (int): + Required parameter. Indicates the season you are trying to find the games for. + + gameType (string) = "R": + Optional parameter. If there's no input, this function will get the info for the regular season. + + Other parts of the season are indicated as follows in the MLBAM API: + + 'S' - Spring Training + 'E' - Exhibition + 'A' - All Star Game + 'D' - Division Series + 'F' - First Round (Wild Card) + 'L' - League Championship + 'W' - World Series + + Returns: + A pandas dataframe containing MLB scheduled games. + """ + main_df = pd.DataFrame() + + searchURL = ( + "http://lookup-service-prod.mlb.com/json/named.org_game_type_date_info.bam?current_sw='Y'&sport_code='mlb'&" + ) + + if ( + gameType == "R" + or gameType == "S" + or gameType == "E" + or gameType == "A" + or gameType == "D" + or gameType == "F" + or gameType == "L" + or gameType == "W" + ): + searchURL = searchURL + f"game_type='{gameType}'&" + else: + print("Check your input for seasonType. Searching for regular season stats instead.") + gameType = "R" + searchURL = searchURL + f"game_type='{gameType}'&" + + now = datetime.now() + if season < 1860: + print("Please input a proper year. The search will continue with the current year instead.") + season = int(now.year) + searchURL = searchURL + f"season='{season}'" + elif int(now.year) < season: + print("Please input a proper year. The search will continue with the current year instead.") + season = int(now.year) + searchURL = searchURL + f"season='{season}'" + else: + searchURL = searchURL + f"season='{season}'" + + resp = download(searchURL) + + resp_str = str(resp.json(), "UTF-8") + + resp_json = json.loads(resp_str) + try: + result_count = int(resp_json["org_game_type_date_info"]["queryResults"]["totalSize"]) + except: + result_count = 0 + + if result_count > 0: + main_df = json_normalize(resp_json["org_game_type_date_info"]["queryResults"]["row"]) + else: + print(f"No results found for the provided playerID. \nTry a diffrient search for better results.") + return main_df diff --git a/sportsdataverse/mlb/mlbam_players.py b/sportsdataverse/mlb/mlbam_players.py index 6c15159..3eb6079 100755 --- a/sportsdataverse/mlb/mlbam_players.py +++ b/sportsdataverse/mlb/mlbam_players.py @@ -1,158 +1,164 @@ ## Script: players.py ## Author: Joseph Armstrong (armstjc) -import pandas as pd -from pandas import json_normalize import json -from sportsdataverse.dl_utils import download - -def mlbam_search_mlb_players(search:str,isActive=""): - """ - Searches for an MLB player in the MLBAM API. +import pandas as pd +from pandas import json_normalize - Args: - search (string): - Inputted string of the player(s) the user is intending to search. - If there is nothing inputted, nothing will be searched. +from sportsdataverse.dl_utils import download - isActive (string, optional): - If called, it will specify if you want active players, or inactive players - in your search. - If you want active players, set isActive to "Y" or "Yes". +def mlbam_search_mlb_players(search: str, isActive=""): + """ + Searches for an MLB player in the MLBAM API. - If you want inactive players, set isActive to "N" or "No". + Args: + search (string): + Inputted string of the player(s) the user is intending to search. + If there is nothing inputted, nothing will be searched. - Returns: - A pandas dataframe containing MLBAM players whose name(s) matches the input string. + isActive (string, optional): + If called, it will specify if you want active players, or inactive players + in your search. - """ - searchURL = "http://lookup-service-prod.mlb.com/json/named.search_player_all.bam?sport_code='mlb'" + If you want active players, set isActive to "Y" or "Yes". - p_df = pd.DataFrame() - main_df = pd.DataFrame() + If you want inactive players, set isActive to "N" or "No". - if len(isActive) == 0: - print('') - elif isActive.lower() == "y" or isActive.lower() == "yes": - searchURL = searchURL + "&active_sw='Y'" - elif isActive.lower() == "n" or isActive.lower() == "no": - searchURL = searchURL + "&active_sw='N'" - else: - print('Improper input for the isActive input. \nIf you want active players, set isActive to "Y" or "Yes". \nIf you want inactive players, set isActive to "N" or "No".\n\nIn the meantime, your search will search for all players in MLB history.') + Returns: + A pandas dataframe containing MLBAM players whose name(s) matches the input string. - if len(search) > 0: - print(f"Searching for a player named \"{search}\".") + """ + searchURL = "http://lookup-service-prod.mlb.com/json/named.search_player_all.bam?sport_code='mlb'" - searchURL= searchURL + f"&name_part='{search}%25'" + p_df = pd.DataFrame() + main_df = pd.DataFrame() - resp = download(searchURL) + if len(isActive) == 0: + print("") + elif isActive.lower() == "y" or isActive.lower() == "yes": + searchURL = searchURL + "&active_sw='Y'" + elif isActive.lower() == "n" or isActive.lower() == "no": + searchURL = searchURL + "&active_sw='N'" + else: + print( + 'Improper input for the isActive input. \nIf you want active players, set isActive to "Y" or "Yes". \nIf you want inactive players, set isActive to "N" or "No".\n\nIn the meantime, your search will search for all players in MLB history.' + ) - resp_str = str(resp.json(), 'UTF-8') + if len(search) > 0: + print(f'Searching for a player named "{search}".') - resp_json = json.loads(resp_str) - result_count = int(resp_json['search_player_all']['queryResults']['totalSize']) - if result_count > 0: - print(f'{result_count} players found,\nParsing results into a dataframe.') + searchURL = searchURL + f"&name_part='{search}%25'" - for i in resp_json['search_player_all']['queryResults']['row']: + resp = download(searchURL) - p_df = json_normalize(resp_json['search_player_all']['queryResults']['row']) - main_df = pd.concat([p_df,main_df],ignore_index=True) - else: - print(f'No results found for {search}. \nTry a different search for better results.') - main_df.drop_duplicates(subset="player_id",keep="first",inplace=True) - return main_df + resp_str = str(resp.json(), "UTF-8") - else: - print("To search for MLB players in the MLBAM API, you must include text relating to the player you're searching for.") + resp_json = json.loads(resp_str) + result_count = int(resp_json["search_player_all"]["queryResults"]["totalSize"]) + if result_count > 0: + print(f"{result_count} players found,\nParsing results into a dataframe.") -def mlbam_player_info(playerID:int): - """Retrieves the player info for an MLB player, given a proper MLBAM ID + for i in resp_json["search_player_all"]["queryResults"]["row"]: + p_df = json_normalize(resp_json["search_player_all"]["queryResults"]["row"]) + main_df = pd.concat([p_df, main_df], ignore_index=True) + else: + print(f"No results found for {search}. \nTry a different search for better results.") + main_df.drop_duplicates(subset="player_id", keep="first", inplace=True) + return main_df - Args: - playerID (int): - Required parameter. If no playerID is provided, the function wil not work. + else: + print( + "To search for MLB players in the MLBAM API, you must include text relating to the player you're searching for." + ) - Returns: - A pandas dataframe cointaining player information for the specified MLBAM player ID. - """ - main_df = pd.DataFrame() - searchURL = "http://lookup-service-prod.mlb.com/json/named.player_info.bam?sport_code='mlb'&player_id=" +def mlbam_player_info(playerID: int): + """Retrieves the player info for an MLB player, given a proper MLBAM ID - if playerID < 1: - print('You must provide a playerID. Without a proper playerID, this function will not work.') - return None - else: - searchURL= searchURL + f"\'{playerID}\'%27" + Args: + playerID (int): + Required parameter. If no playerID is provided, the function wil not work. - resp = download(searchURL) + Returns: + A pandas dataframe cointaining player information for the specified MLBAM player ID. + """ + main_df = pd.DataFrame() - resp_str = str(resp, 'UTF-8') + searchURL = "http://lookup-service-prod.mlb.com/json/named.player_info.bam?sport_code='mlb'&player_id=" - resp_json = json.loads(resp_str) - try: - result_count = int(resp_json['player_info']['queryResults']['totalSize']) - except: - result_count = 0 + if playerID < 1: + print("You must provide a playerID. Without a proper playerID, this function will not work.") + return None + else: + searchURL = searchURL + f"'{playerID}'%27" - if result_count > 0: - main_df = json_normalize(resp_json['player_info']['queryResults']['row']) - print('Done') - else: - print(f'No results found for the provided playerID. \nTry a different search for better results.') + resp = download(searchURL) - return main_df + resp_str = str(resp, "UTF-8") -def mlbam_player_teams(playerID:int,season:int): - """ - Retrieves the info regarding which teams that player played for in a given season, or in the player's career. + resp_json = json.loads(resp_str) + try: + result_count = int(resp_json["player_info"]["queryResults"]["totalSize"]) + except: + result_count = 0 - Args: - playerID (int): - Required parameter. If no playerID is provided, the function wil not work. + if result_count > 0: + main_df = json_normalize(resp_json["player_info"]["queryResults"]["row"]) + print("Done") + else: + print(f"No results found for the provided playerID. \nTry a different search for better results.") - season (int): - Required parameter. If provided, the search will only look for teams - that player played for in that season. + return main_df - Returns: - A pandas dataframe containing teams a player played for in that season. - """ - main_df = pd.DataFrame() - searchURL = "http://lookup-service-prod.mlb.com/json/named.player_teams.bam?" +def mlbam_player_teams(playerID: int, season: int): + """ + Retrieves the info regarding which teams that player played for in a given season, or in the player's career. - if season >1 and season < 1860: - print('Enter a valid season. Baseball wasn\'t really a thing in the year you specified.') - elif season > 1860: - searchURL = searchURL + f'season=\'{season}\'&' - else: - print('Searching for all the teams this player has played on') + Args: + playerID (int): + Required parameter. If no playerID is provided, the function wil not work. - if playerID < 1: - print('You must provide a playerID. Without a proper playerID, this function will not work.') - return None - else: - searchURL= searchURL + f"player_id=\'{playerID}\'" + season (int): + Required parameter. If provided, the search will only look for teams + that player played for in that season. - resp = download(searchURL) + Returns: + A pandas dataframe containing teams a player played for in that season. + """ + main_df = pd.DataFrame() + + searchURL = "http://lookup-service-prod.mlb.com/json/named.player_teams.bam?" - resp_str = str(resp, 'UTF-8') + if season > 1 and season < 1860: + print("Enter a valid season. Baseball wasn't really a thing in the year you specified.") + elif season > 1860: + searchURL = searchURL + f"season='{season}'&" + else: + print("Searching for all the teams this player has played on") + + if playerID < 1: + print("You must provide a playerID. Without a proper playerID, this function will not work.") + return None + else: + searchURL = searchURL + f"player_id='{playerID}'" - resp_json = json.loads(resp_str) - try: - result_count = int(resp_json['player_teams']['queryResults']['totalSize']) - except: - result_count = 0 + resp = download(searchURL) - if result_count > 0: + resp_str = str(resp, "UTF-8") - print(f'{result_count} players found,\nParsing results into a dataframe.') - main_df = json_normalize(resp_json['player_teams']['queryResults']['row']) - print('Done') - else: - print(f'No results found for the provided playerID. \nTry a different search for better results.') - return main_df + resp_json = json.loads(resp_str) + try: + result_count = int(resp_json["player_teams"]["queryResults"]["totalSize"]) + except: + result_count = 0 + + if result_count > 0: + print(f"{result_count} players found,\nParsing results into a dataframe.") + main_df = json_normalize(resp_json["player_teams"]["queryResults"]["row"]) + print("Done") + else: + print(f"No results found for the provided playerID. \nTry a different search for better results.") + return main_df diff --git a/sportsdataverse/mlb/mlbam_reports.py b/sportsdataverse/mlb/mlbam_reports.py index 670301a..eb554ac 100755 --- a/sportsdataverse/mlb/mlbam_reports.py +++ b/sportsdataverse/mlb/mlbam_reports.py @@ -1,134 +1,137 @@ ## Script: reports.py ## Author: Joseph Armstrong (armstjc) -import pandas as pd -from pandas import json_normalize import json -from sportsdataverse.dl_utils import download from datetime import datetime -def mlbam_transactions(startDate:str,endDate:str): - """ - Retrieves all transactions in a given range of dates. - You MUST provide two dates for this function to work, and both dates must be in MM/DD/YYYY format. - For example, December 31st, 2021 would be represented as 12/31/2021. - - Args: - startDate (int): - Required parameter. If no startDate is provided, the function wil not work. - Additionally, startDate must be in MM/DD/YYYY format. - endDate (int): - Required parameter. If no endDate is provided, the function wil not work. - Additionally, endDate must be in MM/DD/YYYY format. - Returns: - A pandas dataframe containing MLB transactions between two dates. - """ - main_df = pd.DataFrame() - - searchURL = "http://lookup-service-prod.mlb.com/json/named.transaction_all.bam?sport_code='mlb'&" - - try: - sd_date = datetime.strptime(startDate, '%m/%d/%Y') - ed_date = datetime.strptime(endDate, '%m/%d/%Y') - sd = sd_date.strftime("%Y%m%d") - ed = ed_date.strftime("%Y%m%d") - - if sd > ed: - print('There is an issue with your inputted dates.\nPlease verify that your start date is older than your end date.') - return None - - diff_days = ed_date.date() - sd_date.date() - if (diff_days.days)> 30: - print('Getting transaction data. This will take some time.') - else: - print('Getting transaction data.') - searchURL = searchURL + f'start_date=\'{sd}\'' - searchURL = searchURL + f'start_date=\'{ed}\'' - - except: - print('There\'s an issue with the way you\'ve formatted you inputs.') - - try: - resp = download(searchURL) - - resp_str = str(resp.json(), 'UTF-8') - - resp_json = json.loads(resp_str) - try: - result_count = int(resp_json['transaction_all']['queryResults']['totalSize']) - except: - result_count = 0 - - if result_count > 0: - - print(f'{result_count} statlines found,\nParsing results into a dataframe.') - main_df = json_normalize(resp_json['transaction_all']['queryResults']['row']) - print('Done') - else: - print(f'No results found for the provided playerID. \nTry a different search for better results.') - return main_df - except: - print('Could not locate dates ') - -def mlbam_broadcast_info(season:int,home_away="e"): - """ - Retrieves the broadcasters (radio and TV) involved with certain games. - - Args: - season (int): - Required parameter. If no season is provided, the function wil not work. - - home_away (string): - Optional parameter. Used to get broadcasters from either the home OR the away side. - Leave blank if you want both home and away broadcasters. - - If you want home broadcasters only, set home_away='H' or home_away='a'. - - If you want away broadcasters only, set home_away='A' or home_away='a'. - - If you want both home and away broadcasters, set home_away='E' or home_away='e'. - - Returns: - A pandas dataframe containing TV and radio broadcast information for various MLB games. - - """ - main_df = pd.DataFrame() - - searchURL = "http://lookup-service-prod.mlb.com/json/named.mlb_broadcast_info.bam?tcid=mm_mlb_schedule&" - - if home_away.lower() == "a": - searchURL = searchURL + '&home_away=\'A\'' - elif home_away.lower() == "h": - searchURL = searchURL + '&home_away=\'H\'' - elif home_away.lower() == "e": - searchURL = searchURL + '&home_away=\'E\'' - else: - pass - - now = datetime.now() - - if season >1860 and season < now.year: - searchURL = searchURL + f'&season=\'{season}\'' - - resp = download(searchURL) - - resp_str = str(resp, 'UTF-8') - - resp_json = json.loads(resp_str) - try: - result_count = int(resp_json['mlb_broadcast_info']['queryResults']['totalSize']) - except: - result_count = 0 +import pandas as pd +from pandas import json_normalize - if result_count > 0: +from sportsdataverse.dl_utils import download - print(f'{result_count} statlines found,\nParsing results into a dataframe.') - main_df = json_normalize(resp_json['mlb_broadcast_info']['queryResults']['row']) - print('Done') - else: - print(f'No results found for the provided inputs. \nTry a different search for better results.') - return main_df - else: - print('Please enter a valid year to use this function.') +def mlbam_transactions(startDate: str, endDate: str): + """ + Retrieves all transactions in a given range of dates. + You MUST provide two dates for this function to work, and both dates must be in MM/DD/YYYY format. + For example, December 31st, 2021 would be represented as 12/31/2021. + + Args: + startDate (int): + Required parameter. If no startDate is provided, the function wil not work. + Additionally, startDate must be in MM/DD/YYYY format. + endDate (int): + Required parameter. If no endDate is provided, the function wil not work. + Additionally, endDate must be in MM/DD/YYYY format. + Returns: + A pandas dataframe containing MLB transactions between two dates. + """ + main_df = pd.DataFrame() + + searchURL = "http://lookup-service-prod.mlb.com/json/named.transaction_all.bam?sport_code='mlb'&" + + try: + sd_date = datetime.strptime(startDate, "%m/%d/%Y") + ed_date = datetime.strptime(endDate, "%m/%d/%Y") + sd = sd_date.strftime("%Y%m%d") + ed = ed_date.strftime("%Y%m%d") + + if sd > ed: + print( + "There is an issue with your inputted dates.\nPlease verify that your start date is older than your end date." + ) + return None + + diff_days = ed_date.date() - sd_date.date() + if (diff_days.days) > 30: + print("Getting transaction data. This will take some time.") + else: + print("Getting transaction data.") + searchURL = searchURL + f"start_date='{sd}'" + searchURL = searchURL + f"start_date='{ed}'" + + except: + print("There's an issue with the way you've formatted you inputs.") + + try: + resp = download(searchURL) + + resp_str = str(resp.json(), "UTF-8") + + resp_json = json.loads(resp_str) + try: + result_count = int(resp_json["transaction_all"]["queryResults"]["totalSize"]) + except: + result_count = 0 + + if result_count > 0: + print(f"{result_count} statlines found,\nParsing results into a dataframe.") + main_df = json_normalize(resp_json["transaction_all"]["queryResults"]["row"]) + print("Done") + else: + print(f"No results found for the provided playerID. \nTry a different search for better results.") + return main_df + except: + print("Could not locate dates ") + + +def mlbam_broadcast_info(season: int, home_away="e"): + """ + Retrieves the broadcasters (radio and TV) involved with certain games. + + Args: + season (int): + Required parameter. If no season is provided, the function wil not work. + + home_away (string): + Optional parameter. Used to get broadcasters from either the home OR the away side. + Leave blank if you want both home and away broadcasters. + + If you want home broadcasters only, set home_away='H' or home_away='a'. + + If you want away broadcasters only, set home_away='A' or home_away='a'. + + If you want both home and away broadcasters, set home_away='E' or home_away='e'. + + Returns: + A pandas dataframe containing TV and radio broadcast information for various MLB games. + + """ + main_df = pd.DataFrame() + + searchURL = "http://lookup-service-prod.mlb.com/json/named.mlb_broadcast_info.bam?tcid=mm_mlb_schedule&" + + if home_away.lower() == "a": + searchURL = searchURL + "&home_away='A'" + elif home_away.lower() == "h": + searchURL = searchURL + "&home_away='H'" + elif home_away.lower() == "e": + searchURL = searchURL + "&home_away='E'" + else: + pass + + now = datetime.now() + + if season > 1860 and season < now.year: + searchURL = searchURL + f"&season='{season}'" + + resp = download(searchURL) + + resp_str = str(resp, "UTF-8") + + resp_json = json.loads(resp_str) + try: + result_count = int(resp_json["mlb_broadcast_info"]["queryResults"]["totalSize"]) + except: + result_count = 0 + if result_count > 0: + print(f"{result_count} statlines found,\nParsing results into a dataframe.") + main_df = json_normalize(resp_json["mlb_broadcast_info"]["queryResults"]["row"]) + print("Done") + else: + print(f"No results found for the provided inputs. \nTry a different search for better results.") + + return main_df + else: + print("Please enter a valid year to use this function.") diff --git a/sportsdataverse/mlb/mlbam_stats.py b/sportsdataverse/mlb/mlbam_stats.py index 73932e3..bcdfee6 100755 --- a/sportsdataverse/mlb/mlbam_stats.py +++ b/sportsdataverse/mlb/mlbam_stats.py @@ -1,311 +1,344 @@ ## Script: stats.py ## Author: Joseph Armstrong (armstjc) -import pandas as pd -from pandas import json_normalize import json -from sportsdataverse.dl_utils import download from datetime import datetime -import os - - - -def mlbam_player_season_hitting_stats(playerID:int,season:int,gameType="R"): - """ - Retrieves the hitting stats for an MLB player in a given season, given a proper MLBAM ID. - - Args: - playerID (int): - Required parameter. If no playerID is provided, the function wil not work. - - season (int): - Required parameter. Indicates the season you are trying to find the games for. - - gameType (string) = "R": - Optional parameter. If there's no input, this function will get the info for the regular season. - - Other parts of the season are indicated as follows in the MLBAM API: - - 'S' - Spring Training - 'E' - Exhibition - 'A' - All Star Game - 'D' - Division Series - 'F' - First Round (Wild Card) - 'L' - League Championship - 'W' - World Series - - Returns: - A pandas dataframe containing career hitting stats for an MLB player. - """ - main_df = pd.DataFrame() - - searchURL = "http://lookup-service-prod.mlb.com/json/named.sport_hitting_tm.bam?league_list_id='mlb'&" - - if len(gameType) >1: - print('Check your input for seasonType. Searching for regular season stats instead.') - gameType = "R" - searchURL = searchURL + f'game_type=\'{gameType}\'&' - else: - pass - - if gameType == "R" or gameType == "S" or gameType == "E" or gameType == "A" or gameType == "D" or gameType == "F" or gameType == "L" or gameType == "W": - searchURL = searchURL + f'game_type=\'{gameType}\'&' - else: - print('Check your input for seasonType. Searching for regular season stats instead.') - gameType = "R" - searchURL = searchURL + f'game_type=\'{gameType}\'&' - - now = datetime.now() - if season < 1860 or season == None: - print('Please input a proper year. The search will continue with the current year instead.') - season = int(now.year) - searchURL = searchURL + f'season=\'{season}\'&' - elif int(now.year) < season: - print('Please input a proper year. The search will continue with the current year instead.') - season = int(now.year) - searchURL = searchURL + f'season=\'{season}\'&' - else: - searchURL = searchURL + f'season=\'{season}\'&' - - if playerID < 1 or playerID == None or season == None or season < 1860: - print('You must provide a playerID and a proper season. Function aborted.') - return None - else: - searchURL= searchURL + f"player_id=\'{playerID}\'" - - resp = download(searchURL) - - resp_str = str(resp.json(), 'UTF-8') - - resp_json = json.loads(resp_str) - try: - result_count = int(resp_json['sport_hitting_tm']['queryResults']['totalSize']) - except: - result_count = 0 +import pandas as pd +from pandas import json_normalize - if result_count > 0: +from sportsdataverse.dl_utils import download - print(f'{result_count} statlines found,\nParsing results into a dataframe.') - main_df = json_normalize(resp_json['sport_hitting_tm']['queryResults']['row']) - print('Done') - else: - print(f'No results found for the provided playerID. \nTry a diffrient search for better results.') - return main_df - - -def mlbam_player_season_pitching_stats(playerID:int,season:int,gameType="R"): - """Retrieves the pitching stats for an MLB player in a given season, given a proper MLBAM ID - - Args: - playerID (int): - Required parameter. If no playerID is provided, the function wil not work. - - season (int): - Required parameter. Indicates the season you are trying to find the games for. - - gameType (string) = "R": - Optional parameter. If there's no input, this function will get the info for the regular season. - - Other parts of the season are indicated as follows in the MLBAM API: - - 'S' - Spring Training - 'E' - Exhibition - 'A' - All Star Game - 'D' - Division Series - 'F' - First Round (Wild Card) - 'L' - League Championship - 'W' - World Series - - Returns: - A pandas dataframe containing pitching stats for an MLB player in a given season. - - """ - main_df = pd.DataFrame() - - searchURL = "http://lookup-service-prod.mlb.com/json/named.sport_pitching_tm.bam?league_list_id='mlb'&" - - if len(gameType) >1: - print('Check your input for seasonType. Searching for regular season stats instead.') - gameType = "R" - - if gameType == "R" or gameType == "S" or gameType == "E" or gameType == "A" or gameType == "D" or gameType == "F" or gameType == "L" or gameType == "W": - searchURL = searchURL + f'game_type=\'{gameType}\'&' - else: - print('Check your input for seasonType. Searching for regular season stats instead.') - gameType = "R" - searchURL = searchURL + f'game_type=\'{gameType}\'&' - - now = datetime.now() - if season < 1860 or season == None: - print('Please input a proper year. The search will continue with the current year instead.') - season = int(now.year) - searchURL = searchURL + f'season=\'{season}\'&' - elif int(now.year) < season: - print('Please input a proper year. The search will continue with the current year instead.') - season = int(now.year) - searchURL = searchURL + f'season=\'{season}\'&' - else: - searchURL = searchURL + f'season=\'{season}\'&' - - if playerID < 1 or playerID == None: - print('You must provide a playerID. Without a proper playerID, this function will not work.') - return None - else: - searchURL= searchURL + f"player_id=\'{playerID}\'" - - resp = download(searchURL) - - resp_str = str(resp, 'UTF-8') - - resp_json = json.loads(resp_str) - try: - result_count = int(resp_json['sport_pitching_tm']['queryResults']['totalSize']) - except: - result_count = 0 - - if result_count > 0: - - print(f'{result_count} statlines found,\nParsing results into a dataframe.') - main_df = json_normalize(resp_json['sport_pitching_tm']['queryResults']['row']) - print('Done') - else: - print(f'No results found for the provided playerID. \nTry a diffrient search for better results.') - - return main_df - -def mlbam_player_career_hitting_stats(playerID:int,gameType="R"): - """ - Retrieves the career hitting stats for an MLB player, given a proper MLBAM ID - - Args: - playerID (int): - Required parameter. If no playerID is provided, the function wil not work. - - gameType (string) = "R": - Optional parameter. If there's no input, this function will get the info for the regular season. - - Other parts of the season are indicated as follows in the MLBAM API: - - 'S' - Spring Training - 'E' - Exhibition - 'A' - All Star Game - 'D' - Division Series - 'F' - First Round (Wild Card) - 'L' - League Championship - 'W' - World Series - Returns: - A pandas dataframe containing hitting stats for an MLB player in a given season. - """ - main_df = pd.DataFrame() - - searchURL = "http://lookup-service-prod.mlb.com/json/named.sport_career_hitting.bam?league_list_id='mlb'&" - - if len(gameType) >1: - print('Check your input for seasonType. Searching for regular season stats instead.') - gameType = "R" - searchURL = searchURL + f'game_type=\'{gameType}\'&' - else: - pass - - if gameType == "R" or gameType == "S" or gameType == "E" or gameType == "A" or gameType == "D" or gameType == "F" or gameType == "L" or gameType == "W": - searchURL = searchURL + f'game_type=\'{gameType}\'&' - else: - print('Check your input for seasonType. Searching for regular season stats instead.') - gameType = "R" - searchURL = searchURL + f'game_type=\'{gameType}\'&' - - if playerID < 1 or playerID == None: - print('You must provide a playerID. Without a proper playerID, this function will not work.') - return None - else: - searchURL= searchURL + f"player_id=\'{playerID}\'" - - resp = download(searchURL) - - resp_str = str(resp, 'UTF-8') - - resp_json = json.loads(resp_str) - try: - result_count = int(resp_json['sport_career_hitting']['queryResults']['totalSize']) - except: - result_count = 0 - - if result_count > 0: - - print(f'{result_count} statlines found,\nParsing results into a dataframe.') - main_df = json_normalize(resp_json['sport_career_hitting']['queryResults']['row']) - print('Done') - else: - print(f'No results found for the provided playerID. \nTry a different search for better results.') - - return main_df - -def mlbam_player_career_pitching_stats(playerID:int,gameType="R"): - """ - Retrieves the career pitching stats for an MLB player, given a proper MLBAM ID - - Args: - playerID (int): - Required parameter. If no playerID is provided, the function wil not work. - - gameType (string) = "R": - Optional parameter. If there's no input, this function will get the info for the regular season. - - Other parts of the season are indicated as follows in the MLBAM API: - - 'S' - Spring Training - 'E' - Exhibition - 'A' - All Star Game - 'D' - Division Series - 'F' - First Round (Wild Card) - 'L' - League Championship - 'W' - World Series - - Returns: - A pandas dataframe containing career pitching stats for an MLB player. - """ - main_df = pd.DataFrame() - - searchURL = "http://lookup-service-prod.mlb.com/json/named.sport_career_pitching.bam?league_list_id='mlb'&" - - if len(gameType) >1: - print('Check your input for seasonType. Searching for regular season stats instead.') - gameType = "R" - searchURL = searchURL + f'game_type=\'{gameType}\'&' - else: - pass - - if gameType == "R" or gameType == "S" or gameType == "E" or gameType == "A" or gameType == "D" or gameType == "F" or gameType == "L" or gameType == "W": - searchURL = searchURL + f'game_type=\'{gameType}\'&' - else: - print('Check your input for seasonType. Searching for regular season stats instead.') - gameType = "R" - searchURL = searchURL + f'game_type=\'{gameType}\'&' - - if playerID < 1 or playerID == None: - print('You must provide a playerID. Without a proper playerID, this function will not work.') - return None - else: - searchURL= searchURL + f"player_id=\'{playerID}\'" - - resp = download(searchURL) - - resp_str = str(resp, 'UTF-8') - - resp_json = json.loads(resp_str) - try: - result_count = int(resp_json['sport_career_pitching']['queryResults']['totalSize']) - except: - result_count = 0 - - if result_count > 0: - - print(f'{result_count} statlines found,\nParsing results into a dataframe.') - main_df = json_normalize(resp_json['sport_career_pitching']['queryResults']['row']) - print('Done') - else: - print(f'No results found for the provided playerID. \nTry a different search for better results.') - - return main_df \ No newline at end of file +def mlbam_player_season_hitting_stats(playerID: int, season: int, gameType="R"): + """ + Retrieves the hitting stats for an MLB player in a given season, given a proper MLBAM ID. + + Args: + playerID (int): + Required parameter. If no playerID is provided, the function wil not work. + + season (int): + Required parameter. Indicates the season you are trying to find the games for. + + gameType (string) = "R": + Optional parameter. If there's no input, this function will get the info for the regular season. + + Other parts of the season are indicated as follows in the MLBAM API: + + 'S' - Spring Training + 'E' - Exhibition + 'A' - All Star Game + 'D' - Division Series + 'F' - First Round (Wild Card) + 'L' - League Championship + 'W' - World Series + + Returns: + A pandas dataframe containing career hitting stats for an MLB player. + """ + main_df = pd.DataFrame() + + searchURL = "http://lookup-service-prod.mlb.com/json/named.sport_hitting_tm.bam?league_list_id='mlb'&" + + if len(gameType) > 1: + print("Check your input for seasonType. Searching for regular season stats instead.") + gameType = "R" + searchURL = searchURL + f"game_type='{gameType}'&" + else: + pass + + if ( + gameType == "R" + or gameType == "S" + or gameType == "E" + or gameType == "A" + or gameType == "D" + or gameType == "F" + or gameType == "L" + or gameType == "W" + ): + searchURL = searchURL + f"game_type='{gameType}'&" + else: + print("Check your input for seasonType. Searching for regular season stats instead.") + gameType = "R" + searchURL = searchURL + f"game_type='{gameType}'&" + + now = datetime.now() + if season < 1860 or season == None: + print("Please input a proper year. The search will continue with the current year instead.") + season = int(now.year) + searchURL = searchURL + f"season='{season}'&" + elif int(now.year) < season: + print("Please input a proper year. The search will continue with the current year instead.") + season = int(now.year) + searchURL = searchURL + f"season='{season}'&" + else: + searchURL = searchURL + f"season='{season}'&" + + if playerID < 1 or playerID == None or season == None or season < 1860: + print("You must provide a playerID and a proper season. Function aborted.") + return None + else: + searchURL = searchURL + f"player_id='{playerID}'" + + resp = download(searchURL) + + resp_str = str(resp.json(), "UTF-8") + + resp_json = json.loads(resp_str) + try: + result_count = int(resp_json["sport_hitting_tm"]["queryResults"]["totalSize"]) + except: + result_count = 0 + + if result_count > 0: + print(f"{result_count} statlines found,\nParsing results into a dataframe.") + main_df = json_normalize(resp_json["sport_hitting_tm"]["queryResults"]["row"]) + print("Done") + else: + print(f"No results found for the provided playerID. \nTry a diffrient search for better results.") + + return main_df + + +def mlbam_player_season_pitching_stats(playerID: int, season: int, gameType="R"): + """Retrieves the pitching stats for an MLB player in a given season, given a proper MLBAM ID + + Args: + playerID (int): + Required parameter. If no playerID is provided, the function wil not work. + + season (int): + Required parameter. Indicates the season you are trying to find the games for. + + gameType (string) = "R": + Optional parameter. If there's no input, this function will get the info for the regular season. + + Other parts of the season are indicated as follows in the MLBAM API: + + 'S' - Spring Training + 'E' - Exhibition + 'A' - All Star Game + 'D' - Division Series + 'F' - First Round (Wild Card) + 'L' - League Championship + 'W' - World Series + + Returns: + A pandas dataframe containing pitching stats for an MLB player in a given season. + + """ + main_df = pd.DataFrame() + + searchURL = "http://lookup-service-prod.mlb.com/json/named.sport_pitching_tm.bam?league_list_id='mlb'&" + + if len(gameType) > 1: + print("Check your input for seasonType. Searching for regular season stats instead.") + gameType = "R" + + if ( + gameType == "R" + or gameType == "S" + or gameType == "E" + or gameType == "A" + or gameType == "D" + or gameType == "F" + or gameType == "L" + or gameType == "W" + ): + searchURL = searchURL + f"game_type='{gameType}'&" + else: + print("Check your input for seasonType. Searching for regular season stats instead.") + gameType = "R" + searchURL = searchURL + f"game_type='{gameType}'&" + + now = datetime.now() + if season < 1860 or season == None: + print("Please input a proper year. The search will continue with the current year instead.") + season = int(now.year) + searchURL = searchURL + f"season='{season}'&" + elif int(now.year) < season: + print("Please input a proper year. The search will continue with the current year instead.") + season = int(now.year) + searchURL = searchURL + f"season='{season}'&" + else: + searchURL = searchURL + f"season='{season}'&" + + if playerID < 1 or playerID == None: + print("You must provide a playerID. Without a proper playerID, this function will not work.") + return None + else: + searchURL = searchURL + f"player_id='{playerID}'" + + resp = download(searchURL) + + resp_str = str(resp, "UTF-8") + + resp_json = json.loads(resp_str) + try: + result_count = int(resp_json["sport_pitching_tm"]["queryResults"]["totalSize"]) + except: + result_count = 0 + + if result_count > 0: + print(f"{result_count} statlines found,\nParsing results into a dataframe.") + main_df = json_normalize(resp_json["sport_pitching_tm"]["queryResults"]["row"]) + print("Done") + else: + print(f"No results found for the provided playerID. \nTry a diffrient search for better results.") + + return main_df + + +def mlbam_player_career_hitting_stats(playerID: int, gameType="R"): + """ + Retrieves the career hitting stats for an MLB player, given a proper MLBAM ID + + Args: + playerID (int): + Required parameter. If no playerID is provided, the function wil not work. + + gameType (string) = "R": + Optional parameter. If there's no input, this function will get the info for the regular season. + + Other parts of the season are indicated as follows in the MLBAM API: + + 'S' - Spring Training + 'E' - Exhibition + 'A' - All Star Game + 'D' - Division Series + 'F' - First Round (Wild Card) + 'L' - League Championship + 'W' - World Series + Returns: + A pandas dataframe containing hitting stats for an MLB player in a given season. + """ + main_df = pd.DataFrame() + + searchURL = "http://lookup-service-prod.mlb.com/json/named.sport_career_hitting.bam?league_list_id='mlb'&" + + if len(gameType) > 1: + print("Check your input for seasonType. Searching for regular season stats instead.") + gameType = "R" + searchURL = searchURL + f"game_type='{gameType}'&" + else: + pass + + if ( + gameType == "R" + or gameType == "S" + or gameType == "E" + or gameType == "A" + or gameType == "D" + or gameType == "F" + or gameType == "L" + or gameType == "W" + ): + searchURL = searchURL + f"game_type='{gameType}'&" + else: + print("Check your input for seasonType. Searching for regular season stats instead.") + gameType = "R" + searchURL = searchURL + f"game_type='{gameType}'&" + + if playerID < 1 or playerID == None: + print("You must provide a playerID. Without a proper playerID, this function will not work.") + return None + else: + searchURL = searchURL + f"player_id='{playerID}'" + + resp = download(searchURL) + + resp_str = str(resp, "UTF-8") + + resp_json = json.loads(resp_str) + try: + result_count = int(resp_json["sport_career_hitting"]["queryResults"]["totalSize"]) + except: + result_count = 0 + + if result_count > 0: + print(f"{result_count} statlines found,\nParsing results into a dataframe.") + main_df = json_normalize(resp_json["sport_career_hitting"]["queryResults"]["row"]) + print("Done") + else: + print(f"No results found for the provided playerID. \nTry a different search for better results.") + + return main_df + + +def mlbam_player_career_pitching_stats(playerID: int, gameType="R"): + """ + Retrieves the career pitching stats for an MLB player, given a proper MLBAM ID + + Args: + playerID (int): + Required parameter. If no playerID is provided, the function wil not work. + + gameType (string) = "R": + Optional parameter. If there's no input, this function will get the info for the regular season. + + Other parts of the season are indicated as follows in the MLBAM API: + + 'S' - Spring Training + 'E' - Exhibition + 'A' - All Star Game + 'D' - Division Series + 'F' - First Round (Wild Card) + 'L' - League Championship + 'W' - World Series + + Returns: + A pandas dataframe containing career pitching stats for an MLB player. + """ + main_df = pd.DataFrame() + + searchURL = "http://lookup-service-prod.mlb.com/json/named.sport_career_pitching.bam?league_list_id='mlb'&" + + if len(gameType) > 1: + print("Check your input for seasonType. Searching for regular season stats instead.") + gameType = "R" + searchURL = searchURL + f"game_type='{gameType}'&" + else: + pass + + if ( + gameType == "R" + or gameType == "S" + or gameType == "E" + or gameType == "A" + or gameType == "D" + or gameType == "F" + or gameType == "L" + or gameType == "W" + ): + searchURL = searchURL + f"game_type='{gameType}'&" + else: + print("Check your input for seasonType. Searching for regular season stats instead.") + gameType = "R" + searchURL = searchURL + f"game_type='{gameType}'&" + + if playerID < 1 or playerID == None: + print("You must provide a playerID. Without a proper playerID, this function will not work.") + return None + else: + searchURL = searchURL + f"player_id='{playerID}'" + + resp = download(searchURL) + + resp_str = str(resp, "UTF-8") + + resp_json = json.loads(resp_str) + try: + result_count = int(resp_json["sport_career_pitching"]["queryResults"]["totalSize"]) + except: + result_count = 0 + + if result_count > 0: + print(f"{result_count} statlines found,\nParsing results into a dataframe.") + main_df = json_normalize(resp_json["sport_career_pitching"]["queryResults"]["row"]) + print("Done") + else: + print(f"No results found for the provided playerID. \nTry a different search for better results.") + + return main_df diff --git a/sportsdataverse/mlb/mlbam_teams.py b/sportsdataverse/mlb/mlbam_teams.py index c5d9a94..54079a7 100755 --- a/sportsdataverse/mlb/mlbam_teams.py +++ b/sportsdataverse/mlb/mlbam_teams.py @@ -1,165 +1,162 @@ ## Script: teams.py ## Author: Joseph Armstrong (armstjc) -from re import search -import pandas as pd -from pandas import json_normalize import json from datetime import datetime -from sportsdataverse.dl_utils import download, underscore - -import os +import pandas as pd +from pandas import json_normalize -def mlbam_teams(season:int,retriveAllStarRosters=False): - """ - Retrieves the player info for an MLB team, given an MLB season. +from sportsdataverse.dl_utils import download - Args: - season (int): - Required parameter. If no season is provided, the function wil not work. - retriveAllStarRosters (boolean): - Optional parameter. If set to 'True', MLB All-Star rosters will be returned when - running this function. +def mlbam_teams(season: int, retriveAllStarRosters=False): + """ + Retrieves the player info for an MLB team, given an MLB season. - Returns: - A pandas dataframe containing information about MLB teams that played in that season. + Args: + season (int): + Required parameter. If no season is provided, the function wil not work. - """ - main_df = pd.DataFrame() + retriveAllStarRosters (boolean): + Optional parameter. If set to 'True', MLB All-Star rosters will be returned when + running this function. - searchURL = "http://lookup-service-prod.mlb.com/json/named.team_all_season.bam?sport_code='mlb'&" + Returns: + A pandas dataframe containing information about MLB teams that played in that season. - if retriveAllStarRosters == True: - searchURL = searchURL + 'all_star_sw=\'Y\'&' - else: - searchURL = searchURL + 'all_star_sw=\'N\'&' + """ + main_df = pd.DataFrame() - now = datetime.now() + searchURL = "http://lookup-service-prod.mlb.com/json/named.team_all_season.bam?sport_code='mlb'&" - if season < 1860 or season == None: - print('1_Please input a proper year. The search will continue with the current year instead.') - season = int(now.year) - searchURL = searchURL + f'sort_order=\'name_asc\'&season=\'{season}\'' - elif int(now.year) < season: - print('0_Please input a proper year. The search will continue with the current year instead.') - season = int(now.year) - searchURL = searchURL + f'sort_order=\'name_asc\'&season=\'{season}\'' - else: - searchURL = searchURL + f'sort_order=\'name_asc\'&season=\'{season}\'' + if retriveAllStarRosters == True: + searchURL = searchURL + "all_star_sw='Y'&" + else: + searchURL = searchURL + "all_star_sw='N'&" - resp = download(searchURL) + now = datetime.now() - resp_str = str(resp.json(), 'UTF-8') + if season < 1860 or season == None: + print("1_Please input a proper year. The search will continue with the current year instead.") + season = int(now.year) + searchURL = searchURL + f"sort_order='name_asc'&season='{season}'" + elif int(now.year) < season: + print("0_Please input a proper year. The search will continue with the current year instead.") + season = int(now.year) + searchURL = searchURL + f"sort_order='name_asc'&season='{season}'" + else: + searchURL = searchURL + f"sort_order='name_asc'&season='{season}'" - resp_json = json.loads(resp_str) - try: - result_count = int(resp_json['team_all_season']['queryResults']['totalSize']) - except: - result_count = 0 + resp = download(searchURL) - if result_count > 0: + resp_str = str(resp.json(), "UTF-8") - print(f'{result_count} statlines found,\nParsing results into a dataframe.') - main_df = json_normalize(resp_json['team_all_season']['queryResults']['row']) - print('Done') - else: - print(f'No results found for the provided playerID. \nTry a different search for better results.') + resp_json = json.loads(resp_str) + try: + result_count = int(resp_json["team_all_season"]["queryResults"]["totalSize"]) + except: + result_count = 0 - return main_df + if result_count > 0: + print(f"{result_count} statlines found,\nParsing results into a dataframe.") + main_df = json_normalize(resp_json["team_all_season"]["queryResults"]["row"]) + print("Done") + else: + print(f"No results found for the provided playerID. \nTry a different search for better results.") -def mlbam_40_man_roster(teamID:int): - """ - Retrieves the current 40-man roster for a team, given a proper MLBAM team ID. + return main_df - Args: - teamID (int): - Required parameter. This should be the MLBAM team ID for the MLB team you want a 40-man roster from. +def mlbam_40_man_roster(teamID: int): + """ + Retrieves the current 40-man roster for a team, given a proper MLBAM team ID. - Returns: - A pandas dataframe containing the current 40-man roster for the given MLBAM team ID. - """ + Args: - main_df = pd.DataFrame() + teamID (int): + Required parameter. This should be the MLBAM team ID for the MLB team you want a 40-man roster from. - searchURL = 'http://lookup-service-prod.mlb.com/json/named.roster_40.bam?team_id=' + Returns: + A pandas dataframe containing the current 40-man roster for the given MLBAM team ID. + """ - searchURL = searchURL + f'\'{teamID}\'' + main_df = pd.DataFrame() - resp = download(searchURL) + searchURL = "http://lookup-service-prod.mlb.com/json/named.roster_40.bam?team_id=" - resp_str = str(resp, 'UTF-8') + searchURL = searchURL + f"'{teamID}'" - resp_json = json.loads(resp_str) - try: - result_count = int(resp_json['roster_40']['queryResults']['totalSize']) - except: - result_count = 0 + resp = download(searchURL) - if result_count > 0: + resp_str = str(resp, "UTF-8") - print(f'{result_count} statlines found,\nParsing results into a dataframe.') - main_df = json_normalize(resp_json['roster_40']['queryResults']['row']) - print('Done') - else: - print(f'No results found for the provided playerID. \nTry a different search for better results.') + resp_json = json.loads(resp_str) + try: + result_count = int(resp_json["roster_40"]["queryResults"]["totalSize"]) + except: + result_count = 0 - return main_df + if result_count > 0: + print(f"{result_count} statlines found,\nParsing results into a dataframe.") + main_df = json_normalize(resp_json["roster_40"]["queryResults"]["row"]) + print("Done") + else: + print(f"No results found for the provided playerID. \nTry a different search for better results.") -def mlbam_team_roster(teamID:int,startSeason:int,endSeason:int): - """ - Retrieves the cumulative roster for a MLB team in a specified timeframe. + return main_df - Args: - teamID (int): - Required parameter. This should be the number MLBAM associates for an MLB team. - For example, the Cincinnati Reds have an MLBAM team ID of 113. - startSeason (int): - Required parameter. This value must be less than endSeason for this function to work. +def mlbam_team_roster(teamID: int, startSeason: int, endSeason: int): + """ + Retrieves the cumulative roster for a MLB team in a specified timeframe. - endSeason (int): - Required parameter. This value must be greater than startSeason for this function to work. + Args: + teamID (int): + Required parameter. This should be the number MLBAM associates for an MLB team. + For example, the Cincinnati Reds have an MLBAM team ID of 113. - Returns: - A pandas dataframe containg the roster(s) for the MLB team. - """ - holding_num = 0 - main_df = pd.DataFrame() + startSeason (int): + Required parameter. This value must be less than endSeason for this function to work. - if endSeason < startSeason: - holding_num = endSeason - startSeason = endSeason - endSeason = holding_num - else: - pass + endSeason (int): + Required parameter. This value must be greater than startSeason for this function to work. - searchURL = 'http://lookup-service-prod.mlb.com/json/named.roster_team_alltime.bam?' + Returns: + A pandas dataframe containg the roster(s) for the MLB team. + """ + holding_num = 0 + main_df = pd.DataFrame() - ## Add the Season ranges - searchURL = searchURL + f'start_season=\'{startSeason}\'&end_season=\'{endSeason}\'&' - ## Add the TeamID - searchURL = searchURL + f'team_id=\'{teamID}\'' + if endSeason < startSeason: + holding_num = endSeason + startSeason = endSeason + endSeason = holding_num + else: + pass - resp = download(searchURL) + searchURL = "http://lookup-service-prod.mlb.com/json/named.roster_team_alltime.bam?" + ## Add the Season ranges + searchURL = searchURL + f"start_season='{startSeason}'&end_season='{endSeason}'&" + ## Add the TeamID + searchURL = searchURL + f"team_id='{teamID}'" - resp_str = str(resp, 'latin-1') + resp = download(searchURL) - resp_json = json.loads(resp_str) - try: - result_count = int(resp_json['roster_team_alltime']['queryResults']['totalSize']) - except: - result_count = 0 + resp_str = str(resp, "latin-1") - if result_count > 0: + resp_json = json.loads(resp_str) + try: + result_count = int(resp_json["roster_team_alltime"]["queryResults"]["totalSize"]) + except: + result_count = 0 - print(f'{result_count} statlines found,\nParsing results into a dataframe.') - main_df = json_normalize(resp_json['roster_team_alltime']['queryResults']['row']) - print('Done') - else: - print(f'No results found for the provided playerID. \nTry a different search for better results.') + if result_count > 0: + print(f"{result_count} statlines found,\nParsing results into a dataframe.") + main_df = json_normalize(resp_json["roster_team_alltime"]["queryResults"]["row"]) + print("Done") + else: + print(f"No results found for the provided playerID. \nTry a different search for better results.") - return main_df \ No newline at end of file + return main_df diff --git a/sportsdataverse/mlb/retrosheet.py b/sportsdataverse/mlb/retrosheet.py index f17ce91..c8c0b7e 100755 --- a/sportsdataverse/mlb/retrosheet.py +++ b/sportsdataverse/mlb/retrosheet.py @@ -10,12 +10,10 @@ charge from and is copyrighted by Retrosheet. Interested parties may contact Retrosheet at "www.retrosheet.org". """ + import pandas as pd -import polars as pl -from sportsdataverse.dl_utils import download -from io import StringIO from tqdm import tqdm -from datetime import datetime + def retrosheet_ballparks() -> pd.DataFrame(): """ @@ -29,14 +27,24 @@ def retrosheet_ballparks() -> pd.DataFrame(): """ park_url = "https://www.retrosheet.org/parkcode.txt" try: - park_columns=['park_id','park_name','park_alt_name','park_city', - 'park_state','park_start_date','park_end_date','park_league','park_notes'] - park_df = pd.read_csv(park_url,sep=",",header=0,names=park_columns) + park_columns = [ + "park_id", + "park_name", + "park_alt_name", + "park_city", + "park_state", + "park_start_date", + "park_end_date", + "park_league", + "park_notes", + ] + park_df = pd.read_csv(park_url, sep=",", header=0, names=park_columns) return park_df except Exception as e: - print(f'Could not download the MLB ballpark file from Retrosheet.\nError:\n{e}') + print(f"Could not download the MLB ballpark file from Retrosheet.\nError:\n{e}") return pd.DataFrame() + def retrosheet_ejections() -> pd.DataFrame(): """ Retrives the current Ejecdata.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. @@ -49,13 +57,26 @@ def retrosheet_ejections() -> pd.DataFrame(): """ ejections_url = "https://www.retrosheet.org/Ejecdata.txt" try: - ejections_df = pd.read_csv(ejections_url,sep=",") - ejections_df.columns = ['game_id','date','dh','ejectee_id','ejectee_name','team','job','umpire_id','umpire_name','inning','reason'] + ejections_df = pd.read_csv(ejections_url, sep=",") + ejections_df.columns = [ + "game_id", + "date", + "dh", + "ejectee_id", + "ejectee_name", + "team", + "job", + "umpire_id", + "umpire_name", + "inning", + "reason", + ] return ejections_df except Exception as e: - print(f'Could not download the MLB ejections file from Retrosheet.\nError:\n{e}') + print(f"Could not download the MLB ejections file from Retrosheet.\nError:\n{e}") return pd.DataFrame() + def retrosheet_franchises() -> pd.DataFrame(): """ Retrives the current TEAMABR.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. @@ -67,15 +88,16 @@ def retrosheet_franchises() -> pd.DataFrame(): A pandas Dataframe with the biographical information of notable major league teams. """ people_url = "https://www.retrosheet.org/TEAMABR.TXT" - fran_columns = ['franchise_id','league','city','nickname','first_year','last_year'] + fran_columns = ["franchise_id", "league", "city", "nickname", "first_year", "last_year"] try: - fran_df = pd.read_csv(people_url,sep=",",header=None,names=fran_columns) + fran_df = pd.read_csv(people_url, sep=",", header=None, names=fran_columns) fran_df.dropna() return fran_df except Exception as e: - print(f'Could not download the MLB franchises file from Retrosheet.\nError:\n{e}') + print(f"Could not download the MLB franchises file from Retrosheet.\nError:\n{e}") return pd.DataFrame() + def retrosheet_people() -> pd.DataFrame(): """ Retrives the current BioFile.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. @@ -88,21 +110,53 @@ def retrosheet_people() -> pd.DataFrame(): """ people_url = "https://www.retrosheet.org/BIOFILE.TXT" try: - people_df = pd.read_csv(people_url,sep=",",) - people_df.columns = ['player_id','last_name','first_name','nickname', - 'birthdate','birth_city','birth_state','birth_country','play_debut', - 'play_last_game','mgr_debut','mgr_last_game','coach_debut', - 'coach_last_game','ump_debut','ump_last_game','death_date', - 'death_city','death_state','death_country','bats','throws', - 'height','weight','cemetery','cemetery_city','cemetery_state','cemetery_country', - 'cemetery_note','birth_name','name_chg','bat_chg','hof'] - #people_df.dropna() + people_df = pd.read_csv( + people_url, + sep=",", + ) + people_df.columns = [ + "player_id", + "last_name", + "first_name", + "nickname", + "birthdate", + "birth_city", + "birth_state", + "birth_country", + "play_debut", + "play_last_game", + "mgr_debut", + "mgr_last_game", + "coach_debut", + "coach_last_game", + "ump_debut", + "ump_last_game", + "death_date", + "death_city", + "death_state", + "death_country", + "bats", + "throws", + "height", + "weight", + "cemetery", + "cemetery_city", + "cemetery_state", + "cemetery_country", + "cemetery_note", + "birth_name", + "name_chg", + "bat_chg", + "hof", + ] + # people_df.dropna() return people_df except Exception as e: - print(f'An error has occurred when downloading the Retrosheet people data.\nError:\n{e}') + print(f"An error has occurred when downloading the Retrosheet people data.\nError:\n{e}") return pd.DataFrame() -def retrosheet_schedule(first_season:int,last_season=None,original_2020_schedule=False) -> pd.DataFrame(): + +def retrosheet_schedule(first_season: int, last_season=None, original_2020_schedule=False) -> pd.DataFrame(): """ Retrives the scheduled games of an MLB season, or MLB seasons. @@ -132,19 +186,19 @@ def retrosheet_schedule(first_season:int,last_season=None,original_2020_schedule if last_season == None: last_season = first_season - print(f'Getting all of the games for the {first_season} MLB season!') + print(f"Getting all of the games for the {first_season} MLB season!") elif last_season == first_season: - print(f'Getting all of the games for the {first_season} MLB season!') + print(f"Getting all of the games for the {first_season} MLB season!") elif first_season > last_season: last_season = first_season - print(f'CAUGHT EXCEPTION!\nlast_season is greater than first_season!') - print(f'Getting all of the games for the {first_season} MLB season instead.') + print(f"CAUGHT EXCEPTION!\nlast_season is greater than first_season!") + print(f"Getting all of the games for the {first_season} MLB season instead.") elif last_season > first_season: - print(f'Getting all MLB games between {first_season} and {last_season}!') + print(f"Getting all MLB games between {first_season} and {last_season}!") else: - print('There is something horrifically wrong with your setup.') + print("There is something horrifically wrong with your setup.") - for i in tqdm(range(first_season,last_season+1)): + for i in tqdm(range(first_season, last_season + 1)): season = i print(season) if season == 2020 and original_2020_schedule == True: @@ -154,21 +208,34 @@ def retrosheet_schedule(first_season:int,last_season=None,original_2020_schedule else: schedule_url = f"https://raw.githubusercontent.com/chadwickbureau/retrosheet/master/schedule/{i}SKED.TXT" - schedule_columns = ['date','game_num','day_of_week', - 'road_team','road_league','road_team_game_num', - 'home_team','home_league','home_team_game_num','time_of_game', - 'postponement_indicator','makeup_date'] + schedule_columns = [ + "date", + "game_num", + "day_of_week", + "road_team", + "road_league", + "road_team_game_num", + "home_team", + "home_league", + "home_team_game_num", + "time_of_game", + "postponement_indicator", + "makeup_date", + ] try: - season_schedule_df = pd.read_csv(schedule_url,sep=",",header=None,names=schedule_columns) + season_schedule_df = pd.read_csv(schedule_url, sep=",", header=None, names=schedule_columns) except Exception as e: season_schedule_df = pd.DataFrame() - print(f'An error has occurred when downloading Retrosheet schedule data.\nError:\n{e}') - schedule_df = pd.concat([schedule_df,season_schedule_df],ignore_index=True) + print(f"An error has occurred when downloading Retrosheet schedule data.\nError:\n{e}") + schedule_df = pd.concat([schedule_df, season_schedule_df], ignore_index=True) del season_schedule_df return schedule_df -def retrosheet_game_logs_team(first_season:int,last_season=None,game_type="regular",filter_out_seasons=True) -> pd.DataFrame(): + +def retrosheet_game_logs_team( + first_season: int, last_season=None, game_type="regular", filter_out_seasons=True +) -> pd.DataFrame(): """ Retrives the team-level stats for MLB games in a season, or range of seasons. THIS DOES NOT GET PLAYER STATS! @@ -208,134 +275,259 @@ def retrosheet_game_logs_team(first_season:int,last_season=None,game_type="regul columns_list = [ ## Game Info - 'date','game_num','day_of_week', - 'away_team','away_league','away_team_game_num', - 'home_team','home_league','home_team_game_num', + "date", + "game_num", + "day_of_week", + "away_team", + "away_league", + "away_team_game_num", + "home_team", + "home_league", + "home_team_game_num", ## Scores - 'away_team_score','home_team_score', + "away_team_score", + "home_team_score", ## Additional Game Info - 'game_length','day_night_indicator','completion_info','forfeit_info','protest_info', - 'park_id','attendance','time_of_game','away_line_score','home_line_score', + "game_length", + "day_night_indicator", + "completion_info", + "forfeit_info", + "protest_info", + "park_id", + "attendance", + "time_of_game", + "away_line_score", + "home_line_score", ## Away Batting stats - 'away_AB','away_H','away_2B','away_3B','away_HR','away_RBI','away_SH','away_SF', - 'away_HBP','away_BB','away_IBB','away_K','away_SB','away_CS','away_GDP','away_CI', - 'away_LOB', + "away_AB", + "away_H", + "away_2B", + "away_3B", + "away_HR", + "away_RBI", + "away_SH", + "away_SF", + "away_HBP", + "away_BB", + "away_IBB", + "away_K", + "away_SB", + "away_CS", + "away_GDP", + "away_CI", + "away_LOB", ## Away Pitching - 'away_pitchers_used','away_ER','away_team_ER','away_WP','away_BK', + "away_pitchers_used", + "away_ER", + "away_team_ER", + "away_WP", + "away_BK", ## Away Fielding - 'away_PO','away_A','away_E','away_PB','away_DP','away_TP', + "away_PO", + "away_A", + "away_E", + "away_PB", + "away_DP", + "away_TP", ## Home Batting stats - 'home_AB','home_H','home_2B','home_3B','home_HR','home_RBI','home_SH','home_SF', - 'home_HBP','home_BB','home_IBB','home_K','home_SB','home_CS','home_GDP','home_CI', - 'home_LOB', + "home_AB", + "home_H", + "home_2B", + "home_3B", + "home_HR", + "home_RBI", + "home_SH", + "home_SF", + "home_HBP", + "home_BB", + "home_IBB", + "home_K", + "home_SB", + "home_CS", + "home_GDP", + "home_CI", + "home_LOB", ## Home Pitching - 'home_pitchers_used','home_ER','home_team_ER','home_WP','home_BK', + "home_pitchers_used", + "home_ER", + "home_team_ER", + "home_WP", + "home_BK", ## Home Fielding - 'home_PO','home_A','home_E','home_PB','home_DP','home_TP', + "home_PO", + "home_A", + "home_E", + "home_PB", + "home_DP", + "home_TP", ## Umpires - 'home_plate_umpire_id','home_plate_umpire_name','1B_umpire_id','1B_umpire_name', - '2B_umpire_id','2B_umpire_name','3B_umpire_id','3B_umpire_name', - 'LF_umpire_id','LF_umpire_name','RF_umpire_id','RF_umpire_name', + "home_plate_umpire_id", + "home_plate_umpire_name", + "1B_umpire_id", + "1B_umpire_name", + "2B_umpire_id", + "2B_umpire_name", + "3B_umpire_id", + "3B_umpire_name", + "LF_umpire_id", + "LF_umpire_name", + "RF_umpire_id", + "RF_umpire_name", ## Managers - 'away_manager_id','away_manager_name','home_manager_id','home_manager_name', + "away_manager_id", + "away_manager_name", + "home_manager_id", + "home_manager_name", ## Winning/Losing/Saving pitchers - 'winning_pitcher_id','winning_pitcher_name', - 'losing_pitcher_id','losing_pitcher_name', - 'saving_pitcher_id','saving_pitcher_name', + "winning_pitcher_id", + "winning_pitcher_name", + "losing_pitcher_id", + "losing_pitcher_name", + "saving_pitcher_id", + "saving_pitcher_name", ## Winning Hit+RBI batter - 'game_winning_hitter_id','game_winning_hitter_name', + "game_winning_hitter_id", + "game_winning_hitter_name", ## Starting Pitchers - 'away_SP_id','away_SP_name','home_SP_id','home_SP_name', + "away_SP_id", + "away_SP_name", + "home_SP_id", + "home_SP_name", ## Away Team Batting Lineup - 'away_batter_01_id','away_batter_01_name','away_batter_01_position', - 'away_batter_02_id','away_batter_02_name','away_batter_02_position', - 'away_batter_03_id','away_batter_03_name','away_batter_03_position', - 'away_batter_04_id','away_batter_04_name','away_batter_04_position', - 'away_batter_05_id','away_batter_05_name','away_batter_05_position', - 'away_batter_06_id','away_batter_06_name','away_batter_06_position', - 'away_batter_07_id','away_batter_07_name','away_batter_07_position', - 'away_batter_08_id','away_batter_08_name','away_batter_08_position', - 'away_batter_09_id','away_batter_09_name','away_batter_09_position', + "away_batter_01_id", + "away_batter_01_name", + "away_batter_01_position", + "away_batter_02_id", + "away_batter_02_name", + "away_batter_02_position", + "away_batter_03_id", + "away_batter_03_name", + "away_batter_03_position", + "away_batter_04_id", + "away_batter_04_name", + "away_batter_04_position", + "away_batter_05_id", + "away_batter_05_name", + "away_batter_05_position", + "away_batter_06_id", + "away_batter_06_name", + "away_batter_06_position", + "away_batter_07_id", + "away_batter_07_name", + "away_batter_07_position", + "away_batter_08_id", + "away_batter_08_name", + "away_batter_08_position", + "away_batter_09_id", + "away_batter_09_name", + "away_batter_09_position", ## Home Team Batting Lineup - 'home_batter_01_id','home_batter_01_name','home_batter_01_position', - 'home_batter_02_id','home_batter_02_name','home_batter_02_position', - 'home_batter_03_id','home_batter_03_name','home_batter_03_position', - 'home_batter_04_id','home_batter_04_name','home_batter_04_position', - 'home_batter_05_id','home_batter_05_name','home_batter_05_position', - 'home_batter_06_id','home_batter_06_name','home_batter_06_position', - 'home_batter_07_id','home_batter_07_name','home_batter_07_position', - 'home_batter_08_id','home_batter_08_name','home_batter_08_position', - 'home_batter_09_id','home_batter_09_name','home_batter_09_position', - 'additional_info','acquisition_info' + "home_batter_01_id", + "home_batter_01_name", + "home_batter_01_position", + "home_batter_02_id", + "home_batter_02_name", + "home_batter_02_position", + "home_batter_03_id", + "home_batter_03_name", + "home_batter_03_position", + "home_batter_04_id", + "home_batter_04_name", + "home_batter_04_position", + "home_batter_05_id", + "home_batter_05_name", + "home_batter_05_position", + "home_batter_06_id", + "home_batter_06_name", + "home_batter_06_position", + "home_batter_07_id", + "home_batter_07_name", + "home_batter_07_position", + "home_batter_08_id", + "home_batter_08_name", + "home_batter_08_position", + "home_batter_09_id", + "home_batter_09_name", + "home_batter_09_position", + "additional_info", + "acquisition_info", ] if last_season == None: last_season = first_season - print(f'Getting all of the games for the {first_season} MLB season!') + print(f"Getting all of the games for the {first_season} MLB season!") elif last_season == first_season: - print(f'Getting all of the games for the {first_season} MLB season!') + print(f"Getting all of the games for the {first_season} MLB season!") elif first_season > last_season: last_season = first_season - print(f'CAUGHT EXCEPTION!\nlast_season is greater than first_season!') - print(f'Getting all of the games for the {first_season} MLB season instead.') + print(f"CAUGHT EXCEPTION!\nlast_season is greater than first_season!") + print(f"Getting all of the games for the {first_season} MLB season instead.") elif last_season > first_season: - print(f'Getting all MLB games between {first_season} and {last_season}!') + print(f"Getting all MLB games between {first_season} and {last_season}!") else: - print('There is something horrifically wrong with your setup.') + print("There is something horrifically wrong with your setup.") if game_type.lower() == "regular" or game_type.lower() == "reg": - for i in tqdm(range(first_season,last_season+1)): + for i in tqdm(range(first_season, last_season + 1)): season = i print(season) game_log_url = f"https://raw.githubusercontent.com/chadwickbureau/retrosheet/master/gamelog/GL{i}.TXT" try: - season_game_log_df = pd.read_csv(game_log_url,sep=",",header=None,names=columns_list) - #season_game_log_df = season_game_log_df.astype({"date":"str"}) - season_game_log_df['season'] = season_game_log_df['date'].astype(str)[0:4] + season_game_log_df = pd.read_csv(game_log_url, sep=",", header=None, names=columns_list) + # season_game_log_df = season_game_log_df.astype({"date":"str"}) + season_game_log_df["season"] = season_game_log_df["date"].astype(str)[0:4] except Exception as e: season_game_log_df = pd.DataFrame() - print(f'Could not download the MLB team game logs file from Retrosheet for the {i} season.\nError:\n{e}') + print( + f"Could not download the MLB team game logs file from Retrosheet for the {i} season.\nError:\n{e}" + ) - game_log_df = pd.concat([game_log_df,season_game_log_df],ignore_index=True) + game_log_df = pd.concat([game_log_df, season_game_log_df], ignore_index=True) del season_game_log_df elif game_type.lower() == "asg" or game_type.lower() == "all star" or game_type.lower() == "all-star": game_log_url = f"https://raw.githubusercontent.com/chadwickbureau/retrosheet/master/gamelog/GLAS.TXT" try: - game_log_df = pd.read_csv(game_log_url,sep=",",header=None,names=columns_list) - #game_log_df = game_log_df.astype({"date":"str"}) - #game_log_df['season'] = game_log_df['date'].str[0:4] - #game_log_df = game_log_df.astype({'season':'int32'}) - game_log_df['season'] = game_log_df['date'].astype(str)[0:4] + game_log_df = pd.read_csv(game_log_url, sep=",", header=None, names=columns_list) + # game_log_df = game_log_df.astype({"date":"str"}) + # game_log_df['season'] = game_log_df['date'].str[0:4] + # game_log_df = game_log_df.astype({'season':'int32'}) + game_log_df["season"] = game_log_df["date"].astype(str)[0:4] if filter_out_seasons == True: game_log_df = game_log_df[game_log_df.season >= first_season] game_log_df = game_log_df[game_log_df.season <= last_season] except Exception as e: - print(f'There was an issue when trying to get the team game logs for All-Star games.\nError:\n{e}') - - elif game_type.lower() == "playoffs" or game_type.lower() == "october baseball" \ - or game_type.lower() == "post" or game_type.lower() == "postseason" \ - or game_type.lower() == "october" or game_type.lower() == "november" \ - or game_type.lower() == "november baseball": - + print(f"There was an issue when trying to get the team game logs for All-Star games.\nError:\n{e}") + + elif ( + game_type.lower() == "playoffs" + or game_type.lower() == "october baseball" + or game_type.lower() == "post" + or game_type.lower() == "postseason" + or game_type.lower() == "october" + or game_type.lower() == "november" + or game_type.lower() == "november baseball" + ): wildcard_round_url = f"https://raw.githubusercontent.com/chadwickbureau/retrosheet/master/gamelog/GLWC.TXT" divisional_round_url = f"https://raw.githubusercontent.com/chadwickbureau/retrosheet/master/gamelog/GLDV.TXT" championship_round_url = f"https://raw.githubusercontent.com/chadwickbureau/retrosheet/master/gamelog/GLLC.TXT" world_series_round_url = f"https://raw.githubusercontent.com/chadwickbureau/retrosheet/master/gamelog/GLWS.TXT" - URL_list = [wildcard_round_url,divisional_round_url,championship_round_url,world_series_round_url] + URL_list = [wildcard_round_url, divisional_round_url, championship_round_url, world_series_round_url] for i in tqdm(URL_list): game_log_url = i try: - season_game_log_df = pd.read_csv(game_log_url,sep=",",header=None,names=columns_list) + season_game_log_df = pd.read_csv(game_log_url, sep=",", header=None, names=columns_list) except Exception as e: season_game_log_df = pd.DataFrame() - print(f'There was an issue when trying to get the team game logs for the post-season games.\nError:\n{e}') - game_log_df = pd.concat([game_log_df,season_game_log_df],ignore_index=True) + print( + f"There was an issue when trying to get the team game logs for the post-season games.\nError:\n{e}" + ) + game_log_df = pd.concat([game_log_df, season_game_log_df], ignore_index=True) del season_game_log_df - game_log_df = game_log_df.astype({"date":"str"}) - game_log_df['season'] = game_log_df['date'].str[0:4] - game_log_df = game_log_df.astype({'season':'int32'}) + game_log_df = game_log_df.astype({"date": "str"}) + game_log_df["season"] = game_log_df["date"].str[0:4] + game_log_df = game_log_df.astype({"season": "int32"}) if filter_out_seasons == True: game_log_df = game_log_df[game_log_df.season >= first_season] game_log_df = game_log_df[game_log_df.season <= last_season] diff --git a/sportsdataverse/mlb/retrosplits.py b/sportsdataverse/mlb/retrosplits.py index 8bef170..703bdc9 100755 --- a/sportsdataverse/mlb/retrosplits.py +++ b/sportsdataverse/mlb/retrosplits.py @@ -11,15 +11,18 @@ charge from and is copyrighted by Retrosheet. Interested parties may contact Retrosheet at "www.retrosheet.org". """ -import pandas as pd -#from sportsdataverse.dl_utils import download +# from sportsdataverse.dl_utils import download from datetime import datetime -#from io import StringIO + +import pandas as pd + +# from io import StringIO from tqdm import tqdm -from datetime import datetime + from sportsdataverse.errors import SeasonNotFoundError -def retrosplits_game_logs_player(first_season:int,last_season=None) -> pd.DataFrame(): + +def retrosplits_game_logs_player(first_season: int, last_season=None) -> pd.DataFrame(): """ Retrives game-level player stats from the Retrosplits project. @@ -28,7 +31,7 @@ def retrosplits_game_logs_player(first_season:int,last_season=None) -> pd.DataFr Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. last_season (int): - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. + Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. Returns: A pandas dataframe containing game-level player stats from historical MLB games. @@ -39,36 +42,37 @@ def retrosplits_game_logs_player(first_season:int,last_season=None) -> pd.DataFr last_season = int(last_season) except: last_season = None - + if last_season == None: last_season = first_season - print(f'Getting all of the games for the {first_season} MLB season!') + print(f"Getting all of the games for the {first_season} MLB season!") elif last_season == first_season: - print(f'Getting all of the games for the {first_season} MLB season!') - elif first_season > last_season: + print(f"Getting all of the games for the {first_season} MLB season!") + elif first_season > last_season: last_season = first_season - print(f'CAUGHT EXCEPTION!\nlast_season is greater than first_season!') - print(f'Getting all of the games for the {first_season} MLB season instead.') + print(f"CAUGHT EXCEPTION!\nlast_season is greater than first_season!") + print(f"Getting all of the games for the {first_season} MLB season instead.") elif last_season > first_season: - print(f'Getting all MLB games between {first_season} and {last_season}!') + print(f"Getting all MLB games between {first_season} and {last_season}!") else: - print('There is something horrifically wrong with your setup.') + print("There is something horrifically wrong with your setup.") - - for i in tqdm(range(first_season,last_season+1)): + for i in tqdm(range(first_season, last_season + 1)): season = i print(season) try: - game_log_url = f"https://raw.githubusercontent.com/chadwickbureau/retrosplits/master/daybyday/playing-{season}.csv" - game_log_df = pd.read_csv(game_log_url,sep=",") + game_log_url = ( + f"https://raw.githubusercontent.com/chadwickbureau/retrosplits/master/daybyday/playing-{season}.csv" + ) + game_log_df = pd.read_csv(game_log_url, sep=",") except Exception as e: game_log_df = pd.DataFrame() - print(f'Could not get player game logs for the {i} season.\nError:\n{e}') - main_df = pd.concat([main_df,game_log_df],ignore_index=True) + print(f"Could not get player game logs for the {i} season.\nError:\n{e}") + main_df = pd.concat([main_df, game_log_df], ignore_index=True) return main_df -def retrosplits_game_logs_team(first_season:int,last_season=None) -> pd.DataFrame(): +def retrosplits_game_logs_team(first_season: int, last_season=None) -> pd.DataFrame(): """ Retrives game-level team stats from the Retrosplits project. @@ -77,57 +81,60 @@ def retrosplits_game_logs_team(first_season:int,last_season=None) -> pd.DataFram Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. last_season (int): - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. + Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. Returns: A pandas dataframe containing game-level team stats from historical MLB games. """ game_log_df = pd.DataFrame() main_df = pd.DataFrame() - + try: last_season = int(last_season) except: last_season = None - + if last_season == None: last_season = first_season - print(f'Getting all of the games for the {first_season} MLB season!') + print(f"Getting all of the games for the {first_season} MLB season!") elif last_season == first_season: - print(f'Getting all of the games for the {first_season} MLB season!') - elif first_season > last_season: + print(f"Getting all of the games for the {first_season} MLB season!") + elif first_season > last_season: last_season = first_season - print(f'CAUGHT EXCEPTION!\nlast_season is greater than first_season!') - print(f'Getting all of the games for the {first_season} MLB season instead.') + print(f"CAUGHT EXCEPTION!\nlast_season is greater than first_season!") + print(f"Getting all of the games for the {first_season} MLB season instead.") elif last_season > first_season: - print(f'Getting all MLB games between {first_season} and {last_season}!') + print(f"Getting all MLB games between {first_season} and {last_season}!") else: - print('There is something horrifically wrong with your setup.') + print("There is something horrifically wrong with your setup.") - for i in tqdm(range(first_season,last_season+1)): + for i in tqdm(range(first_season, last_season + 1)): season = i print(season) try: - game_log_url = f"https://raw.githubusercontent.com/chadwickbureau/retrosplits/master/daybyday/team-{season}.csv" - game_log_df = pd.read_csv(game_log_url,sep=",") + game_log_url = ( + f"https://raw.githubusercontent.com/chadwickbureau/retrosplits/master/daybyday/team-{season}.csv" + ) + game_log_df = pd.read_csv(game_log_url, sep=",") except Exception as e: game_log_df = pd.DataFrame() - print(f'Could not get team game logs for the {i} season.\nError:\n{e}') + print(f"Could not get team game logs for the {i} season.\nError:\n{e}") - main_df = pd.concat([game_log_df,main_df],ignore_index=True) + main_df = pd.concat([game_log_df, main_df], ignore_index=True) return main_df -def retrosplits_player_batting_by_position(first_season:int,last_season=None) -> pd.DataFrame(): + +def retrosplits_player_batting_by_position(first_season: int, last_season=None) -> pd.DataFrame(): """ Retrives player-level, batting by position split stats from the Retrosplits project. The stats returned by this function are season-level stats, not game-level stats. - + Args: first_season (int): Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. last_season (int): - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. + Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. Returns: A pandas dataframe containing batting by position split stats for MLB players. @@ -141,55 +148,57 @@ def retrosplits_player_batting_by_position(first_season:int,last_season=None) -> last_season = int(last_season) except: last_season = None - + if first_season < 1974: raise SeasonNotFoundError("Batting by position splits are not advalible for seasons before 1974.") # first_season = 1974 # print("Batting by position splits are not advalible for seasons before 1974.") elif first_season > current_year: first_season = current_year - print(f"The people behind retrosplits do not have a time machine to get stats for {first_season} at this time.") + print( + f"The people behind retrosplits do not have a time machine to get stats for {first_season} at this time." + ) if last_season == None: last_season = first_season - print(f'Getting all of the games for the {first_season} MLB season!') + print(f"Getting all of the games for the {first_season} MLB season!") elif last_season == first_season: - print(f'Getting all of the games for the {first_season} MLB season!') - elif first_season > last_season: + print(f"Getting all of the games for the {first_season} MLB season!") + elif first_season > last_season: last_season = first_season - print(f'CAUGHT EXCEPTION!\nlast_season is greater than first_season!') - print(f'Getting all of the games for the {first_season} MLB season instead.') + print(f"CAUGHT EXCEPTION!\nlast_season is greater than first_season!") + print(f"Getting all of the games for the {first_season} MLB season instead.") elif last_season > first_season: - print(f'Getting all MLB games between {first_season} and {last_season}!') + print(f"Getting all MLB games between {first_season} and {last_season}!") else: - print('There is something horrifically wrong with your setup.') + print("There is something horrifically wrong with your setup.") - for i in tqdm(range(first_season,last_season+1)): + for i in tqdm(range(first_season, last_season + 1)): season = i print(season) game_log_url = f"https://raw.githubusercontent.com/chadwickbureau/retrosplits/master/splits/batting-byposition-{season}.csv" try: - game_log_df = pd.read_csv(game_log_url,sep=",") + game_log_df = pd.read_csv(game_log_url, sep=",") except Exception as e: game_log_df = pd.DataFrame() - print(f'Could not get batting by position split stats for the {i} season.\nError:\n{e}') + print(f"Could not get batting by position split stats for the {i} season.\nError:\n{e}") - main_df = pd.concat([game_log_df,main_df],ignore_index=True) + main_df = pd.concat([game_log_df, main_df], ignore_index=True) return main_df -def retrosplits_player_batting_by_runners(first_season:int,last_season=None) -> pd.DataFrame(): +def retrosplits_player_batting_by_runners(first_season: int, last_season=None) -> pd.DataFrame(): """ Retrives player-level, batting by runners split stats from the Retrosplits project. The stats are batting stats, based off of how many runners are on base at the time of the at bat. The stats returned by this function are season-level stats, not game-level stats. - + Args: first_season (int): Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. last_season (int): - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. + Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. Returns: A pandas dataframe containing player-level, batting by runners split stats. @@ -204,53 +213,56 @@ def retrosplits_player_batting_by_runners(first_season:int,last_season=None) -> last_season = int(last_season) except: last_season = None - + if first_season < 1974: raise SeasonNotFoundError("Batting by runners splits are not advalible for seasons before 1974.") first_season = 1974 print("Batting by runners splits are not advalible for seasons before 1974.") elif first_season > current_year: first_season = current_year - print(f"The people behind retrosplits do not have a time machine to get stats for {first_season} at this time.") + print( + f"The people behind retrosplits do not have a time machine to get stats for {first_season} at this time." + ) if last_season == None: last_season = first_season - print(f'Getting all of the games for the {first_season} MLB season!') + print(f"Getting all of the games for the {first_season} MLB season!") elif last_season == first_season: - print(f'Getting all of the games for the {first_season} MLB season!') - elif first_season > last_season: + print(f"Getting all of the games for the {first_season} MLB season!") + elif first_season > last_season: last_season = first_season - print(f'CAUGHT EXCEPTION!\nlast_season is greater than first_season!') - print(f'Getting all of the games for the {first_season} MLB season instead.') + print(f"CAUGHT EXCEPTION!\nlast_season is greater than first_season!") + print(f"Getting all of the games for the {first_season} MLB season instead.") elif last_season > first_season: - print(f'Getting all MLB games between {first_season} and {last_season}!') + print(f"Getting all MLB games between {first_season} and {last_season}!") else: - print('There is something horrifically wrong with your setup.') + print("There is something horrifically wrong with your setup.") - for i in tqdm(range(first_season,last_season+1)): + for i in tqdm(range(first_season, last_season + 1)): season = i print(season) game_log_url = f"https://raw.githubusercontent.com/chadwickbureau/retrosplits/master/splits/batting-byrunners-{season}.csv" try: - game_log_df = pd.read_csv(game_log_url,sep=",") + game_log_df = pd.read_csv(game_log_url, sep=",") except Exception as e: game_log_df = pd.DataFrame() - print(f'Could not get batting by runners split stats for the {i} season.\nError:\n{e}') - main_df = pd.concat([game_log_df,main_df],ignore_index=True) + print(f"Could not get batting by runners split stats for the {i} season.\nError:\n{e}") + main_df = pd.concat([game_log_df, main_df], ignore_index=True) return main_df -def retrosplits_player_batting_by_platoon(first_season:int,last_season=None) -> pd.DataFrame(): + +def retrosplits_player_batting_by_platoon(first_season: int, last_season=None) -> pd.DataFrame(): """ Retrives player-level, batting by platoon (left/right hitting vs. left/right pitching) split stats from the Retrosplits project. The stats are batting stats, based off of the handedness of the batter vs the handedness of the pitcher. The stats returned by this function are season-level stats, not game-level stats. - + Args: first_season (int): Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. last_season (int): - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. + Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. Returns: A pandas dataframe containing player-level, batting by platoon stats for batters. @@ -265,55 +277,59 @@ def retrosplits_player_batting_by_platoon(first_season:int,last_season=None) -> last_season = int(last_season) except: last_season = None - + if first_season < 1974: raise SeasonNotFoundError("Batting by platoon splits are not advalible for seasons before 1974.") - #first_season = 1974 - #print("Batting by platoon splits are not advalible for seasons before 1974.") + # first_season = 1974 + # print("Batting by platoon splits are not advalible for seasons before 1974.") elif first_season > current_year: first_season = current_year - print(f"The people behind retrosplits do not have a time machine to get stats for {first_season} at this time.") + print( + f"The people behind retrosplits do not have a time machine to get stats for {first_season} at this time." + ) if last_season == None: last_season = first_season - print(f'Getting all of the games for the {first_season} MLB season!') + print(f"Getting all of the games for the {first_season} MLB season!") elif last_season == first_season: - print(f'Getting all of the games for the {first_season} MLB season!') - elif first_season > last_season: + print(f"Getting all of the games for the {first_season} MLB season!") + elif first_season > last_season: last_season = first_season - print(f'CAUGHT EXCEPTION!\nlast_season is greater than first_season!') - print(f'Getting all of the games for the {first_season} MLB season instead.') + print(f"CAUGHT EXCEPTION!\nlast_season is greater than first_season!") + print(f"Getting all of the games for the {first_season} MLB season instead.") elif last_season > first_season: - print(f'Getting all MLB games between {first_season} and {last_season}!') + print(f"Getting all MLB games between {first_season} and {last_season}!") else: - print('There is something horrifically wrong with your setup.') + print("There is something horrifically wrong with your setup.") - for i in tqdm(range(first_season,last_season+1)): + for i in tqdm(range(first_season, last_season + 1)): season = i print(season) - game_log_url = f"https://raw.githubusercontent.com/chadwickbureau/retrosplits/master/splits/batting-platoon-{season}.csv" + game_log_url = ( + f"https://raw.githubusercontent.com/chadwickbureau/retrosplits/master/splits/batting-platoon-{season}.csv" + ) try: - game_log_df = pd.read_csv(game_log_url,sep=",") + game_log_df = pd.read_csv(game_log_url, sep=",") except Exception as e: game_log_df = pd.DataFrame() - print(f'Could not get batting by platoon split stats for the {i} season.\nError:\n{e}') - main_df = pd.concat([game_log_df,main_df],ignore_index=True) - #print(game_log_df) + print(f"Could not get batting by platoon split stats for the {i} season.\nError:\n{e}") + main_df = pd.concat([game_log_df, main_df], ignore_index=True) + # print(game_log_df) return main_df -def retrosplits_player_head_to_head_stats(first_season:int,last_season=None) -> pd.DataFrame(): +def retrosplits_player_head_to_head_stats(first_season: int, last_season=None) -> pd.DataFrame(): """ Retrives batter vs. pitcher stats from the Retrosplits project. The stats are batting stats, based off of the preformance of that specific batter agianst a specific pitcher for the durration of that specific season. The stats returned by this function are season-level stats, not game-level stats. - + Args: first_season (int): Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. last_season (int): - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. + Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. Returns: A pandas dataframe containing batter vs. pitcher stats for a season, or for a range of seasons. @@ -328,54 +344,59 @@ def retrosplits_player_head_to_head_stats(first_season:int,last_season=None) -> last_season = int(last_season) except: last_season = None - + if first_season < 1974: raise SeasonNotFoundError("Batter vs. pitcher splits are not advalible for seasons before 1974.") - #first_season = 1974 - #print("Batter vs. pitcher splits are not advalible for seasons before 1974.") + # first_season = 1974 + # print("Batter vs. pitcher splits are not advalible for seasons before 1974.") elif first_season > current_year: first_season = current_year - print(f"The people behind retrosplits do not have a time machine to get stats for {first_season} at this time.") + print( + f"The people behind retrosplits do not have a time machine to get stats for {first_season} at this time." + ) if last_season == None: last_season = first_season - print(f'Getting all of the games for the {first_season} MLB season!') + print(f"Getting all of the games for the {first_season} MLB season!") elif last_season == first_season: - print(f'Getting all of the games for the {first_season} MLB season!') - elif first_season > last_season: + print(f"Getting all of the games for the {first_season} MLB season!") + elif first_season > last_season: last_season = first_season - print(f'CAUGHT EXCEPTION!\nlast_season is greater than first_season!') - print(f'Getting all of the games for the {first_season} MLB season instead.') + print(f"CAUGHT EXCEPTION!\nlast_season is greater than first_season!") + print(f"Getting all of the games for the {first_season} MLB season instead.") elif last_season > first_season: - print(f'Getting all MLB games between {first_season} and {last_season}!') + print(f"Getting all MLB games between {first_season} and {last_season}!") else: - print('There is something horrifically wrong with your setup.') + print("There is something horrifically wrong with your setup.") - for i in tqdm(range(first_season,last_season+1)): + for i in tqdm(range(first_season, last_season + 1)): season = i print(season) - game_log_url = f"https://raw.githubusercontent.com/chadwickbureau/retrosplits/master/splits/headtohead-{season}.csv" + game_log_url = ( + f"https://raw.githubusercontent.com/chadwickbureau/retrosplits/master/splits/headtohead-{season}.csv" + ) try: - game_log_df = pd.read_csv(game_log_url,sep=",") + game_log_df = pd.read_csv(game_log_url, sep=",") except Exception as e: game_log_df = pd.DataFrame() - print(f'Could not get batter vs pitcher split stats for the {i} season.\nError:\n{e}') - main_df = pd.concat([game_log_df,main_df],ignore_index=True) - #print(game_log_df) + print(f"Could not get batter vs pitcher split stats for the {i} season.\nError:\n{e}") + main_df = pd.concat([game_log_df, main_df], ignore_index=True) + # print(game_log_df) return main_df -def retrosplits_player_pitching_by_runners(first_season:int,last_season=None) -> pd.DataFrame(): + +def retrosplits_player_pitching_by_runners(first_season: int, last_season=None) -> pd.DataFrame(): """ Retrives player-level, pitching by runners split stats from the Retrosplits project. The stats are pitching stats, based off of how many runners are on base at the time of the at bat. The stats returned by this function are season-level stats, not game-level stats. - + Args: first_season (int): Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. last_season (int): - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. + Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. Returns: A pandas dataframe containing pitching by runners split stats for a season, or for a range of seasons. @@ -390,55 +411,57 @@ def retrosplits_player_pitching_by_runners(first_season:int,last_season=None) -> last_season = int(last_season) except: last_season = None - + if first_season < 1974: raise SeasonNotFoundError("Pitching by runners splits are not advalible for seasons before 1974.") - #first_season = 1974 - #print("Pitching by runners splits are not advalible for seasons before 1974.") + # first_season = 1974 + # print("Pitching by runners splits are not advalible for seasons before 1974.") elif first_season > current_year: first_season = current_year - print(f"The people behind retrosplits do not have a time machine to get stats for {first_season} at this time.") + print( + f"The people behind retrosplits do not have a time machine to get stats for {first_season} at this time." + ) if last_season == None: last_season = first_season - print(f'Getting all of the games for the {first_season} MLB season!') + print(f"Getting all of the games for the {first_season} MLB season!") elif last_season == first_season: - print(f'Getting all of the games for the {first_season} MLB season!') - elif first_season > last_season: + print(f"Getting all of the games for the {first_season} MLB season!") + elif first_season > last_season: last_season = first_season - print(f'CAUGHT EXCEPTION!\nlast_season is greater than first_season!') - print(f'Getting all of the games for the {first_season} MLB season instead.') + print(f"CAUGHT EXCEPTION!\nlast_season is greater than first_season!") + print(f"Getting all of the games for the {first_season} MLB season instead.") elif last_season > first_season: - print(f'Getting all MLB games between {first_season} and {last_season}!') + print(f"Getting all MLB games between {first_season} and {last_season}!") else: - print('There is something horrifically wrong with your setup.') + print("There is something horrifically wrong with your setup.") - for i in tqdm(range(first_season,last_season+1)): + for i in tqdm(range(first_season, last_season + 1)): season = i print(season) game_log_url = f"https://raw.githubusercontent.com/chadwickbureau/retrosplits/master/splits/pitching-byrunners-{season}.csv" try: - game_log_df = pd.read_csv(game_log_url,sep=",") + game_log_df = pd.read_csv(game_log_url, sep=",") except Exception as e: game_log_df = pd.DataFrame() - print(f'Could not get pitching by runners split stats for the {i} season.\nError:\n{e}') - main_df = pd.concat([game_log_df,main_df],ignore_index=True) - #print(game_log_df) + print(f"Could not get pitching by runners split stats for the {i} season.\nError:\n{e}") + main_df = pd.concat([game_log_df, main_df], ignore_index=True) + # print(game_log_df) return main_df -def retrosplits_player_pitching_by_platoon(first_season:int,last_season=None) -> pd.DataFrame(): +def retrosplits_player_pitching_by_platoon(first_season: int, last_season=None) -> pd.DataFrame(): """ Retrives player-level, pitching by platoon (left/right pitching vs. left/right hitting) split stats from the Retrosplits project. The stats are pitching stats, based off of the handedness of the pitcher vs the handedness of the batter. The stats returned by this function are season-level stats, not game-level stats. - + Args: first_season (int): Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. last_season (int): - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. + Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. Returns: A pandas dataframe containing player-level, pitching by platoon stats for pitchers. @@ -453,39 +476,43 @@ def retrosplits_player_pitching_by_platoon(first_season:int,last_season=None) -> last_season = int(last_season) except: last_season = None - + if first_season < 1974: raise SeasonNotFoundError("Pitching by platoon splits are not advalible for seasons before 1974.") # first_season = 1974 # print("Pitching by platoon splits are not advalible for seasons before 1974.") elif first_season > current_year: first_season = current_year - print(f"The people behind retrosplits do not have a time machine to get stats for {first_season} at this time.") + print( + f"The people behind retrosplits do not have a time machine to get stats for {first_season} at this time." + ) if last_season == None: last_season = first_season - print(f'Getting all of the games for the {first_season} MLB season!') + print(f"Getting all of the games for the {first_season} MLB season!") elif last_season == first_season: - print(f'Getting all of the games for the {first_season} MLB season!') - elif first_season > last_season: + print(f"Getting all of the games for the {first_season} MLB season!") + elif first_season > last_season: last_season = first_season - print(f'CAUGHT EXCEPTION!\nlast_season is greater than first_season!') - print(f'Getting all of the games for the {first_season} MLB season instead.') + print(f"CAUGHT EXCEPTION!\nlast_season is greater than first_season!") + print(f"Getting all of the games for the {first_season} MLB season instead.") elif last_season > first_season: - print(f'Getting all MLB games between {first_season} and {last_season}!') + print(f"Getting all MLB games between {first_season} and {last_season}!") else: - print('There is something horrifically wrong with your setup.') + print("There is something horrifically wrong with your setup.") - for i in tqdm(range(first_season,last_season+1)): + for i in tqdm(range(first_season, last_season + 1)): season = i print(season) - game_log_url = f"https://raw.githubusercontent.com/chadwickbureau/retrosplits/master/splits/pitching-platoon-{season}.csv" + game_log_url = ( + f"https://raw.githubusercontent.com/chadwickbureau/retrosplits/master/splits/pitching-platoon-{season}.csv" + ) try: - game_log_df = pd.read_csv(game_log_url,sep=",") + game_log_df = pd.read_csv(game_log_url, sep=",") except Exception as e: game_log_df = pd.DataFrame() - print(f'There was an error getting pitching by platoon split stats for the {i} season.\nError:\n{e}') + print(f"There was an error getting pitching by platoon split stats for the {i} season.\nError:\n{e}") - main_df = pd.concat([game_log_df,main_df],ignore_index=True) - #print(game_log_df) + main_df = pd.concat([game_log_df, main_df], ignore_index=True) + # print(game_log_df) return main_df diff --git a/sportsdataverse/nba/__init__.py b/sportsdataverse/nba/__init__.py index 9405686..0e53b98 100755 --- a/sportsdataverse/nba/__init__.py +++ b/sportsdataverse/nba/__init__.py @@ -2,4 +2,4 @@ from sportsdataverse.nba.nba_loaders import * from sportsdataverse.nba.nba_pbp import * from sportsdataverse.nba.nba_schedule import * -from sportsdataverse.nba.nba_teams import * \ No newline at end of file +from sportsdataverse.nba.nba_teams import * diff --git a/sportsdataverse/nba/nba_game_rosters.py b/sportsdataverse/nba/nba_game_rosters.py index 7cea3ed..c3c162b 100644 --- a/sportsdataverse/nba/nba_game_rosters.py +++ b/sportsdataverse/nba/nba_game_rosters.py @@ -1,5 +1,6 @@ import pandas as pd import polars as pl + from sportsdataverse.dl_utils import download, underscore diff --git a/sportsdataverse/nba/nba_loaders.py b/sportsdataverse/nba/nba_loaders.py index 48c3fd5..52edb3f 100755 --- a/sportsdataverse/nba/nba_loaders.py +++ b/sportsdataverse/nba/nba_loaders.py @@ -1,8 +1,15 @@ +from typing import List + import pandas as pd import polars as pl from tqdm import tqdm -from typing import List -from sportsdataverse.config import NBA_BASE_URL, NBA_TEAM_BOX_URL, NBA_PLAYER_BOX_URL, NBA_TEAM_SCHEDULE_URL + +from sportsdataverse.config import ( + NBA_BASE_URL, + NBA_PLAYER_BOX_URL, + NBA_TEAM_BOX_URL, + NBA_TEAM_SCHEDULE_URL, +) from sportsdataverse.errors import SeasonNotFoundError diff --git a/sportsdataverse/nba/nba_pbp.py b/sportsdataverse/nba/nba_pbp.py index eebee04..764caa5 100755 --- a/sportsdataverse/nba/nba_pbp.py +++ b/sportsdataverse/nba/nba_pbp.py @@ -1,10 +1,12 @@ -import pandas as pd -import polars as pl -import numpy as np -import os import json +import os import re from typing import Dict + +import numpy as np +import pandas as pd +import polars as pl + from sportsdataverse.dl_utils import download, flatten_json_iterative diff --git a/sportsdataverse/nba/nba_schedule.py b/sportsdataverse/nba/nba_schedule.py index c4280e5..281d448 100755 --- a/sportsdataverse/nba/nba_schedule.py +++ b/sportsdataverse/nba/nba_schedule.py @@ -1,6 +1,8 @@ +import datetime + import pandas as pd import polars as pl -import datetime + from sportsdataverse.dl_utils import download, underscore diff --git a/sportsdataverse/nba/nba_teams.py b/sportsdataverse/nba/nba_teams.py index 43c3ce2..d8cf845 100755 --- a/sportsdataverse/nba/nba_teams.py +++ b/sportsdataverse/nba/nba_teams.py @@ -1,8 +1,12 @@ +from functools import lru_cache + import pandas as pd import polars as pl + from sportsdataverse.dl_utils import download, underscore +@lru_cache(maxsize=None) def espn_nba_teams(return_as_pandas=True, **kwargs) -> pd.DataFrame: """espn_nba_teams - look up NBA teams diff --git a/sportsdataverse/nfl/__init__.py b/sportsdataverse/nfl/__init__.py index f5d2edc..436665a 100755 --- a/sportsdataverse/nfl/__init__.py +++ b/sportsdataverse/nfl/__init__.py @@ -1,6 +1,6 @@ from sportsdataverse.nfl.nfl_game_rosters import * +from sportsdataverse.nfl.nfl_games import * from sportsdataverse.nfl.nfl_loaders import * from sportsdataverse.nfl.nfl_pbp import * from sportsdataverse.nfl.nfl_schedule import * from sportsdataverse.nfl.nfl_teams import * -from sportsdataverse.nfl.nfl_games import * \ No newline at end of file diff --git a/sportsdataverse/nfl/nfl_game_rosters.py b/sportsdataverse/nfl/nfl_game_rosters.py index 78ff179..81e9a52 100644 --- a/sportsdataverse/nfl/nfl_game_rosters.py +++ b/sportsdataverse/nfl/nfl_game_rosters.py @@ -1,5 +1,6 @@ import pandas as pd import polars as pl + from sportsdataverse.dl_utils import download, underscore diff --git a/sportsdataverse/nfl/nfl_games.py b/sportsdataverse/nfl/nfl_games.py index 012801e..9079330 100755 --- a/sportsdataverse/nfl/nfl_games.py +++ b/sportsdataverse/nfl/nfl_games.py @@ -1,7 +1,8 @@ import json -import requests from typing import Dict +import requests + def nfl_token_gen(): url = "https://api.nfl.com/v1/reroute" diff --git a/sportsdataverse/nfl/nfl_loaders.py b/sportsdataverse/nfl/nfl_loaders.py index 31bd24a..1683895 100755 --- a/sportsdataverse/nfl/nfl_loaders.py +++ b/sportsdataverse/nfl/nfl_loaders.py @@ -1,39 +1,39 @@ +import tempfile +from typing import List + import pandas as pd import polars as pl -import tempfile -import json +from pyreadr import download_file, read_r from tqdm import tqdm -from pyreadr import read_r, download_file -from typing import List + from sportsdataverse.config import ( NFL_BASE_URL, - NFL_PLAYER_URL, - NFL_ROSTER_URL, - NFL_WEEKLY_ROSTER_URL, - NFL_TEAM_LOGO_URL, - NFL_TEAM_SCHEDULE_URL, - NFL_PLAYER_STATS_URL, - NFL_PLAYER_KICKING_STATS_URL, - NFL_SNAP_COUNTS_URL, - NFL_PBP_PARTICIPATION_URL, + NFL_COMBINE_URL, NFL_CONTRACTS_URL, + NFL_DEPTH_CHARTS_URL, NFL_DRAFT_PICKS_URL, - NFL_COMBINE_URL, NFL_INJURIES_URL, - NFL_DEPTH_CHARTS_URL, - NFL_OFFICIALS_URL, - NFL_OTC_PLAYER_DETAILS_URL, NFL_NGS_PASSING_URL, - NFL_NGS_RUSHING_URL, NFL_NGS_RECEIVING_URL, + NFL_NGS_RUSHING_URL, + NFL_OFFICIALS_URL, + NFL_PBP_PARTICIPATION_URL, NFL_PFR_SEASON_DEF_URL, - NFL_PFR_WEEK_DEF_URL, NFL_PFR_SEASON_PASS_URL, - NFL_PFR_WEEK_PASS_URL, NFL_PFR_SEASON_REC_URL, - NFL_PFR_WEEK_REC_URL, NFL_PFR_SEASON_RUSH_URL, + NFL_PFR_WEEK_DEF_URL, + NFL_PFR_WEEK_PASS_URL, + NFL_PFR_WEEK_REC_URL, NFL_PFR_WEEK_RUSH_URL, + NFL_PLAYER_KICKING_STATS_URL, + NFL_PLAYER_STATS_URL, + NFL_PLAYER_URL, + NFL_ROSTER_URL, + NFL_SNAP_COUNTS_URL, + NFL_TEAM_LOGO_URL, + NFL_TEAM_SCHEDULE_URL, + NFL_WEEKLY_ROSTER_URL, ) from sportsdataverse.errors import season_not_found_error diff --git a/sportsdataverse/nfl/nfl_pbp.py b/sportsdataverse/nfl/nfl_pbp.py index 3f9fc9b..734c0e5 100755 --- a/sportsdataverse/nfl/nfl_pbp.py +++ b/sportsdataverse/nfl/nfl_pbp.py @@ -1,15 +1,17 @@ -import pandas as pd -import polars as pl -import numpy as np -import re -import os import json +import os +import re import time +from functools import partial, reduce + +import numpy as np +import pandas as pd import pkg_resources +from numpy.core.fromnumeric import mean from xgboost import Booster, DMatrix + from sportsdataverse.dl_utils import download, key_check -from numpy.core.fromnumeric import mean -from functools import reduce, partial + from .model_vars import * # "td" : float(p[0]), @@ -19,15 +21,9 @@ # "safety" : float(p[4]), # "opp_safety" : float(p[5]), # "no_score" : float(p[6]) -ep_model_file = pkg_resources.resource_filename( - "sportsdataverse", "nfl/models/ep_model.model" -) -wp_spread_file = pkg_resources.resource_filename( - "sportsdataverse", "nfl/models/wp_spread.model" -) -qbr_model_file = pkg_resources.resource_filename( - "sportsdataverse", "nfl/models/qbr_model.model" -) +ep_model_file = pkg_resources.resource_filename("sportsdataverse", "nfl/models/ep_model.model") +wp_spread_file = pkg_resources.resource_filename("sportsdataverse", "nfl/models/wp_spread.model") +qbr_model_file = pkg_resources.resource_filename("sportsdataverse", "nfl/models/qbr_model.model") ep_model = Booster({"nthread": 4}) # init model ep_model.load_model(ep_model_file) @@ -38,15 +34,16 @@ qbr_model = Booster({"nthread": 4}) # init model qbr_model.load_model(qbr_model_file) -class NFLPlayProcess(object): +class NFLPlayProcess(object): gameId = 0 # logger = None ran_pipeline = False ran_cleaning_pipeline = False raw = False - path_to_json = '/' - def __init__(self, gameId=0, raw=False, path_to_json='/'): + path_to_json = "/" + + def __init__(self, gameId=0, raw=False, path_to_json="/"): self.gameId = int(gameId) # self.logger = logger self.ran_pipeline = False @@ -73,21 +70,38 @@ def espn_nfl_pbp(self): pbp_txt = {} pbp_txt["timeouts"] = {} # summary endpoint for pickcenter array - summary_url = f"http://site.api.espn.com/apis/site/v2/sports/football/nfl/summary?event={self.gameId}&{cache_buster}" + summary_url = ( + f"http://site.api.espn.com/apis/site/v2/sports/football/nfl/summary?event={self.gameId}&{cache_buster}" + ) summary_resp = download(summary_url) summary = summary_resp.json() incoming_keys_expected = [ - 'boxscore', 'format', 'gameInfo', 'drives', 'leaders', 'broadcasts', - 'predictor', 'pickcenter', 'againstTheSpread', 'odds', 'winprobability', - 'header', 'scoringPlays', 'videos', 'standings' - ] - dict_keys_expected = [ - 'boxscore', 'format', 'gameInfo', 'drives', 'predictor', - 'header', 'standings' + "boxscore", + "format", + "gameInfo", + "drives", + "leaders", + "broadcasts", + "predictor", + "pickcenter", + "againstTheSpread", + "odds", + "winprobability", + "header", + "scoringPlays", + "videos", + "standings", ] + dict_keys_expected = ["boxscore", "format", "gameInfo", "drives", "predictor", "header", "standings"] array_keys_expected = [ - 'leaders', 'broadcasts', 'pickcenter','againstTheSpread', - 'odds', 'winprobability', 'scoringPlays', 'videos' + "leaders", + "broadcasts", + "pickcenter", + "againstTheSpread", + "odds", + "winprobability", + "scoringPlays", + "videos", ] if self.raw == True: # reorder keys in raw format, appending empty keys which are defined later to the end @@ -120,8 +134,8 @@ def espn_nfl_pbp(self): "leaders", ]: pbp_txt[k] = key_check(obj=summary, key=k) - for k in ['news','shop']: - pbp_txt.pop(f'{k}', None) + for k in ["news", "shop"]: + pbp_txt.pop(f"{k}", None) self.json = pbp_txt return self.json @@ -133,34 +147,70 @@ def nfl_pbp_disk(self): return self.json def __helper_nfl_pbp_drives(self, pbp_txt): - pbp_txt, gameSpread, overUnder, homeFavorite, gameSpreadAvailable, \ - homeTeamId, homeTeamMascot, homeTeamName,\ - homeTeamAbbrev, homeTeamNameAlt,\ - awayTeamId, awayTeamMascot, awayTeamName,\ - awayTeamAbbrev, awayTeamNameAlt = self.__helper_nfl_pbp(pbp_txt) + ( + pbp_txt, + gameSpread, + overUnder, + homeFavorite, + gameSpreadAvailable, + homeTeamId, + homeTeamMascot, + homeTeamName, + homeTeamAbbrev, + homeTeamNameAlt, + awayTeamId, + awayTeamMascot, + awayTeamName, + awayTeamAbbrev, + awayTeamNameAlt, + ) = self.__helper_nfl_pbp(pbp_txt) pbp_txt["plays"] = pd.DataFrame() # negotiating the drive meta keys into columns after unnesting drive plays # concatenating the previous and current drives categories when necessary - if "drives" in pbp_txt.keys() and pbp_txt.get('header').get('competitions')[0].get('playByPlaySource') != 'none': - pbp_txt = self.__helper_nfl_pbp_features(pbp_txt, \ - gameSpread, gameSpreadAvailable, \ - overUnder, homeFavorite, \ - homeTeamId, homeTeamMascot, \ - homeTeamName, homeTeamAbbrev, homeTeamNameAlt, \ - awayTeamId, awayTeamMascot, awayTeamName, \ - awayTeamAbbrev, awayTeamNameAlt) + if ( + "drives" in pbp_txt.keys() + and pbp_txt.get("header").get("competitions")[0].get("playByPlaySource") != "none" + ): + pbp_txt = self.__helper_nfl_pbp_features( + pbp_txt, + gameSpread, + gameSpreadAvailable, + overUnder, + homeFavorite, + homeTeamId, + homeTeamMascot, + homeTeamName, + homeTeamAbbrev, + homeTeamNameAlt, + awayTeamId, + awayTeamMascot, + awayTeamName, + awayTeamAbbrev, + awayTeamNameAlt, + ) else: pbp_txt["drives"] = {} return pbp_txt - def __helper_nfl_pbp_features(self, pbp_txt, - gameSpread, gameSpreadAvailable, - overUnder, homeFavorite, - homeTeamId, homeTeamMascot, - homeTeamName, homeTeamAbbrev, homeTeamNameAlt, - awayTeamId, awayTeamMascot, awayTeamName, - awayTeamAbbrev, awayTeamNameAlt): + def __helper_nfl_pbp_features( + self, + pbp_txt, + gameSpread, + gameSpreadAvailable, + overUnder, + homeFavorite, + homeTeamId, + homeTeamMascot, + homeTeamName, + homeTeamAbbrev, + homeTeamNameAlt, + awayTeamId, + awayTeamMascot, + awayTeamName, + awayTeamAbbrev, + awayTeamNameAlt, + ): pbp_txt["plays"] = pd.DataFrame() for key in pbp_txt.get("drives").keys(): prev_drives = pd.json_normalize( @@ -193,9 +243,7 @@ def __helper_nfl_pbp_features(self, pbp_txt, meta_prefix="drive.", errors="ignore", ) - pbp_txt["plays"] = pd.concat( - [pbp_txt["plays"], prev_drives], ignore_index=True - ) + pbp_txt["plays"] = pd.concat([pbp_txt["plays"], prev_drives], ignore_index=True) pbp_txt["plays"] = pbp_txt["plays"].to_dict(orient="records") pbp_txt["plays"] = pd.DataFrame(pbp_txt["plays"]) @@ -213,19 +261,13 @@ def __helper_nfl_pbp_features(self, pbp_txt, pbp_txt["plays"]["awayTeamAbbrev"] = str(awayTeamAbbrev) pbp_txt["plays"]["homeTeamNameAlt"] = str(homeTeamNameAlt) pbp_txt["plays"]["awayTeamNameAlt"] = str(awayTeamNameAlt) - pbp_txt["plays"]["period.number"] = pbp_txt["plays"]["period.number"].apply( - lambda x: int(x) - ) - pbp_txt["plays"]["homeTeamSpread"] = np.where( - homeFavorite == True, abs(gameSpread), -1 * abs(gameSpread) - ) + pbp_txt["plays"]["period.number"] = pbp_txt["plays"]["period.number"].apply(lambda x: int(x)) + pbp_txt["plays"]["homeTeamSpread"] = np.where(homeFavorite == True, abs(gameSpread), -1 * abs(gameSpread)) pbp_txt["plays"]["gameSpread"] = gameSpread pbp_txt["plays"]["gameSpreadAvailable"] = gameSpreadAvailable pbp_txt["plays"]["overUnder"] = float(overUnder) pbp_txt["plays"]["homeFavorite"] = homeFavorite - pbp_txt["homeTeamSpread"] = np.where( - homeFavorite == True, abs(gameSpread), -1 * abs(gameSpread) - ) + pbp_txt["homeTeamSpread"] = np.where(homeFavorite == True, abs(gameSpread), -1 * abs(gameSpread)) pbp_txt["gameSpread"] = gameSpread pbp_txt["gameSpreadAvailable"] = gameSpreadAvailable pbp_txt["overUnder"] = float(overUnder) @@ -235,544 +277,393 @@ def __helper_nfl_pbp_features(self, pbp_txt, homeTeamId: {"1": [], "2": []}, awayTeamId: {"1": [], "2": []}, } - pbp_txt["plays"]["clock.mm"] = pbp_txt["plays"][ - "clock.displayValue" - ].str.split(pat=":") - pbp_txt["plays"][["clock.minutes", "clock.seconds"]] = pbp_txt["plays"][ - "clock.mm" - ].to_list() - pbp_txt["plays"]["half"] = np.where( - pbp_txt["plays"]["period.number"] <= 2, "1", "2" - ) + pbp_txt["plays"]["clock.mm"] = pbp_txt["plays"]["clock.displayValue"].str.split(pat=":") + pbp_txt["plays"][["clock.minutes", "clock.seconds"]] = pbp_txt["plays"]["clock.mm"].to_list() + pbp_txt["plays"]["half"] = np.where(pbp_txt["plays"]["period.number"] <= 2, "1", "2") pbp_txt["plays"]["lag_half"] = pbp_txt["plays"]["half"].shift(1) pbp_txt["plays"]["lead_half"] = pbp_txt["plays"]["half"].shift(-1) pbp_txt["plays"]["start.TimeSecsRem"] = np.where( - pbp_txt["plays"]["period.number"].isin([1, 3]), - 900 + pbp_txt["plays"]["period.number"].isin([1, 3]), + 900 + 60 * pbp_txt["plays"]["clock.minutes"].astype(int) + pbp_txt["plays"]["clock.seconds"].astype(int), + 60 * pbp_txt["plays"]["clock.minutes"].astype(int) + pbp_txt["plays"]["clock.seconds"].astype(int), + ) + pbp_txt["plays"]["start.adj_TimeSecsRem"] = np.select( + [ + pbp_txt["plays"]["period.number"] == 1, + pbp_txt["plays"]["period.number"] == 2, + pbp_txt["plays"]["period.number"] == 3, + pbp_txt["plays"]["period.number"] == 4, + ], + [ + 2700 + 60 * pbp_txt["plays"]["clock.minutes"].astype(int) + pbp_txt["plays"]["clock.seconds"].astype(int), - 60 * pbp_txt["plays"]["clock.minutes"].astype(int) + 1800 + + 60 * pbp_txt["plays"]["clock.minutes"].astype(int) + pbp_txt["plays"]["clock.seconds"].astype(int), - ) - pbp_txt["plays"]["start.adj_TimeSecsRem"] = np.select( - [ - pbp_txt["plays"]["period.number"] == 1, - pbp_txt["plays"]["period.number"] == 2, - pbp_txt["plays"]["period.number"] == 3, - pbp_txt["plays"]["period.number"] == 4, - ], - [ - 2700 - + 60 * pbp_txt["plays"]["clock.minutes"].astype(int) - + pbp_txt["plays"]["clock.seconds"].astype(int), - 1800 - + 60 * pbp_txt["plays"]["clock.minutes"].astype(int) - + pbp_txt["plays"]["clock.seconds"].astype(int), - 900 - + 60 * pbp_txt["plays"]["clock.minutes"].astype(int) - + pbp_txt["plays"]["clock.seconds"].astype(int), - 60 * pbp_txt["plays"]["clock.minutes"].astype(int) - + pbp_txt["plays"]["clock.seconds"].astype(int), - ], - default=60 * pbp_txt["plays"]["clock.minutes"].astype(int) + 900 + + 60 * pbp_txt["plays"]["clock.minutes"].astype(int) + pbp_txt["plays"]["clock.seconds"].astype(int), - ) + 60 * pbp_txt["plays"]["clock.minutes"].astype(int) + pbp_txt["plays"]["clock.seconds"].astype(int), + ], + default=60 * pbp_txt["plays"]["clock.minutes"].astype(int) + pbp_txt["plays"]["clock.seconds"].astype(int), + ) pbp_txt["plays"]["id"] = pbp_txt["plays"]["id"].apply(lambda x: int(x)) - pbp_txt["plays"] = pbp_txt["plays"].sort_values( - by=["id", "start.adj_TimeSecsRem"] - ) + pbp_txt["plays"] = pbp_txt["plays"].sort_values(by=["id", "start.adj_TimeSecsRem"]) pbp_txt["plays"]["game_play_number"] = np.arange(len(pbp_txt["plays"])) + 1 pbp_txt["plays"]["text"] = pbp_txt["plays"]["text"].astype(str) pbp_txt["plays"]["start.team.id"] = ( - pbp_txt["plays"]["start.team.id"] - .fillna(method="ffill") # fill downward first to make sure all playIDs are accurate - .fillna(method="bfill") # fill upward so that any remaining NAs are covered - .apply(lambda x: int(x)) - ) + pbp_txt["plays"]["start.team.id"] + .fillna(method="ffill") # fill downward first to make sure all playIDs are accurate + .fillna(method="bfill") # fill upward so that any remaining NAs are covered + .apply(lambda x: int(x)) + ) pbp_txt["plays"]["end.team.id"] = ( - pbp_txt["plays"]["end.team.id"] - .fillna(value=pbp_txt["plays"]["start.team.id"]) - .apply(lambda x: int(x)) - ) + pbp_txt["plays"]["end.team.id"].fillna(value=pbp_txt["plays"]["start.team.id"]).apply(lambda x: int(x)) + ) pbp_txt["plays"]["start.pos_team.id"] = np.select( - [ - (pbp_txt["plays"]["type.text"].isin(kickoff_vec)) - & ( - pbp_txt["plays"]["start.team.id"].astype(int) - == pbp_txt["plays"]["homeTeamId"].astype(int) - ), - (pbp_txt["plays"]["type.text"].isin(kickoff_vec)) - & ( - pbp_txt["plays"]["start.team.id"].astype(int) - == pbp_txt["plays"]["awayTeamId"].astype(int) - ), - ], - [ - pbp_txt["plays"]["awayTeamId"].astype(int), - pbp_txt["plays"]["homeTeamId"].astype(int), - ], - default=pbp_txt["plays"]["start.team.id"].astype(int), - ) - pbp_txt["plays"]["start.def_pos_team.id"] = np.where( - pbp_txt["plays"]["start.pos_team.id"].astype(int) - == pbp_txt["plays"]["homeTeamId"].astype(int), + [ + (pbp_txt["plays"]["type.text"].isin(kickoff_vec)) + & (pbp_txt["plays"]["start.team.id"].astype(int) == pbp_txt["plays"]["homeTeamId"].astype(int)), + (pbp_txt["plays"]["type.text"].isin(kickoff_vec)) + & (pbp_txt["plays"]["start.team.id"].astype(int) == pbp_txt["plays"]["awayTeamId"].astype(int)), + ], + [ pbp_txt["plays"]["awayTeamId"].astype(int), pbp_txt["plays"]["homeTeamId"].astype(int), - ) + ], + default=pbp_txt["plays"]["start.team.id"].astype(int), + ) + pbp_txt["plays"]["start.def_pos_team.id"] = np.where( + pbp_txt["plays"]["start.pos_team.id"].astype(int) == pbp_txt["plays"]["homeTeamId"].astype(int), + pbp_txt["plays"]["awayTeamId"].astype(int), + pbp_txt["plays"]["homeTeamId"].astype(int), + ) pbp_txt["plays"]["end.def_team.id"] = np.where( - pbp_txt["plays"]["end.team.id"].astype(int) - == pbp_txt["plays"]["homeTeamId"].astype(int), - pbp_txt["plays"]["awayTeamId"].astype(int), - pbp_txt["plays"]["homeTeamId"].astype(int), - ) - pbp_txt["plays"]["end.pos_team.id"] = pbp_txt["plays"]["end.team.id"].apply( - lambda x: int(x) - ) - pbp_txt["plays"]["end.def_pos_team.id"] = pbp_txt["plays"][ - "end.def_team.id" - ].apply(lambda x: int(x)) + pbp_txt["plays"]["end.team.id"].astype(int) == pbp_txt["plays"]["homeTeamId"].astype(int), + pbp_txt["plays"]["awayTeamId"].astype(int), + pbp_txt["plays"]["homeTeamId"].astype(int), + ) + pbp_txt["plays"]["end.pos_team.id"] = pbp_txt["plays"]["end.team.id"].apply(lambda x: int(x)) + pbp_txt["plays"]["end.def_pos_team.id"] = pbp_txt["plays"]["end.def_team.id"].apply(lambda x: int(x)) pbp_txt["plays"]["start.pos_team.name"] = np.where( - pbp_txt["plays"]["start.pos_team.id"].astype(int) - == pbp_txt["plays"]["homeTeamId"].astype(int), - pbp_txt["plays"]["homeTeamName"], - pbp_txt["plays"]["awayTeamName"], - ) + pbp_txt["plays"]["start.pos_team.id"].astype(int) == pbp_txt["plays"]["homeTeamId"].astype(int), + pbp_txt["plays"]["homeTeamName"], + pbp_txt["plays"]["awayTeamName"], + ) pbp_txt["plays"]["start.def_pos_team.name"] = np.where( - pbp_txt["plays"]["start.pos_team.id"].astype(int) - == pbp_txt["plays"]["homeTeamId"].astype(int), - pbp_txt["plays"]["awayTeamName"], - pbp_txt["plays"]["homeTeamName"], - ) + pbp_txt["plays"]["start.pos_team.id"].astype(int) == pbp_txt["plays"]["homeTeamId"].astype(int), + pbp_txt["plays"]["awayTeamName"], + pbp_txt["plays"]["homeTeamName"], + ) pbp_txt["plays"]["end.pos_team.name"] = np.where( - pbp_txt["plays"]["end.pos_team.id"].astype(int) - == pbp_txt["plays"]["homeTeamId"].astype(int), - pbp_txt["plays"]["homeTeamName"], - pbp_txt["plays"]["awayTeamName"], - ) + pbp_txt["plays"]["end.pos_team.id"].astype(int) == pbp_txt["plays"]["homeTeamId"].astype(int), + pbp_txt["plays"]["homeTeamName"], + pbp_txt["plays"]["awayTeamName"], + ) pbp_txt["plays"]["end.def_pos_team.name"] = np.where( - pbp_txt["plays"]["end.pos_team.id"].astype(int) - == pbp_txt["plays"]["homeTeamId"].astype(int), - pbp_txt["plays"]["awayTeamName"], - pbp_txt["plays"]["homeTeamName"], - ) + pbp_txt["plays"]["end.pos_team.id"].astype(int) == pbp_txt["plays"]["homeTeamId"].astype(int), + pbp_txt["plays"]["awayTeamName"], + pbp_txt["plays"]["homeTeamName"], + ) pbp_txt["plays"]["start.is_home"] = np.where( - pbp_txt["plays"]["start.pos_team.id"].astype(int) - == pbp_txt["plays"]["homeTeamId"].astype(int), - True, - False, - ) + pbp_txt["plays"]["start.pos_team.id"].astype(int) == pbp_txt["plays"]["homeTeamId"].astype(int), + True, + False, + ) pbp_txt["plays"]["end.is_home"] = np.where( - pbp_txt["plays"]["end.pos_team.id"].astype(int) - == pbp_txt["plays"]["homeTeamId"].astype(int), - True, - False, - ) + pbp_txt["plays"]["end.pos_team.id"].astype(int) == pbp_txt["plays"]["homeTeamId"].astype(int), + True, + False, + ) pbp_txt["plays"]["homeTimeoutCalled"] = np.where( - (pbp_txt["plays"]["type.text"] == "Timeout") - & ( - ( - pbp_txt["plays"]["text"] - .str.lower() - .str.contains(str(homeTeamAbbrev), case=False) - ) - | ( - pbp_txt["plays"]["text"] - .str.lower() - .str.contains(str(homeTeamName), case=False) - ) - | ( - pbp_txt["plays"]["text"] - .str.lower() - .str.contains(str(homeTeamMascot), case=False) - ) - | ( - pbp_txt["plays"]["text"] - .str.lower() - .str.contains(str(homeTeamNameAlt), case=False) - ) - ), - True, - False, - ) + (pbp_txt["plays"]["type.text"] == "Timeout") + & ( + (pbp_txt["plays"]["text"].str.lower().str.contains(str(homeTeamAbbrev), case=False)) + | (pbp_txt["plays"]["text"].str.lower().str.contains(str(homeTeamName), case=False)) + | (pbp_txt["plays"]["text"].str.lower().str.contains(str(homeTeamMascot), case=False)) + | (pbp_txt["plays"]["text"].str.lower().str.contains(str(homeTeamNameAlt), case=False)) + ), + True, + False, + ) pbp_txt["plays"]["awayTimeoutCalled"] = np.where( - (pbp_txt["plays"]["type.text"] == "Timeout") - & ( - ( - pbp_txt["plays"]["text"] - .str.lower() - .str.contains(str(awayTeamAbbrev), case=False) - ) - | ( - pbp_txt["plays"]["text"] - .str.lower() - .str.contains(str(awayTeamName), case=False) - ) - | ( - pbp_txt["plays"]["text"] - .str.lower() - .str.contains(str(awayTeamMascot), case=False) - ) - | ( - pbp_txt["plays"]["text"] - .str.lower() - .str.contains(str(awayTeamNameAlt), case=False) - ) - ), - True, - False, - ) + (pbp_txt["plays"]["type.text"] == "Timeout") + & ( + (pbp_txt["plays"]["text"].str.lower().str.contains(str(awayTeamAbbrev), case=False)) + | (pbp_txt["plays"]["text"].str.lower().str.contains(str(awayTeamName), case=False)) + | (pbp_txt["plays"]["text"].str.lower().str.contains(str(awayTeamMascot), case=False)) + | (pbp_txt["plays"]["text"].str.lower().str.contains(str(awayTeamNameAlt), case=False)) + ), + True, + False, + ) pbp_txt["timeouts"][homeTeamId]["1"] = ( - pbp_txt["plays"] - .loc[ - (pbp_txt["plays"]["homeTimeoutCalled"] == True) - & (pbp_txt["plays"]["period.number"] <= 2) - ] - .reset_index()["id"] - ) + pbp_txt["plays"] + .loc[(pbp_txt["plays"]["homeTimeoutCalled"] == True) & (pbp_txt["plays"]["period.number"] <= 2)] + .reset_index()["id"] + ) pbp_txt["timeouts"][homeTeamId]["2"] = ( - pbp_txt["plays"] - .loc[ - (pbp_txt["plays"]["homeTimeoutCalled"] == True) - & (pbp_txt["plays"]["period.number"] > 2) - ] - .reset_index()["id"] - ) + pbp_txt["plays"] + .loc[(pbp_txt["plays"]["homeTimeoutCalled"] == True) & (pbp_txt["plays"]["period.number"] > 2)] + .reset_index()["id"] + ) pbp_txt["timeouts"][awayTeamId]["1"] = ( - pbp_txt["plays"] - .loc[ - (pbp_txt["plays"]["awayTimeoutCalled"] == True) - & (pbp_txt["plays"]["period.number"] <= 2) - ] - .reset_index()["id"] - ) + pbp_txt["plays"] + .loc[(pbp_txt["plays"]["awayTimeoutCalled"] == True) & (pbp_txt["plays"]["period.number"] <= 2)] + .reset_index()["id"] + ) pbp_txt["timeouts"][awayTeamId]["2"] = ( - pbp_txt["plays"] - .loc[ - (pbp_txt["plays"]["awayTimeoutCalled"] == True) - & (pbp_txt["plays"]["period.number"] > 2) - ] - .reset_index()["id"] - ) - - pbp_txt["timeouts"][homeTeamId]["1"] = pbp_txt["timeouts"][homeTeamId][ - "1" - ].apply(lambda x: int(x)) - pbp_txt["timeouts"][homeTeamId]["2"] = pbp_txt["timeouts"][homeTeamId][ - "2" - ].apply(lambda x: int(x)) - pbp_txt["timeouts"][awayTeamId]["1"] = pbp_txt["timeouts"][awayTeamId][ - "1" - ].apply(lambda x: int(x)) - pbp_txt["timeouts"][awayTeamId]["2"] = pbp_txt["timeouts"][awayTeamId][ - "2" - ].apply(lambda x: int(x)) - pbp_txt["plays"]["end.homeTeamTimeouts"] = ( - 3 - - pbp_txt["plays"] - .apply( - lambda x: ( - (pbp_txt["timeouts"][homeTeamId]["1"] <= x["id"]) - & (x["period.number"] <= 2) - ) - | ( - (pbp_txt["timeouts"][homeTeamId]["2"] <= x["id"]) - & (x["period.number"] > 2) - ), - axis=1, - ) - .apply(lambda x: int(x.sum()), axis=1) - ) - pbp_txt["plays"]["end.awayTeamTimeouts"] = ( - 3 - - pbp_txt["plays"] - .apply( - lambda x: ( - (pbp_txt["timeouts"][awayTeamId]["1"] <= x["id"]) - & (x["period.number"] <= 2) - ) - | ( - (pbp_txt["timeouts"][awayTeamId]["2"] <= x["id"]) - & (x["period.number"] > 2) - ), - axis=1, - ) - .apply(lambda x: int(x.sum()), axis=1) - ) - pbp_txt["plays"]["start.homeTeamTimeouts"] = pbp_txt["plays"][ - "end.homeTeamTimeouts" - ].shift(1) - pbp_txt["plays"]["start.awayTeamTimeouts"] = pbp_txt["plays"][ - "end.awayTeamTimeouts" - ].shift(1) + pbp_txt["plays"] + .loc[(pbp_txt["plays"]["awayTimeoutCalled"] == True) & (pbp_txt["plays"]["period.number"] > 2)] + .reset_index()["id"] + ) + + pbp_txt["timeouts"][homeTeamId]["1"] = pbp_txt["timeouts"][homeTeamId]["1"].apply(lambda x: int(x)) + pbp_txt["timeouts"][homeTeamId]["2"] = pbp_txt["timeouts"][homeTeamId]["2"].apply(lambda x: int(x)) + pbp_txt["timeouts"][awayTeamId]["1"] = pbp_txt["timeouts"][awayTeamId]["1"].apply(lambda x: int(x)) + pbp_txt["timeouts"][awayTeamId]["2"] = pbp_txt["timeouts"][awayTeamId]["2"].apply(lambda x: int(x)) + pbp_txt["plays"]["end.homeTeamTimeouts"] = 3 - pbp_txt["plays"].apply( + lambda x: ((pbp_txt["timeouts"][homeTeamId]["1"] <= x["id"]) & (x["period.number"] <= 2)) + | ((pbp_txt["timeouts"][homeTeamId]["2"] <= x["id"]) & (x["period.number"] > 2)), + axis=1, + ).apply(lambda x: int(x.sum()), axis=1) + pbp_txt["plays"]["end.awayTeamTimeouts"] = 3 - pbp_txt["plays"].apply( + lambda x: ((pbp_txt["timeouts"][awayTeamId]["1"] <= x["id"]) & (x["period.number"] <= 2)) + | ((pbp_txt["timeouts"][awayTeamId]["2"] <= x["id"]) & (x["period.number"] > 2)), + axis=1, + ).apply(lambda x: int(x.sum()), axis=1) + pbp_txt["plays"]["start.homeTeamTimeouts"] = pbp_txt["plays"]["end.homeTeamTimeouts"].shift(1) + pbp_txt["plays"]["start.awayTeamTimeouts"] = pbp_txt["plays"]["end.awayTeamTimeouts"].shift(1) pbp_txt["plays"]["start.homeTeamTimeouts"] = np.where( - (pbp_txt["plays"]["game_play_number"] == 1) - | ( - (pbp_txt["plays"]["half"] == "2") - & (pbp_txt["plays"]["lag_half"] == "1") - ), - 3, - pbp_txt["plays"]["start.homeTeamTimeouts"], - ) + (pbp_txt["plays"]["game_play_number"] == 1) + | ((pbp_txt["plays"]["half"] == "2") & (pbp_txt["plays"]["lag_half"] == "1")), + 3, + pbp_txt["plays"]["start.homeTeamTimeouts"], + ) pbp_txt["plays"]["start.awayTeamTimeouts"] = np.where( - (pbp_txt["plays"]["game_play_number"] == 1) - | ( - (pbp_txt["plays"]["half"] == "2") - & (pbp_txt["plays"]["lag_half"] == "1") - ), - 3, - pbp_txt["plays"]["start.awayTeamTimeouts"], - ) - pbp_txt["plays"]["start.homeTeamTimeouts"] = pbp_txt["plays"][ - "start.homeTeamTimeouts" - ].apply(lambda x: int(x)) - pbp_txt["plays"]["start.awayTeamTimeouts"] = pbp_txt["plays"][ - "start.awayTeamTimeouts" - ].apply(lambda x: int(x)) - pbp_txt["plays"]["end.TimeSecsRem"] = pbp_txt["plays"][ - "start.TimeSecsRem" - ].shift(1) - pbp_txt["plays"]["end.adj_TimeSecsRem"] = pbp_txt["plays"][ - "start.adj_TimeSecsRem" - ].shift(1) + (pbp_txt["plays"]["game_play_number"] == 1) + | ((pbp_txt["plays"]["half"] == "2") & (pbp_txt["plays"]["lag_half"] == "1")), + 3, + pbp_txt["plays"]["start.awayTeamTimeouts"], + ) + pbp_txt["plays"]["start.homeTeamTimeouts"] = pbp_txt["plays"]["start.homeTeamTimeouts"].apply(lambda x: int(x)) + pbp_txt["plays"]["start.awayTeamTimeouts"] = pbp_txt["plays"]["start.awayTeamTimeouts"].apply(lambda x: int(x)) + pbp_txt["plays"]["end.TimeSecsRem"] = pbp_txt["plays"]["start.TimeSecsRem"].shift(1) + pbp_txt["plays"]["end.adj_TimeSecsRem"] = pbp_txt["plays"]["start.adj_TimeSecsRem"].shift(1) pbp_txt["plays"]["end.TimeSecsRem"] = np.where( - (pbp_txt["plays"]["game_play_number"] == 1) - | ( - (pbp_txt["plays"]["half"] == "2") - & (pbp_txt["plays"]["lag_half"] == "1") - ), - 1800, - pbp_txt["plays"]["end.TimeSecsRem"], - ) + (pbp_txt["plays"]["game_play_number"] == 1) + | ((pbp_txt["plays"]["half"] == "2") & (pbp_txt["plays"]["lag_half"] == "1")), + 1800, + pbp_txt["plays"]["end.TimeSecsRem"], + ) pbp_txt["plays"]["end.adj_TimeSecsRem"] = np.select( - [ - (pbp_txt["plays"]["game_play_number"] == 1), - ( - (pbp_txt["plays"]["half"] == "2") - & (pbp_txt["plays"]["lag_half"] == "1") - ), - ], - [3600, 1800], - default=pbp_txt["plays"]["end.adj_TimeSecsRem"], - ) + [ + (pbp_txt["plays"]["game_play_number"] == 1), + ((pbp_txt["plays"]["half"] == "2") & (pbp_txt["plays"]["lag_half"] == "1")), + ], + [3600, 1800], + default=pbp_txt["plays"]["end.adj_TimeSecsRem"], + ) pbp_txt["plays"]["start.posTeamTimeouts"] = np.where( - pbp_txt["plays"]["start.pos_team.id"] == pbp_txt["plays"]["homeTeamId"], - pbp_txt["plays"]["start.homeTeamTimeouts"], - pbp_txt["plays"]["start.awayTeamTimeouts"], - ) + pbp_txt["plays"]["start.pos_team.id"] == pbp_txt["plays"]["homeTeamId"], + pbp_txt["plays"]["start.homeTeamTimeouts"], + pbp_txt["plays"]["start.awayTeamTimeouts"], + ) pbp_txt["plays"]["start.defPosTeamTimeouts"] = np.where( - pbp_txt["plays"]["start.def_pos_team.id"] - == pbp_txt["plays"]["homeTeamId"], - pbp_txt["plays"]["start.homeTeamTimeouts"], - pbp_txt["plays"]["start.awayTeamTimeouts"], - ) + pbp_txt["plays"]["start.def_pos_team.id"] == pbp_txt["plays"]["homeTeamId"], + pbp_txt["plays"]["start.homeTeamTimeouts"], + pbp_txt["plays"]["start.awayTeamTimeouts"], + ) pbp_txt["plays"]["end.posTeamTimeouts"] = np.where( - pbp_txt["plays"]["end.pos_team.id"] == pbp_txt["plays"]["homeTeamId"], - pbp_txt["plays"]["end.homeTeamTimeouts"], - pbp_txt["plays"]["end.awayTeamTimeouts"], - ) + pbp_txt["plays"]["end.pos_team.id"] == pbp_txt["plays"]["homeTeamId"], + pbp_txt["plays"]["end.homeTeamTimeouts"], + pbp_txt["plays"]["end.awayTeamTimeouts"], + ) pbp_txt["plays"]["end.defPosTeamTimeouts"] = np.where( - pbp_txt["plays"]["end.def_pos_team.id"] - == pbp_txt["plays"]["homeTeamId"], - pbp_txt["plays"]["end.homeTeamTimeouts"], - pbp_txt["plays"]["end.awayTeamTimeouts"], - ) + pbp_txt["plays"]["end.def_pos_team.id"] == pbp_txt["plays"]["homeTeamId"], + pbp_txt["plays"]["end.homeTeamTimeouts"], + pbp_txt["plays"]["end.awayTeamTimeouts"], + ) pbp_txt["firstHalfKickoffTeamId"] = np.where( - (pbp_txt["plays"]["game_play_number"] == 1) - & (pbp_txt["plays"]["type.text"].isin(kickoff_vec)) - & (pbp_txt["plays"]["start.team.id"] == pbp_txt["plays"]["homeTeamId"]), - pbp_txt["plays"]["homeTeamId"], - pbp_txt["plays"]["awayTeamId"], - ) - pbp_txt["plays"]["firstHalfKickoffTeamId"] = pbp_txt[ - "firstHalfKickoffTeamId" - ] + (pbp_txt["plays"]["game_play_number"] == 1) + & (pbp_txt["plays"]["type.text"].isin(kickoff_vec)) + & (pbp_txt["plays"]["start.team.id"] == pbp_txt["plays"]["homeTeamId"]), + pbp_txt["plays"]["homeTeamId"], + pbp_txt["plays"]["awayTeamId"], + ) + pbp_txt["plays"]["firstHalfKickoffTeamId"] = pbp_txt["firstHalfKickoffTeamId"] pbp_txt["plays"]["period"] = pbp_txt["plays"]["period.number"] pbp_txt["plays"]["start.yard"] = np.where( - (pbp_txt["plays"]["start.team.id"] == homeTeamId), - 100 - pbp_txt["plays"]["start.yardLine"], - pbp_txt["plays"]["start.yardLine"], - ) + (pbp_txt["plays"]["start.team.id"] == homeTeamId), + 100 - pbp_txt["plays"]["start.yardLine"], + pbp_txt["plays"]["start.yardLine"], + ) pbp_txt["plays"]["start.yardsToEndzone"] = np.where( - pbp_txt["plays"]["start.yardLine"].isna() == False, - pbp_txt["plays"]["start.yardsToEndzone"], - pbp_txt["plays"]["start.yard"], - ) + pbp_txt["plays"]["start.yardLine"].isna() == False, + pbp_txt["plays"]["start.yardsToEndzone"], + pbp_txt["plays"]["start.yard"], + ) pbp_txt["plays"]["start.yardsToEndzone"] = np.where( - pbp_txt["plays"]["start.yardsToEndzone"] == 0, - pbp_txt["plays"]["start.yard"], - pbp_txt["plays"]["start.yardsToEndzone"], - ) + pbp_txt["plays"]["start.yardsToEndzone"] == 0, + pbp_txt["plays"]["start.yard"], + pbp_txt["plays"]["start.yardsToEndzone"], + ) pbp_txt["plays"]["end.yard"] = np.where( - (pbp_txt["plays"]["end.team.id"] == homeTeamId), - 100 - pbp_txt["plays"]["end.yardLine"], - pbp_txt["plays"]["end.yardLine"], - ) + (pbp_txt["plays"]["end.team.id"] == homeTeamId), + 100 - pbp_txt["plays"]["end.yardLine"], + pbp_txt["plays"]["end.yardLine"], + ) pbp_txt["plays"]["end.yard"] = np.where( - (pbp_txt["plays"]["type.text"] == "Penalty") - & ( - pbp_txt["plays"]["text"].str.contains( - "declined", case=False, flags=0, na=False, regex=True - ) - ), - pbp_txt["plays"]["start.yard"], - pbp_txt["plays"]["end.yard"], - ) + (pbp_txt["plays"]["type.text"] == "Penalty") + & (pbp_txt["plays"]["text"].str.contains("declined", case=False, flags=0, na=False, regex=True)), + pbp_txt["plays"]["start.yard"], + pbp_txt["plays"]["end.yard"], + ) pbp_txt["plays"]["end.yardsToEndzone"] = np.where( - pbp_txt["plays"]["end.yardLine"].isna() == False, - pbp_txt["plays"]["end.yardsToEndzone"], - pbp_txt["plays"]["end.yard"], - ) + pbp_txt["plays"]["end.yardLine"].isna() == False, + pbp_txt["plays"]["end.yardsToEndzone"], + pbp_txt["plays"]["end.yard"], + ) pbp_txt["plays"]["end.yardsToEndzone"] = np.where( - (pbp_txt["plays"]["type.text"] == "Penalty") - & ( - pbp_txt["plays"]["text"].str.contains( - "declined", case=False, flags=0, na=False, regex=True - ) - ), - pbp_txt["plays"]["start.yardsToEndzone"], - pbp_txt["plays"]["end.yardsToEndzone"], - ) + (pbp_txt["plays"]["type.text"] == "Penalty") + & (pbp_txt["plays"]["text"].str.contains("declined", case=False, flags=0, na=False, regex=True)), + pbp_txt["plays"]["start.yardsToEndzone"], + pbp_txt["plays"]["end.yardsToEndzone"], + ) pbp_txt["plays"]["start.distance"] = np.where( - (pbp_txt["plays"]["start.distance"] == 0) - & ( - pbp_txt["plays"]["start.downDistanceText"] - .str.lower() - .str.contains("goal") - ), - pbp_txt["plays"]["start.yardsToEndzone"], - pbp_txt["plays"]["start.distance"], - ) + (pbp_txt["plays"]["start.distance"] == 0) + & (pbp_txt["plays"]["start.downDistanceText"].str.lower().str.contains("goal")), + pbp_txt["plays"]["start.yardsToEndzone"], + pbp_txt["plays"]["start.distance"], + ) - pbp_txt["timeouts"][homeTeamId]["1"] = np.array( - pbp_txt["timeouts"][homeTeamId]["1"] - ).tolist() - pbp_txt["timeouts"][homeTeamId]["2"] = np.array( - pbp_txt["timeouts"][homeTeamId]["2"] - ).tolist() - pbp_txt["timeouts"][awayTeamId]["1"] = np.array( - pbp_txt["timeouts"][awayTeamId]["1"] - ).tolist() - pbp_txt["timeouts"][awayTeamId]["2"] = np.array( - pbp_txt["timeouts"][awayTeamId]["2"] - ).tolist() + pbp_txt["timeouts"][homeTeamId]["1"] = np.array(pbp_txt["timeouts"][homeTeamId]["1"]).tolist() + pbp_txt["timeouts"][homeTeamId]["2"] = np.array(pbp_txt["timeouts"][homeTeamId]["2"]).tolist() + pbp_txt["timeouts"][awayTeamId]["1"] = np.array(pbp_txt["timeouts"][awayTeamId]["1"]).tolist() + pbp_txt["timeouts"][awayTeamId]["2"] = np.array(pbp_txt["timeouts"][awayTeamId]["2"]).tolist() if "scoringType.displayName" in pbp_txt["plays"].keys(): pbp_txt["plays"]["type.text"] = np.where( - pbp_txt["plays"]["scoringType.displayName"] == "Field Goal", - "Field Goal Good", - pbp_txt["plays"]["type.text"], - ) - pbp_txt["plays"]["type.text"] = np.where( - pbp_txt["plays"]["scoringType.displayName"] == "Extra Point", - "Extra Point Good", - pbp_txt["plays"]["type.text"], - ) - - pbp_txt["plays"]["playType"] = np.where( - pbp_txt["plays"]["type.text"].isna() == False, + pbp_txt["plays"]["scoringType.displayName"] == "Field Goal", + "Field Goal Good", pbp_txt["plays"]["type.text"], - "Unknown", ) - pbp_txt["plays"]["type.text"] = np.where( - pbp_txt["plays"]["text"] - .str.lower() - .str.contains("extra point", case=False) - & pbp_txt["plays"]["text"] - .str.lower() - .str.contains("no good", case=False), - "Extra Point Missed", + pbp_txt["plays"]["type.text"] = np.where( + pbp_txt["plays"]["scoringType.displayName"] == "Extra Point", + "Extra Point Good", pbp_txt["plays"]["type.text"], ) + + pbp_txt["plays"]["playType"] = np.where( + pbp_txt["plays"]["type.text"].isna() == False, + pbp_txt["plays"]["type.text"], + "Unknown", + ) pbp_txt["plays"]["type.text"] = np.where( - pbp_txt["plays"]["text"] - .str.lower() - .str.contains("extra point", case=False) - & pbp_txt["plays"]["text"] - .str.lower() - .str.contains("blocked", case=False), - "Extra Point Missed", - pbp_txt["plays"]["type.text"], - ) + pbp_txt["plays"]["text"].str.lower().str.contains("extra point", case=False) + & pbp_txt["plays"]["text"].str.lower().str.contains("no good", case=False), + "Extra Point Missed", + pbp_txt["plays"]["type.text"], + ) pbp_txt["plays"]["type.text"] = np.where( - pbp_txt["plays"]["text"] - .str.lower() - .str.contains("field goal", case=False) - & pbp_txt["plays"]["text"] - .str.lower() - .str.contains("blocked", case=False), - "Blocked Field Goal", - pbp_txt["plays"]["type.text"], - ) + pbp_txt["plays"]["text"].str.lower().str.contains("extra point", case=False) + & pbp_txt["plays"]["text"].str.lower().str.contains("blocked", case=False), + "Extra Point Missed", + pbp_txt["plays"]["type.text"], + ) pbp_txt["plays"]["type.text"] = np.where( - pbp_txt["plays"]["text"] - .str.lower() - .str.contains("field goal", case=False) - & pbp_txt["plays"]["text"] - .str.lower() - .str.contains("no good", case=False), - "Field Goal Missed", - pbp_txt["plays"]["type.text"], - ) + pbp_txt["plays"]["text"].str.lower().str.contains("field goal", case=False) + & pbp_txt["plays"]["text"].str.lower().str.contains("blocked", case=False), + "Blocked Field Goal", + pbp_txt["plays"]["type.text"], + ) + pbp_txt["plays"]["type.text"] = np.where( + pbp_txt["plays"]["text"].str.lower().str.contains("field goal", case=False) + & pbp_txt["plays"]["text"].str.lower().str.contains("no good", case=False), + "Field Goal Missed", + pbp_txt["plays"]["type.text"], + ) del pbp_txt["plays"]["clock.mm"] pbp_txt["plays"] = pbp_txt["plays"].replace({np.nan: None}) return pbp_txt def __helper_nfl_pbp(self, pbp_txt): gameSpread, overUnder, homeFavorite, gameSpreadAvailable = self.__helper_nfl_pickcenter(pbp_txt) - pbp_txt['timeouts'] = {} - pbp_txt['teamInfo'] = pbp_txt['header']['competitions'][0] - pbp_txt['season'] = pbp_txt['header']['season'] - pbp_txt['playByPlaySource'] = pbp_txt['header']['competitions'][0]['playByPlaySource'] - pbp_txt['boxscoreSource'] = pbp_txt['header']['competitions'][0]['boxscoreSource'] - pbp_txt['gameSpreadAvailable'] = gameSpreadAvailable - pbp_txt['gameSpread'] = gameSpread + pbp_txt["timeouts"] = {} + pbp_txt["teamInfo"] = pbp_txt["header"]["competitions"][0] + pbp_txt["season"] = pbp_txt["header"]["season"] + pbp_txt["playByPlaySource"] = pbp_txt["header"]["competitions"][0]["playByPlaySource"] + pbp_txt["boxscoreSource"] = pbp_txt["header"]["competitions"][0]["boxscoreSource"] + pbp_txt["gameSpreadAvailable"] = gameSpreadAvailable + pbp_txt["gameSpread"] = gameSpread pbp_txt["homeFavorite"] = homeFavorite - pbp_txt["homeTeamSpread"] = np.where( - homeFavorite == True, abs(gameSpread), -1 * abs(gameSpread) - ) + pbp_txt["homeTeamSpread"] = np.where(homeFavorite == True, abs(gameSpread), -1 * abs(gameSpread)) pbp_txt["overUnder"] = overUnder # Home and Away identification variables - if pbp_txt['header']['competitions'][0]['competitors'][0]['homeAway']=='home': - pbp_txt['header']['competitions'][0]['home'] = pbp_txt['header']['competitions'][0]['competitors'][0]['team'] - homeTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['id']) - homeTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['name']) - homeTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['location']) - homeTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['abbreviation']) + if pbp_txt["header"]["competitions"][0]["competitors"][0]["homeAway"] == "home": + pbp_txt["header"]["competitions"][0]["home"] = pbp_txt["header"]["competitions"][0]["competitors"][0][ + "team" + ] + homeTeamId = int(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["id"]) + homeTeamMascot = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["name"]) + homeTeamName = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["location"]) + homeTeamAbbrev = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["abbreviation"]) homeTeamNameAlt = re.sub("Stat(.+)", "St", str(homeTeamName)) - pbp_txt['header']['competitions'][0]['away'] = pbp_txt['header']['competitions'][0]['competitors'][1]['team'] - awayTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['id']) - awayTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['name']) - awayTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['location']) - awayTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['abbreviation']) + pbp_txt["header"]["competitions"][0]["away"] = pbp_txt["header"]["competitions"][0]["competitors"][1][ + "team" + ] + awayTeamId = int(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["id"]) + awayTeamMascot = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["name"]) + awayTeamName = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["location"]) + awayTeamAbbrev = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["abbreviation"]) awayTeamNameAlt = re.sub("Stat(.+)", "St", str(awayTeamName)) else: - pbp_txt['header']['competitions'][0]['away'] = pbp_txt['header']['competitions'][0]['competitors'][0]['team'] - awayTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['id']) - awayTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['name']) - awayTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['location']) - awayTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][0]['team']['abbreviation']) + pbp_txt["header"]["competitions"][0]["away"] = pbp_txt["header"]["competitions"][0]["competitors"][0][ + "team" + ] + awayTeamId = int(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["id"]) + awayTeamMascot = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["name"]) + awayTeamName = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["location"]) + awayTeamAbbrev = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["abbreviation"]) awayTeamNameAlt = re.sub("Stat(.+)", "St", str(awayTeamName)) - pbp_txt['header']['competitions'][0]['home'] = pbp_txt['header']['competitions'][0]['competitors'][1]['team'] - homeTeamId = int(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['id']) - homeTeamMascot = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['name']) - homeTeamName = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['location']) - homeTeamAbbrev = str(pbp_txt['header']['competitions'][0]['competitors'][1]['team']['abbreviation']) + pbp_txt["header"]["competitions"][0]["home"] = pbp_txt["header"]["competitions"][0]["competitors"][1][ + "team" + ] + homeTeamId = int(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["id"]) + homeTeamMascot = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["name"]) + homeTeamName = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["location"]) + homeTeamAbbrev = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["abbreviation"]) homeTeamNameAlt = re.sub("Stat(.+)", "St", str(homeTeamName)) - return pbp_txt, gameSpread, overUnder, homeFavorite, gameSpreadAvailable, homeTeamId,\ - homeTeamMascot,homeTeamName,homeTeamAbbrev,homeTeamNameAlt,\ - awayTeamId,awayTeamMascot,awayTeamName,awayTeamAbbrev,awayTeamNameAlt + return ( + pbp_txt, + gameSpread, + overUnder, + homeFavorite, + gameSpreadAvailable, + homeTeamId, + homeTeamMascot, + homeTeamName, + homeTeamAbbrev, + homeTeamNameAlt, + awayTeamId, + awayTeamMascot, + awayTeamName, + awayTeamAbbrev, + awayTeamNameAlt, + ) def __helper_nfl_pickcenter(self, pbp_txt): # Spread definition - if len(pbp_txt.get("pickcenter",[])) > 1: - homeFavorite = pbp_txt.get("pickcenter",{})[0].get("homeTeamOdds",{}).get("favorite","") - if "spread" in pbp_txt.get("pickcenter",{})[1].keys(): - gameSpread = pbp_txt.get("pickcenter",{})[1].get("spread","") - overUnder = pbp_txt.get("pickcenter",{})[1].get("overUnder","") + if len(pbp_txt.get("pickcenter", [])) > 1: + homeFavorite = pbp_txt.get("pickcenter", {})[0].get("homeTeamOdds", {}).get("favorite", "") + if "spread" in pbp_txt.get("pickcenter", {})[1].keys(): + gameSpread = pbp_txt.get("pickcenter", {})[1].get("spread", "") + overUnder = pbp_txt.get("pickcenter", {})[1].get("overUnder", "") gameSpreadAvailable = True else: - gameSpread = pbp_txt.get("pickcenter",{})[0].get("spread","") - overUnder = pbp_txt.get("pickcenter",{})[0].get("overUnder","") + gameSpread = pbp_txt.get("pickcenter", {})[0].get("spread", "") + overUnder = pbp_txt.get("pickcenter", {})[0].get("overUnder", "") gameSpreadAvailable = True # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") else: @@ -798,9 +689,7 @@ def __setup_penalty_data(self, play_df): play_df["penalty_flag"] = False play_df.loc[(play_df["type.text"] == "Penalty"), "penalty_flag"] = True play_df.loc[ - play_df["text"].str.contains( - "penalty", case=False, flags=0, na=False, regex=True - ), + play_df["text"].str.contains("penalty", case=False, flags=0, na=False, regex=True), "penalty_flag", ] = True @@ -808,24 +697,12 @@ def __setup_penalty_data(self, play_df): play_df["penalty_declined"] = False play_df.loc[ (play_df["type.text"] == "Penalty") - & ( - play_df["text"].str.contains( - "declined", case=False, flags=0, na=False, regex=True - ) - ), + & (play_df["text"].str.contains("declined", case=False, flags=0, na=False, regex=True)), "penalty_declined", ] = True play_df.loc[ - ( - play_df["text"].str.contains( - "penalty", case=False, flags=0, na=False, regex=True - ) - ) - & ( - play_df["text"].str.contains( - "declined", case=False, flags=0, na=False, regex=True - ) - ), + (play_df["text"].str.contains("penalty", case=False, flags=0, na=False, regex=True)) + & (play_df["text"].str.contains("declined", case=False, flags=0, na=False, regex=True)), "penalty_declined", ] = True @@ -833,24 +710,12 @@ def __setup_penalty_data(self, play_df): play_df["penalty_no_play"] = False play_df.loc[ (play_df["type.text"] == "Penalty") - & ( - play_df["text"].str.contains( - "no play", case=False, flags=0, na=False, regex=True - ) - ), + & (play_df["text"].str.contains("no play", case=False, flags=0, na=False, regex=True)), "penalty_no_play", ] = True play_df.loc[ - ( - play_df["text"].str.contains( - "penalty", case=False, flags=0, na=False, regex=True - ) - ) - & ( - play_df["text"].str.contains( - "no play", case=False, flags=0, na=False, regex=True - ) - ), + (play_df["text"].str.contains("penalty", case=False, flags=0, na=False, regex=True)) + & (play_df["text"].str.contains("no play", case=False, flags=0, na=False, regex=True)), "penalty_no_play", ] = True @@ -858,24 +723,12 @@ def __setup_penalty_data(self, play_df): play_df["penalty_offset"] = False play_df.loc[ (play_df["type.text"] == "Penalty") - & ( - play_df["text"].str.contains( - r"off-setting", case=False, flags=0, na=False, regex=True - ) - ), + & (play_df["text"].str.contains(r"off-setting", case=False, flags=0, na=False, regex=True)), "penalty_offset", ] = True play_df.loc[ - ( - play_df["text"].str.contains( - "penalty", case=False, flags=0, na=False, regex=True - ) - ) - & ( - play_df["text"].str.contains( - r"off-setting", case=False, flags=0, na=False, regex=True - ) - ), + (play_df["text"].str.contains("penalty", case=False, flags=0, na=False, regex=True)) + & (play_df["text"].str.contains(r"off-setting", case=False, flags=0, na=False, regex=True)), "penalty_offset", ] = True @@ -883,51 +736,23 @@ def __setup_penalty_data(self, play_df): play_df["penalty_1st_conv"] = False play_df.loc[ (play_df["type.text"] == "Penalty") - & ( - play_df["text"].str.contains( - "1st down", case=False, flags=0, na=False, regex=True - ) - ), + & (play_df["text"].str.contains("1st down", case=False, flags=0, na=False, regex=True)), "penalty_1st_conv", ] = True play_df.loc[ - ( - play_df["text"].str.contains( - "penalty", case=False, flags=0, na=False, regex=True - ) - ) - & ( - play_df["text"].str.contains( - "1st down", case=False, flags=0, na=False, regex=True - ) - ), + (play_df["text"].str.contains("penalty", case=False, flags=0, na=False, regex=True)) + & (play_df["text"].str.contains("1st down", case=False, flags=0, na=False, regex=True)), "penalty_1st_conv", ] = True # -- T/F flag for penalty text but not penalty play type -- play_df["penalty_in_text"] = False play_df.loc[ - ( - play_df["text"].str.contains( - "penalty", case=False, flags=0, na=False, regex=True - ) - ) + (play_df["text"].str.contains("penalty", case=False, flags=0, na=False, regex=True)) & (~(play_df["type.text"] == "Penalty")) - & ( - ~play_df["text"].str.contains( - "declined", case=False, flags=0, na=False, regex=True - ) - ) - & ( - ~play_df["text"].str.contains( - r"off-setting", case=False, flags=0, na=False, regex=True - ) - ) - & ( - ~play_df["text"].str.contains( - "no play", case=False, flags=0, na=False, regex=True - ) - ), + & (~play_df["text"].str.contains("declined", case=False, flags=0, na=False, regex=True)) + & (~play_df["text"].str.contains(r"off-setting", case=False, flags=0, na=False, regex=True)) + & (~play_df["text"].str.contains("no play", case=False, flags=0, na=False, regex=True)), "penalty_in_text", ] = True @@ -936,117 +761,61 @@ def __setup_penalty_data(self, play_df): (play_df.penalty_offset == 1), (play_df.penalty_declined == 1), play_df.text.str.contains(" roughing passer ", case=False, regex=True), - play_df.text.str.contains( - " offensive holding ", case=False, regex=True - ), + play_df.text.str.contains(" offensive holding ", case=False, regex=True), play_df.text.str.contains(" pass interference", case=False, regex=True), play_df.text.str.contains(" encroachment", case=False, regex=True), - play_df.text.str.contains( - " defensive pass interference ", case=False, regex=True - ), - play_df.text.str.contains( - " offensive pass interference ", case=False, regex=True - ), - play_df.text.str.contains( - " illegal procedure ", case=False, regex=True - ), - play_df.text.str.contains( - " defensive holding ", case=False, regex=True - ), - play_df.text.str.contains(" holding ", case=False, regex=True), - play_df.text.str.contains( - " offensive offside | offside offense", case=False, regex=True - ), - play_df.text.str.contains( - " defensive offside | offside defense", case=False, regex=True - ), + play_df.text.str.contains(" defensive pass interference ", case=False, regex=True), + play_df.text.str.contains(" offensive pass interference ", case=False, regex=True), + play_df.text.str.contains(" illegal procedure ", case=False, regex=True), + play_df.text.str.contains(" defensive holding ", case=False, regex=True), + play_df.text.str.contains(" holding ", case=False, regex=True), + play_df.text.str.contains(" offensive offside | offside offense", case=False, regex=True), + play_df.text.str.contains(" defensive offside | offside defense", case=False, regex=True), play_df.text.str.contains(" offside ", case=False, regex=True), - play_df.text.str.contains( - " illegal fair catch signal ", case=False, regex=True - ), + play_df.text.str.contains(" illegal fair catch signal ", case=False, regex=True), play_df.text.str.contains(" illegal batting ", case=False, regex=True), - play_df.text.str.contains( - " neutral zone infraction ", case=False, regex=True - ), - play_df.text.str.contains( - " ineligible downfield ", case=False, regex=True - ), - play_df.text.str.contains( - " illegal use of hands ", case=False, regex=True - ), + play_df.text.str.contains(" neutral zone infraction ", case=False, regex=True), + play_df.text.str.contains(" ineligible downfield ", case=False, regex=True), + play_df.text.str.contains(" illegal use of hands ", case=False, regex=True), play_df.text.str.contains( " kickoff out of bounds | kickoff out-of-bounds ", case=False, regex=True, ), - play_df.text.str.contains( - " 12 men on the field ", case=False, regex=True - ), + play_df.text.str.contains(" 12 men on the field ", case=False, regex=True), play_df.text.str.contains(" illegal block ", case=False, regex=True), play_df.text.str.contains(" personal foul ", case=False, regex=True), play_df.text.str.contains(" false start ", case=False, regex=True), - play_df.text.str.contains( - " substitution infraction ", case=False, regex=True - ), - play_df.text.str.contains( - " illegal formation ", case=False, regex=True - ), + play_df.text.str.contains(" substitution infraction ", case=False, regex=True), + play_df.text.str.contains(" illegal formation ", case=False, regex=True), play_df.text.str.contains(" illegal touching ", case=False, regex=True), - play_df.text.str.contains( - " sideline interference ", case=False, regex=True - ), + play_df.text.str.contains(" sideline interference ", case=False, regex=True), play_df.text.str.contains(" clipping ", case=False, regex=True), - play_df.text.str.contains( - " sideline infraction ", case=False, regex=True - ), + play_df.text.str.contains(" sideline infraction ", case=False, regex=True), play_df.text.str.contains(" crackback ", case=False, regex=True), play_df.text.str.contains(" illegal snap ", case=False, regex=True), - play_df.text.str.contains( - " illegal helmet contact ", case=False, regex=True - ), + play_df.text.str.contains(" illegal helmet contact ", case=False, regex=True), play_df.text.str.contains(" roughing holder ", case=False, regex=True), - play_df.text.str.contains( - " horse collar tackle ", case=False, regex=True - ), - play_df.text.str.contains( - " illegal participation ", case=False, regex=True - ), + play_df.text.str.contains(" horse collar tackle ", case=False, regex=True), + play_df.text.str.contains(" illegal participation ", case=False, regex=True), play_df.text.str.contains(" tripping ", case=False, regex=True), play_df.text.str.contains(" illegal shift ", case=False, regex=True), play_df.text.str.contains(" illegal motion ", case=False, regex=True), - play_df.text.str.contains( - " roughing the kicker ", case=False, regex=True - ), + play_df.text.str.contains(" roughing the kicker ", case=False, regex=True), play_df.text.str.contains(" delay of game ", case=False, regex=True), play_df.text.str.contains(" targeting ", case=False, regex=True), play_df.text.str.contains(" face mask ", case=False, regex=True), - play_df.text.str.contains( - " illegal forward pass ", case=False, regex=True - ), - play_df.text.str.contains( - " intentional grounding ", case=False, regex=True - ), + play_df.text.str.contains(" illegal forward pass ", case=False, regex=True), + play_df.text.str.contains(" intentional grounding ", case=False, regex=True), play_df.text.str.contains(" illegal kicking ", case=False, regex=True), play_df.text.str.contains(" illegal conduct ", case=False, regex=True), - play_df.text.str.contains( - " kick catching interference ", case=False, regex=True - ), - play_df.text.str.contains( - " unnecessary roughness ", case=False, regex=True - ), + play_df.text.str.contains(" kick catching interference ", case=False, regex=True), + play_df.text.str.contains(" unnecessary roughness ", case=False, regex=True), play_df.text.str.contains("Penalty, UR"), - play_df.text.str.contains( - " unsportsmanlike conduct ", case=False, regex=True - ), - play_df.text.str.contains( - " running into kicker ", case=False, regex=True - ), - play_df.text.str.contains( - " failure to wear required equipment ", case=False, regex=True - ), - play_df.text.str.contains( - " player disqualification ", case=False, regex=True - ), + play_df.text.str.contains(" unsportsmanlike conduct ", case=False, regex=True), + play_df.text.str.contains(" running into kicker ", case=False, regex=True), + play_df.text.str.contains(" failure to wear required equipment ", case=False, regex=True), + play_df.text.str.contains(" player disqualification ", case=False, regex=True), (play_df.penalty_flag == True), ], [ @@ -1117,32 +886,22 @@ def __setup_penalty_data(self, play_df): play_df["yds_penalty"] = np.select( [(play_df.penalty_flag == True)], - [ - play_df.penalty_text.str.extract( - "(.{0,3})yards|yds|yd to the ", flags=re.IGNORECASE - )[0] - ], + [play_df.penalty_text.str.extract("(.{0,3})yards|yds|yd to the ", flags=re.IGNORECASE)[0]], default=None, ) - play_df["yds_penalty"] = play_df["yds_penalty"].str.replace( - " yards to the | yds to the | yd to the ", "" - ) + play_df["yds_penalty"] = play_df["yds_penalty"].str.replace(" yards to the | yds to the | yd to the ", "") play_df["yds_penalty"] = np.select( [ (play_df.penalty_flag == True) & (play_df.text.str.contains(r"ards\)", case=False, regex=True)) & (play_df.yds_penalty.isna()), ], - [ - play_df.text.str.extract( - r"(.{0,4})yards\)|Yards\)|yds\)|Yds\)", flags=re.IGNORECASE - )[0] - ], + [play_df.text.str.extract(r"(.{0,4})yards\)|Yards\)|yds\)|Yds\)", flags=re.IGNORECASE)[0]], default=play_df.yds_penalty, ) - play_df["yds_penalty"] = play_df.yds_penalty.str.replace( - "yards\\)|Yards\\)|yds\\)|Yds\\)", "" - ).str.replace("\\(", "") + play_df["yds_penalty"] = play_df.yds_penalty.str.replace("yards\\)|Yards\\)|yds\\)|Yds\\)", "").str.replace( + "\\(", "" + ) return play_df def __add_downs_data(self, play_df): @@ -1154,14 +913,10 @@ def __add_downs_data(self, play_df): play_df = play_df.copy(deep=True) play_df.loc[:, "id"] = play_df["id"].astype(float) play_df.sort_values(by=["id", "start.adj_TimeSecsRem"], inplace=True) - play_df.drop_duplicates( - subset=["text", "id", "type.text", "start.down"], keep="last", inplace=True - ) + play_df.drop_duplicates(subset=["text", "id", "type.text", "start.down"], keep="last", inplace=True) play_df = play_df[ ( - play_df["type.text"].str.contains( - "end of|coin toss|end period|wins toss", case=False, regex=True - ) + play_df["type.text"].str.contains("end of|coin toss|end period|wins toss", case=False, regex=True) == False ) ] @@ -1200,53 +955,37 @@ def __add_play_type_flags(self, play_df): "touchdown", case=False, flags=0, na=False, regex=True ) ## Portion of touchdown check for plays where touchdown is not listed in the play_type-- - play_df["td_check"] = play_df["text"].str.contains( - "Touchdown", case=False, flags=0, na=False, regex=True - ) - play_df["safety"] = play_df["text"].str.contains( - "safety", case=False, flags=0, na=False, regex=True - ) + play_df["td_check"] = play_df["text"].str.contains("Touchdown", case=False, flags=0, na=False, regex=True) + play_df["safety"] = play_df["text"].str.contains("safety", case=False, flags=0, na=False, regex=True) # --- Fumbles---- play_df["fumble_vec"] = np.select( [ play_df["text"].str.contains("fumble", case=False, flags=0, na=False, regex=True), - (~play_df["text"].str.contains("fumble", case=False, flags=0, na=False, regex=True)) & (play_df["type.text"] == "Rush") & (play_df["start.pos_team.id"] != play_df["end.pos_team.id"]), - (~play_df["text"].str.contains("fumble", case=False, flags=0, na=False, regex=True)) & (play_df["type.text"] == "Sack") & (play_df["start.pos_team.id"] != play_df["end.pos_team.id"]), - ], - [ - True, - True, - True + (~play_df["text"].str.contains("fumble", case=False, flags=0, na=False, regex=True)) + & (play_df["type.text"] == "Rush") + & (play_df["start.pos_team.id"] != play_df["end.pos_team.id"]), + (~play_df["text"].str.contains("fumble", case=False, flags=0, na=False, regex=True)) + & (play_df["type.text"] == "Sack") + & (play_df["start.pos_team.id"] != play_df["end.pos_team.id"]), ], + [True, True, True], default=False, ) - play_df["forced_fumble"] = play_df["text"].str.contains( - "forced by", case=False, flags=0, na=False, regex=True - ) + play_df["forced_fumble"] = play_df["text"].str.contains("forced by", case=False, flags=0, na=False, regex=True) # --- Kicks---- play_df["kickoff_play"] = play_df["type.text"].isin(kickoff_vec) play_df["kickoff_tb"] = np.select( [ - ( - play_df["text"].str.contains( - "touchback", case=False, flags=0, na=False, regex=True - ) - ) + (play_df["text"].str.contains("touchback", case=False, flags=0, na=False, regex=True)) & (play_df.kickoff_play == True), - ( - play_df["text"].str.contains( - "kickoff$", case=False, flags=0, na=False, regex=True - ) - ) + (play_df["text"].str.contains("kickoff$", case=False, flags=0, na=False, regex=True)) & (play_df.kickoff_play == True), ], [True, True], default=False, ) play_df["kickoff_onside"] = ( - play_df["text"].str.contains( - r"on-side|onside|on side", case=False, flags=0, na=False, regex=True - ) + play_df["text"].str.contains(r"on-side|onside|on side", case=False, flags=0, na=False, regex=True) ) & (play_df.kickoff_play == True) play_df["kickoff_oob"] = ( play_df["text"].str.contains( @@ -1258,38 +997,22 @@ def __add_play_type_flags(self, play_df): ) ) & (play_df.kickoff_play == True) play_df["kickoff_fair_catch"] = ( - play_df["text"].str.contains( - r"fair catch|fair caught", case=False, flags=0, na=False, regex=True - ) + play_df["text"].str.contains(r"fair catch|fair caught", case=False, flags=0, na=False, regex=True) ) & (play_df.kickoff_play == True) play_df["kickoff_downed"] = ( - play_df["text"].str.contains( - "downed", case=False, flags=0, na=False, regex=True - ) + play_df["text"].str.contains("downed", case=False, flags=0, na=False, regex=True) ) & (play_df.kickoff_play == True) - play_df["kick_play"] = play_df["text"].str.contains( - r"kick|kickoff", case=False, flags=0, na=False, regex=True - ) + play_df["kick_play"] = play_df["text"].str.contains(r"kick|kickoff", case=False, flags=0, na=False, regex=True) play_df["kickoff_safety"] = ( (~play_df["type.text"].isin(["Blocked Punt", "Penalty"])) - & ( - play_df["text"].str.contains( - "kickoff", case=False, flags=0, na=False, regex=True - ) - ) + & (play_df["text"].str.contains("kickoff", case=False, flags=0, na=False, regex=True)) & (play_df.safety == True) ) # --- Punts---- play_df["punt"] = np.where(play_df["type.text"].isin(punt_vec), True, False) - play_df["punt_play"] = play_df["text"].str.contains( - "punt", case=False, flags=0, na=False, regex=True - ) + play_df["punt_play"] = play_df["text"].str.contains("punt", case=False, flags=0, na=False, regex=True) play_df["punt_tb"] = np.where( - ( - play_df["text"].str.contains( - "touchback", case=False, flags=0, na=False, regex=True - ) - ) + (play_df["text"].str.contains("touchback", case=False, flags=0, na=False, regex=True)) & (play_df.punt == True), True, False, @@ -1309,32 +1032,20 @@ def __add_play_type_flags(self, play_df): False, ) play_df["punt_fair_catch"] = np.where( - ( - play_df["text"].str.contains( - r"fair catch|fair caught", case=False, flags=0, na=False, regex=True - ) - ) + (play_df["text"].str.contains(r"fair catch|fair caught", case=False, flags=0, na=False, regex=True)) & (play_df.punt == True), True, False, ) play_df["punt_downed"] = np.where( - ( - play_df["text"].str.contains( - "downed", case=False, flags=0, na=False, regex=True - ) - ) + (play_df["text"].str.contains("downed", case=False, flags=0, na=False, regex=True)) & (play_df.punt == True), True, False, ) play_df["punt_safety"] = np.where( (play_df["type.text"].isin(["Blocked Punt", "Punt"])) - & ( - play_df["text"].str.contains( - "punt", case=False, flags=0, na=False, regex=True - ) - ) + & (play_df["text"].str.contains("punt", case=False, flags=0, na=False, regex=True)) & (play_df.safety == True), True, False, @@ -1345,11 +1056,7 @@ def __add_play_type_flags(self, play_df): False, ) play_df["punt_blocked"] = np.where( - ( - play_df["text"].str.contains( - "blocked", case=False, flags=0, na=False, regex=True - ) - ) + (play_df["text"].str.contains("blocked", case=False, flags=0, na=False, regex=True)) & (play_df.punt == True), True, False, @@ -1404,19 +1111,11 @@ def __add_rush_pass_flags(self, play_df): ) | ( (play_df["type.text"] == "Safety") - & ( - play_df["text"].str.contains( - "sacked", case=False, flags=0, na=False, regex=True - ) - ) + & (play_df["text"].str.contains("sacked", case=False, flags=0, na=False, regex=True)) ) | ( (play_df["type.text"] == "Safety") - & ( - play_df["text"].str.contains( - "pass complete", case=False, flags=0, na=False, regex=True - ) - ) + & (play_df["text"].str.contains("pass complete", case=False, flags=0, na=False, regex=True)) ) | ( (play_df["type.text"] == "Fumble Recovery (Own)") @@ -1432,11 +1131,7 @@ def __add_rush_pass_flags(self, play_df): ) | ( (play_df["type.text"] == "Fumble Recovery (Own)") - & ( - play_df["text"].str.contains( - "sacked", case=False, flags=0, na=False, regex=True - ) - ) + & (play_df["text"].str.contains("sacked", case=False, flags=0, na=False, regex=True)) ) | ( (play_df["type.text"] == "Fumble Recovery (Own) Touchdown") @@ -1452,11 +1147,7 @@ def __add_rush_pass_flags(self, play_df): ) | ( (play_df["type.text"] == "Fumble Recovery (Own) Touchdown") - & ( - play_df["text"].str.contains( - "sacked", case=False, flags=0, na=False, regex=True - ) - ) + & (play_df["text"].str.contains("sacked", case=False, flags=0, na=False, regex=True)) ) | ( (play_df["type.text"] == "Fumble Recovery (Opponent)") @@ -1472,11 +1163,7 @@ def __add_rush_pass_flags(self, play_df): ) | ( (play_df["type.text"] == "Fumble Recovery (Opponent)") - & ( - play_df["text"].str.contains( - "sacked", case=False, flags=0, na=False, regex=True - ) - ) + & (play_df["text"].str.contains("sacked", case=False, flags=0, na=False, regex=True)) ) | ( (play_df["type.text"] == "Fumble Recovery (Opponent) Touchdown") @@ -1504,11 +1191,7 @@ def __add_rush_pass_flags(self, play_df): ) | ( (play_df["type.text"] == "Fumble Return Touchdown") - & ( - play_df["text"].str.contains( - "sacked", case=False, flags=0, na=False, regex=True - ) - ) + & (play_df["text"].str.contains("sacked", case=False, flags=0, na=False, regex=True)) ) ), True, @@ -1530,11 +1213,7 @@ def __add_rush_pass_flags(self, play_df): ] ) & (play_df["pass"] == True) - & ( - play_df["text"].str.contains( - "sacked", case=False, flags=0, na=False, regex=True - ) - ) + & (play_df["text"].str.contains("sacked", case=False, flags=0, na=False, regex=True)) ) ) ), @@ -1557,20 +1236,14 @@ def __add_team_score_variables(self, play_df): # --- Team Score variables ------ play_df["lag_homeScore"] = play_df["homeScore"].shift(1) play_df["lag_awayScore"] = play_df["awayScore"].shift(1) - play_df["lag_HA_score_diff"] = ( - play_df["lag_homeScore"] - play_df["lag_awayScore"] - ) + play_df["lag_HA_score_diff"] = play_df["lag_homeScore"] - play_df["lag_awayScore"] play_df["HA_score_diff"] = play_df["homeScore"] - play_df["awayScore"] - play_df["net_HA_score_pts"] = ( - play_df["HA_score_diff"] - play_df["lag_HA_score_diff"] - ) + play_df["net_HA_score_pts"] = play_df["HA_score_diff"] - play_df["lag_HA_score_diff"] play_df["H_score_diff"] = play_df["homeScore"] - play_df["lag_homeScore"] play_df["A_score_diff"] = play_df["awayScore"] - play_df["lag_awayScore"] play_df["homeScore"] = np.select( [ - (play_df.scoringPlay == False) - & (play_df["game_play_number"] != 1) - & (play_df["H_score_diff"] >= 9), + (play_df.scoringPlay == False) & (play_df["game_play_number"] != 1) & (play_df["H_score_diff"] >= 9), (play_df.scoringPlay == False) & (play_df["game_play_number"] != 1) & (play_df["H_score_diff"] < 9) @@ -1585,9 +1258,7 @@ def __add_team_score_variables(self, play_df): ) play_df["awayScore"] = np.select( [ - (play_df.scoringPlay == False) - & (play_df["game_play_number"] != 1) - & (play_df["A_score_diff"] >= 9), + (play_df.scoringPlay == False) & (play_df["game_play_number"] != 1) & (play_df["A_score_diff"] >= 9), (play_df.scoringPlay == False) & (play_df["game_play_number"] != 1) & (play_df["A_score_diff"] < 9) @@ -1602,19 +1273,11 @@ def __add_team_score_variables(self, play_df): ) play_df.drop(["lag_homeScore", "lag_awayScore"], axis=1, inplace=True) play_df["lag_homeScore"] = play_df["homeScore"].shift(1) - play_df["lag_homeScore"] = np.where( - (play_df.lag_homeScore.isna()), 0, play_df["lag_homeScore"] - ) + play_df["lag_homeScore"] = np.where((play_df.lag_homeScore.isna()), 0, play_df["lag_homeScore"]) play_df["lag_awayScore"] = play_df["awayScore"].shift(1) - play_df["lag_awayScore"] = np.where( - (play_df.lag_awayScore.isna()), 0, play_df["lag_awayScore"] - ) - play_df["start.homeScore"] = np.where( - (play_df["game_play_number"] == 1), 0, play_df["lag_homeScore"] - ) - play_df["start.awayScore"] = np.where( - (play_df["game_play_number"] == 1), 0, play_df["lag_awayScore"] - ) + play_df["lag_awayScore"] = np.where((play_df.lag_awayScore.isna()), 0, play_df["lag_awayScore"]) + play_df["start.homeScore"] = np.where((play_df["game_play_number"] == 1), 0, play_df["lag_homeScore"]) + play_df["start.awayScore"] = np.where((play_df["game_play_number"] == 1), 0, play_df["lag_awayScore"]) play_df["end.homeScore"] = play_df["homeScore"] play_df["end.awayScore"] = play_df["awayScore"] play_df["pos_team_score"] = np.where( @@ -1637,9 +1300,7 @@ def __add_team_score_variables(self, play_df): play_df["start.awayScore"], play_df["start.homeScore"], ) - play_df["start.pos_score_diff"] = ( - play_df["start.pos_team_score"] - play_df["start.def_pos_team_score"] - ) + play_df["start.pos_score_diff"] = play_df["start.pos_team_score"] - play_df["start.def_pos_team_score"] play_df["end.pos_team_score"] = np.where( play_df["end.pos_team.id"] == play_df["homeTeamId"], play_df["end.homeScore"], @@ -1650,13 +1311,9 @@ def __add_team_score_variables(self, play_df): play_df["end.awayScore"], play_df["end.homeScore"], ) - play_df["end.pos_score_diff"] = ( - play_df["end.pos_team_score"] - play_df["end.def_pos_team_score"] - ) + play_df["end.pos_score_diff"] = play_df["end.pos_team_score"] - play_df["end.def_pos_team_score"] play_df["lag_pos_team"] = play_df["pos_team"].shift(1) - play_df.loc[ - play_df.lag_pos_team.isna() == True, "lag_pos_team" - ] = play_df.pos_team + play_df.loc[play_df.lag_pos_team.isna() == True, "lag_pos_team"] = play_df.pos_team play_df["lead_pos_team"] = play_df["pos_team"].shift(-1) play_df["lead_pos_team2"] = play_df["pos_team"].shift(-2) play_df["pos_score_diff"] = play_df.pos_team_score - play_df.def_pos_team_score @@ -1669,30 +1326,18 @@ def __add_team_score_variables(self, play_df): ) play_df["pos_score_diff_start"] = np.select( [ - (play_df.kickoff_play == True) - & (play_df.lag_pos_team == play_df.pos_team), - (play_df.kickoff_play == True) - | (play_df.lag_pos_team != play_df.pos_team), + (play_df.kickoff_play == True) & (play_df.lag_pos_team == play_df.pos_team), + (play_df.kickoff_play == True) | (play_df.lag_pos_team != play_df.pos_team), ], [play_df.lag_pos_score_diff, -1 * play_df.lag_pos_score_diff], default=play_df.lag_pos_score_diff, ) # --- Timeouts ------ - play_df.loc[ - play_df.pos_score_diff_start.isna() == True, "pos_score_diff_start" - ] = play_df.pos_score_diff - play_df["start.pos_team_receives_2H_kickoff"] = ( - play_df["start.pos_team.id"] == play_df.firstHalfKickoffTeamId - ) - play_df["end.pos_team_receives_2H_kickoff"] = ( - play_df["end.pos_team.id"] == play_df.firstHalfKickoffTeamId - ) - play_df["change_of_poss"] = np.where( - play_df["start.pos_team.id"] == play_df["end.pos_team.id"], False, True - ) - play_df["change_of_poss"] = np.where( - play_df["change_of_poss"].isna(), 0, play_df["change_of_poss"] - ) + play_df.loc[play_df.pos_score_diff_start.isna() == True, "pos_score_diff_start"] = play_df.pos_score_diff + play_df["start.pos_team_receives_2H_kickoff"] = play_df["start.pos_team.id"] == play_df.firstHalfKickoffTeamId + play_df["end.pos_team_receives_2H_kickoff"] = play_df["end.pos_team.id"] == play_df.firstHalfKickoffTeamId + play_df["change_of_poss"] = np.where(play_df["start.pos_team.id"] == play_df["end.pos_team.id"], False, True) + play_df["change_of_poss"] = np.where(play_df["change_of_poss"].isna(), 0, play_df["change_of_poss"]) return play_df def __add_new_play_types(self, play_df): @@ -1751,9 +1396,7 @@ def __add_new_play_types(self, play_df): ) # -- Fix punt return TDs ---- play_df["type.text"] = np.where( - (play_df.punt_play == True) - & (play_df.td_play == True) - & (play_df.td_check == True), + (play_df.punt_play == True) & (play_df.td_play == True) & (play_df.td_check == True), "Punt Return Touchdown", play_df["type.text"], ) @@ -1795,22 +1438,14 @@ def __add_new_play_types(self, play_df): ) play_df["type.text"] = np.where( (play_df["type.text"].isin(["Blocked Field Goal"])) - & ( - play_df["text"].str.contains( - "for a TD", case=False, flags=0, na=False, regex=True - ) - ), + & (play_df["text"].str.contains("for a TD", case=False, flags=0, na=False, regex=True)), "Blocked Field Goal Touchdown", play_df["type.text"], ) play_df["type.text"] = np.where( (play_df["type.text"].isin(["Blocked Punt"])) - & ( - play_df["text"].str.contains( - "for a TD", case=False, flags=0, na=False, regex=True - ) - ), + & (play_df["text"].str.contains("for a TD", case=False, flags=0, na=False, regex=True)), "Blocked Punt Touchdown", play_df["type.text"], ) @@ -1837,29 +1472,15 @@ def __add_new_play_types(self, play_df): ) # -- Fix Pass Interception Return TD play_type labels---- play_df["type.text"] = np.where( - play_df["text"].str.contains( - "pass intercepted for a TD", case=False, flags=0, na=False, regex=True - ), + play_df["text"].str.contains("pass intercepted for a TD", case=False, flags=0, na=False, regex=True), "Interception Return Touchdown", play_df["type.text"], ) # -- Fix Sack/Fumbles Touchdown play_type labels---- play_df["type.text"] = np.where( - ( - play_df["text"].str.contains( - "sacked", case=False, flags=0, na=False, regex=True - ) - ) - & ( - play_df["text"].str.contains( - "fumbled", case=False, flags=0, na=False, regex=True - ) - ) - & ( - play_df["text"].str.contains( - "TD", case=False, flags=0, na=False, regex=True - ) - ), + (play_df["text"].str.contains("sacked", case=False, flags=0, na=False, regex=True)) + & (play_df["text"].str.contains("fumbled", case=False, flags=0, na=False, regex=True)) + & (play_df["text"].str.contains("TD", case=False, flags=0, na=False, regex=True)), "Fumble Recovery (Opponent) Touchdown", play_df["type.text"], ) @@ -1867,44 +1488,28 @@ def __add_new_play_types(self, play_df): ##-- first one looks for complete pass play_df["type.text"] = np.where( (play_df["type.text"] == "Pass") - & ( - play_df.text.str.contains( - "pass complete", case=False, flags=0, na=False, regex=True - ) - ), + & (play_df.text.str.contains("pass complete", case=False, flags=0, na=False, regex=True)), "Pass Completion", play_df["type.text"], ) ##-- second one looks for incomplete pass play_df["type.text"] = np.where( (play_df["type.text"] == "Pass") - & ( - play_df.text.str.contains( - "pass incomplete", case=False, flags=0, na=False, regex=True - ) - ), + & (play_df.text.str.contains("pass incomplete", case=False, flags=0, na=False, regex=True)), "Pass Incompletion", play_df["type.text"], ) ##-- third one looks for interceptions play_df["type.text"] = np.where( (play_df["type.text"] == "Pass") - & ( - play_df.text.str.contains( - "pass intercepted", case=False, flags=0, na=False, regex=True - ) - ), + & (play_df.text.str.contains("pass intercepted", case=False, flags=0, na=False, regex=True)), "Pass Interception", play_df["type.text"], ) ##-- fourth one looks for sacked play_df["type.text"] = np.where( (play_df["type.text"] == "Pass") - & ( - play_df.text.str.contains( - "sacked", case=False, flags=0, na=False, regex=True - ) - ), + & (play_df.text.str.contains("sacked", case=False, flags=0, na=False, regex=True)), "Sack", play_df["type.text"], ) @@ -1956,25 +1561,17 @@ def __add_new_play_types(self, play_df): # --- Moving Kickoff/Punt Touchdowns without fumbles to Kickoff/Punt Return Touchdown play_df["type.text"] = np.where( - (play_df["type.text"] == "Kickoff Touchdown") - & (play_df.fumble_vec == False), + (play_df["type.text"] == "Kickoff Touchdown") & (play_df.fumble_vec == False), "Kickoff Return Touchdown", play_df["type.text"], ) play_df["type.text"] = np.select( [ - (play_df["type.text"] == "Kickoff Touchdown") - & (play_df.fumble_vec == False), - (play_df["type.text"] == "Kickoff") - & (play_df["td_play"] == True) - & (play_df.fumble_vec == False), + (play_df["type.text"] == "Kickoff Touchdown") & (play_df.fumble_vec == False), + (play_df["type.text"] == "Kickoff") & (play_df["td_play"] == True) & (play_df.fumble_vec == False), (play_df["type.text"] == "Kickoff") - & ( - play_df.text.str.contains( - "for a TD", case=False, flags=0, na=False, regex=True - ) - ) + & (play_df.text.str.contains("for a TD", case=False, flags=0, na=False, regex=True)) & (play_df.fumble_vec == False), ], [ @@ -1999,11 +1596,7 @@ def __add_new_play_types(self, play_df): & (play_df.fumble_vec == False) & (play_df.change_of_poss == 1), (play_df["type.text"] == "Punt") - & ( - play_df.text.str.contains( - "for a TD", case=False, flags=0, na=False, regex=True - ) - ) + & (play_df.text.str.contains("for a TD", case=False, flags=0, na=False, regex=True)) & (play_df.change_of_poss == 1), ], ["Punt Return Touchdown", "Punt Return Touchdown"], @@ -2011,19 +1604,13 @@ def __add_new_play_types(self, play_df): ) play_df["type.text"] = np.where( - (play_df["type.text"] == "Punt") - & (play_df.fumble_vec == True) - & (play_df.change_of_poss == 0), + (play_df["type.text"] == "Punt") & (play_df.fumble_vec == True) & (play_df.change_of_poss == 0), "Punt Team Fumble Recovery", play_df["type.text"], ) play_df["type.text"] = np.where( (play_df["type.text"].isin(["Punt Touchdown"])) - | ( - (play_df["scoringPlay"] == True) - & (play_df["punt_play"] == True) - & (play_df.change_of_poss == 0) - ), + | ((play_df["scoringPlay"] == True) & (play_df["punt_play"] == True) & (play_df.change_of_poss == 0)), "Punt Team Fumble Recovery Touchdown", play_df["type.text"], ) @@ -2042,41 +1629,25 @@ def __add_new_play_types(self, play_df): # --- Safeties (kickoff, punt, penalty) ---- play_df["type.text"] = np.where( ( - play_df["type.text"].isin( - ["Pass Reception", "Rush", "Rushing Touchdown"] - ) + play_df["type.text"].isin(["Pass Reception", "Rush", "Rushing Touchdown"]) & ((play_df["pass"] == True) | (play_df["rush"] == True)) & (play_df["safety"] == True) ), "Safety", play_df["type.text"], ) - play_df["type.text"] = np.where( - (play_df.kickoff_safety == True), "Kickoff (Safety)", play_df["type.text"] - ) - play_df["type.text"] = np.where( - (play_df.punt_safety == True), "Punt (Safety)", play_df["type.text"] - ) - play_df["type.text"] = np.where( - (play_df.penalty_safety == True), "Penalty (Safety)", play_df["type.text"] - ) + play_df["type.text"] = np.where((play_df.kickoff_safety == True), "Kickoff (Safety)", play_df["type.text"]) + play_df["type.text"] = np.where((play_df.punt_safety == True), "Punt (Safety)", play_df["type.text"]) + play_df["type.text"] = np.where((play_df.penalty_safety == True), "Penalty (Safety)", play_df["type.text"]) play_df["type.text"] = np.where( (play_df["type.text"] == "Extra Point Good") - & ( - play_df["text"].str.contains( - "Two-Point", case=False, flags=0, na=False, regex=True - ) - ), + & (play_df["text"].str.contains("Two-Point", case=False, flags=0, na=False, regex=True)), "Two-Point Conversion Good", play_df["type.text"], ) play_df["type.text"] = np.where( (play_df["type.text"] == "Extra Point Missed") - & ( - play_df["text"].str.contains( - "Two-Point", case=False, flags=0, na=False, regex=True - ) - ), + & (play_df["text"].str.contains("Two-Point", case=False, flags=0, na=False, regex=True)), "Two-Point Conversion Missed", play_df["type.text"], ) @@ -2099,35 +1670,23 @@ def __add_play_category_flags(self, play_df): ) ) & (play_df["pass"] == True) - & ( - play_df["text"].str.contains( - "sacked", case=False, flags=0, na=False, regex=True - ) - ), + & (play_df["text"].str.contains("sacked", case=False, flags=0, na=False, regex=True)), ( (play_df["type.text"].isin(["Safety"])) - & ( - play_df["text"].str.contains( - "sacked", case=False, flags=0, na=False, regex=True - ) - ) + & (play_df["text"].str.contains("sacked", case=False, flags=0, na=False, regex=True)) ), ], [True, True, True], default=False, ) # --- Interceptions ------ - play_df["int"] = play_df["type.text"].isin( - ["Interception Return", "Interception Return Touchdown"] - ) + play_df["int"] = play_df["type.text"].isin(["Interception Return", "Interception Return Touchdown"]) play_df["int_td"] = play_df["type.text"].isin(["Interception Return Touchdown"]) # --- Pass Completions, Attempts and Targets ------- play_df["completion"] = np.select( [ - play_df["type.text"].isin( - ["Pass Reception", "Pass Completion", "Passing Touchdown"] - ), + play_df["type.text"].isin(["Pass Reception", "Pass Completion", "Passing Touchdown"]), ( play_df["type.text"].isin( [ @@ -2138,11 +1697,7 @@ def __add_play_category_flags(self, play_df): ] ) & (play_df["pass"] == True) - & ~( - play_df["text"].str.contains( - "sacked", case=False, flags=0, na=False, regex=True - ) - ) + & ~(play_df["text"].str.contains("sacked", case=False, flags=0, na=False, regex=True)) ), ], [True, True], @@ -2171,19 +1726,11 @@ def __add_play_category_flags(self, play_df): ] ) & (play_df["pass"] == True) - & ~( - play_df["text"].str.contains( - "sacked", case=False, flags=0, na=False, regex=True - ) - ) + & ~(play_df["text"].str.contains("sacked", case=False, flags=0, na=False, regex=True)) ), ( (play_df["pass"] == True) - & ~( - play_df["text"].str.contains( - "sacked", case=False, flags=0, na=False, regex=True - ) - ) + & ~(play_df["text"].str.contains("sacked", case=False, flags=0, na=False, regex=True)) ), ], [True, True, True], @@ -2212,19 +1759,11 @@ def __add_play_category_flags(self, play_df): ] ) & (play_df["pass"] == True) - & ~( - play_df["text"].str.contains( - "sacked", case=False, flags=0, na=False, regex=True - ) - ) + & ~(play_df["text"].str.contains("sacked", case=False, flags=0, na=False, regex=True)) ), ( (play_df["pass"] == True) - & ~( - play_df["text"].str.contains( - "sacked", case=False, flags=0, na=False, regex=True - ) - ) + & ~(play_df["text"].str.contains("sacked", case=False, flags=0, na=False, regex=True)) ), ], [True, True, True], @@ -2256,24 +1795,12 @@ def __add_play_category_flags(self, play_df): # --- Touchdowns---- play_df["scoring_play"] = play_df["type.text"].isin(scores_vec) play_df["yds_punted"] = ( - play_df["text"] - .str.extract(r"(?<= punt for)[^,]+(\d+)", flags=re.IGNORECASE) - .astype(float) - ) - play_df["yds_punt_gained"] = np.where( - play_df.punt == True, play_df["statYardage"], None + play_df["text"].str.extract(r"(?<= punt for)[^,]+(\d+)", flags=re.IGNORECASE).astype(float) ) + play_df["yds_punt_gained"] = np.where(play_df.punt == True, play_df["statYardage"], None) play_df["fg_attempt"] = np.where( - ( - play_df["type.text"].str.contains( - "Field Goal", case=False, flags=0, na=False, regex=True - ) - ) - | ( - play_df["text"].str.contains( - "Field Goal", case=False, flags=0, na=False, regex=True - ) - ), + (play_df["type.text"].str.contains("Field Goal", case=False, flags=0, na=False, regex=True)) + | (play_df["text"].str.contains("Field Goal", case=False, flags=0, na=False, regex=True)), True, False, ) @@ -2332,18 +1859,12 @@ def __add_play_category_flags(self, play_df): play_df["lead_play_type"] = play_df["type.text"].shift(-1) play_df["sp"] = np.where( - (play_df.fg_attempt == True) - | (play_df.punt == True) - | (play_df.kickoff_play == True), + (play_df.fg_attempt == True) | (play_df.punt == True) | (play_df.kickoff_play == True), True, False, ) play_df["play"] = np.where( - ( - ~play_df["type.text"].isin( - ["Timeout", "End Period", "End of Half", "Penalty"] - ) - ), + (~play_df["type.text"].isin(["Timeout", "End Period", "End of Half", "Penalty"])), True, False, ) @@ -2368,17 +1889,12 @@ def __add_play_category_flags(self, play_df): # --- Change of pos_team by lead('pos_team', 1)---- play_df["change_of_pos_team"] = np.where( (play_df.pos_team == play_df.lead_pos_team) - & ( - ~(play_df.lead_play_type.isin(["End Period", "End of Half"])) - | play_df.lead_play_type.isna() - == True - ), + & (~(play_df.lead_play_type.isin(["End Period", "End of Half"])) | play_df.lead_play_type.isna() == True), False, np.where( (play_df.pos_team == play_df.lead_pos_team2) & ( - (play_df.lead_play_type.isin(["End Period", "End of Half"])) - | play_df.lead_play_type.isna() + (play_df.lead_play_type.isin(["End Period", "End of Half"])) | play_df.lead_play_type.isna() == True ), False, @@ -2410,9 +1926,7 @@ def __add_play_category_flags(self, play_df): default=play_df["pos_score_diff_end"], ) - play_df["fumble_lost"] = np.where( - (play_df.fumble_vec == True) & (play_df.change_of_poss == True), True, False - ) + play_df["fumble_lost"] = np.where((play_df.fumble_vec == True) & (play_df.change_of_poss == True), True, False) play_df["fumble_recovered"] = np.where( (play_df.fumble_vec == True) & (play_df.change_of_poss == False), True, @@ -2421,129 +1935,61 @@ def __add_play_category_flags(self, play_df): return play_df def __add_yardage_cols(self, play_df): - play_df.insert(0,"yds_rushed", None) + play_df.insert(0, "yds_rushed", None) play_df["yds_rushed"] = np.select( [ (play_df.rush == True) - & ( - play_df.text.str.contains( - "run for no gain", case=False, flags=0, na=False, regex=True - ) - ), + & (play_df.text.str.contains("run for no gain", case=False, flags=0, na=False, regex=True)), (play_df.rush == True) - & ( - play_df.text.str.contains( - "for no gain", case=False, flags=0, na=False, regex=True - ) - ), + & (play_df.text.str.contains("for no gain", case=False, flags=0, na=False, regex=True)), (play_df.rush == True) - & ( - play_df.text.str.contains( - "run for a loss of", case=False, flags=0, na=False, regex=True - ) - ), + & (play_df.text.str.contains("run for a loss of", case=False, flags=0, na=False, regex=True)), (play_df.rush == True) - & ( - play_df.text.str.contains( - "rush for a loss of", case=False, flags=0, na=False, regex=True - ) - ), + & (play_df.text.str.contains("rush for a loss of", case=False, flags=0, na=False, regex=True)), (play_df.rush == True) - & ( - play_df.text.str.contains( - "run for", case=False, flags=0, na=False, regex=True - ) - ), + & (play_df.text.str.contains("run for", case=False, flags=0, na=False, regex=True)), (play_df.rush == True) - & ( - play_df.text.str.contains( - "rush for", case=False, flags=0, na=False, regex=True - ) - ), + & (play_df.text.str.contains("rush for", case=False, flags=0, na=False, regex=True)), (play_df.rush == True) - & ( - play_df.text.str.contains( - "Yd Run", case=False, flags=0, na=False, regex=True - ) - ), + & (play_df.text.str.contains("Yd Run", case=False, flags=0, na=False, regex=True)), (play_df.rush == True) - & ( - play_df.text.str.contains( - "Yd Rush", case=False, flags=0, na=False, regex=True - ) - ), + & (play_df.text.str.contains("Yd Rush", case=False, flags=0, na=False, regex=True)), (play_df.rush == True) - & ( - play_df.text.str.contains( - "Yard Rush", case=False, flags=0, na=False, regex=True - ) - ), + & (play_df.text.str.contains("Yard Rush", case=False, flags=0, na=False, regex=True)), (play_df.rush == True) - & ( - play_df.text.str.contains( - "rushed", case=False, flags=0, na=False, regex=True - ) - ) - & ( - ~play_df.text.str.contains( - "touchdown", case=False, flags=0, na=False, regex=True - ) - ), + & (play_df.text.str.contains("rushed", case=False, flags=0, na=False, regex=True)) + & (~play_df.text.str.contains("touchdown", case=False, flags=0, na=False, regex=True)), (play_df.rush == True) - & ( - play_df.text.str.contains( - "rushed", case=False, flags=0, na=False, regex=True - ) - ) - & ( - play_df.text.str.contains( - "touchdown", case=False, flags=0, na=False, regex=True - ) - ), + & (play_df.text.str.contains("rushed", case=False, flags=0, na=False, regex=True)) + & (play_df.text.str.contains("touchdown", case=False, flags=0, na=False, regex=True)), ], [ 0.0, 0.0, -1 - * play_df.text.str.extract( - r"((?<=run for a loss of)[^,]+)", flags=re.IGNORECASE - )[0] + * play_df.text.str.extract(r"((?<=run for a loss of)[^,]+)", flags=re.IGNORECASE)[0] .str.extract(r"(\d+)")[0] .astype(float), -1 - * play_df.text.str.extract( - r"((?<=rush for a loss of)[^,]+)", flags=re.IGNORECASE - )[0] + * play_df.text.str.extract(r"((?<=rush for a loss of)[^,]+)", flags=re.IGNORECASE)[0] .str.extract(r"(\d+)")[0] .astype(float), play_df.text.str.extract(r"((?<=run for)[^,]+)", flags=re.IGNORECASE)[0] .str.extract(r"(\d+)")[0] .astype(float), - play_df.text.str.extract(r"((?<=rush for)[^,]+)", flags=re.IGNORECASE)[ - 0 - ] + play_df.text.str.extract(r"((?<=rush for)[^,]+)", flags=re.IGNORECASE)[0] .str.extract(r"(\d+)")[0] .astype(float), - play_df.text.str.extract(r"(\d+) Yd Run", flags=re.IGNORECASE)[ - 0 - ].astype(float), - play_df.text.str.extract(r"(\d+) Yd Rush", flags=re.IGNORECASE)[ - 0 - ].astype(float), - play_df.text.str.extract(r"(\d+) Yard Rush", flags=re.IGNORECASE)[ - 0 - ].astype(float), - play_df.text.str.extract(r"for (\d+) yards", flags=re.IGNORECASE)[ - 0 - ].astype(float), - play_df.text.str.extract(r"for a (\d+) yard", flags=re.IGNORECASE)[ - 0 - ].astype(float), + play_df.text.str.extract(r"(\d+) Yd Run", flags=re.IGNORECASE)[0].astype(float), + play_df.text.str.extract(r"(\d+) Yd Rush", flags=re.IGNORECASE)[0].astype(float), + play_df.text.str.extract(r"(\d+) Yard Rush", flags=re.IGNORECASE)[0].astype(float), + play_df.text.str.extract(r"for (\d+) yards", flags=re.IGNORECASE)[0].astype(float), + play_df.text.str.extract(r"for a (\d+) yard", flags=re.IGNORECASE)[0].astype(float), ], default=None, ) - play_df.insert(0,"yds_receiving", None) + play_df.insert(0, "yds_receiving", None) play_df["yds_receiving"] = np.select( [ (play_df["pass"] == True) @@ -2552,23 +1998,16 @@ def __add_yardage_cols(self, play_df): (play_df["pass"] == True) & (play_df.text.str.contains("complete to", case=False)) & (play_df.text.str.contains("for a loss", case=False)), - (play_df["pass"] == True) - & (play_df.text.str.contains("complete to", case=False)), - (play_df["pass"] == True) - & (play_df.text.str.contains("complete to", case=False)), - (play_df["pass"] == True) - & (play_df.text.str.contains("incomplete", case=False)), - (play_df["pass"] == True) - & (play_df["type.text"].str.contains("incompletion", case=False)), - (play_df["pass"] == True) - & (play_df.text.str.contains("Yd pass", case=False)), + (play_df["pass"] == True) & (play_df.text.str.contains("complete to", case=False)), + (play_df["pass"] == True) & (play_df.text.str.contains("complete to", case=False)), + (play_df["pass"] == True) & (play_df.text.str.contains("incomplete", case=False)), + (play_df["pass"] == True) & (play_df["type.text"].str.contains("incompletion", case=False)), + (play_df["pass"] == True) & (play_df.text.str.contains("Yd pass", case=False)), ], [ 0.0, -1 - * play_df.text.str.extract( - r"((?<=for a loss of)[^,]+)", flags=re.IGNORECASE - )[0] + * play_df.text.str.extract(r"((?<=for a loss of)[^,]+)", flags=re.IGNORECASE)[0] .str.extract(r"(\d+)")[0] .astype(float), play_df.text.str.extract(r"((?<=for)[^,]+)", flags=re.IGNORECASE)[0] @@ -2586,7 +2025,7 @@ def __add_yardage_cols(self, play_df): default=None, ) - play_df.insert(0,"yds_int_return", None) + play_df.insert(0, "yds_int_return", None) play_df["yds_int_return"] = np.select( [ (play_df["pass"] == True) @@ -2607,26 +2046,18 @@ def __add_yardage_cols(self, play_df): (play_df["pass"] == True) & (play_df["int"] == True), ], [ - play_df.text.str.extract( - r"(.+) Interception Return", flags=re.IGNORECASE - )[0] + play_df.text.str.extract(r"(.+) Interception Return", flags=re.IGNORECASE)[0] .str.extract(r"(\d+)")[0] .astype(float), 0.0, -1 - * play_df.text.str.extract( - r"((?<= for a loss of)[^,]+)", flags=re.IGNORECASE - )[0] + * play_df.text.str.extract(r"((?<= for a loss of)[^,]+)", flags=re.IGNORECASE)[0] .str.extract(r"(\d+)")[0] .astype(float), - play_df.text.str.extract( - r"((?<= return for)[^,]+)", flags=re.IGNORECASE - )[0] + play_df.text.str.extract(r"((?<= return for)[^,]+)", flags=re.IGNORECASE)[0] .str.extract(r"(\d+)")[0] .astype(float), - play_df.text.str.extract( - r"((?<= return for)[^,]+)", flags=re.IGNORECASE - )[0] + play_df.text.str.extract(r"((?<= return for)[^,]+)", flags=re.IGNORECASE)[0] .str.extract(r"(\d+)")[0] .astype(float), play_df.text.str.replace("for a 1st", "") @@ -2640,48 +2071,29 @@ def __add_yardage_cols(self, play_df): # play_df['yds_fumble_return'] = None # play_df['yds_penalty'] = None - play_df.insert(0,"yds_kickoff", None) + play_df.insert(0, "yds_kickoff", None) play_df["yds_kickoff"] = np.where( (play_df["kickoff_play"] == True), - play_df.text.str.extract(r"((?<= kickoff for)[^,]+)", flags=re.IGNORECASE)[ - 0 - ] + play_df.text.str.extract(r"((?<= kickoff for)[^,]+)", flags=re.IGNORECASE)[0] .str.extract(r"(\d+)")[0] .astype(float), play_df["yds_kickoff"], ) - play_df.insert(0,"yds_kickoff_return", None) + play_df.insert(0, "yds_kickoff_return", None) play_df["yds_kickoff_return"] = np.select( [ - (play_df.kickoff_play == True) - & (play_df.kickoff_tb == True) - & (play_df.season > 2013), - (play_df.kickoff_play == True) - & (play_df.kickoff_tb == True) - & (play_df.season <= 2013), + (play_df.kickoff_play == True) & (play_df.kickoff_tb == True) & (play_df.season > 2013), + (play_df.kickoff_play == True) & (play_df.kickoff_tb == True) & (play_df.season <= 2013), (play_df.kickoff_play == True) & (play_df.fumble_vec == False) - & ( - play_df.text.str.contains( - r"for no gain|fair catch|fair caught", regex=True, case=False - ) - ), + & (play_df.text.str.contains(r"for no gain|fair catch|fair caught", regex=True, case=False)), (play_df.kickoff_play == True) & (play_df.fumble_vec == False) - & ( - play_df.text.str.contains( - r"out-of-bounds|out of bounds", regex=True, case=False - ) - ), - ( - (play_df.kickoff_downed == True) - | (play_df.kickoff_fair_catch == True) - ), - (play_df.kickoff_play == True) - & (play_df.text.str.contains(r"returned by", regex=True, case=False)), - (play_df.kickoff_play == True) - & (play_df.text.str.contains(r"return for", regex=True, case=False)), + & (play_df.text.str.contains(r"out-of-bounds|out of bounds", regex=True, case=False)), + ((play_df.kickoff_downed == True) | (play_df.kickoff_fair_catch == True)), + (play_df.kickoff_play == True) & (play_df.text.str.contains(r"returned by", regex=True, case=False)), + (play_df.kickoff_play == True) & (play_df.text.str.contains(r"return for", regex=True, case=False)), (play_df.kickoff_play == True), ], [ @@ -2693,14 +2105,10 @@ def __add_yardage_cols(self, play_df): play_df.text.str.extract(r"((?<= for)[^,]+)", flags=re.IGNORECASE)[0] .str.extract(r"(\d+)")[0] .astype(float), - play_df.text.str.extract( - r"((?<= return for)[^,]+)", flags=re.IGNORECASE - )[0] + play_df.text.str.extract(r"((?<= return for)[^,]+)", flags=re.IGNORECASE)[0] .str.extract(r"(\d+)")[0] .astype(float), - play_df.text.str.extract( - r"((?<= returned for)[^,]+)", flags=re.IGNORECASE - )[0] + play_df.text.str.extract(r"((?<= returned for)[^,]+)", flags=re.IGNORECASE)[0] .str.extract(r"(\d+)")[0] .astype(float), ], @@ -2715,16 +2123,14 @@ def __add_yardage_cols(self, play_df): ], [ 0, - play_df.text.str.extract(r"((?<= punt for)[^,]+)", flags=re.IGNORECASE)[ - 0 - ] + play_df.text.str.extract(r"((?<= punt for)[^,]+)", flags=re.IGNORECASE)[0] .str.extract(r"(\d+)")[0] .astype(float), ], default=play_df.yds_punted, ) - play_df.insert(0,"yds_punt_return", None) + play_df.insert(0, "yds_punt_return", None) play_df["yds_punt_return"] = np.select( [ (play_df.punt == True) & (play_df.punt_tb == 1), @@ -2739,23 +2145,11 @@ def __add_yardage_cols(self, play_df): ) ), (play_df.punt == True) - & ( - (play_df.punt_downed == True) - | (play_df.punt_oob == True) - | (play_df.punt_fair_catch == True) - ), + & ((play_df.punt_downed == True) | (play_df.punt_oob == True) | (play_df.punt_fair_catch == True)), (play_df.punt == True) - & ( - play_df["text"].str.contains( - r"no return", case=False, flags=0, na=False, regex=True - ) - ), + & (play_df["text"].str.contains(r"no return", case=False, flags=0, na=False, regex=True)), (play_df.punt == True) - & ( - play_df["text"].str.contains( - r"returned \d+ yards", case=False, flags=0, na=False, regex=True - ) - ), + & (play_df["text"].str.contains(r"returned \d+ yards", case=False, flags=0, na=False, regex=True)), (play_df.punt == True) & (play_df.punt_blocked == False), (play_df.punt == True) & (play_df.punt_blocked == True), ], @@ -2764,60 +2158,47 @@ def __add_yardage_cols(self, play_df): 0, 0, 0, - play_df.text.str.extract(r"((?<= returned)[^,]+)", flags=re.IGNORECASE)[ - 0 - ] + play_df.text.str.extract(r"((?<= returned)[^,]+)", flags=re.IGNORECASE)[0] .str.extract(r"(\d+)")[0] .astype(float), - play_df.text.str.extract( - r"((?<= returns for)[^,]+)", flags=re.IGNORECASE - )[0] + play_df.text.str.extract(r"((?<= returns for)[^,]+)", flags=re.IGNORECASE)[0] .str.extract(r"(\d+)")[0] .astype(float), - play_df.text.str.extract( - r"((?<= return for)[^,]+)", flags=re.IGNORECASE - )[0] + play_df.text.str.extract(r"((?<= return for)[^,]+)", flags=re.IGNORECASE)[0] .str.extract(r"(\d+)")[0] .astype(float), ], default=None, ) - play_df.insert(0,"yds_fumble_return", None) + play_df.insert(0, "yds_fumble_return", None) play_df["yds_fumble_return"] = np.select( [(play_df.fumble_vec == True) & (play_df.kickoff_play == False)], [ - play_df.text.str.extract( - r"((?<= return for)[^,]+)", flags=re.IGNORECASE - )[0] + play_df.text.str.extract(r"((?<= return for)[^,]+)", flags=re.IGNORECASE)[0] .str.extract(r"(\d+)")[0] .astype(float) ], default=None, ) - play_df.insert(0,"yds_sacked", None) + play_df.insert(0, "yds_sacked", None) play_df["yds_sacked"] = np.select( [(play_df.sack == True)], [ -1 - * play_df.text.str.extract(r"((?<= sacked)[^,]+)", flags=re.IGNORECASE)[ - 0 - ] + * play_df.text.str.extract(r"((?<= sacked)[^,]+)", flags=re.IGNORECASE)[0] .str.extract(r"(\d+)")[0] .astype(float) ], default=None, ) - play_df["yds_penalty"] = np.select( [(play_df.penalty_detail == 1)], [ -1 - * play_df.text.str.extract(r"((?<= sacked)[^,]+)", flags=re.IGNORECASE)[ - 0 - ] + * play_df.text.str.extract(r"((?<= sacked)[^,]+)", flags=re.IGNORECASE)[0] .str.extract(r"(\d+)")[0] .astype(float) ], @@ -2828,12 +2209,8 @@ def __add_yardage_cols(self, play_df): [ play_df.penalty_detail.isin(["Penalty Declined", "Penalty Offset"]), play_df.yds_penalty.notna(), - (play_df.penalty_detail.notna()) - & (play_df.yds_penalty.isna()) - & (play_df.rush == True), - (play_df.penalty_detail.notna()) - & (play_df.yds_penalty.isna()) - & (play_df.int == True), + (play_df.penalty_detail.notna()) & (play_df.yds_penalty.isna()) & (play_df.rush == True), + (play_df.penalty_detail.notna()) & (play_df.yds_penalty.isna()) & (play_df.int == True), (play_df.penalty_detail.notna()) & (play_df.yds_penalty.isna()) & (play_df["pass"] == 1) @@ -2854,8 +2231,7 @@ def __add_yardage_cols(self, play_df): 0, play_df.yds_penalty.astype(float), play_df.statYardage.astype(float) - play_df.yds_rushed.astype(float), - play_df.statYardage.astype(float) - - play_df.yds_int_return.astype(float), + play_df.statYardage.astype(float) - play_df.yds_int_return.astype(float), play_df.statYardage.astype(float) - play_df.yds_receiving.astype(float), play_df.statYardage.astype(float), play_df.statYardage.astype(float) - play_df.yds_sacked.astype(float), @@ -2866,45 +2242,45 @@ def __add_yardage_cols(self, play_df): return play_df def __add_player_cols(self, play_df): - play_df.insert(0,"rush_player", None) - play_df.insert(0,"receiver_player", None) - play_df.insert(0,"pass_player", None) - play_df.insert(0,"sack_players", None) - play_df.insert(0,"sack_player1", None) - play_df.insert(0,"sack_player2", None) - play_df.insert(0,"interception_player", None) - play_df.insert(0,"pass_breakup_player", None) - play_df.insert(0,"fg_kicker_player", None) - play_df.insert(0,"fg_return_player", None) - play_df.insert(0,"fg_block_player", None) - play_df.insert(0,"punter_player", None) - play_df.insert(0,"punt_return_player", None) - play_df.insert(0,"punt_block_player", None) - play_df.insert(0,"punt_block_return_player", None) - play_df.insert(0,"kickoff_player", None) - play_df.insert(0,"kickoff_return_player", None) - play_df.insert(0,"fumble_player", None) - play_df.insert(0,"fumble_forced_player", None) - play_df.insert(0,"fumble_recovered_player", None) - play_df.insert(0,"rush_player_name", None) - play_df.insert(0,"receiver_player_name", None) - play_df.insert(0,"passer_player_name", None) - play_df.insert(0,"sack_player_name", None) - play_df.insert(0,"sack_player_name2", None) - play_df.insert(0,"interception_player_name", None) - play_df.insert(0,"pass_breakup_player_name", None) - play_df.insert(0,"fg_kicker_player_name", None) - play_df.insert(0,"fg_return_player_name", None) - play_df.insert(0,"fg_block_player_name", None) - play_df.insert(0,"punter_player_name", None) - play_df.insert(0,"punt_return_player_name", None) - play_df.insert(0,"punt_block_player_name", None) - play_df.insert(0,"punt_block_return_player_name", None) - play_df.insert(0,"kickoff_player_name", None) - play_df.insert(0,"kickoff_return_player_name", None) - play_df.insert(0,"fumble_player_name", None) - play_df.insert(0,"fumble_forced_player_name", None) - play_df.insert(0,"fumble_recovered_player_name", None) + play_df.insert(0, "rush_player", None) + play_df.insert(0, "receiver_player", None) + play_df.insert(0, "pass_player", None) + play_df.insert(0, "sack_players", None) + play_df.insert(0, "sack_player1", None) + play_df.insert(0, "sack_player2", None) + play_df.insert(0, "interception_player", None) + play_df.insert(0, "pass_breakup_player", None) + play_df.insert(0, "fg_kicker_player", None) + play_df.insert(0, "fg_return_player", None) + play_df.insert(0, "fg_block_player", None) + play_df.insert(0, "punter_player", None) + play_df.insert(0, "punt_return_player", None) + play_df.insert(0, "punt_block_player", None) + play_df.insert(0, "punt_block_return_player", None) + play_df.insert(0, "kickoff_player", None) + play_df.insert(0, "kickoff_return_player", None) + play_df.insert(0, "fumble_player", None) + play_df.insert(0, "fumble_forced_player", None) + play_df.insert(0, "fumble_recovered_player", None) + play_df.insert(0, "rush_player_name", None) + play_df.insert(0, "receiver_player_name", None) + play_df.insert(0, "passer_player_name", None) + play_df.insert(0, "sack_player_name", None) + play_df.insert(0, "sack_player_name2", None) + play_df.insert(0, "interception_player_name", None) + play_df.insert(0, "pass_breakup_player_name", None) + play_df.insert(0, "fg_kicker_player_name", None) + play_df.insert(0, "fg_return_player_name", None) + play_df.insert(0, "fg_block_player_name", None) + play_df.insert(0, "punter_player_name", None) + play_df.insert(0, "punt_return_player_name", None) + play_df.insert(0, "punt_block_player_name", None) + play_df.insert(0, "punt_block_return_player_name", None) + play_df.insert(0, "kickoff_player_name", None) + play_df.insert(0, "kickoff_return_player_name", None) + play_df.insert(0, "fumble_player_name", None) + play_df.insert(0, "fumble_forced_player_name", None) + play_df.insert(0, "fumble_recovered_player_name", None) ## Extract player names # RB names @@ -2915,12 +2291,8 @@ def __add_player_cols(self, play_df): ).bfill(axis=1)[0], None, ) - play_df["rush_player"] = play_df.rush_player.str.replace( - r" run | \d+ Yd Run| rush ", "", regex=True - ) - play_df["rush_player"] = play_df.rush_player.str.replace( - r" \((.+)\)", "", regex=True - ) + play_df["rush_player"] = play_df.rush_player.str.replace(r" run | \d+ Yd Run| rush ", "", regex=True) + play_df["rush_player"] = play_df.rush_player.str.replace(r" \((.+)\)", "", regex=True) # QB names play_df["pass_player"] = np.where( @@ -2939,12 +2311,8 @@ def __add_player_cols(self, play_df): play_df.text.str.extract("pass from(.+)")[0], play_df["pass_player"], ) - play_df["pass_player"] = play_df.pass_player.str.replace( - "pass from", "", regex=True - ) - play_df["pass_player"] = play_df.pass_player.str.replace( - r"\(.+\)", "", regex=True - ) + play_df["pass_player"] = play_df.pass_player.str.replace("pass from", "", regex=True) + play_df["pass_player"] = play_df.pass_player.str.replace(r"\(.+\)", "", regex=True) play_df["pass_player"] = play_df.pass_player.str.replace(r" \,", "", regex=True) play_df["pass_player"] = np.where( @@ -2952,12 +2320,8 @@ def __add_player_cols(self, play_df): play_df.text.str.extract("(.+)pass(.+)? complete to")[0], play_df["pass_player"], ) - play_df["pass_player"] = play_df.pass_player.str.replace( - r" pass complete to(.+)", "", regex=True - ) - play_df["pass_player"] = play_df.pass_player.str.replace( - " pass complete to", "", regex=True - ) + play_df["pass_player"] = play_df.pass_player.str.replace(r" pass complete to(.+)", "", regex=True) + play_df["pass_player"] = play_df.pass_player.str.replace(" pass complete to", "", regex=True) play_df["pass_player"] = np.where( (play_df["type.text"] == "Passing Touchdown") & play_df.pass_player.isna(), @@ -2965,49 +2329,30 @@ def __add_player_cols(self, play_df): play_df["pass_player"], ) - play_df["pass_player"] = play_df.pass_player.str.replace( - r" pass,to(.+)", "", regex=True - ) - play_df["pass_player"] = play_df.pass_player.str.replace( - r" pass,to", "", regex=True - ) - play_df["pass_player"] = play_df.pass_player.str.replace( - r" \((.+)\)", "", regex=True - ) + play_df["pass_player"] = play_df.pass_player.str.replace(r" pass,to(.+)", "", regex=True) + play_df["pass_player"] = play_df.pass_player.str.replace(r" pass,to", "", regex=True) + play_df["pass_player"] = play_df.pass_player.str.replace(r" \((.+)\)", "", regex=True) play_df["pass_player"] = np.where( - (play_df["pass"] == 1) - & ( - (play_df.pass_player.str.strip().str.len == 0) - | play_df.pass_player.isna() - ), + (play_df["pass"] == 1) & ((play_df.pass_player.str.strip().str.len == 0) | play_df.pass_player.isna()), "TEAM", play_df.pass_player, ) play_df["receiver_player"] = np.where( - (play_df["pass"] == 1) - & ~play_df.text.str.contains( - "sacked", case=False, flags=0, na=False, regex=True - ), + (play_df["pass"] == 1) & ~play_df.text.str.contains("sacked", case=False, flags=0, na=False, regex=True), play_df.text.str.extract("to (.+)")[0], None, ) play_df["receiver_player"] = np.where( - play_df.text.str.contains( - "Yd pass", case=False, flags=0, na=False, regex=True - ), - play_df.text.str.extract("(.{0,25} )\\d{0,2} Yd pass", flags=re.IGNORECASE)[ - 0 - ], + play_df.text.str.contains("Yd pass", case=False, flags=0, na=False, regex=True), + play_df.text.str.extract("(.{0,25} )\\d{0,2} Yd pass", flags=re.IGNORECASE)[0], play_df["receiver_player"], ) play_df["receiver_player"] = np.where( play_df.text.str.contains("Yd TD pass", case=False), - play_df.text.str.extract( - "(.{0,25} )\\d{0,2} Yd TD pass", flags=re.IGNORECASE - )[0], + play_df.text.str.extract("(.{0,25} )\\d{0,2} Yd TD pass", flags=re.IGNORECASE)[0], play_df["receiver_player"], ) @@ -3028,110 +2373,53 @@ def __add_player_cols(self, play_df): play_df["receiver_player"], ) - play_df.receiver_player = play_df.receiver_player.str.replace( - "to ", "", case=False, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - "\\,.+", "", case=False, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - "for (.+)", "", case=False, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - r" (\d{1,2})", "", case=False, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - " Yd pass", "", case=False, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - " Yd TD pass", "", case=False, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - "pass complete to", "", case=False, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - "penalty", "", case=False, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - ' "', "", case=False, regex=True - ) + play_df.receiver_player = play_df.receiver_player.str.replace("to ", "", case=False, regex=True) + play_df.receiver_player = play_df.receiver_player.str.replace("\\,.+", "", case=False, regex=True) + play_df.receiver_player = play_df.receiver_player.str.replace("for (.+)", "", case=False, regex=True) + play_df.receiver_player = play_df.receiver_player.str.replace(r" (\d{1,2})", "", case=False, regex=True) + play_df.receiver_player = play_df.receiver_player.str.replace(" Yd pass", "", case=False, regex=True) + play_df.receiver_player = play_df.receiver_player.str.replace(" Yd TD pass", "", case=False, regex=True) + play_df.receiver_player = play_df.receiver_player.str.replace("pass complete to", "", case=False, regex=True) + play_df.receiver_player = play_df.receiver_player.str.replace("penalty", "", case=False, regex=True) + play_df.receiver_player = play_df.receiver_player.str.replace(' "', "", case=False, regex=True) play_df.receiver_player = np.where( ~(play_df.receiver_player.str.contains("III", na=False)), play_df.receiver_player.str.replace("[A-Z]{3,}", "", case=True, regex=True), play_df.receiver_player, ) - play_df.receiver_player = play_df.receiver_player.str.replace( - " &", "", case=True, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - "A&M", "", case=True, regex=False - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - " ST", "", case=True, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - " GA", "", case=True, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - " UL", "", case=True, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - " FL", "", case=True, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - " OH", "", case=True, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - " NC", "", case=True, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - ' "', "", case=True, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - " \\u00c9", "", case=True, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - " fumbled,", "", case=False, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - "the (.+)", "", case=False, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - "pass incomplete to", "", case=False, regex=True - ) + play_df.receiver_player = play_df.receiver_player.str.replace(" &", "", case=True, regex=True) + play_df.receiver_player = play_df.receiver_player.str.replace("A&M", "", case=True, regex=False) + play_df.receiver_player = play_df.receiver_player.str.replace(" ST", "", case=True, regex=True) + play_df.receiver_player = play_df.receiver_player.str.replace(" GA", "", case=True, regex=True) + play_df.receiver_player = play_df.receiver_player.str.replace(" UL", "", case=True, regex=True) + play_df.receiver_player = play_df.receiver_player.str.replace(" FL", "", case=True, regex=True) + play_df.receiver_player = play_df.receiver_player.str.replace(" OH", "", case=True, regex=True) + play_df.receiver_player = play_df.receiver_player.str.replace(" NC", "", case=True, regex=True) + play_df.receiver_player = play_df.receiver_player.str.replace(' "', "", case=True, regex=True) + play_df.receiver_player = play_df.receiver_player.str.replace(" \\u00c9", "", case=True, regex=True) + play_df.receiver_player = play_df.receiver_player.str.replace(" fumbled,", "", case=False, regex=True) + play_df.receiver_player = play_df.receiver_player.str.replace("the (.+)", "", case=False, regex=True) + play_df.receiver_player = play_df.receiver_player.str.replace("pass incomplete to", "", case=False, regex=True) play_df.receiver_player = play_df.receiver_player.str.replace( "(.+)pass incomplete to", "", case=False, regex=True ) play_df.receiver_player = play_df.receiver_player.str.replace( "(.+)pass incomplete", "", case=False, regex=True ) - play_df.receiver_player = play_df.receiver_player.str.replace( - "pass incomplete", "", case=False, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - r" \((.+)\)", "", regex=True - ) + play_df.receiver_player = play_df.receiver_player.str.replace("pass incomplete", "", case=False, regex=True) + play_df.receiver_player = play_df.receiver_player.str.replace(r" \((.+)\)", "", regex=True) play_df["sack_players"] = np.where( - (play_df["sack"] == True) - | (play_df["fumble_vec"] == True) & (play_df["pass"] == True), + (play_df["sack"] == True) | (play_df["fumble_vec"] == True) & (play_df["pass"] == True), play_df.text.str.extract("sacked by(.+)", flags=re.IGNORECASE)[0], play_df.sack_players, ) - play_df["sack_players"] = play_df["sack_players"].str.replace( - "for (.+)", "", case=True, regex=True - ) - play_df["sack_players"] = play_df["sack_players"].str.replace( - "(.+) by ", "", case=True, regex=True - ) - play_df["sack_players"] = play_df["sack_players"].str.replace( - " at the (.+)", "", case=True, regex=True - ) - play_df["sack_player1"] = play_df["sack_players"].str.replace( - "and (.+)", "", case=True, regex=True - ) + play_df["sack_players"] = play_df["sack_players"].str.replace("for (.+)", "", case=True, regex=True) + play_df["sack_players"] = play_df["sack_players"].str.replace("(.+) by ", "", case=True, regex=True) + play_df["sack_players"] = play_df["sack_players"].str.replace(" at the (.+)", "", case=True, regex=True) + play_df["sack_player1"] = play_df["sack_players"].str.replace("and (.+)", "", case=True, regex=True) play_df["sack_player2"] = np.where( play_df["sack_players"].str.contains("and (.+)"), play_df["sack_players"].str.replace("(.+) and", "", case=True, regex=True), @@ -3140,8 +2428,7 @@ def __add_player_cols(self, play_df): play_df["interception_player"] = np.where( (play_df["type.text"] == "Interception Return") - | (play_df["type.text"] == "Interception Return Touchdown") - & play_df["pass"] + | (play_df["type.text"] == "Interception Return Touchdown") & play_df["pass"] == True, play_df.text.str.extract("intercepted (.+)", flags=re.IGNORECASE)[0], play_df.interception_player, @@ -3179,9 +2466,7 @@ def __add_player_cols(self, play_df): play_df["interception_player"] = play_df["interception_player"].str.replace( "at the (.+)", "", case=True, regex=True ) - play_df["interception_player"] = play_df["interception_player"].str.replace( - " by ", "", case=True, regex=True - ) + play_df["interception_player"] = play_df["interception_player"].str.replace(" by ", "", case=True, regex=True) play_df["pass_breakup_player"] = np.where( play_df["pass"] == True, @@ -3215,32 +2500,16 @@ def __add_player_cols(self, play_df): play_df["punter_player"] = np.where( play_df["type.text"].str.contains("Punt", regex=True), - play_df.text.str.extract( - r"(.{0,30}) punt|Punt by (.{0,30})", flags=re.IGNORECASE - ).bfill(axis=1)[0], + play_df.text.str.extract(r"(.{0,30}) punt|Punt by (.{0,30})", flags=re.IGNORECASE).bfill(axis=1)[0], play_df.punter_player, ) - play_df["punter_player"] = play_df["punter_player"].str.replace( - " punt", "", case=False, regex=True - ) - play_df["punter_player"] = play_df["punter_player"].str.replace( - r" for(.+)", "", case=False, regex=True - ) - play_df["punter_player"] = play_df["punter_player"].str.replace( - "Punt by ", "", case=False, regex=True - ) - play_df["punter_player"] = play_df["punter_player"].str.replace( - r"\((.+)\)", "", case=False, regex=True - ) - play_df["punter_player"] = play_df["punter_player"].str.replace( - r" returned \d+", "", case=False, regex=True - ) - play_df["punter_player"] = play_df["punter_player"].str.replace( - " returned", "", case=False, regex=True - ) - play_df["punter_player"] = play_df["punter_player"].str.replace( - " no return", "", case=False, regex=True - ) + play_df["punter_player"] = play_df["punter_player"].str.replace(" punt", "", case=False, regex=True) + play_df["punter_player"] = play_df["punter_player"].str.replace(r" for(.+)", "", case=False, regex=True) + play_df["punter_player"] = play_df["punter_player"].str.replace("Punt by ", "", case=False, regex=True) + play_df["punter_player"] = play_df["punter_player"].str.replace(r"\((.+)\)", "", case=False, regex=True) + play_df["punter_player"] = play_df["punter_player"].str.replace(r" returned \d+", "", case=False, regex=True) + play_df["punter_player"] = play_df["punter_player"].str.replace(" returned", "", case=False, regex=True) + play_df["punter_player"] = play_df["punter_player"].str.replace(" no return", "", case=False, regex=True) play_df["punt_return_player"] = np.where( play_df["type.text"].str.contains("Punt", regex=True), @@ -3250,9 +2519,7 @@ def __add_player_cols(self, play_df): ).bfill(axis=1)[0], play_df.punt_return_player, ) - play_df["punt_return_player"] = play_df["punt_return_player"].str.replace( - ", ", "", case=False, regex=True - ) + play_df["punt_return_player"] = play_df["punt_return_player"].str.replace(", ", "", case=False, regex=True) play_df["punt_return_player"] = play_df["punt_return_player"].str.replace( " returns", "", case=False, regex=True ) @@ -3283,9 +2550,9 @@ def __add_player_cols(self, play_df): play_df["punt_block_player"] = np.where( play_df["type.text"].str.contains("Punt", case=True, regex=True), - play_df.text.str.extract( - "punt blocked by (.{0,25})| blocked by(.+)", flags=re.IGNORECASE - ).bfill(axis=1)[0], + play_df.text.str.extract("punt blocked by (.{0,25})| blocked by(.+)", flags=re.IGNORECASE).bfill(axis=1)[ + 0 + ], play_df.punt_block_player, ) play_df["punt_block_player"] = play_df["punt_block_player"].str.replace( @@ -3297,12 +2564,8 @@ def __add_player_cols(self, play_df): play_df["punt_block_player"] = play_df["punt_block_player"].str.replace( r"blocked(.+)", "", case=True, regex=True ) - play_df["punt_block_player"] = play_df["punt_block_player"].str.replace( - r" for(.+)", "", case=True, regex=True - ) - play_df["punt_block_player"] = play_df["punt_block_player"].str.replace( - r",(.+)", "", case=True, regex=True - ) + play_df["punt_block_player"] = play_df["punt_block_player"].str.replace(r" for(.+)", "", case=True, regex=True) + play_df["punt_block_player"] = play_df["punt_block_player"].str.replace(r",(.+)", "", case=True, regex=True) play_df["punt_block_player"] = play_df["punt_block_player"].str.replace( r"punt blocked by |for a(.+)", "", case=True, regex=True ) @@ -3312,68 +2575,46 @@ def __add_player_cols(self, play_df): play_df.text.str.extract("(.+) yd return of blocked"), play_df.punt_block_player, ) - play_df["punt_block_player"] = play_df["punt_block_player"].str.replace( - "blocked|Blocked", "", regex=True - ) - play_df["punt_block_player"] = play_df["punt_block_player"].str.replace( - r"\\d+", "", regex=True - ) - play_df["punt_block_player"] = play_df["punt_block_player"].str.replace( - "yd return of", "", regex=True - ) + play_df["punt_block_player"] = play_df["punt_block_player"].str.replace("blocked|Blocked", "", regex=True) + play_df["punt_block_player"] = play_df["punt_block_player"].str.replace(r"\\d+", "", regex=True) + play_df["punt_block_player"] = play_df["punt_block_player"].str.replace("yd return of", "", regex=True) play_df["punt_block_return_player"] = np.where( - ( - play_df["type.text"].str.contains( - "Punt", case=False, flags=0, na=False, regex=True - ) - ) + (play_df["type.text"].str.contains("Punt", case=False, flags=0, na=False, regex=True)) & ( - play_df.text.str.contains( - "blocked", case=False, flags=0, na=False, regex=True - ) - & play_df.text.str.contains( - "return", case=False, flags=0, na=False, regex=True - ) + play_df.text.str.contains("blocked", case=False, flags=0, na=False, regex=True) + & play_df.text.str.contains("return", case=False, flags=0, na=False, regex=True) ), play_df.text.str.extract("(.+) return"), play_df.punt_block_return_player, ) - play_df["punt_block_return_player"] = play_df[ - "punt_block_return_player" - ].str.replace("(.+)blocked by {punt_block_player}", "") - play_df["punt_block_return_player"] = play_df[ - "punt_block_return_player" - ].str.replace("blocked by {punt_block_player}", "") - play_df["punt_block_return_player"] = play_df[ - "punt_block_return_player" - ].str.replace("return(.+)", "", regex=True) - play_df["punt_block_return_player"] = play_df[ - "punt_block_return_player" - ].str.replace("return", "", regex=True) - play_df["punt_block_return_player"] = play_df[ - "punt_block_return_player" - ].str.replace("(.+)blocked by", "", regex=True) - play_df["punt_block_return_player"] = play_df[ - "punt_block_return_player" - ].str.replace("for a TD(.+)|for a SAFETY(.+)", "", regex=True) - play_df["punt_block_return_player"] = play_df[ - "punt_block_return_player" - ].str.replace("blocked by", "", regex=True) - play_df["punt_block_return_player"] = play_df[ - "punt_block_return_player" - ].str.replace(", ", "", regex=True) + play_df["punt_block_return_player"] = play_df["punt_block_return_player"].str.replace( + "(.+)blocked by {punt_block_player}", "" + ) + play_df["punt_block_return_player"] = play_df["punt_block_return_player"].str.replace( + "blocked by {punt_block_player}", "" + ) + play_df["punt_block_return_player"] = play_df["punt_block_return_player"].str.replace( + "return(.+)", "", regex=True + ) + play_df["punt_block_return_player"] = play_df["punt_block_return_player"].str.replace("return", "", regex=True) + play_df["punt_block_return_player"] = play_df["punt_block_return_player"].str.replace( + "(.+)blocked by", "", regex=True + ) + play_df["punt_block_return_player"] = play_df["punt_block_return_player"].str.replace( + "for a TD(.+)|for a SAFETY(.+)", "", regex=True + ) + play_df["punt_block_return_player"] = play_df["punt_block_return_player"].str.replace( + "blocked by", "", regex=True + ) + play_df["punt_block_return_player"] = play_df["punt_block_return_player"].str.replace(", ", "", regex=True) play_df["kickoff_player"] = np.where( play_df["type.text"].str.contains("Kickoff"), - play_df.text.str.extract("(.{0,25}) kickoff|(.{0,25}) on-side").bfill( - axis=1 - )[0], + play_df.text.str.extract("(.{0,25}) kickoff|(.{0,25}) on-side").bfill(axis=1)[0], play_df.kickoff_player, ) - play_df["kickoff_player"] = play_df["kickoff_player"].str.replace( - " on-side| kickoff", "", regex=True - ) + play_df["kickoff_player"] = play_df["kickoff_player"].str.replace(" on-side| kickoff", "", regex=True) play_df["kickoff_return_player"] = np.where( play_df["type.text"].str.contains("ickoff"), @@ -3405,121 +2646,63 @@ def __add_player_cols(self, play_df): play_df["fg_kicker_player"] = play_df["fg_kicker_player"].str.replace( " Yd Field Goal|Yd FG |yd FG| yd FG", "", case=False, regex=True ) - play_df["fg_kicker_player"] = play_df["fg_kicker_player"].str.replace( - "(\\d{1,2})", "", case=False, regex=True - ) + play_df["fg_kicker_player"] = play_df["fg_kicker_player"].str.replace("(\\d{1,2})", "", case=False, regex=True) play_df["fg_block_player"] = np.where( play_df["type.text"].str.contains("Field Goal"), play_df.text.str.extract("blocked by (.{0,25})"), play_df.fg_block_player, ) - play_df["fg_block_player"] = play_df["fg_block_player"].str.replace( - ",(.+)", "", case=False, regex=True - ) - play_df["fg_block_player"] = play_df["fg_block_player"].str.replace( - "blocked by ", "", case=False, regex=True - ) - play_df["fg_block_player"] = play_df["fg_block_player"].str.replace( - " (.)+", "", case=False, regex=True - ) - - play_df["fg_return_player"] = np.where( - (play_df["type.text"].str.contains("Field Goal")) - & (play_df["type.text"].str.contains("blocked by|missed")) - & (play_df["type.text"].str.contains("return")), - play_df.text.str.extract(" (.+)"), - play_df.fg_return_player, - ) - - play_df["fg_return_player"] = play_df["fg_return_player"].str.replace( - ",(.+)", "", case=False, regex=True - ) - play_df["fg_return_player"] = play_df["fg_return_player"].str.replace( - "return ", "", case=False, regex=True - ) - play_df["fg_return_player"] = play_df["fg_return_player"].str.replace( - "returned ", "", case=False, regex=True - ) - play_df["fg_return_player"] = play_df["fg_return_player"].str.replace( - " for (.+)", "", case=False, regex=True - ) - play_df["fg_return_player"] = play_df["fg_return_player"].str.replace( - " for (.+)", "", case=False, regex=True - ) - - play_df["fg_return_player"] = np.where( - play_df["type.text"].isin( - ["Missed Field Goal Return", "Missed Field Goal Return Touchdown"] - ), - play_df.text.str.extract("(.+)return"), - play_df.fg_return_player, - ) - play_df["fg_return_player"] = play_df["fg_return_player"].str.replace( - " return", "", case=False, regex=True - ) - play_df["fg_return_player"] = play_df["fg_return_player"].str.replace( - "(.+),", "", case=False, regex=True - ) - - play_df["fumble_player"] = np.where( - play_df["text"].str.contains( - "fumble", case=False, flags=0, na=False, regex=True - ), - play_df["text"].str.extract("(.{0,25} )fumble"), - play_df.fumble_player, - ) - play_df["fumble_player"] = play_df["fumble_player"].str.replace( - " fumble(.+)", "", case=False, regex=True - ) - play_df["fumble_player"] = play_df["fumble_player"].str.replace( - "fumble", "", case=False, regex=True - ) - play_df["fumble_player"] = play_df["fumble_player"].str.replace( - " yds", "", case=False, regex=True - ) - play_df["fumble_player"] = play_df["fumble_player"].str.replace( - " yd", "", case=False, regex=True - ) - play_df["fumble_player"] = play_df["fumble_player"].str.replace( - "yardline", "", case=False, regex=True - ) - play_df["fumble_player"] = play_df["fumble_player"].str.replace( - " yards| yard|for a TD|or a safety", "", case=False, regex=True - ) - play_df["fumble_player"] = play_df["fumble_player"].str.replace( - " for ", "", case=False, regex=True - ) - play_df["fumble_player"] = play_df["fumble_player"].str.replace( - " a safety", "", case=False, regex=True - ) - play_df["fumble_player"] = play_df["fumble_player"].str.replace( - "r no gain", "", case=False, regex=True - ) - play_df["fumble_player"] = play_df["fumble_player"].str.replace( - "(.+)(\\d{1,2})", "", case=False, regex=True - ) - play_df["fumble_player"] = play_df["fumble_player"].str.replace( - "(\\d{1,2})", "", case=False, regex=True + play_df["fg_block_player"] = play_df["fg_block_player"].str.replace(",(.+)", "", case=False, regex=True) + play_df["fg_block_player"] = play_df["fg_block_player"].str.replace("blocked by ", "", case=False, regex=True) + play_df["fg_block_player"] = play_df["fg_block_player"].str.replace(" (.)+", "", case=False, regex=True) + + play_df["fg_return_player"] = np.where( + (play_df["type.text"].str.contains("Field Goal")) + & (play_df["type.text"].str.contains("blocked by|missed")) + & (play_df["type.text"].str.contains("return")), + play_df.text.str.extract(" (.+)"), + play_df.fg_return_player, ) - play_df["fumble_player"] = play_df["fumble_player"].str.replace( - ", ", "", case=False, regex=True + + play_df["fg_return_player"] = play_df["fg_return_player"].str.replace(",(.+)", "", case=False, regex=True) + play_df["fg_return_player"] = play_df["fg_return_player"].str.replace("return ", "", case=False, regex=True) + play_df["fg_return_player"] = play_df["fg_return_player"].str.replace("returned ", "", case=False, regex=True) + play_df["fg_return_player"] = play_df["fg_return_player"].str.replace(" for (.+)", "", case=False, regex=True) + play_df["fg_return_player"] = play_df["fg_return_player"].str.replace(" for (.+)", "", case=False, regex=True) + + play_df["fg_return_player"] = np.where( + play_df["type.text"].isin(["Missed Field Goal Return", "Missed Field Goal Return Touchdown"]), + play_df.text.str.extract("(.+)return"), + play_df.fg_return_player, ) + play_df["fg_return_player"] = play_df["fg_return_player"].str.replace(" return", "", case=False, regex=True) + play_df["fg_return_player"] = play_df["fg_return_player"].str.replace("(.+),", "", case=False, regex=True) + play_df["fumble_player"] = np.where( - play_df["type.text"] == "Penalty", None, play_df.fumble_player + play_df["text"].str.contains("fumble", case=False, flags=0, na=False, regex=True), + play_df["text"].str.extract("(.{0,25} )fumble"), + play_df.fumble_player, + ) + play_df["fumble_player"] = play_df["fumble_player"].str.replace(" fumble(.+)", "", case=False, regex=True) + play_df["fumble_player"] = play_df["fumble_player"].str.replace("fumble", "", case=False, regex=True) + play_df["fumble_player"] = play_df["fumble_player"].str.replace(" yds", "", case=False, regex=True) + play_df["fumble_player"] = play_df["fumble_player"].str.replace(" yd", "", case=False, regex=True) + play_df["fumble_player"] = play_df["fumble_player"].str.replace("yardline", "", case=False, regex=True) + play_df["fumble_player"] = play_df["fumble_player"].str.replace( + " yards| yard|for a TD|or a safety", "", case=False, regex=True ) + play_df["fumble_player"] = play_df["fumble_player"].str.replace(" for ", "", case=False, regex=True) + play_df["fumble_player"] = play_df["fumble_player"].str.replace(" a safety", "", case=False, regex=True) + play_df["fumble_player"] = play_df["fumble_player"].str.replace("r no gain", "", case=False, regex=True) + play_df["fumble_player"] = play_df["fumble_player"].str.replace("(.+)(\\d{1,2})", "", case=False, regex=True) + play_df["fumble_player"] = play_df["fumble_player"].str.replace("(\\d{1,2})", "", case=False, regex=True) + play_df["fumble_player"] = play_df["fumble_player"].str.replace(", ", "", case=False, regex=True) + play_df["fumble_player"] = np.where(play_df["type.text"] == "Penalty", None, play_df.fumble_player) play_df["fumble_forced_player"] = np.where( - ( - play_df.text.str.contains( - "fumble", case=False, flags=0, na=False, regex=True - ) - ) - & ( - play_df.text.str.contains( - "forced by", case=False, flags=0, na=False, regex=True - ) - ), + (play_df.text.str.contains("fumble", case=False, flags=0, na=False, regex=True)) + & (play_df.text.str.contains("forced by", case=False, flags=0, na=False, regex=True)), play_df.text.str.extract("forced by(.{0,25})"), play_df.fumble_forced_player, ) @@ -3542,79 +2725,69 @@ def __add_player_cols(self, play_df): play_df["fumble_forced_player"] = play_df["fumble_forced_player"].str.replace( ", r", "", case=False, regex=True ) - play_df["fumble_forced_player"] = play_df["fumble_forced_player"].str.replace( - ", ", "", case=False, regex=True - ) + play_df["fumble_forced_player"] = play_df["fumble_forced_player"].str.replace(", ", "", case=False, regex=True) play_df["fumble_forced_player"] = np.where( play_df["type.text"] == "Penalty", None, play_df.fumble_forced_player ) play_df["fumble_recovered_player"] = np.where( - ( - play_df.text.str.contains( - "fumble", case=False, flags=0, na=False, regex=True - ) - ) - & ( - play_df.text.str.contains( - "recovered by", case=False, flags=0, na=False, regex=True - ) - ), + (play_df.text.str.contains("fumble", case=False, flags=0, na=False, regex=True)) + & (play_df.text.str.contains("recovered by", case=False, flags=0, na=False, regex=True)), play_df.text.str.extract("recovered by(.{0,30})"), play_df.fumble_recovered_player, ) - play_df["fumble_recovered_player"] = play_df[ - "fumble_recovered_player" - ].str.replace("for a 1ST down", "", case=False, regex=True) - play_df["fumble_recovered_player"] = play_df[ - "fumble_recovered_player" - ].str.replace("for a 1st down", "", case=False, regex=True) - play_df["fumble_recovered_player"] = play_df[ - "fumble_recovered_player" - ].str.replace("(.+)recovered", "", case=False, regex=True) - play_df["fumble_recovered_player"] = play_df[ - "fumble_recovered_player" - ].str.replace("(.+) by", "", case=False, regex=True) - play_df["fumble_recovered_player"] = play_df[ - "fumble_recovered_player" - ].str.replace(", recove(.+)", "", case=False, regex=True) - play_df["fumble_recovered_player"] = play_df[ - "fumble_recovered_player" - ].str.replace(", re(.+)", "", case=False, regex=True) - play_df["fumble_recovered_player"] = play_df[ - "fumble_recovered_player" - ].str.replace("a 1st down", "", case=False, regex=True) - play_df["fumble_recovered_player"] = play_df[ - "fumble_recovered_player" - ].str.replace(" a 1st down", "", case=False, regex=True) - play_df["fumble_recovered_player"] = play_df[ - "fumble_recovered_player" - ].str.replace(", for(.+)", "", case=False, regex=True) - play_df["fumble_recovered_player"] = play_df[ - "fumble_recovered_player" - ].str.replace(" for a", "", case=False, regex=True) - play_df["fumble_recovered_player"] = play_df[ - "fumble_recovered_player" - ].str.replace(" fo", "", case=False, regex=True) - play_df["fumble_recovered_player"] = play_df[ - "fumble_recovered_player" - ].str.replace(" , r", "", case=False, regex=True) - play_df["fumble_recovered_player"] = play_df[ - "fumble_recovered_player" - ].str.replace(", r", "", case=False, regex=True) - play_df["fumble_recovered_player"] = play_df[ - "fumble_recovered_player" - ].str.replace(" (.+)", "", case=False, regex=True) - play_df["fumble_recovered_player"] = play_df[ - "fumble_recovered_player" - ].str.replace(" ,", "", case=False, regex=True) - play_df["fumble_recovered_player"] = play_df[ - "fumble_recovered_player" - ].str.replace("penalty(.+)", "", case=False, regex=True) - play_df["fumble_recovered_player"] = play_df[ - "fumble_recovered_player" - ].str.replace("for a 1ST down", "", case=False, regex=True) + play_df["fumble_recovered_player"] = play_df["fumble_recovered_player"].str.replace( + "for a 1ST down", "", case=False, regex=True + ) + play_df["fumble_recovered_player"] = play_df["fumble_recovered_player"].str.replace( + "for a 1st down", "", case=False, regex=True + ) + play_df["fumble_recovered_player"] = play_df["fumble_recovered_player"].str.replace( + "(.+)recovered", "", case=False, regex=True + ) + play_df["fumble_recovered_player"] = play_df["fumble_recovered_player"].str.replace( + "(.+) by", "", case=False, regex=True + ) + play_df["fumble_recovered_player"] = play_df["fumble_recovered_player"].str.replace( + ", recove(.+)", "", case=False, regex=True + ) + play_df["fumble_recovered_player"] = play_df["fumble_recovered_player"].str.replace( + ", re(.+)", "", case=False, regex=True + ) + play_df["fumble_recovered_player"] = play_df["fumble_recovered_player"].str.replace( + "a 1st down", "", case=False, regex=True + ) + play_df["fumble_recovered_player"] = play_df["fumble_recovered_player"].str.replace( + " a 1st down", "", case=False, regex=True + ) + play_df["fumble_recovered_player"] = play_df["fumble_recovered_player"].str.replace( + ", for(.+)", "", case=False, regex=True + ) + play_df["fumble_recovered_player"] = play_df["fumble_recovered_player"].str.replace( + " for a", "", case=False, regex=True + ) + play_df["fumble_recovered_player"] = play_df["fumble_recovered_player"].str.replace( + " fo", "", case=False, regex=True + ) + play_df["fumble_recovered_player"] = play_df["fumble_recovered_player"].str.replace( + " , r", "", case=False, regex=True + ) + play_df["fumble_recovered_player"] = play_df["fumble_recovered_player"].str.replace( + ", r", "", case=False, regex=True + ) + play_df["fumble_recovered_player"] = play_df["fumble_recovered_player"].str.replace( + " (.+)", "", case=False, regex=True + ) + play_df["fumble_recovered_player"] = play_df["fumble_recovered_player"].str.replace( + " ,", "", case=False, regex=True + ) + play_df["fumble_recovered_player"] = play_df["fumble_recovered_player"].str.replace( + "penalty(.+)", "", case=False, regex=True + ) + play_df["fumble_recovered_player"] = play_df["fumble_recovered_player"].str.replace( + "for a 1ST down", "", case=False, regex=True + ) play_df["fumble_recovered_player"] = np.where( play_df["type.text"] == "Penalty", None, play_df.fumble_recovered_player ) @@ -3631,22 +2804,14 @@ def __add_player_cols(self, play_df): play_df["fg_block_player_name"] = play_df["fg_block_player"].str.strip() play_df["fg_return_player_name"] = play_df["fg_return_player"].str.strip() play_df["kickoff_player_name"] = play_df["kickoff_player"].str.strip() - play_df["kickoff_return_player_name"] = play_df[ - "kickoff_return_player" - ].str.strip() + play_df["kickoff_return_player_name"] = play_df["kickoff_return_player"].str.strip() play_df["punter_player_name"] = play_df["punter_player"].str.strip() play_df["punt_block_player_name"] = play_df["punt_block_player"].str.strip() play_df["punt_return_player_name"] = play_df["punt_return_player"].str.strip() - play_df["punt_block_return_player_name"] = play_df[ - "punt_block_return_player" - ].str.strip() + play_df["punt_block_return_player_name"] = play_df["punt_block_return_player"].str.strip() play_df["fumble_player_name"] = play_df["fumble_player"].str.strip() - play_df["fumble_forced_player_name"] = play_df[ - "fumble_forced_player" - ].str.strip() - play_df["fumble_recovered_player_name"] = play_df[ - "fumble_recovered_player" - ].str.strip() + play_df["fumble_forced_player_name"] = play_df["fumble_forced_player"].str.strip() + play_df["fumble_recovered_player_name"] = play_df["fumble_recovered_player"].str.strip() play_df.drop( [ @@ -3681,8 +2846,7 @@ def __after_cols(self, play_df): (play_df["type.text"] == "Timeout"), # 8 cases with three T/F penalty flags # 4 cases in 1 - (play_df["type.text"].isin(penalty)) - & (play_df["penalty_1st_conv"] == True), + (play_df["type.text"].isin(penalty)) & (play_df["penalty_1st_conv"] == True), # offsetting penalties, no penalties declined, no 1st down by penalty (1 case) (play_df["type.text"].isin(penalty)) & (play_df["penalty_1st_conv"] == False) @@ -3744,8 +2908,7 @@ def __after_cols(self, play_df): (play_df["type.text"] == "Timeout"), # 8 cases with three T/F penalty flags # 4 cases in 1 - (play_df["type.text"].isin(penalty)) - & (play_df["penalty_1st_conv"] == True), + (play_df["type.text"].isin(penalty)) & (play_df["penalty_1st_conv"] == True), # offsetting penalties, no penalties declined, no 1st down by penalty (1 case) (play_df["type.text"].isin(penalty)) & (play_df["penalty_1st_conv"] == False) @@ -3804,29 +2967,16 @@ def __after_cols(self, play_df): ) play_df["middle_8"] = np.where( - (play_df["start.adj_TimeSecsRem"] >= 1560) - & (play_df["start.adj_TimeSecsRem"] <= 2040), + (play_df["start.adj_TimeSecsRem"] >= 1560) & (play_df["start.adj_TimeSecsRem"] <= 2040), True, False, ) - play_df["rz_play"] = np.where( - play_df["start.yardsToEndzone"] <= 20, True, False - ) - play_df["scoring_opp"] = np.where( - play_df["start.yardsToEndzone"] <= 40, True, False - ) - play_df["stuffed_run"] = np.where( - (play_df.rush == True) & (play_df.yds_rushed <= 0), True, False - ) - play_df["stopped_run"] = np.where( - (play_df.rush == True) & (play_df.yds_rushed <= 2), True, False - ) - play_df["opportunity_run"] = np.where( - (play_df.rush == True) & (play_df.yds_rushed >= 4), True, False - ) - play_df["highlight_run"] = np.where( - (play_df.rush == True) & (play_df.yds_rushed >= 8), True, False - ) + play_df["rz_play"] = np.where(play_df["start.yardsToEndzone"] <= 20, True, False) + play_df["scoring_opp"] = np.where(play_df["start.yardsToEndzone"] <= 40, True, False) + play_df["stuffed_run"] = np.where((play_df.rush == True) & (play_df.yds_rushed <= 0), True, False) + play_df["stopped_run"] = np.where((play_df.rush == True) & (play_df.yds_rushed <= 2), True, False) + play_df["opportunity_run"] = np.where((play_df.rush == True) & (play_df.yds_rushed >= 4), True, False) + play_df["highlight_run"] = np.where((play_df.rush == True) & (play_df.yds_rushed >= 8), True, False) play_df["adj_rush_yardage"] = np.select( [ @@ -3839,12 +2989,8 @@ def __after_cols(self, play_df): play_df["line_yards"] = np.select( [ (play_df.rush == 1) & (play_df.yds_rushed < 0), - (play_df.rush == 1) - & (play_df.yds_rushed >= 0) - & (play_df.yds_rushed <= 4), - (play_df.rush == 1) - & (play_df.yds_rushed >= 5) - & (play_df.yds_rushed <= 10), + (play_df.rush == 1) & (play_df.yds_rushed >= 0) & (play_df.yds_rushed <= 4), + (play_df.rush == 1) & (play_df.yds_rushed >= 5) & (play_df.yds_rushed <= 10), (play_df.rush == 1) & (play_df.yds_rushed >= 11), ], [ @@ -3868,9 +3014,7 @@ def __after_cols(self, play_df): default=None, ) - play_df["highlight_yards"] = ( - play_df["second_level_yards"] + play_df["open_field_yards"] - ) + play_df["highlight_yards"] = play_df["second_level_yards"] + play_df["open_field_yards"] play_df["opp_highlight_yards"] = np.select( [ @@ -3888,9 +3032,7 @@ def __after_cols(self, play_df): True, False, ) - play_df["short_rush_attempt"] = np.where( - (play_df["start.distance"] < 2) & (play_df.rush == True), True, False - ) + play_df["short_rush_attempt"] = np.where((play_df["start.distance"] < 2) & (play_df.rush == True), True, False) play_df["power_rush_success"] = np.where( (play_df["start.distance"] < 2) & (play_df["start.down"].isin([3, 4])) @@ -3900,15 +3042,12 @@ def __after_cols(self, play_df): False, ) play_df["power_rush_attempt"] = np.where( - (play_df["start.distance"] < 2) - & (play_df["start.down"].isin([3, 4])) - & (play_df.rush == True), + (play_df["start.distance"] < 2) & (play_df["start.down"].isin([3, 4])) & (play_df.rush == True), True, False, ) play_df["early_down"] = np.where( - ((play_df.down_1 == True) | (play_df.down_2 == True)) - & (play_df.scrimmage_play == True), + ((play_df.down_1 == True) | (play_df.down_2 == True)) & (play_df.scrimmage_play == True), True, False, ) @@ -3917,65 +3056,39 @@ def __after_cols(self, play_df): True, False, ) - play_df["early_down_pass"] = np.where( - (play_df["pass"] == 1) & (play_df.early_down == True), True, False - ) - play_df["early_down_rush"] = np.where( - (play_df["rush"] == 1) & (play_df.early_down == True), True, False - ) - play_df["late_down_pass"] = np.where( - (play_df["pass"] == 1) & (play_df.late_down == True), True, False - ) - play_df["late_down_rush"] = np.where( - (play_df["rush"] == 1) & (play_df.late_down == True), True, False - ) + play_df["early_down_pass"] = np.where((play_df["pass"] == 1) & (play_df.early_down == True), True, False) + play_df["early_down_rush"] = np.where((play_df["rush"] == 1) & (play_df.early_down == True), True, False) + play_df["late_down_pass"] = np.where((play_df["pass"] == 1) & (play_df.late_down == True), True, False) + play_df["late_down_rush"] = np.where((play_df["rush"] == 1) & (play_df.late_down == True), True, False) play_df["standard_down"] = np.select( [ (play_df.scrimmage_play == True) & (play_df.down_1 == True), - (play_df.scrimmage_play == True) - & (play_df.down_2 == True) - & (play_df["start.distance"] < 8), - (play_df.scrimmage_play == True) - & (play_df.down_3 == True) - & (play_df["start.distance"] < 5), - (play_df.scrimmage_play == True) - & (play_df.down_4 == True) - & (play_df["start.distance"] < 5), + (play_df.scrimmage_play == True) & (play_df.down_2 == True) & (play_df["start.distance"] < 8), + (play_df.scrimmage_play == True) & (play_df.down_3 == True) & (play_df["start.distance"] < 5), + (play_df.scrimmage_play == True) & (play_df.down_4 == True) & (play_df["start.distance"] < 5), ], [True, True, True, True], default=False, ) play_df["passing_down"] = np.select( [ - (play_df.scrimmage_play == True) - & (play_df.down_2 == True) - & (play_df["start.distance"] >= 8), - (play_df.scrimmage_play == True) - & (play_df.down_3 == True) - & (play_df["start.distance"] >= 5), - (play_df.scrimmage_play == True) - & (play_df.down_4 == True) - & (play_df["start.distance"] >= 5), + (play_df.scrimmage_play == True) & (play_df.down_2 == True) & (play_df["start.distance"] >= 8), + (play_df.scrimmage_play == True) & (play_df.down_3 == True) & (play_df["start.distance"] >= 5), + (play_df.scrimmage_play == True) & (play_df.down_4 == True) & (play_df["start.distance"] >= 5), ], [True, True, True], default=False, ) play_df["TFL"] = np.select( [ - (play_df["type.text"] != "Penalty") - & (play_df.sp == False) - & (play_df.statYardage < 0), + (play_df["type.text"] != "Penalty") & (play_df.sp == False) & (play_df.statYardage < 0), (play_df["sack_vec"] == True), ], [True, True], default=False, ) - play_df["TFL_pass"] = np.where( - (play_df["TFL"] == True) & (play_df["pass"] == True), True, False - ) - play_df["TFL_rush"] = np.where( - (play_df["TFL"] == True) & (play_df["rush"] == True), True, False - ) + play_df["TFL_pass"] = np.where((play_df["TFL"] == True) & (play_df["pass"] == True), True, False) + play_df["TFL_rush"] = np.where((play_df["TFL"] == True) & (play_df["rush"] == True), True, False) play_df["havoc"] = np.select( [ (play_df["pass_breakup"] == True), @@ -3994,12 +3107,8 @@ def __add_spread_time(self, play_df): play_df["homeTeamSpread"], -1 * play_df["homeTeamSpread"], ) - play_df["start.elapsed_share"] = ( - (3600 - play_df["start.adj_TimeSecsRem"]) / 3600 - ).clip(0, 3600) - play_df["start.spread_time"] = play_df["start.pos_team_spread"] * np.exp( - -4 * play_df["start.elapsed_share"] - ) + play_df["start.elapsed_share"] = ((3600 - play_df["start.adj_TimeSecsRem"]) / 3600).clip(0, 3600) + play_df["start.spread_time"] = play_df["start.pos_team_spread"] * np.exp(-4 * play_df["start.elapsed_share"]) play_df["end.pos_team_spread"] = np.where( (play_df["end.pos_team.id"] == play_df["homeTeamId"]), play_df["homeTeamSpread"], @@ -4010,12 +3119,8 @@ def __add_spread_time(self, play_df): play_df["homeTeamSpread"], -1 * play_df["homeTeamSpread"], ) - play_df["end.elapsed_share"] = ( - (3600 - play_df["end.adj_TimeSecsRem"]) / 3600 - ).clip(0, 3600) - play_df["end.spread_time"] = play_df["end.pos_team_spread"] * np.exp( - -4 * play_df["end.elapsed_share"] - ) + play_df["end.elapsed_share"] = ((3600 - play_df["end.adj_TimeSecsRem"]) / 3600).clip(0, 3600) + play_df["end.spread_time"] = play_df["end.pos_team_spread"] * np.exp(-4 * play_df["end.elapsed_share"]) return play_df def __calculate_ep_exp_val(self, matrix): @@ -4026,7 +3131,8 @@ def __calculate_ep_exp_val(self, matrix): + matrix[:, 3] * ep_class_to_score_mapping[3] + matrix[:, 4] * ep_class_to_score_mapping[4] + matrix[:, 5] * ep_class_to_score_mapping[5] - + matrix[:, 6] * ep_class_to_score_mapping[6]) + + matrix[:, 6] * ep_class_to_score_mapping[6] + ) def __process_epa(self, play_df): play_df.loc[play_df["type.text"].isin(kickoff_vec), "down"] = 1 @@ -4068,18 +3174,10 @@ def __process_epa(self, play_df): (play_df["end.TimeSecsRem"] <= 0) & (play_df.period < 5), "end.yardsToEndzone", ] = 99 - play_df.loc[ - (play_df["end.TimeSecsRem"] <= 0) & (play_df.period < 5), "down_1_end" - ] = True - play_df.loc[ - (play_df["end.TimeSecsRem"] <= 0) & (play_df.period < 5), "down_2_end" - ] = False - play_df.loc[ - (play_df["end.TimeSecsRem"] <= 0) & (play_df.period < 5), "down_3_end" - ] = False - play_df.loc[ - (play_df["end.TimeSecsRem"] <= 0) & (play_df.period < 5), "down_4_end" - ] = False + play_df.loc[(play_df["end.TimeSecsRem"] <= 0) & (play_df.period < 5), "down_1_end"] = True + play_df.loc[(play_df["end.TimeSecsRem"] <= 0) & (play_df.period < 5), "down_2_end"] = False + play_df.loc[(play_df["end.TimeSecsRem"] <= 0) & (play_df.period < 5), "down_3_end"] = False + play_df.loc[(play_df["end.TimeSecsRem"] <= 0) & (play_df.period < 5), "down_4_end"] = False play_df.loc[play_df["end.yardsToEndzone"] >= 100, "end.yardsToEndzone"] = 99 play_df.loc[play_df["end.yardsToEndzone"] <= 0, "end.yardsToEndzone"] = 99 @@ -4125,118 +3223,66 @@ def __process_epa(self, play_df): ( play_df["type.text"] .str.lower() - .str.contains( - "end of game", case=False, flags=0, na=False, regex=True - ) + .str.contains("end of game", case=False, flags=0, na=False, regex=True) ) | ( play_df["type.text"] .str.lower() - .str.contains( - "end of game", case=False, flags=0, na=False, regex=True - ) + .str.contains("end of game", case=False, flags=0, na=False, regex=True) ) | ( play_df["type.text"] .str.lower() - .str.contains( - "end of half", case=False, flags=0, na=False, regex=True - ) + .str.contains("end of half", case=False, flags=0, na=False, regex=True) ) | ( play_df["type.text"] .str.lower() - .str.contains( - "end of half", case=False, flags=0, na=False, regex=True - ) + .str.contains("end of half", case=False, flags=0, na=False, regex=True) ), # Def 2pt conversion is its own play (play_df["type.text"].isin(["Defensive 2pt Conversion"])), # Safeties ( (play_df["type.text"].isin(defense_score_vec)) - & ( - play_df["text"] - .str.lower() - .str.contains("safety", case=False, regex=True) - ) + & (play_df["text"].str.lower().str.contains("safety", case=False, regex=True)) ), # Defense TD + Successful Two-Point Conversion ( (play_df["type.text"].isin(defense_score_vec)) - & ( - play_df["text"] - .str.lower() - .str.contains("conversion", case=False, regex=False) - ) - & ( - ~play_df["text"] - .str.lower() - .str.contains(r"failed\s?\)", case=False, regex=True) - ) + & (play_df["text"].str.lower().str.contains("conversion", case=False, regex=False)) + & (~play_df["text"].str.lower().str.contains(r"failed\s?\)", case=False, regex=True)) ), # Defense TD + Failed Two-Point Conversion ( (play_df["type.text"].isin(defense_score_vec)) - & ( - play_df["text"] - .str.lower() - .str.contains("conversion", case=False, regex=False) - ) - & ( - play_df["text"] - .str.lower() - .str.contains(r"failed\s?\)", case=False, regex=True) - ) + & (play_df["text"].str.lower().str.contains("conversion", case=False, regex=False)) + & (play_df["text"].str.lower().str.contains(r"failed\s?\)", case=False, regex=True)) ), # Defense TD + Kick/PAT Missed ( (play_df["type.text"].isin(defense_score_vec)) & (play_df["text"].str.contains("PAT", case=True, regex=False)) - & ( - play_df["text"] - .str.lower() - .str.contains(r"missed\s?\)", case=False, regex=True) - ) + & (play_df["text"].str.lower().str.contains(r"missed\s?\)", case=False, regex=True)) ), # Defense TD + Kick/PAT Good ( (play_df["type.text"].isin(defense_score_vec)) - & ( - play_df["text"] - .str.lower() - .str.contains(kick, case=False, regex=False) - ) + & (play_df["text"].str.lower().str.contains(kick, case=False, regex=False)) ), # Defense TD (play_df["type.text"].isin(defense_score_vec)), # Offense TD + Failed Two-Point Conversion ( (play_df["type.text"].isin(offense_score_vec)) - & ( - play_df["text"] - .str.lower() - .str.contains("conversion", case=False, regex=False) - ) - & ( - play_df["text"] - .str.lower() - .str.contains(r"failed\s?\)", case=False, regex=True) - ) + & (play_df["text"].str.lower().str.contains("conversion", case=False, regex=False)) + & (play_df["text"].str.lower().str.contains(r"failed\s?\)", case=False, regex=True)) ), # Offense TD + Successful Two-Point Conversion ( (play_df["type.text"].isin(offense_score_vec)) - & ( - play_df["text"] - .str.lower() - .str.contains("conversion", case=False, regex=False) - ) - & ( - ~play_df["text"] - .str.lower() - .str.contains(r"failed\s?\)", case=False, regex=True) - ) + & (play_df["text"].str.lower().str.contains("conversion", case=False, regex=False)) + & (~play_df["text"].str.lower().str.contains(r"failed\s?\)", case=False, regex=True)) ), # Offense Made FG ( @@ -4244,9 +3290,7 @@ def __process_epa(self, play_df): & ( play_df["type.text"] .str.lower() - .str.contains( - "field goal", case=False, flags=0, na=False, regex=True - ) + .str.contains("field goal", case=False, flags=0, na=False, regex=True) ) & ( play_df["type.text"] @@ -4261,28 +3305,14 @@ def __process_epa(self, play_df): # Offense TD + Kick/PAT Missed ( (play_df["type.text"].isin(offense_score_vec)) - & ( - ~play_df["text"] - .str.lower() - .str.contains("conversion", case=False, regex=False) - ) + & (~play_df["text"].str.lower().str.contains("conversion", case=False, regex=False)) & ((play_df["text"].str.contains("PAT", case=True, regex=False))) - & ( - ( - play_df["text"] - .str.lower() - .str.contains(r"missed\s?\)", case=False, regex=True) - ) - ) + & ((play_df["text"].str.lower().str.contains(r"missed\s?\)", case=False, regex=True))) ), # Offense TD + Kick PAT Good ( (play_df["type.text"].isin(offense_score_vec)) - & ( - play_df["text"] - .str.lower() - .str.contains(kick, case=False, regex=False) - ) + & (play_df["text"].str.lower().str.contains(kick, case=False, regex=False)) ), # Offense TD (play_df["type.text"].isin(offense_score_vec)), @@ -4298,34 +3328,17 @@ def __process_epa(self, play_df): (play_df["type.text"] == "Two-Point Conversion Missed"), # Two-Point No Good (pre-2014 data) ( - ( - (play_df["type.text"] == "Two Point Pass") - | (play_df["type.text"] == "Two Point Rush") - ) - & ( - play_df["text"] - .str.lower() - .str.contains("no good", case=False, regex=False) - ) + ((play_df["type.text"] == "Two Point Pass") | (play_df["type.text"] == "Two Point Rush")) + & (play_df["text"].str.lower().str.contains("no good", case=False, regex=False)) ), # Two-Point Good (pre-2014 data) ( - ( - (play_df["type.text"] == "Two Point Pass") - | (play_df["type.text"] == "Two Point Rush") - ) - & ( - ~play_df["text"] - .str.lower() - .str.contains("no good", case=False, regex=False) - ) + ((play_df["type.text"] == "Two Point Pass") | (play_df["type.text"] == "Two Point Rush")) + & (~play_df["text"].str.lower().str.contains("no good", case=False, regex=False)) ), # Flips for Turnovers that aren't kickoffs ( - ( - (play_df["type.text"].isin(end_change_vec)) - | (play_df.downs_turnover == True) - ) + ((play_df["type.text"].isin(end_change_vec)) | (play_df.downs_turnover == True)) & (play_df.kickoff_play == False) ), # Flips for Turnovers that are on kickoffs @@ -4371,8 +3384,7 @@ def __process_epa(self, play_df): play_df["EP_start"] - play_df["lag_EP_end"], ) play_df["EP_start"] = np.where( - (play_df["type.text"].isin(["Timeout", "End Period"])) - & (play_df["lag_change_of_pos_team"] == False), + (play_df["type.text"].isin(["Timeout", "End Period"])) & (play_df["lag_change_of_pos_team"] == False), play_df["lag_EP_end"], play_df["EP_start"], ) @@ -4381,15 +3393,12 @@ def __process_epa(self, play_df): play_df["EP_start_touchback"], play_df["EP_start"], ) - play_df["EP_end"] = np.where( - (play_df["type.text"] == "Timeout"), play_df["EP_start"], play_df["EP_end"] - ) + play_df["EP_end"] = np.where((play_df["type.text"] == "Timeout"), play_df["EP_start"], play_df["EP_end"]) play_df["EPA"] = np.select( [ (play_df["type.text"] == "Timeout"), (play_df["scoring_play"] == False) & (play_df["end_of_half"] == True), - (play_df["type.text"].isin(kickoff_vec)) - & (play_df["penalty_in_text"] == True), + (play_df["type.text"].isin(kickoff_vec)) & (play_df["penalty_in_text"] == True), (play_df["penalty_in_text"] == True) & (play_df["type.text"] != "Penalty") & (~play_df["type.text"].isin(kickoff_vec)), @@ -4404,9 +3413,7 @@ def __process_epa(self, play_df): ) play_df["def_EPA"] = -1 * play_df["EPA"] # ----- EPA Summary flags ------ - play_df["EPA_scrimmage"] = np.select( - [(play_df.scrimmage_play == True)], [play_df.EPA], default=None - ) + play_df["EPA_scrimmage"] = np.select([(play_df.scrimmage_play == True)], [play_df.EPA], default=None) play_df["EPA_rush"] = np.select( [ (play_df.rush == True) & (play_df["penalty_in_text"] == True), @@ -4425,12 +3432,8 @@ def __process_epa(self, play_df): ) play_df["EPA_non_explosive"] = np.where((play_df["EPA_explosive"] == False), play_df.EPA, None) - play_df["EPA_explosive_pass"] = np.where( - ((play_df["pass"] == True) & (play_df["EPA"] >= 2.4)), True, False - ) - play_df["EPA_explosive_rush"] = np.where( - (((play_df["rush"] == True) & (play_df["EPA"] >= 1.8))), True, False - ) + play_df["EPA_explosive_pass"] = np.where(((play_df["pass"] == True) & (play_df["EPA"] >= 2.4)), True, False) + play_df["EPA_explosive_rush"] = np.where((((play_df["rush"] == True) & (play_df["EPA"] >= 1.8))), True, False) play_df["first_down_created"] = np.where( (play_df.scrimmage_play == True) @@ -4441,26 +3444,18 @@ def __process_epa(self, play_df): ) play_df["EPA_success"] = np.where(play_df.EPA > 0, True, False) - play_df["EPA_success_early_down"] = np.where( - (play_df.EPA > 0) & (play_df.early_down == True), True, False - ) + play_df["EPA_success_early_down"] = np.where((play_df.EPA > 0) & (play_df.early_down == True), True, False) play_df["EPA_success_early_down_pass"] = np.where( - (play_df["pass"] == True) - & (play_df.EPA > 0) - & (play_df.early_down == True), + (play_df["pass"] == True) & (play_df.EPA > 0) & (play_df.early_down == True), True, False, ) play_df["EPA_success_early_down_rush"] = np.where( - (play_df["rush"] == True) - & (play_df.EPA > 0) - & (play_df.early_down == True), + (play_df["rush"] == True) & (play_df.EPA > 0) & (play_df.early_down == True), True, False, ) - play_df["EPA_success_late_down"] = np.where( - (play_df.EPA > 0) & (play_df.late_down == True), True, False - ) + play_df["EPA_success_late_down"] = np.where((play_df.EPA > 0) & (play_df.late_down == True), True, False) play_df["EPA_success_late_down_pass"] = np.where( (play_df["pass"] == True) & (play_df.EPA > 0) & (play_df.late_down == True), True, @@ -4474,15 +3469,9 @@ def __process_epa(self, play_df): play_df["EPA_success_standard_down"] = np.where( (play_df.EPA > 0) & (play_df.standard_down == True), True, False ) - play_df["EPA_success_passing_down"] = np.where( - (play_df.EPA > 0) & (play_df.passing_down == True), True, False - ) - play_df["EPA_success_pass"] = np.where( - (play_df.EPA > 0) & (play_df["pass"] == True), True, False - ) - play_df["EPA_success_rush"] = np.where( - (play_df.EPA > 0) & (play_df.rush == True), True, False - ) + play_df["EPA_success_passing_down"] = np.where((play_df.EPA > 0) & (play_df.passing_down == True), True, False) + play_df["EPA_success_pass"] = np.where((play_df.EPA > 0) & (play_df["pass"] == True), True, False) + play_df["EPA_success_rush"] = np.where((play_df.EPA > 0) & (play_df.rush == True), True, False) play_df["EPA_success_EPA"] = np.where(play_df.EPA > 0, play_df.EPA, None) play_df["EPA_success_standard_down_EPA"] = np.where( (play_df.EPA > 0) & (play_df.standard_down == True), play_df.EPA, None @@ -4490,26 +3479,16 @@ def __process_epa(self, play_df): play_df["EPA_success_passing_down_EPA"] = np.where( (play_df.EPA > 0) & (play_df.passing_down == True), play_df.EPA, None ) - play_df["EPA_success_pass_EPA"] = np.where( - (play_df.EPA > 0) & (play_df["pass"] == True), play_df.EPA, None - ) - play_df["EPA_success_rush_EPA"] = np.where( - (play_df.EPA > 0) & (play_df.rush == True), True, False - ) - play_df["EPA_middle_8_success"] = np.where( - (play_df.EPA > 0) & (play_df["middle_8"] == True), True, False - ) + play_df["EPA_success_pass_EPA"] = np.where((play_df.EPA > 0) & (play_df["pass"] == True), play_df.EPA, None) + play_df["EPA_success_rush_EPA"] = np.where((play_df.EPA > 0) & (play_df.rush == True), True, False) + play_df["EPA_middle_8_success"] = np.where((play_df.EPA > 0) & (play_df["middle_8"] == True), True, False) play_df["EPA_middle_8_success_pass"] = np.where( - (play_df["pass"] == True) - & (play_df.EPA > 0) - & (play_df["middle_8"] == True), + (play_df["pass"] == True) & (play_df.EPA > 0) & (play_df["middle_8"] == True), True, False, ) play_df["EPA_middle_8_success_rush"] = np.where( - (play_df["rush"] == True) - & (play_df.EPA > 0) - & (play_df["middle_8"] == True), + (play_df["rush"] == True) & (play_df.EPA > 0) & (play_df["middle_8"] == True), True, False, ) @@ -4522,17 +3501,13 @@ def __process_epa(self, play_df): default=None, ) play_df["EPA_sp"] = np.where( - (play_df.fg_attempt == True) - | (play_df.punt == True) - | (play_df.kickoff_play == True), + (play_df.fg_attempt == True) | (play_df.punt == True) | (play_df.kickoff_play == True), play_df["EPA"], False, ) play_df["EPA_fg"] = np.where((play_df.fg_attempt == True), play_df["EPA"], None) play_df["EPA_punt"] = np.where((play_df.punt == True), play_df["EPA"], None) - play_df["EPA_kickoff"] = np.where( - (play_df.kickoff_play == True), play_df["EPA"], None - ) + play_df["EPA_kickoff"] = np.where((play_df.kickoff_play == True), play_df["EPA"], None) return play_df def __process_qbr(self, play_df): @@ -4555,35 +3530,17 @@ def __process_qbr(self, play_df): [0.6, 0.9, 0.9, 0.6], default=1, ) - play_df["non_fumble_sack"] = (play_df["sack_vec"] == True) & ( - play_df["fumble_vec"] == False - ) + play_df["non_fumble_sack"] = (play_df["sack_vec"] == True) & (play_df["fumble_vec"] == False) - play_df["sack_epa"] = np.where( - play_df["non_fumble_sack"] == True, play_df["qbr_epa"], np.NaN - ) - play_df["pass_epa"] = np.where( - play_df["pass"] == True, play_df["qbr_epa"], np.NaN - ) - play_df["rush_epa"] = np.where( - play_df["rush"] == True, play_df["qbr_epa"], np.NaN - ) - play_df["pen_epa"] = np.where( - play_df["penalty_flag"] == True, play_df["qbr_epa"], np.NaN - ) + play_df["sack_epa"] = np.where(play_df["non_fumble_sack"] == True, play_df["qbr_epa"], np.NaN) + play_df["pass_epa"] = np.where(play_df["pass"] == True, play_df["qbr_epa"], np.NaN) + play_df["rush_epa"] = np.where(play_df["rush"] == True, play_df["qbr_epa"], np.NaN) + play_df["pen_epa"] = np.where(play_df["penalty_flag"] == True, play_df["qbr_epa"], np.NaN) - play_df["sack_weight"] = np.where( - play_df["non_fumble_sack"] == True, play_df["weight"], np.NaN - ) - play_df["pass_weight"] = np.where( - play_df["pass"] == True, play_df["weight"], np.NaN - ) - play_df["rush_weight"] = np.where( - play_df["rush"] == True, play_df["weight"], np.NaN - ) - play_df["pen_weight"] = np.where( - play_df["penalty_flag"] == True, play_df["weight"], np.NaN - ) + play_df["sack_weight"] = np.where(play_df["non_fumble_sack"] == True, play_df["weight"], np.NaN) + play_df["pass_weight"] = np.where(play_df["pass"] == True, play_df["weight"], np.NaN) + play_df["rush_weight"] = np.where(play_df["rush"] == True, play_df["weight"], np.NaN) + play_df["pen_weight"] = np.where(play_df["penalty_flag"] == True, play_df["weight"], np.NaN) play_df["action_play"] = play_df.EPA != 0 play_df["athlete_name"] = np.select( @@ -4605,22 +3562,18 @@ def __process_wpa(self, play_df): ) play_df["start.ExpScoreDiff"] = np.select( [ - (play_df["penalty_in_text"] == True) - & (play_df["type.text"] != "Penalty"), - (play_df["type.text"] == "Timeout") - & (play_df["lag_scoringPlay"] == True), + (play_df["penalty_in_text"] == True) & (play_df["type.text"] != "Penalty"), + (play_df["type.text"] == "Timeout") & (play_df["lag_scoringPlay"] == True), ], [ - play_df["pos_score_diff_start"] - + play_df["EP_start"] - - play_df["EP_between"], + play_df["pos_score_diff_start"] + play_df["EP_start"] - play_df["EP_between"], (play_df["pos_score_diff_start"] + 0.92), ], default=play_df["pos_score_diff_start"] + play_df.EP_start, ) - play_df["start.ExpScoreDiff_Time_Ratio_touchback"] = play_df[ - "start.ExpScoreDiff_touchback" - ] / (play_df["start.adj_TimeSecsRem"] + 1) + play_df["start.ExpScoreDiff_Time_Ratio_touchback"] = play_df["start.ExpScoreDiff_touchback"] / ( + play_df["start.adj_TimeSecsRem"] + 1 + ) play_df["start.ExpScoreDiff_Time_Ratio"] = play_df["start.ExpScoreDiff"] / ( play_df["start.adj_TimeSecsRem"] + 1 ) @@ -4630,16 +3583,12 @@ def __process_wpa(self, play_df): [ # Flips for Turnovers that aren't kickoffs ( - ( - (play_df["type.text"].isin(end_change_vec)) - | (play_df.downs_turnover == True) - ) + ((play_df["type.text"].isin(end_change_vec)) | (play_df.downs_turnover == True)) & (play_df.kickoff_play == False) & (play_df["scoringPlay"] == False) ), # Flips for Turnovers that are on kickoffs - (play_df["type.text"].isin(kickoff_turnovers)) - & (play_df["scoringPlay"] == False), + (play_df["type.text"].isin(kickoff_turnovers)) & (play_df["scoringPlay"] == False), (play_df["scoringPlay"] == False) & (play_df["type.text"] != "Timeout"), (play_df["scoringPlay"] == False) & (play_df["type.text"] == "Timeout"), (play_df["scoringPlay"] == True) @@ -4650,9 +3599,7 @@ def __process_wpa(self, play_df): & (play_df["td_play"] == True) & (play_df["type.text"].isin(offense_score_vec)) & (play_df.season <= 2013), - (play_df["type.text"] == "Timeout") - & (play_df["lag_scoringPlay"] == True) - & (play_df.season <= 2013), + (play_df["type.text"] == "Timeout") & (play_df["lag_scoringPlay"] == True) & (play_df.season <= 2013), ], [ play_df["pos_score_diff_end"] - play_df.EP_end, @@ -4665,9 +3612,7 @@ def __process_wpa(self, play_df): ], default=play_df["pos_score_diff_end"], ) - play_df["end.ExpScoreDiff_Time_Ratio"] = play_df["end.ExpScoreDiff"] / ( - play_df["end.adj_TimeSecsRem"] + 1 - ) + play_df["end.ExpScoreDiff_Time_Ratio"] = play_df["end.ExpScoreDiff"] / (play_df["end.adj_TimeSecsRem"] + 1) # ---- wp_before ---- start_touchback_data = play_df[wp_start_touchback_columns] start_touchback_data.columns = wp_final_names @@ -4713,16 +3658,10 @@ def __process_wpa(self, play_df): [ (play_df["type.text"] == "Timeout"), game_complete - & ( - (play_df.lead_play_type.isna()) - | (play_df.game_play_number == max(play_df.game_play_number)) - ) + & ((play_df.lead_play_type.isna()) | (play_df.game_play_number == max(play_df.game_play_number))) & (play_df.pos_score_diff_end > 0), game_complete - & ( - (play_df.lead_play_type.isna()) - | (play_df.game_play_number == max(play_df.game_play_number)) - ) + & ((play_df.lead_play_type.isna()) | (play_df.game_play_number == max(play_df.game_play_number))) & (play_df.pos_score_diff_end < 0), (play_df.end_of_half == 1) & (play_df["start.pos_team.id"] == play_df.lead_pos_team) @@ -4733,14 +3672,10 @@ def __process_wpa(self, play_df): (play_df.end_of_half == 1) & (play_df["start.pos_team_receives_2H_kickoff"] == False) & (play_df["type.text"] == "Timeout"), - (play_df.lead_play_type.isin(["End Period", "End of Half"])) - & (play_df.change_of_pos_team == 0), - (play_df.lead_play_type.isin(["End Period", "End of Half"])) - & (play_df.change_of_pos_team == 1), + (play_df.lead_play_type.isin(["End Period", "End of Half"])) & (play_df.change_of_pos_team == 0), + (play_df.lead_play_type.isin(["End Period", "End of Half"])) & (play_df.change_of_pos_team == 1), (play_df["kickoff_onside"] == True) - & ( - play_df["start.def_pos_team.id"] == play_df["end.pos_team.id"] - ), # onside recovery + & (play_df["start.def_pos_team.id"] == play_df["end.pos_team.id"]), # onside recovery (play_df["start.pos_team.id"] != play_df["end.pos_team.id"]), ], [ @@ -4784,231 +3719,284 @@ def __add_drive_data(self, play_df): "punt|fumble|interception|downs", regex=True, case=False ) play_df["drive_start"] = play_df["drive_start"].astype(float) - play_df["drive_play_index"] = base_groups["scrimmage_play"].apply( - lambda x: x.cumsum() - ) + play_df["drive_play_index"] = base_groups["scrimmage_play"].apply(lambda x: x.cumsum()) play_df["drive_offense_plays"] = np.where( (play_df["sp"] == False) & (play_df["scrimmage_play"] == True), play_df["play"].astype(int), 0, ) - play_df["prog_drive_EPA"] = base_groups["EPA_scrimmage"].apply( - lambda x: x.cumsum() - ) + play_df["prog_drive_EPA"] = base_groups["EPA_scrimmage"].apply(lambda x: x.cumsum()) play_df["prog_drive_WPA"] = base_groups["wpa"].apply(lambda x: x.cumsum()) play_df["drive_offense_yards"] = np.where( (play_df["sp"] == False) & (play_df["scrimmage_play"] == True), play_df["statYardage"], 0, ) - play_df["drive_total_yards"] = play_df.groupby(["drive.id"])[ - "drive_offense_yards" - ].apply(lambda x: x.cumsum()) + play_df["drive_total_yards"] = play_df.groupby(["drive.id"])["drive_offense_yards"].apply(lambda x: x.cumsum()) return play_df def create_box_score(self): - if (self.ran_pipeline == False): + if self.ran_pipeline == False: self.run_processing_pipeline() # have to run the pipeline before pulling this in - self.plays_json['completion'] = self.plays_json['completion'].astype(float) - self.plays_json['pass_attempt'] = self.plays_json['pass_attempt'].astype(float) - self.plays_json['target'] = self.plays_json['target'].astype(float) - self.plays_json['yds_receiving'] = self.plays_json['yds_receiving'].astype(float) - self.plays_json['yds_rushed'] = self.plays_json['yds_rushed'].astype(float) - self.plays_json['rush'] = self.plays_json['rush'].astype(float) - self.plays_json['rush_td'] = self.plays_json['rush_td'].astype(float) - self.plays_json['pass'] = self.plays_json['pass'].astype(float) - self.plays_json['pass_td'] = self.plays_json['pass_td'].astype(float) - self.plays_json['EPA'] = self.plays_json['EPA'].astype(float) - self.plays_json['wpa'] = self.plays_json['wpa'].astype(float) - self.plays_json['int'] = self.plays_json['int'].astype(float) - self.plays_json['int_td'] = self.plays_json['int_td'].astype(float) - self.plays_json['def_EPA'] = self.plays_json['def_EPA'].astype(float) - self.plays_json['EPA_rush'] = self.plays_json['EPA_rush'].astype(float) - self.plays_json['EPA_pass'] = self.plays_json['EPA_pass'].astype(float) - self.plays_json['EPA_success'] = self.plays_json['EPA_success'].astype(float) - self.plays_json['EPA_success_pass'] = self.plays_json['EPA_success_pass'].astype(float) - self.plays_json['EPA_success_rush'] = self.plays_json['EPA_success_rush'].astype(float) - self.plays_json['EPA_success_standard_down'] = self.plays_json['EPA_success_standard_down'].astype(float) - self.plays_json['EPA_success_passing_down'] = self.plays_json['EPA_success_passing_down'].astype(float) - self.plays_json['middle_8'] = self.plays_json['middle_8'].astype(float) - self.plays_json['rz_play'] = self.plays_json['rz_play'].astype(float) - self.plays_json['scoring_opp'] = self.plays_json['scoring_opp'].astype(float) - self.plays_json['stuffed_run'] = self.plays_json['stuffed_run'].astype(float) - self.plays_json['stopped_run'] = self.plays_json['stopped_run'].astype(float) - self.plays_json['opportunity_run'] = self.plays_json['opportunity_run'].astype(float) - self.plays_json['highlight_run'] = self.plays_json['highlight_run'].astype(float) - self.plays_json['short_rush_success'] = self.plays_json['short_rush_success'].astype(float) - self.plays_json['short_rush_attempt'] = self.plays_json['short_rush_attempt'].astype(float) - self.plays_json['power_rush_success'] = self.plays_json['power_rush_success'].astype(float) - self.plays_json['power_rush_attempt'] = self.plays_json['power_rush_attempt'].astype(float) - self.plays_json['EPA_explosive'] = self.plays_json['EPA_explosive'].astype(float) - self.plays_json['EPA_explosive_pass'] = self.plays_json['EPA_explosive_pass'].astype(float) - self.plays_json['EPA_explosive_rush'] = self.plays_json['EPA_explosive_rush'].astype(float) - self.plays_json['standard_down'] = self.plays_json['standard_down'].astype(float) - self.plays_json['passing_down'] = self.plays_json['passing_down'].astype(float) - self.plays_json['fumble_vec'] = self.plays_json['fumble_vec'].astype(float) - self.plays_json['sack'] = self.plays_json['sack'].astype(float) - self.plays_json['penalty_flag'] = self.plays_json['penalty_flag'].astype(float) - self.plays_json['play'] = self.plays_json['play'].astype(float) - self.plays_json['scrimmage_play'] = self.plays_json['scrimmage_play'].astype(float) - self.plays_json['sp'] = self.plays_json['sp'].astype(float) - self.plays_json['kickoff_play'] = self.plays_json['kickoff_play'].astype(float) - self.plays_json['punt'] = self.plays_json['punt'].astype(float) - self.plays_json['fg_attempt'] = self.plays_json['fg_attempt'].astype(float) - self.plays_json['EPA_penalty'] = self.plays_json['EPA_penalty'].astype(float) - self.plays_json['EPA_sp'] = self.plays_json['EPA_sp'].astype(float) - self.plays_json['EPA_fg'] = self.plays_json['EPA_fg'].astype(float) - self.plays_json['EPA_punt'] = self.plays_json['EPA_punt'].astype(float) - self.plays_json['EPA_kickoff'] = self.plays_json['EPA_kickoff'].astype(float) - self.plays_json['TFL'] = self.plays_json['TFL'].astype(float) - self.plays_json['TFL_pass'] = self.plays_json['TFL_pass'].astype(float) - self.plays_json['TFL_rush'] = self.plays_json['TFL_rush'].astype(float) - self.plays_json['havoc'] = self.plays_json['havoc'].astype(float) + self.plays_json["completion"] = self.plays_json["completion"].astype(float) + self.plays_json["pass_attempt"] = self.plays_json["pass_attempt"].astype(float) + self.plays_json["target"] = self.plays_json["target"].astype(float) + self.plays_json["yds_receiving"] = self.plays_json["yds_receiving"].astype(float) + self.plays_json["yds_rushed"] = self.plays_json["yds_rushed"].astype(float) + self.plays_json["rush"] = self.plays_json["rush"].astype(float) + self.plays_json["rush_td"] = self.plays_json["rush_td"].astype(float) + self.plays_json["pass"] = self.plays_json["pass"].astype(float) + self.plays_json["pass_td"] = self.plays_json["pass_td"].astype(float) + self.plays_json["EPA"] = self.plays_json["EPA"].astype(float) + self.plays_json["wpa"] = self.plays_json["wpa"].astype(float) + self.plays_json["int"] = self.plays_json["int"].astype(float) + self.plays_json["int_td"] = self.plays_json["int_td"].astype(float) + self.plays_json["def_EPA"] = self.plays_json["def_EPA"].astype(float) + self.plays_json["EPA_rush"] = self.plays_json["EPA_rush"].astype(float) + self.plays_json["EPA_pass"] = self.plays_json["EPA_pass"].astype(float) + self.plays_json["EPA_success"] = self.plays_json["EPA_success"].astype(float) + self.plays_json["EPA_success_pass"] = self.plays_json["EPA_success_pass"].astype(float) + self.plays_json["EPA_success_rush"] = self.plays_json["EPA_success_rush"].astype(float) + self.plays_json["EPA_success_standard_down"] = self.plays_json["EPA_success_standard_down"].astype(float) + self.plays_json["EPA_success_passing_down"] = self.plays_json["EPA_success_passing_down"].astype(float) + self.plays_json["middle_8"] = self.plays_json["middle_8"].astype(float) + self.plays_json["rz_play"] = self.plays_json["rz_play"].astype(float) + self.plays_json["scoring_opp"] = self.plays_json["scoring_opp"].astype(float) + self.plays_json["stuffed_run"] = self.plays_json["stuffed_run"].astype(float) + self.plays_json["stopped_run"] = self.plays_json["stopped_run"].astype(float) + self.plays_json["opportunity_run"] = self.plays_json["opportunity_run"].astype(float) + self.plays_json["highlight_run"] = self.plays_json["highlight_run"].astype(float) + self.plays_json["short_rush_success"] = self.plays_json["short_rush_success"].astype(float) + self.plays_json["short_rush_attempt"] = self.plays_json["short_rush_attempt"].astype(float) + self.plays_json["power_rush_success"] = self.plays_json["power_rush_success"].astype(float) + self.plays_json["power_rush_attempt"] = self.plays_json["power_rush_attempt"].astype(float) + self.plays_json["EPA_explosive"] = self.plays_json["EPA_explosive"].astype(float) + self.plays_json["EPA_explosive_pass"] = self.plays_json["EPA_explosive_pass"].astype(float) + self.plays_json["EPA_explosive_rush"] = self.plays_json["EPA_explosive_rush"].astype(float) + self.plays_json["standard_down"] = self.plays_json["standard_down"].astype(float) + self.plays_json["passing_down"] = self.plays_json["passing_down"].astype(float) + self.plays_json["fumble_vec"] = self.plays_json["fumble_vec"].astype(float) + self.plays_json["sack"] = self.plays_json["sack"].astype(float) + self.plays_json["penalty_flag"] = self.plays_json["penalty_flag"].astype(float) + self.plays_json["play"] = self.plays_json["play"].astype(float) + self.plays_json["scrimmage_play"] = self.plays_json["scrimmage_play"].astype(float) + self.plays_json["sp"] = self.plays_json["sp"].astype(float) + self.plays_json["kickoff_play"] = self.plays_json["kickoff_play"].astype(float) + self.plays_json["punt"] = self.plays_json["punt"].astype(float) + self.plays_json["fg_attempt"] = self.plays_json["fg_attempt"].astype(float) + self.plays_json["EPA_penalty"] = self.plays_json["EPA_penalty"].astype(float) + self.plays_json["EPA_sp"] = self.plays_json["EPA_sp"].astype(float) + self.plays_json["EPA_fg"] = self.plays_json["EPA_fg"].astype(float) + self.plays_json["EPA_punt"] = self.plays_json["EPA_punt"].astype(float) + self.plays_json["EPA_kickoff"] = self.plays_json["EPA_kickoff"].astype(float) + self.plays_json["TFL"] = self.plays_json["TFL"].astype(float) + self.plays_json["TFL_pass"] = self.plays_json["TFL_pass"].astype(float) + self.plays_json["TFL_rush"] = self.plays_json["TFL_rush"].astype(float) + self.plays_json["havoc"] = self.plays_json["havoc"].astype(float) pass_box = self.plays_json[(self.plays_json["pass"] == True) & (self.plays_json.scrimmage_play == True)] rush_box = self.plays_json[(self.plays_json["rush"] == True) & (self.plays_json.scrimmage_play == True)] # pass_box.yds_receiving.fillna(0.0, inplace=True) - passer_box = pass_box[(pass_box["pass"] == True) & (pass_box["scrimmage_play"] == True)].fillna(0.0).groupby(by=["pos_team","passer_player_name"], as_index=False).agg( - Comp = ('completion', sum), - Att = ('pass_attempt',sum), - Yds = ('yds_receiving',sum), - Pass_TD = ('pass_td', sum), - Int = ('int', sum), - YPA = ('yds_receiving', mean), - EPA = ('EPA', sum), - EPA_per_Play = ('EPA', mean), - WPA = ('wpa', sum), - SR = ('EPA_success', mean), - Sck = ('sack_vec', sum) - ).round(2) + passer_box = ( + pass_box[(pass_box["pass"] == True) & (pass_box["scrimmage_play"] == True)] + .fillna(0.0) + .groupby(by=["pos_team", "passer_player_name"], as_index=False) + .agg( + Comp=("completion", sum), + Att=("pass_attempt", sum), + Yds=("yds_receiving", sum), + Pass_TD=("pass_td", sum), + Int=("int", sum), + YPA=("yds_receiving", mean), + EPA=("EPA", sum), + EPA_per_Play=("EPA", mean), + WPA=("wpa", sum), + SR=("EPA_success", mean), + Sck=("sack_vec", sum), + ) + .round(2) + ) passer_box = passer_box.replace({np.nan: None}) qbs_list = passer_box.passer_player_name.to_list() def weighted_mean(s, df, wcol): s = s[s.notna() == True] # self.logger.info(s) - if (len(s) == 0): + if len(s) == 0: return 0 return np.average(s, weights=df.loc[s.index, wcol]) - pass_qbr_box = self.plays_json[(self.plays_json.athlete_name.notna() == True) & (self.plays_json.scrimmage_play == True) & (self.plays_json.athlete_name.isin(qbs_list))] - pass_qbr = pass_qbr_box.groupby(by=["pos_team","athlete_name"], as_index=False).agg( - qbr_epa = ('qbr_epa', partial(weighted_mean, df=pass_qbr_box, wcol='weight')), - sack_epa = ('sack_epa', partial(weighted_mean, df=pass_qbr_box, wcol='sack_weight')), - pass_epa = ('pass_epa', partial(weighted_mean, df=pass_qbr_box, wcol='pass_weight')), - rush_epa = ('rush_epa', partial(weighted_mean, df=pass_qbr_box, wcol='rush_weight')), - pen_epa = ('pen_epa', partial(weighted_mean, df=pass_qbr_box, wcol='pen_weight')), - spread = ('start.pos_team_spread', lambda x: x.iloc[0]) + pass_qbr_box = self.plays_json[ + (self.plays_json.athlete_name.notna() == True) + & (self.plays_json.scrimmage_play == True) + & (self.plays_json.athlete_name.isin(qbs_list)) + ] + pass_qbr = pass_qbr_box.groupby(by=["pos_team", "athlete_name"], as_index=False).agg( + qbr_epa=("qbr_epa", partial(weighted_mean, df=pass_qbr_box, wcol="weight")), + sack_epa=("sack_epa", partial(weighted_mean, df=pass_qbr_box, wcol="sack_weight")), + pass_epa=("pass_epa", partial(weighted_mean, df=pass_qbr_box, wcol="pass_weight")), + rush_epa=("rush_epa", partial(weighted_mean, df=pass_qbr_box, wcol="rush_weight")), + pen_epa=("pen_epa", partial(weighted_mean, df=pass_qbr_box, wcol="pen_weight")), + spread=("start.pos_team_spread", lambda x: x.iloc[0]), ) # self.logger.info(pass_qbr) dtest_qbr = DMatrix(pass_qbr[qbr_vars]) qbr_result = qbr_model.predict(dtest_qbr) pass_qbr["exp_qbr"] = qbr_result - passer_box = pd.merge(passer_box, pass_qbr, left_on=["passer_player_name","pos_team"], right_on=["athlete_name","pos_team"]) - - rusher_box = rush_box.fillna(0.0).groupby(by=["pos_team","rusher_player_name"], as_index=False).agg( - Car= ('rush', sum), - Yds= ('yds_rushed',sum), - Rush_TD = ('rush_td',sum), - YPC= ('yds_rushed', mean), - EPA= ('EPA', sum), - EPA_per_Play= ('EPA', mean), - WPA= ('wpa', sum), - SR = ('EPA_success', mean), - Fum = ('fumble_vec', sum), - Fum_Lost = ('fumble_lost', sum) - ).round(2) + passer_box = pd.merge( + passer_box, pass_qbr, left_on=["passer_player_name", "pos_team"], right_on=["athlete_name", "pos_team"] + ) + + rusher_box = ( + rush_box.fillna(0.0) + .groupby(by=["pos_team", "rusher_player_name"], as_index=False) + .agg( + Car=("rush", sum), + Yds=("yds_rushed", sum), + Rush_TD=("rush_td", sum), + YPC=("yds_rushed", mean), + EPA=("EPA", sum), + EPA_per_Play=("EPA", mean), + WPA=("wpa", sum), + SR=("EPA_success", mean), + Fum=("fumble_vec", sum), + Fum_Lost=("fumble_lost", sum), + ) + .round(2) + ) rusher_box = rusher_box.replace({np.nan: None}) - receiver_box = pass_box.groupby(by=["pos_team","receiver_player_name"], as_index=False).agg( - Rec= ('completion', sum), - Tar= ('target',sum), - Yds= ('yds_receiving',sum), - Rec_TD = ('pass_td', sum), - YPT= ('yds_receiving', mean), - EPA= ('EPA', sum), - EPA_per_Play= ('EPA', mean), - WPA= ('wpa', sum), - SR = ('EPA_success', mean), - Fum = ('fumble_vec', sum), - Fum_Lost = ('fumble_lost', sum) - ).round(2) + receiver_box = ( + pass_box.groupby(by=["pos_team", "receiver_player_name"], as_index=False) + .agg( + Rec=("completion", sum), + Tar=("target", sum), + Yds=("yds_receiving", sum), + Rec_TD=("pass_td", sum), + YPT=("yds_receiving", mean), + EPA=("EPA", sum), + EPA_per_Play=("EPA", mean), + WPA=("wpa", sum), + SR=("EPA_success", mean), + Fum=("fumble_vec", sum), + Fum_Lost=("fumble_lost", sum), + ) + .round(2) + ) receiver_box = receiver_box.replace({np.nan: None}) - team_base_box = self.plays_json.groupby(by=["pos_team"], as_index=False).agg( - EPA_plays = ('play', sum), - total_yards = ('statYardage', sum), - EPA_overall_total = ('EPA', sum), - ).round(2) - - team_pen_box = self.plays_json[(self.plays_json.penalty_flag == True)].groupby(by=["pos_team"], as_index=False).agg( - total_pen_yards = ('statYardage', sum), - EPA_penalty = ('EPA_penalty', sum), - ).round(2) - - team_scrimmage_box = self.plays_json[(self.plays_json.scrimmage_play == True)].groupby(by=["pos_team"], as_index=False).agg( - scrimmage_plays = ('scrimmage_play', sum), - EPA_overall_off = ('EPA', sum), - EPA_overall_offense = ('EPA', sum), - EPA_per_play = ('EPA', mean), - EPA_non_explosive = ('EPA_non_explosive', sum), - EPA_non_explosive_per_play = ('EPA_non_explosive', mean), - EPA_explosive = ('EPA_explosive', sum), - EPA_explosive_rate = ('EPA_explosive', mean), - passes_rate = ('pass', mean), - off_yards = ('statYardage', sum), - total_off_yards = ('statYardage', sum), - yards_per_play = ('statYardage', mean) - ).round(2) - - team_sp_box = self.plays_json[(self.plays_json.sp == True)].groupby(by=["pos_team"], as_index=False).agg( - special_teams_plays = ('sp', sum), - EPA_sp = ('EPA_sp', sum), - EPA_special_teams = ('EPA_sp', sum), - EPA_fg = ('EPA_fg', sum), - EPA_punt = ('EPA_punt', sum), - kickoff_plays = ('kickoff_play', sum), - EPA_kickoff = ('EPA_kickoff', sum) - ).round(2) - - team_scrimmage_box_pass = self.plays_json[(self.plays_json["pass"] == True) & (self.plays_json["scrimmage_play"] == True)].fillna(0).groupby(by=["pos_team"], as_index=False).agg( - passes = ('pass', sum), - pass_yards = ('yds_receiving', sum), - yards_per_pass = ('yds_receiving', mean), - EPA_passing_overall = ('EPA', sum), - EPA_passing_per_play = ('EPA', mean), - EPA_explosive_passing = ('EPA_explosive', sum), - EPA_explosive_passing_rate = ('EPA_explosive', mean), - EPA_non_explosive_passing = ('EPA_non_explosive', sum), - EPA_non_explosive_passing_per_play = ('EPA_non_explosive', mean), - ).round(2) + team_base_box = ( + self.plays_json.groupby(by=["pos_team"], as_index=False) + .agg( + EPA_plays=("play", sum), + total_yards=("statYardage", sum), + EPA_overall_total=("EPA", sum), + ) + .round(2) + ) - team_scrimmage_box_rush = self.plays_json[(self.plays_json["rush"] == True) & (self.plays_json["scrimmage_play"] == True)].groupby(by=["pos_team"], as_index=False).agg( - EPA_rushing_overall = ('EPA', sum), - EPA_rushing_per_play = ('EPA', mean), - EPA_explosive_rushing = ('EPA_explosive', sum), - EPA_explosive_rushing_rate = ('EPA_explosive', mean), - EPA_non_explosive_rushing = ('EPA_non_explosive', sum), - EPA_non_explosive_rushing_per_play = ('EPA_non_explosive', mean), - rushes = ('rush', sum), - rush_yards = ('yds_rushed', sum), - yards_per_rush = ('yds_rushed', mean), - rushing_power_rate = ('power_rush_attempt', mean), - ).round(2) + team_pen_box = ( + self.plays_json[(self.plays_json.penalty_flag == True)] + .groupby(by=["pos_team"], as_index=False) + .agg( + total_pen_yards=("statYardage", sum), + EPA_penalty=("EPA_penalty", sum), + ) + .round(2) + ) + + team_scrimmage_box = ( + self.plays_json[(self.plays_json.scrimmage_play == True)] + .groupby(by=["pos_team"], as_index=False) + .agg( + scrimmage_plays=("scrimmage_play", sum), + EPA_overall_off=("EPA", sum), + EPA_overall_offense=("EPA", sum), + EPA_per_play=("EPA", mean), + EPA_non_explosive=("EPA_non_explosive", sum), + EPA_non_explosive_per_play=("EPA_non_explosive", mean), + EPA_explosive=("EPA_explosive", sum), + EPA_explosive_rate=("EPA_explosive", mean), + passes_rate=("pass", mean), + off_yards=("statYardage", sum), + total_off_yards=("statYardage", sum), + yards_per_play=("statYardage", mean), + ) + .round(2) + ) + + team_sp_box = ( + self.plays_json[(self.plays_json.sp == True)] + .groupby(by=["pos_team"], as_index=False) + .agg( + special_teams_plays=("sp", sum), + EPA_sp=("EPA_sp", sum), + EPA_special_teams=("EPA_sp", sum), + EPA_fg=("EPA_fg", sum), + EPA_punt=("EPA_punt", sum), + kickoff_plays=("kickoff_play", sum), + EPA_kickoff=("EPA_kickoff", sum), + ) + .round(2) + ) + + team_scrimmage_box_pass = ( + self.plays_json[(self.plays_json["pass"] == True) & (self.plays_json["scrimmage_play"] == True)] + .fillna(0) + .groupby(by=["pos_team"], as_index=False) + .agg( + passes=("pass", sum), + pass_yards=("yds_receiving", sum), + yards_per_pass=("yds_receiving", mean), + EPA_passing_overall=("EPA", sum), + EPA_passing_per_play=("EPA", mean), + EPA_explosive_passing=("EPA_explosive", sum), + EPA_explosive_passing_rate=("EPA_explosive", mean), + EPA_non_explosive_passing=("EPA_non_explosive", sum), + EPA_non_explosive_passing_per_play=("EPA_non_explosive", mean), + ) + .round(2) + ) + + team_scrimmage_box_rush = ( + self.plays_json[(self.plays_json["rush"] == True) & (self.plays_json["scrimmage_play"] == True)] + .groupby(by=["pos_team"], as_index=False) + .agg( + EPA_rushing_overall=("EPA", sum), + EPA_rushing_per_play=("EPA", mean), + EPA_explosive_rushing=("EPA_explosive", sum), + EPA_explosive_rushing_rate=("EPA_explosive", mean), + EPA_non_explosive_rushing=("EPA_non_explosive", sum), + EPA_non_explosive_rushing_per_play=("EPA_non_explosive", mean), + rushes=("rush", sum), + rush_yards=("yds_rushed", sum), + yards_per_rush=("yds_rushed", mean), + rushing_power_rate=("power_rush_attempt", mean), + ) + .round(2) + ) - team_rush_base_box = self.plays_json[(self.plays_json["scrimmage_play"] == True)].groupby(by=["pos_team"], as_index=False).agg( - rushes_rate = ('rush', mean), - first_downs_created = ('first_down_created', sum), - first_downs_created_rate = ('first_down_created', mean) + team_rush_base_box = ( + self.plays_json[(self.plays_json["scrimmage_play"] == True)] + .groupby(by=["pos_team"], as_index=False) + .agg( + rushes_rate=("rush", mean), + first_downs_created=("first_down_created", sum), + first_downs_created_rate=("first_down_created", mean), + ) ) - team_rush_power_box = self.plays_json[(self.plays_json["power_rush_attempt"] == True)].groupby(by=["pos_team"], as_index=False).agg( - EPA_rushing_power = ('EPA', sum), - EPA_rushing_power_per_play = ('EPA', mean), - rushing_power_success = ('power_rush_success', sum), - rushing_power_success_rate = ('power_rush_success', mean), - rushing_power = ('power_rush_attempt', sum), + team_rush_power_box = ( + self.plays_json[(self.plays_json["power_rush_attempt"] == True)] + .groupby(by=["pos_team"], as_index=False) + .agg( + EPA_rushing_power=("EPA", sum), + EPA_rushing_power_per_play=("EPA", mean), + rushing_power_success=("power_rush_success", sum), + rushing_power_success_rate=("power_rush_success", mean), + rushing_power=("power_rush_attempt", sum), + ) ) self.plays_json.opp_highlight_yards = self.plays_json.opp_highlight_yards.astype(float) @@ -5016,197 +4004,318 @@ def weighted_mean(s, df, wcol): self.plays_json.line_yards = self.plays_json.line_yards.astype(float) self.plays_json.second_level_yards = self.plays_json.second_level_yards.astype(float) self.plays_json.open_field_yards = self.plays_json.open_field_yards.astype(float) - team_rush_box = self.plays_json[(self.plays_json["rush"] == True) & (self.plays_json["scrimmage_play"] == True)].fillna(0).groupby(by=["pos_team"], as_index=False).agg( - rushing_stuff = ('stuffed_run', sum), - rushing_stuff_rate = ('stuffed_run', mean), - rushing_stopped = ('stopped_run', sum), - rushing_stopped_rate = ('stopped_run', mean), - rushing_opportunity = ('opportunity_run', sum), - rushing_opportunity_rate = ('opportunity_run', mean), - rushing_highlight = ('highlight_run', sum), - rushing_highlight_rate = ('highlight_run', mean), - rushing_highlight_yards = ('highlight_yards', sum), - line_yards = ('line_yards', sum), - line_yards_per_carry = ('line_yards', mean), - second_level_yards = ('second_level_yards', sum), - open_field_yards = ('open_field_yards', sum) - ).round(2) - - team_rush_opp_box = self.plays_json[(self.plays_json["rush"] == True) & (self.plays_json["scrimmage_play"] == True) & (self.plays_json.opportunity_run == True)].fillna(0).groupby(by=["pos_team"], as_index=False).agg( - rushing_highlight_yards_per_opp = ('opp_highlight_yards', mean), - ).round(2) - - team_data_frames = [team_rush_opp_box, team_pen_box, team_sp_box, team_scrimmage_box_rush, team_scrimmage_box_pass, team_scrimmage_box, team_base_box, team_rush_base_box, team_rush_power_box, team_rush_box] - team_box = reduce(lambda left,right: pd.merge(left,right,on=['pos_team'], how='outer'), team_data_frames) - team_box = team_box.replace({np.nan:None}) - - situation_box_normal = self.plays_json[(self.plays_json.scrimmage_play == True)].groupby(by=["pos_team"], as_index=False).agg( - EPA_success = ('EPA_success', sum), - EPA_success_rate = ('EPA_success', mean), + team_rush_box = ( + self.plays_json[(self.plays_json["rush"] == True) & (self.plays_json["scrimmage_play"] == True)] + .fillna(0) + .groupby(by=["pos_team"], as_index=False) + .agg( + rushing_stuff=("stuffed_run", sum), + rushing_stuff_rate=("stuffed_run", mean), + rushing_stopped=("stopped_run", sum), + rushing_stopped_rate=("stopped_run", mean), + rushing_opportunity=("opportunity_run", sum), + rushing_opportunity_rate=("opportunity_run", mean), + rushing_highlight=("highlight_run", sum), + rushing_highlight_rate=("highlight_run", mean), + rushing_highlight_yards=("highlight_yards", sum), + line_yards=("line_yards", sum), + line_yards_per_carry=("line_yards", mean), + second_level_yards=("second_level_yards", sum), + open_field_yards=("open_field_yards", sum), + ) + .round(2) ) - situation_box_pass = self.plays_json[(self.plays_json["pass"] == True) & (self.plays_json.scrimmage_play == True)].groupby(by=["pos_team"], as_index=False).agg( - EPA_success_pass = ('EPA_success', sum), - EPA_success_pass_rate = ('EPA_success', mean), + team_rush_opp_box = ( + self.plays_json[ + (self.plays_json["rush"] == True) + & (self.plays_json["scrimmage_play"] == True) + & (self.plays_json.opportunity_run == True) + ] + .fillna(0) + .groupby(by=["pos_team"], as_index=False) + .agg( + rushing_highlight_yards_per_opp=("opp_highlight_yards", mean), + ) + .round(2) + ) + + team_data_frames = [ + team_rush_opp_box, + team_pen_box, + team_sp_box, + team_scrimmage_box_rush, + team_scrimmage_box_pass, + team_scrimmage_box, + team_base_box, + team_rush_base_box, + team_rush_power_box, + team_rush_box, + ] + team_box = reduce(lambda left, right: pd.merge(left, right, on=["pos_team"], how="outer"), team_data_frames) + team_box = team_box.replace({np.nan: None}) + + situation_box_normal = ( + self.plays_json[(self.plays_json.scrimmage_play == True)] + .groupby(by=["pos_team"], as_index=False) + .agg( + EPA_success=("EPA_success", sum), + EPA_success_rate=("EPA_success", mean), + ) ) - situation_box_rush = self.plays_json[(self.plays_json["rush"] == True) & (self.plays_json.scrimmage_play == True)].groupby(by=["pos_team"], as_index=False).agg( - EPA_success_rush = ('EPA_success', sum), - EPA_success_rush_rate = ('EPA_success', mean), + situation_box_pass = ( + self.plays_json[(self.plays_json["pass"] == True) & (self.plays_json.scrimmage_play == True)] + .groupby(by=["pos_team"], as_index=False) + .agg( + EPA_success_pass=("EPA_success", sum), + EPA_success_pass_rate=("EPA_success", mean), + ) ) - situation_box_middle8 = self.plays_json[(self.plays_json["middle_8"] == True) & (self.plays_json.scrimmage_play == True)].groupby(by=["pos_team"], as_index=False).agg( - middle_8 = ('middle_8', sum), - middle_8_pass_rate = ('pass', mean), - middle_8_rush_rate = ('rush', mean), - EPA_middle_8 = ('EPA', sum), - EPA_middle_8_per_play = ('EPA', mean), - EPA_middle_8_success = ('EPA_success', sum), - EPA_middle_8_success_rate = ('EPA_success', mean), + situation_box_rush = ( + self.plays_json[(self.plays_json["rush"] == True) & (self.plays_json.scrimmage_play == True)] + .groupby(by=["pos_team"], as_index=False) + .agg( + EPA_success_rush=("EPA_success", sum), + EPA_success_rush_rate=("EPA_success", mean), + ) ) - situation_box_middle8_pass = self.plays_json[(self.plays_json["pass"] == True) & (self.plays_json["middle_8"] == True) & (self.plays_json.scrimmage_play == True)].groupby(by=["pos_team"], as_index=False).agg( - middle_8_pass = ('pass', sum), - EPA_middle_8_pass = ('EPA', sum), - EPA_middle_8_pass_per_play = ('EPA', mean), - EPA_middle_8_success_pass = ('EPA_success', sum), - EPA_middle_8_success_pass_rate = ('EPA_success', mean), + situation_box_middle8 = ( + self.plays_json[(self.plays_json["middle_8"] == True) & (self.plays_json.scrimmage_play == True)] + .groupby(by=["pos_team"], as_index=False) + .agg( + middle_8=("middle_8", sum), + middle_8_pass_rate=("pass", mean), + middle_8_rush_rate=("rush", mean), + EPA_middle_8=("EPA", sum), + EPA_middle_8_per_play=("EPA", mean), + EPA_middle_8_success=("EPA_success", sum), + EPA_middle_8_success_rate=("EPA_success", mean), + ) ) - situation_box_middle8_rush = self.plays_json[(self.plays_json["rush"] == True) & (self.plays_json["middle_8"] == True) & (self.plays_json.scrimmage_play == True)].groupby(by=["pos_team"], as_index=False).agg( - middle_8_rush = ('rush', sum), - - EPA_middle_8_rush = ('EPA', sum), - EPA_middle_8_rush_per_play = ('EPA', mean), + situation_box_middle8_pass = ( + self.plays_json[ + (self.plays_json["pass"] == True) + & (self.plays_json["middle_8"] == True) + & (self.plays_json.scrimmage_play == True) + ] + .groupby(by=["pos_team"], as_index=False) + .agg( + middle_8_pass=("pass", sum), + EPA_middle_8_pass=("EPA", sum), + EPA_middle_8_pass_per_play=("EPA", mean), + EPA_middle_8_success_pass=("EPA_success", sum), + EPA_middle_8_success_pass_rate=("EPA_success", mean), + ) + ) - EPA_middle_8_success_rush = ('EPA_success', sum), - EPA_middle_8_success_rush_rate = ('EPA_success', mean), + situation_box_middle8_rush = ( + self.plays_json[ + (self.plays_json["rush"] == True) + & (self.plays_json["middle_8"] == True) + & (self.plays_json.scrimmage_play == True) + ] + .groupby(by=["pos_team"], as_index=False) + .agg( + middle_8_rush=("rush", sum), + EPA_middle_8_rush=("EPA", sum), + EPA_middle_8_rush_per_play=("EPA", mean), + EPA_middle_8_success_rush=("EPA_success", sum), + EPA_middle_8_success_rush_rate=("EPA_success", mean), + ) ) - situation_box_early = self.plays_json[(self.plays_json.early_down == True)].groupby(by=["pos_team"], as_index=False).agg( - EPA_success_early_down = ('EPA_success', sum), - EPA_success_early_down_rate = ('EPA_success', mean), - early_downs = ('early_down', sum), - early_down_pass_rate = ('pass', mean), - early_down_rush_rate = ('rush', mean), - EPA_early_down = ('EPA', sum), - EPA_early_down_per_play = ('EPA', mean), - early_down_first_down = ('first_down_created', sum), - early_down_first_down_rate = ('first_down_created', mean) + situation_box_early = ( + self.plays_json[(self.plays_json.early_down == True)] + .groupby(by=["pos_team"], as_index=False) + .agg( + EPA_success_early_down=("EPA_success", sum), + EPA_success_early_down_rate=("EPA_success", mean), + early_downs=("early_down", sum), + early_down_pass_rate=("pass", mean), + early_down_rush_rate=("rush", mean), + EPA_early_down=("EPA", sum), + EPA_early_down_per_play=("EPA", mean), + early_down_first_down=("first_down_created", sum), + early_down_first_down_rate=("first_down_created", mean), + ) ) - situation_box_early_pass = self.plays_json[(self.plays_json["pass"] == True) & (self.plays_json.early_down == True)].groupby(by=["pos_team"], as_index=False).agg( - early_down_pass = ('pass', sum), - EPA_early_down_pass = ('EPA', sum), - EPA_early_down_pass_per_play = ('EPA', mean), - EPA_success_early_down_pass = ('EPA_success', sum), - EPA_success_early_down_pass_rate = ('EPA_success', mean), + situation_box_early_pass = ( + self.plays_json[(self.plays_json["pass"] == True) & (self.plays_json.early_down == True)] + .groupby(by=["pos_team"], as_index=False) + .agg( + early_down_pass=("pass", sum), + EPA_early_down_pass=("EPA", sum), + EPA_early_down_pass_per_play=("EPA", mean), + EPA_success_early_down_pass=("EPA_success", sum), + EPA_success_early_down_pass_rate=("EPA_success", mean), + ) ) - situation_box_early_rush = self.plays_json[(self.plays_json["rush"] == True) & (self.plays_json.early_down == True)].groupby(by=["pos_team"], as_index=False).agg( - early_down_rush = ('rush', sum), - EPA_early_down_rush = ('EPA', sum), - EPA_early_down_rush_per_play = ('EPA', mean), - EPA_success_early_down_rush = ('EPA_success', sum), - EPA_success_early_down_rush_rate = ('EPA_success', mean), + situation_box_early_rush = ( + self.plays_json[(self.plays_json["rush"] == True) & (self.plays_json.early_down == True)] + .groupby(by=["pos_team"], as_index=False) + .agg( + early_down_rush=("rush", sum), + EPA_early_down_rush=("EPA", sum), + EPA_early_down_rush_per_play=("EPA", mean), + EPA_success_early_down_rush=("EPA_success", sum), + EPA_success_early_down_rush_rate=("EPA_success", mean), + ) ) - situation_box_late = self.plays_json[(self.plays_json.late_down == True)].groupby(by=["pos_team"], as_index=False).agg( - EPA_success_late_down = ('EPA_success_late_down', sum), - EPA_success_late_down_pass = ('EPA_success_late_down_pass', sum), - EPA_success_late_down_rush = ('EPA_success_late_down_rush', sum), - late_downs = ('late_down', sum), - late_down_pass = ('late_down_pass', sum), - late_down_rush = ('late_down_rush', sum), - EPA_late_down = ('EPA', sum), - EPA_late_down_per_play = ('EPA', mean), - EPA_success_late_down_rate = ('EPA_success_late_down', mean), - EPA_success_late_down_pass_rate = ('EPA_success_late_down_pass', mean), - EPA_success_late_down_rush_rate = ('EPA_success_late_down_rush', mean), - late_down_pass_rate = ('late_down_pass', mean), - late_down_rush_rate = ('late_down_rush', mean) + situation_box_late = ( + self.plays_json[(self.plays_json.late_down == True)] + .groupby(by=["pos_team"], as_index=False) + .agg( + EPA_success_late_down=("EPA_success_late_down", sum), + EPA_success_late_down_pass=("EPA_success_late_down_pass", sum), + EPA_success_late_down_rush=("EPA_success_late_down_rush", sum), + late_downs=("late_down", sum), + late_down_pass=("late_down_pass", sum), + late_down_rush=("late_down_rush", sum), + EPA_late_down=("EPA", sum), + EPA_late_down_per_play=("EPA", mean), + EPA_success_late_down_rate=("EPA_success_late_down", mean), + EPA_success_late_down_pass_rate=("EPA_success_late_down_pass", mean), + EPA_success_late_down_rush_rate=("EPA_success_late_down_rush", mean), + late_down_pass_rate=("late_down_pass", mean), + late_down_rush_rate=("late_down_rush", mean), + ) ) - situation_box_standard = self.plays_json[self.plays_json.standard_down == True].groupby(by=["pos_team"], as_index=False).agg( - EPA_success_standard_down = ('EPA_success_standard_down', sum), - EPA_success_standard_down_rate = ('EPA_success_standard_down', mean), - EPA_standard_down = ('EPA_success_standard_down', sum), - EPA_standard_down_per_play = ('EPA_success_standard_down', mean) + situation_box_standard = ( + self.plays_json[self.plays_json.standard_down == True] + .groupby(by=["pos_team"], as_index=False) + .agg( + EPA_success_standard_down=("EPA_success_standard_down", sum), + EPA_success_standard_down_rate=("EPA_success_standard_down", mean), + EPA_standard_down=("EPA_success_standard_down", sum), + EPA_standard_down_per_play=("EPA_success_standard_down", mean), + ) + ) + situation_box_passing = ( + self.plays_json[self.plays_json.passing_down == True] + .groupby(by=["pos_team"], as_index=False) + .agg( + EPA_success_passing_down=("EPA_success_passing_down", sum), + EPA_success_passing_down_rate=("EPA_success_passing_down", mean), + EPA_passing_down=("EPA_success_standard_down", sum), + EPA_passing_down_per_play=("EPA_success_standard_down", mean), + ) ) - situation_box_passing = self.plays_json[self.plays_json.passing_down == True].groupby(by=["pos_team"], as_index=False).agg( - EPA_success_passing_down = ('EPA_success_passing_down', sum), - EPA_success_passing_down_rate = ('EPA_success_passing_down', mean), - EPA_passing_down = ('EPA_success_standard_down', sum), - EPA_passing_down_per_play = ('EPA_success_standard_down', mean) + situation_data_frames = [ + situation_box_normal, + situation_box_pass, + situation_box_rush, + situation_box_early, + situation_box_early_pass, + situation_box_early_rush, + situation_box_middle8, + situation_box_middle8_pass, + situation_box_middle8_rush, + situation_box_late, + situation_box_standard, + situation_box_passing, + ] + situation_box = reduce( + lambda left, right: pd.merge(left, right, on=["pos_team"], how="outer"), situation_data_frames ) - situation_data_frames = [situation_box_normal, situation_box_pass, situation_box_rush, situation_box_early, situation_box_early_pass, situation_box_early_rush, situation_box_middle8, situation_box_middle8_pass, situation_box_middle8_rush, situation_box_late, situation_box_standard, situation_box_passing] - situation_box = reduce(lambda left,right: pd.merge(left,right,on=['pos_team'], how='outer'), situation_data_frames) - situation_box = situation_box.replace({np.nan:None}) + situation_box = situation_box.replace({np.nan: None}) self.plays_json.drive_stopped = self.plays_json.drive_stopped.astype(float) - def_base_box = self.plays_json[(self.plays_json.scrimmage_play == True)].groupby(by=["def_pos_team"], as_index=False).agg( - scrimmage_plays = ('scrimmage_play', sum), - TFL = ('TFL', sum), - TFL_pass = ('TFL_pass', sum), - TFL_rush = ('TFL_rush', sum), - havoc_total = ('havoc', sum), - havoc_total_rate = ('havoc', mean), - fumbles = ('forced_fumble', sum), - def_int = ('int', sum), - drive_stopped_rate = ('drive_stopped', mean) + def_base_box = ( + self.plays_json[(self.plays_json.scrimmage_play == True)] + .groupby(by=["def_pos_team"], as_index=False) + .agg( + scrimmage_plays=("scrimmage_play", sum), + TFL=("TFL", sum), + TFL_pass=("TFL_pass", sum), + TFL_rush=("TFL_rush", sum), + havoc_total=("havoc", sum), + havoc_total_rate=("havoc", mean), + fumbles=("forced_fumble", sum), + def_int=("int", sum), + drive_stopped_rate=("drive_stopped", mean), + ) ) def_base_box.drive_stopped_rate = 100 * def_base_box.drive_stopped_rate - def_base_box = def_base_box.replace({np.nan:None}) - - def_box_havoc_pass = self.plays_json[(self.plays_json.scrimmage_play == True) & (self.plays_json["pass"] == True)].groupby(by=["def_pos_team"], as_index=False).agg( - num_pass_plays = ('pass', sum), - havoc_total_pass = ('havoc', sum), - havoc_total_pass_rate = ('havoc', mean), - sacks = ('sack_vec', sum), - sacks_rate = ('sack_vec', mean), - pass_breakups = ('pass_breakup', sum) + def_base_box = def_base_box.replace({np.nan: None}) + + def_box_havoc_pass = ( + self.plays_json[(self.plays_json.scrimmage_play == True) & (self.plays_json["pass"] == True)] + .groupby(by=["def_pos_team"], as_index=False) + .agg( + num_pass_plays=("pass", sum), + havoc_total_pass=("havoc", sum), + havoc_total_pass_rate=("havoc", mean), + sacks=("sack_vec", sum), + sacks_rate=("sack_vec", mean), + pass_breakups=("pass_breakup", sum), + ) ) - def_box_havoc_pass = def_box_havoc_pass.replace({np.nan:None}) + def_box_havoc_pass = def_box_havoc_pass.replace({np.nan: None}) - def_box_havoc_rush = self.plays_json[(self.plays_json.scrimmage_play == True) & (self.plays_json["rush"] == True)].groupby(by=["def_pos_team"], as_index=False).agg( - havoc_total_rush = ('havoc', sum), - havoc_total_rush_rate = ('havoc', mean), + def_box_havoc_rush = ( + self.plays_json[(self.plays_json.scrimmage_play == True) & (self.plays_json["rush"] == True)] + .groupby(by=["def_pos_team"], as_index=False) + .agg( + havoc_total_rush=("havoc", sum), + havoc_total_rush_rate=("havoc", mean), + ) ) - def_box_havoc_rush = def_box_havoc_rush.replace({np.nan:None}) + def_box_havoc_rush = def_box_havoc_rush.replace({np.nan: None}) - def_data_frames = [def_base_box,def_box_havoc_pass,def_box_havoc_rush] - def_box = reduce(lambda left,right: pd.merge(left,right,on=['def_pos_team'], how='outer'), def_data_frames) - def_box = def_box.replace({np.nan:None}) + def_data_frames = [def_base_box, def_box_havoc_pass, def_box_havoc_rush] + def_box = reduce(lambda left, right: pd.merge(left, right, on=["def_pos_team"], how="outer"), def_data_frames) + def_box = def_box.replace({np.nan: None}) def_box_json = json.loads(def_box.to_json(orient="records")) - turnover_box = self.plays_json[(self.plays_json.scrimmage_play == True)].groupby(by=["pos_team"], as_index=False).agg( - fumbles_lost = ('fumble_lost', sum), - fumbles_recovered = ('fumble_recovered', sum), - total_fumbles = ('fumble_vec', sum), - Int = ('int', sum), - ).round(2) - turnover_box = turnover_box.replace({np.nan:None}) + turnover_box = ( + self.plays_json[(self.plays_json.scrimmage_play == True)] + .groupby(by=["pos_team"], as_index=False) + .agg( + fumbles_lost=("fumble_lost", sum), + fumbles_recovered=("fumble_recovered", sum), + total_fumbles=("fumble_vec", sum), + Int=("int", sum), + ) + .round(2) + ) + turnover_box = turnover_box.replace({np.nan: None}) turnover_box_json = json.loads(turnover_box.to_json(orient="records")) - if (len(turnover_box_json) < 2): + if len(turnover_box_json) < 2: for i in range(len(turnover_box_json), 2): turnover_box_json.append({}) - total_fumbles = reduce(lambda x, y: x+y, map(lambda x: x.get("total_fumbles", 0), turnover_box_json)) + total_fumbles = reduce(lambda x, y: x + y, map(lambda x: x.get("total_fumbles", 0), turnover_box_json)) away_passes_def = turnover_box_json[1].get("pass_breakups", 0) away_passes_int = turnover_box_json[0].get("Int", 0) - turnover_box_json[0]["expected_turnovers"] = (0.5 * total_fumbles) + (0.22 * (away_passes_def + away_passes_int)) + turnover_box_json[0]["expected_turnovers"] = (0.5 * total_fumbles) + ( + 0.22 * (away_passes_def + away_passes_int) + ) home_passes_def = turnover_box_json[0].get("pass_breakups", 0) home_passes_int = turnover_box_json[1].get("Int", 0) - turnover_box_json[1]["expected_turnovers"] = (0.5 * total_fumbles) + (0.22 * (home_passes_def + home_passes_int)) + turnover_box_json[1]["expected_turnovers"] = (0.5 * total_fumbles) + ( + 0.22 * (home_passes_def + home_passes_int) + ) turnover_box_json[0]["Int"] = int(turnover_box_json[0].get("Int", 0)) turnover_box_json[1]["Int"] = int(turnover_box_json[1].get("Int", 0)) - turnover_box_json[0]["expected_turnover_margin"] = turnover_box_json[1]["expected_turnovers"] - turnover_box_json[0]["expected_turnovers"] - turnover_box_json[1]["expected_turnover_margin"] = turnover_box_json[0]["expected_turnovers"] - turnover_box_json[1]["expected_turnovers"] + turnover_box_json[0]["expected_turnover_margin"] = ( + turnover_box_json[1]["expected_turnovers"] - turnover_box_json[0]["expected_turnovers"] + ) + turnover_box_json[1]["expected_turnover_margin"] = ( + turnover_box_json[0]["expected_turnovers"] - turnover_box_json[1]["expected_turnovers"] + ) away_to = turnover_box_json[0]["fumbles_lost"] + turnover_box_json[0]["Int"] home_to = turnover_box_json[1]["fumbles_lost"] + turnover_box_json[1]["Int"] @@ -5217,45 +4326,55 @@ def weighted_mean(s, df, wcol): turnover_box_json[0]["turnover_margin"] = home_to - away_to turnover_box_json[1]["turnover_margin"] = away_to - home_to - turnover_box_json[0]["turnover_luck"] = 5.0 * (turnover_box_json[0]["turnover_margin"] - turnover_box_json[0]["expected_turnover_margin"]) - turnover_box_json[1]["turnover_luck"] = 5.0 * (turnover_box_json[1]["turnover_margin"] - turnover_box_json[1]["expected_turnover_margin"]) + turnover_box_json[0]["turnover_luck"] = 5.0 * ( + turnover_box_json[0]["turnover_margin"] - turnover_box_json[0]["expected_turnover_margin"] + ) + turnover_box_json[1]["turnover_luck"] = 5.0 * ( + turnover_box_json[1]["turnover_margin"] - turnover_box_json[1]["expected_turnover_margin"] + ) self.plays_json.drive_start = self.plays_json.drive_start.astype(float) - drives_data = self.plays_json[(self.plays_json.scrimmage_play == True)].groupby(by=["pos_team"], as_index=False).agg( - drive_total_available_yards = ('drive_start', sum), - drive_total_gained_yards = ('drive.yards', sum), - avg_field_position = ('drive_start', mean), - plays_per_drive = ('drive.offensivePlays', mean), - yards_per_drive = ('drive.yards', mean), - drives = ('drive.id', pd.Series.nunique) + drives_data = ( + self.plays_json[(self.plays_json.scrimmage_play == True)] + .groupby(by=["pos_team"], as_index=False) + .agg( + drive_total_available_yards=("drive_start", sum), + drive_total_gained_yards=("drive.yards", sum), + avg_field_position=("drive_start", mean), + plays_per_drive=("drive.offensivePlays", mean), + yards_per_drive=("drive.yards", mean), + drives=("drive.id", pd.Series.nunique), + ) ) - drives_data['drive_total_gained_yards_rate'] = (100 * drives_data.drive_total_gained_yards / drives_data.drive_total_available_yards).round(2) + drives_data["drive_total_gained_yards_rate"] = ( + 100 * drives_data.drive_total_gained_yards / drives_data.drive_total_available_yards + ).round(2) return { - "pass" : json.loads(passer_box.to_json(orient="records")), - "rush" : json.loads(rusher_box.to_json(orient="records")), - "receiver" : json.loads(receiver_box.to_json(orient="records")), - "team" : json.loads(team_box.to_json(orient="records")), - "situational" : json.loads(situation_box.to_json(orient="records")), - "defensive" : def_box_json, - "turnover" : turnover_box_json, - "drives" : json.loads(drives_data.to_json(orient="records")) + "pass": json.loads(passer_box.to_json(orient="records")), + "rush": json.loads(rusher_box.to_json(orient="records")), + "receiver": json.loads(receiver_box.to_json(orient="records")), + "team": json.loads(team_box.to_json(orient="records")), + "situational": json.loads(situation_box.to_json(orient="records")), + "defensive": def_box_json, + "turnover": turnover_box_json, + "drives": json.loads(drives_data.to_json(orient="records")), } def run_processing_pipeline(self): if self.ran_pipeline == False: pbp_txt = self.__helper_nfl_pbp_drives(self.json) - pbp_txt['plays']['week'] = pbp_txt['header']['week'] - self.plays_json = pbp_txt['plays'] + pbp_txt["plays"]["week"] = pbp_txt["header"]["week"] + self.plays_json = pbp_txt["plays"] pbp_json = { "gameId": self.gameId, "plays": np.array(self.plays_json).tolist(), "season": pbp_txt["season"], - "week": pbp_txt['header']['week'], + "week": pbp_txt["header"]["week"], "gameInfo": pbp_txt["gameInfo"], "teamInfo": pbp_txt["header"]["competitions"][0], - "playByPlaySource": pbp_txt.get('header').get('competitions')[0].get('playByPlaySource'), + "playByPlaySource": pbp_txt.get("header").get("competitions")[0].get("playByPlaySource"), "drives": pbp_txt["drives"], "boxscore": pbp_txt["boxscore"], "header": pbp_txt["header"], @@ -5273,8 +4392,8 @@ def run_processing_pipeline(self): "videos": np.array(pbp_txt["videos"]).tolist(), } self.json = pbp_json - self.plays_json = pd.DataFrame(pbp_txt['plays'].to_dict(orient="records")) - if pbp_json.get('header').get('competitions')[0].get('playByPlaySource') != 'none': + self.plays_json = pd.DataFrame(pbp_txt["plays"].to_dict(orient="records")) + if pbp_json.get("header").get("competitions")[0].get("playByPlaySource") != "none": self.plays_json = self.__add_downs_data(self.plays_json) self.plays_json = self.__add_play_type_flags(self.plays_json) self.plays_json = self.__add_rush_pass_flags(self.plays_json) @@ -5295,7 +4414,7 @@ def run_processing_pipeline(self): "gameId": self.gameId, "plays": self.plays_json.to_dict(orient="records"), "season": pbp_txt["season"], - "week": pbp_txt['header']['week'], + "week": pbp_txt["header"]["week"], "gameInfo": pbp_txt["gameInfo"], "teamInfo": pbp_txt["header"]["competitions"][0], "playByPlaySource": pbp_txt["playByPlaySource"], @@ -5322,17 +4441,17 @@ def run_processing_pipeline(self): def run_cleaning_pipeline(self): if self.ran_cleaning_pipeline == False: pbp_txt = self.__helper_nfl_pbp_drives(self.json) - pbp_txt['plays']['week'] = pbp_txt['header']['week'] - self.plays_json = pbp_txt['plays'] + pbp_txt["plays"]["week"] = pbp_txt["header"]["week"] + self.plays_json = pbp_txt["plays"] pbp_json = { "gameId": self.gameId, "plays": np.array(self.plays_json).tolist(), "season": pbp_txt["season"], - "week": pbp_txt['header']['week'], + "week": pbp_txt["header"]["week"], "gameInfo": pbp_txt["gameInfo"], "teamInfo": pbp_txt["header"]["competitions"][0], - "playByPlaySource": pbp_txt.get('header').get('competitions')[0].get('playByPlaySource'), + "playByPlaySource": pbp_txt.get("header").get("competitions")[0].get("playByPlaySource"), "drives": pbp_txt["drives"], "boxscore": pbp_txt["boxscore"], "header": pbp_txt["header"], @@ -5350,8 +4469,8 @@ def run_cleaning_pipeline(self): "videos": np.array(pbp_txt["videos"]).tolist(), } self.json = pbp_json - self.plays_json = pd.DataFrame(pbp_txt['plays'].to_dict(orient="records")) - if pbp_json.get('header').get('competitions')[0].get('playByPlaySource') != 'none': + self.plays_json = pd.DataFrame(pbp_txt["plays"].to_dict(orient="records")) + if pbp_json.get("header").get("competitions")[0].get("playByPlaySource") != "none": self.plays_json = self.__add_downs_data(self.plays_json) self.plays_json = self.__add_play_type_flags(self.plays_json) self.plays_json = self.__add_rush_pass_flags(self.plays_json) @@ -5368,7 +4487,7 @@ def run_cleaning_pipeline(self): "gameId": self.gameId, "plays": self.plays_json.to_dict(orient="records"), "season": pbp_txt["season"], - "week": pbp_txt['header']['week'], + "week": pbp_txt["header"]["week"], "gameInfo": pbp_txt["gameInfo"], "teamInfo": pbp_txt["header"]["competitions"][0], "playByPlaySource": pbp_txt["playByPlaySource"], diff --git a/sportsdataverse/nfl/nfl_schedule.py b/sportsdataverse/nfl/nfl_schedule.py index 8f5fbb1..e340937 100755 --- a/sportsdataverse/nfl/nfl_schedule.py +++ b/sportsdataverse/nfl/nfl_schedule.py @@ -1,6 +1,8 @@ +import datetime + import pandas as pd import polars as pl -import datetime + from sportsdataverse.dl_utils import download, underscore diff --git a/sportsdataverse/nfl/nfl_teams.py b/sportsdataverse/nfl/nfl_teams.py index ae6a122..f250450 100755 --- a/sportsdataverse/nfl/nfl_teams.py +++ b/sportsdataverse/nfl/nfl_teams.py @@ -1,8 +1,12 @@ +from functools import lru_cache + import pandas as pd import polars as pl + from sportsdataverse.dl_utils import download, underscore +@lru_cache(maxsize=None) def espn_nfl_teams(return_as_pandas=True, **kwargs) -> pd.DataFrame: """espn_nfl_teams - look up NFL teams diff --git a/sportsdataverse/nhl/__init__.py b/sportsdataverse/nhl/__init__.py index 0f27674..150cc18 100755 --- a/sportsdataverse/nhl/__init__.py +++ b/sportsdataverse/nhl/__init__.py @@ -2,4 +2,4 @@ from sportsdataverse.nhl.nhl_loaders import * from sportsdataverse.nhl.nhl_pbp import * from sportsdataverse.nhl.nhl_schedule import * -from sportsdataverse.nhl.nhl_teams import * \ No newline at end of file +from sportsdataverse.nhl.nhl_teams import * diff --git a/sportsdataverse/nhl/nhl_api.py b/sportsdataverse/nhl/nhl_api.py index acaef3a..160ce38 100755 --- a/sportsdataverse/nhl/nhl_api.py +++ b/sportsdataverse/nhl/nhl_api.py @@ -1,10 +1,9 @@ +from typing import Dict + import pandas as pd import polars as pl -import numpy as np -import re -import json -from typing import Dict -from sportsdataverse.dl_utils import download, flatten_json_iterative, key_check + +from sportsdataverse.dl_utils import download def nhl_api_pbp(game_id: int, **kwargs) -> Dict: diff --git a/sportsdataverse/nhl/nhl_game_rosters.py b/sportsdataverse/nhl/nhl_game_rosters.py index 05fcad3..798696a 100644 --- a/sportsdataverse/nhl/nhl_game_rosters.py +++ b/sportsdataverse/nhl/nhl_game_rosters.py @@ -1,5 +1,6 @@ import pandas as pd import polars as pl + from sportsdataverse.dl_utils import download, underscore diff --git a/sportsdataverse/nhl/nhl_loaders.py b/sportsdataverse/nhl/nhl_loaders.py index 73c57ff..79777e3 100755 --- a/sportsdataverse/nhl/nhl_loaders.py +++ b/sportsdataverse/nhl/nhl_loaders.py @@ -1,13 +1,15 @@ +from typing import List + import pandas as pd import polars as pl from tqdm import tqdm -from typing import List + from sportsdataverse.config import ( NHL_BASE_URL, + NHL_PLAYER_BOX_URL, NHL_TEAM_BOX_URL, - NHL_TEAM_SCHEDULE_URL, NHL_TEAM_LOGO_URL, - NHL_PLAYER_BOX_URL, + NHL_TEAM_SCHEDULE_URL, ) from sportsdataverse.errors import SeasonNotFoundError diff --git a/sportsdataverse/nhl/nhl_pbp.py b/sportsdataverse/nhl/nhl_pbp.py index d907b38..e5070f9 100755 --- a/sportsdataverse/nhl/nhl_pbp.py +++ b/sportsdataverse/nhl/nhl_pbp.py @@ -1,10 +1,12 @@ -import pandas as pd -import polars as pl -import numpy as np -import os import json +import os import re from typing import Dict + +import numpy as np +import pandas as pd +import polars as pl + from sportsdataverse.dl_utils import download, flatten_json_iterative, key_check diff --git a/sportsdataverse/nhl/nhl_schedule.py b/sportsdataverse/nhl/nhl_schedule.py index 8aae4f2..606231b 100755 --- a/sportsdataverse/nhl/nhl_schedule.py +++ b/sportsdataverse/nhl/nhl_schedule.py @@ -1,6 +1,8 @@ +import datetime + import pandas as pd import polars as pl -import datetime + from sportsdataverse.dl_utils import download, underscore diff --git a/sportsdataverse/nhl/nhl_teams.py b/sportsdataverse/nhl/nhl_teams.py index 27c9676..672b112 100755 --- a/sportsdataverse/nhl/nhl_teams.py +++ b/sportsdataverse/nhl/nhl_teams.py @@ -1,8 +1,12 @@ +from functools import lru_cache + import pandas as pd import polars as pl + from sportsdataverse.dl_utils import download, underscore +@lru_cache(maxsize=None) def espn_nhl_teams(return_as_pandas=True, **kwargs) -> pd.DataFrame: """espn_nhl_teams - look up NHL teams diff --git a/sportsdataverse/wbb/__init__.py b/sportsdataverse/wbb/__init__.py index 74f80bc..699590b 100755 --- a/sportsdataverse/wbb/__init__.py +++ b/sportsdataverse/wbb/__init__.py @@ -2,4 +2,4 @@ from sportsdataverse.wbb.wbb_loaders import * from sportsdataverse.wbb.wbb_pbp import * from sportsdataverse.wbb.wbb_schedule import * -from sportsdataverse.wbb.wbb_teams import * \ No newline at end of file +from sportsdataverse.wbb.wbb_teams import * diff --git a/sportsdataverse/wbb/wbb_game_rosters.py b/sportsdataverse/wbb/wbb_game_rosters.py index b100c2c..4ccc679 100644 --- a/sportsdataverse/wbb/wbb_game_rosters.py +++ b/sportsdataverse/wbb/wbb_game_rosters.py @@ -1,5 +1,6 @@ import pandas as pd import polars as pl + from sportsdataverse.dl_utils import download, underscore diff --git a/sportsdataverse/wbb/wbb_loaders.py b/sportsdataverse/wbb/wbb_loaders.py index bdf7cf0..9ce487a 100755 --- a/sportsdataverse/wbb/wbb_loaders.py +++ b/sportsdataverse/wbb/wbb_loaders.py @@ -1,8 +1,15 @@ +from typing import List + import pandas as pd import polars as pl from tqdm import tqdm -from typing import List -from sportsdataverse.config import WBB_BASE_URL, WBB_TEAM_BOX_URL, WBB_PLAYER_BOX_URL, WBB_TEAM_SCHEDULE_URL + +from sportsdataverse.config import ( + WBB_BASE_URL, + WBB_PLAYER_BOX_URL, + WBB_TEAM_BOX_URL, + WBB_TEAM_SCHEDULE_URL, +) from sportsdataverse.errors import SeasonNotFoundError diff --git a/sportsdataverse/wbb/wbb_pbp.py b/sportsdataverse/wbb/wbb_pbp.py index 2525a7e..fda5700 100755 --- a/sportsdataverse/wbb/wbb_pbp.py +++ b/sportsdataverse/wbb/wbb_pbp.py @@ -1,10 +1,12 @@ -import pandas as pd -import polars as pl -import numpy as np -import os import json +import os import re from typing import Dict + +import numpy as np +import pandas as pd +import polars as pl + from sportsdataverse.dl_utils import download, flatten_json_iterative diff --git a/sportsdataverse/wbb/wbb_schedule.py b/sportsdataverse/wbb/wbb_schedule.py index ce34d75..c1037b7 100755 --- a/sportsdataverse/wbb/wbb_schedule.py +++ b/sportsdataverse/wbb/wbb_schedule.py @@ -1,8 +1,10 @@ +import datetime + import pandas as pd import polars as pl -import datetime -from sportsdataverse.errors import SeasonNotFoundError + from sportsdataverse.dl_utils import download, underscore +from sportsdataverse.errors import SeasonNotFoundError def espn_wbb_schedule( diff --git a/sportsdataverse/wbb/wbb_teams.py b/sportsdataverse/wbb/wbb_teams.py index 95cbce9..5c74a1c 100755 --- a/sportsdataverse/wbb/wbb_teams.py +++ b/sportsdataverse/wbb/wbb_teams.py @@ -1,8 +1,12 @@ +from functools import lru_cache + import pandas as pd import polars as pl + from sportsdataverse.dl_utils import download, underscore +@lru_cache(maxsize=None) def espn_wbb_teams(groups=None, return_as_pandas=True, **kwargs) -> pd.DataFrame: """espn_wbb_teams - look up the women's college basketball teams diff --git a/sportsdataverse/wnba/__init__.py b/sportsdataverse/wnba/__init__.py index 0c90c4d..50c6130 100755 --- a/sportsdataverse/wnba/__init__.py +++ b/sportsdataverse/wnba/__init__.py @@ -2,4 +2,4 @@ from sportsdataverse.wnba.wnba_loaders import * from sportsdataverse.wnba.wnba_pbp import * from sportsdataverse.wnba.wnba_schedule import * -from sportsdataverse.wnba.wnba_teams import * \ No newline at end of file +from sportsdataverse.wnba.wnba_teams import * diff --git a/sportsdataverse/wnba/wnba_game_rosters.py b/sportsdataverse/wnba/wnba_game_rosters.py index bacbe7a..bbcd1fa 100644 --- a/sportsdataverse/wnba/wnba_game_rosters.py +++ b/sportsdataverse/wnba/wnba_game_rosters.py @@ -1,6 +1,6 @@ import pandas as pd import polars as pl -from typing import Dict + from sportsdataverse.dl_utils import download, underscore diff --git a/sportsdataverse/wnba/wnba_loaders.py b/sportsdataverse/wnba/wnba_loaders.py index 06c6743..84b5c34 100755 --- a/sportsdataverse/wnba/wnba_loaders.py +++ b/sportsdataverse/wnba/wnba_loaders.py @@ -1,8 +1,15 @@ +from typing import List + import pandas as pd import polars as pl from tqdm import tqdm -from typing import List -from sportsdataverse.config import WNBA_BASE_URL, WNBA_TEAM_BOX_URL, WNBA_PLAYER_BOX_URL, WNBA_TEAM_SCHEDULE_URL + +from sportsdataverse.config import ( + WNBA_BASE_URL, + WNBA_PLAYER_BOX_URL, + WNBA_TEAM_BOX_URL, + WNBA_TEAM_SCHEDULE_URL, +) from sportsdataverse.errors import SeasonNotFoundError diff --git a/sportsdataverse/wnba/wnba_pbp.py b/sportsdataverse/wnba/wnba_pbp.py index 0629c4e..1d89409 100755 --- a/sportsdataverse/wnba/wnba_pbp.py +++ b/sportsdataverse/wnba/wnba_pbp.py @@ -1,10 +1,12 @@ -import pandas as pd -import polars as pl -import numpy as np -import os import json +import os import re from typing import Dict + +import numpy as np +import pandas as pd +import polars as pl + from sportsdataverse.dl_utils import download, flatten_json_iterative diff --git a/sportsdataverse/wnba/wnba_schedule.py b/sportsdataverse/wnba/wnba_schedule.py index 3c299f9..80e9eb8 100755 --- a/sportsdataverse/wnba/wnba_schedule.py +++ b/sportsdataverse/wnba/wnba_schedule.py @@ -1,8 +1,8 @@ +from datetime import datetime + import pandas as pd import polars as pl -from datetime import datetime -from sportsdataverse.config import WNBA_BASE_URL, WNBA_TEAM_BOX_URL, WNBA_PLAYER_BOX_URL, WNBA_TEAM_SCHEDULE_URL -from sportsdataverse.errors import SeasonNotFoundError + from sportsdataverse.dl_utils import download, underscore diff --git a/sportsdataverse/wnba/wnba_teams.py b/sportsdataverse/wnba/wnba_teams.py index f01468c..7ef4b32 100755 --- a/sportsdataverse/wnba/wnba_teams.py +++ b/sportsdataverse/wnba/wnba_teams.py @@ -1,8 +1,12 @@ +from functools import lru_cache + import pandas as pd import polars as pl + from sportsdataverse.dl_utils import download, underscore +@lru_cache(maxsize=None) def espn_wnba_teams(return_as_pandas=True, **kwargs) -> pd.DataFrame: """espn_wnba_teams - look up WNBA teams diff --git a/tests/cfb/__init__.py b/tests/cfb/__init__.py index 1ee2999..29e3d30 100755 --- a/tests/cfb/__init__.py +++ b/tests/cfb/__init__.py @@ -1 +1 @@ -"tests " \ No newline at end of file +"tests " diff --git a/tests/cfb/test_cfb_schedule.py b/tests/cfb/test_cfb_schedule.py index a9f0163..5e09a3e 100644 --- a/tests/cfb/test_cfb_schedule.py +++ b/tests/cfb/test_cfb_schedule.py @@ -1,16 +1,23 @@ -from sportsdataverse.cfb.cfb_schedule import espn_cfb_calendar, espn_cfb_schedule, most_recent_cfb_season -import pandas as pd import polars as pl import pytest +from sportsdataverse.cfb.cfb_schedule import ( + espn_cfb_calendar, + espn_cfb_schedule, + most_recent_cfb_season, +) + + @pytest.fixture() def calendar_data(): yield espn_cfb_calendar(season=most_recent_cfb_season(), return_as_pandas=False) + def calendar_data_check(calendar_data): assert isinstance(calendar_data, pl.DataFrame) assert len(calendar_data) > 0 + @pytest.fixture() def calendar_ondays_data(): yield espn_cfb_calendar(season=2021, ondays=True, return_as_pandas=False) @@ -20,27 +27,32 @@ def calendar_ondays_data_check(calendar_ondays_data): assert isinstance(calendar_ondays_data, pl.DataFrame) assert len(calendar_ondays_data) > 0 + @pytest.fixture() def schedule_data(): yield espn_cfb_schedule(return_as_pandas=False) + def schedule_data_check(schedule_data): assert isinstance(schedule_data, pl.DataFrame) assert len(schedule_data) > 0 + @pytest.fixture() def schedule_data2(): yield espn_cfb_schedule(dates=20220901, return_as_pandas=False) + def schedule_data_check2(schedule_data2): assert isinstance(schedule_data, pl.DataFrame) assert len(schedule_data) > 0 + @pytest.fixture() def week_1_schedule(): - yield espn_cfb_schedule(dates = 2022, week = 1, return_as_pandas=False) + yield espn_cfb_schedule(dates=2022, week=1, return_as_pandas=False) def week_1_schedule_check(week_1_schedule): assert isinstance(week_1_schedule, pl.DataFrame) - assert len(week_1_schedule) > 0 \ No newline at end of file + assert len(week_1_schedule) > 0 diff --git a/tests/cfb/test_pbp.py b/tests/cfb/test_pbp.py index 1b15157..b62d931 100755 --- a/tests/cfb/test_pbp.py +++ b/tests/cfb/test_pbp.py @@ -1,22 +1,27 @@ -from sportsdataverse.cfb.cfb_pbp import CFBPlayProcess -from sportsdataverse.cfb.cfb_schedule import espn_cfb_calendar, espn_cfb_schedule, most_recent_cfb_season - import pandas as pd import polars as pl import pytest +from sportsdataverse.cfb.cfb_pbp import CFBPlayProcess +from sportsdataverse.cfb.cfb_schedule import ( + espn_cfb_calendar, + espn_cfb_schedule, + most_recent_cfb_season, +) + + @pytest.fixture() def generated_data(): - test = CFBPlayProcess(gameId = 401301025) + test = CFBPlayProcess(gameId=401301025) test.espn_cfb_pbp() test.run_processing_pipeline() yield test + @pytest.fixture() def box_score(generated_data): - yield generated_data.create_box_score( - pl.DataFrame(generated_data.plays_json, infer_schema_length=400) - ) + yield generated_data.create_box_score(pl.DataFrame(generated_data.plays_json, infer_schema_length=400)) + def test_basic_pbp(generated_data): assert generated_data.json != None @@ -26,6 +31,7 @@ def test_basic_pbp(generated_data): assert generated_data.ran_pipeline == True assert isinstance(pl.DataFrame(generated_data.plays_json, infer_schema_length=400), pl.DataFrame) + def test_adv_box_score(box_score): assert box_score != None assert not set(box_score.keys()).difference( @@ -42,6 +48,7 @@ def test_adv_box_score(box_score): } ) + def test_havoc_rate(box_score): defense_home = box_score["defensive"][0] # print(defense_home) @@ -55,149 +62,175 @@ def test_havoc_rate(box_score): assert defense_home["havoc_total"] == (pd + home_int + tfl + fum) assert round(defense_home["havoc_total_rate"], 4) == round(((pd + home_int + tfl + fum) / plays), 4) + @pytest.fixture() def dupe_fsu_play_base(): - test = CFBPlayProcess(gameId = 401411109) + test = CFBPlayProcess(gameId=401411109) test.espn_cfb_pbp() test.run_processing_pipeline() yield pl.DataFrame(test.plays_json, infer_schema_length=400) + def test_fsu_play_dedupe(dupe_fsu_play_base): target_strings = [ { "text": "Jordan Travis pass intercepted Rance Conner return for no gain to the FlaSt 45", "down": 3, "distance": 9, - "yardsToEndzone": 74 + "yardsToEndzone": 74, }, - { - "down" : 4, - "text": "Malik Cunningham pass incomplete to Tyler Hudson", - "distance": 2, - "yardsToEndzone": 45 - } + {"down": 4, "text": "Malik Cunningham pass incomplete to Tyler Hudson", "distance": 2, "yardsToEndzone": 45}, ] regression_cases = [ { - "text" : "Alex Mastromanno punt for 52 yds , Braden Smith returns for no gain to the Lvile 37", - "down" : 4, - "distance" : 9, - "yardsToEndzone" : 89 + "text": "Alex Mastromanno punt for 52 yds , Braden Smith returns for no gain to the Lvile 37", + "down": 4, + "distance": 9, + "yardsToEndzone": 89, } ] for item in target_strings: print(f"Checking known test cases for dupes for play_text '{item}'") - assert len(dupe_fsu_play_base.filter( - (pl.col("text") == item["text"]) - & (pl.col("start.down") == item["down"]) - & (pl.col("start.distance") == item["distance"]) - & (pl.col("start.yardsToEndzone") == item["yardsToEndzone"]) - )) == 1 + assert ( + len( + dupe_fsu_play_base.filter( + (pl.col("text") == item["text"]) + & (pl.col("start.down") == item["down"]) + & (pl.col("start.distance") == item["distance"]) + & (pl.col("start.yardsToEndzone") == item["yardsToEndzone"]) + ) + ) + == 1 + ) print(f"No dupes for play_text '{item}'") - for item in regression_cases: print(f"Checking non-dupe base cases for dupes for play_text '{item}'") - assert len(dupe_fsu_play_base.filter( - (pl.col("text") == item["text"]) - & (pl.col("start.down") == item["down"]) - & (pl.col("start.distance") == item["distance"]) - & (pl.col("start.yardsToEndzone") == item["yardsToEndzone"]) - )) == 1 + assert ( + len( + dupe_fsu_play_base.filter( + (pl.col("text") == item["text"]) + & (pl.col("start.down") == item["down"]) + & (pl.col("start.distance") == item["distance"]) + & (pl.col("start.yardsToEndzone") == item["yardsToEndzone"]) + ) + ) + == 1 + ) print(f"confirmed no dupes for regression case of play_text '{item}'") + @pytest.fixture() def iu_play_base(): - test = CFBPlayProcess(gameId = 401426563) + test = CFBPlayProcess(gameId=401426563) test.espn_cfb_pbp() test.run_processing_pipeline() yield test + @pytest.fixture() def dupe_iu_play_base(iu_play_base): yield pl.DataFrame(iu_play_base.plays_json, infer_schema_length=400) + def test_iu_play_dedupe(dupe_iu_play_base): target_strings = [ { "text": "A. Reed pass,to J. Beljan for 26 yds for a TD, (B. Narveson KICK)", "down": 2, "distance": 9, - "yardsToEndzone": 26 + "yardsToEndzone": 26, } ] elimination_strings = [ { - "text" : "Austin Reed pass complete to Joey Beljan for 26 yds for a TD", + "text": "Austin Reed pass complete to Joey Beljan for 26 yds for a TD", "down": 2, "distance": 9, - "yardsToEndzone": 26 + "yardsToEndzone": 26, } ] for item in target_strings: print(f"Checking known test cases for dupes for play_text '{item}'") - assert len(dupe_iu_play_base.filter( - (pl.col("text") == item["text"]) - & (pl.col("start.down") == item["down"]) - & (pl.col("start.distance") == item["distance"]) - & (pl.col("start.yardsToEndzone") == item["yardsToEndzone"]) - )) == 1 + assert ( + len( + dupe_iu_play_base.filter( + (pl.col("text") == item["text"]) + & (pl.col("start.down") == item["down"]) + & (pl.col("start.distance") == item["distance"]) + & (pl.col("start.yardsToEndzone") == item["yardsToEndzone"]) + ) + ) + == 1 + ) print(f"No dupes for play_text '{item}'") for item in elimination_strings: print(f"Checking for strings that should have been removed by dupe check for play_text '{item}'") - assert len(dupe_iu_play_base.filter( - (pl.col("text") == item["text"]) - & (pl.col("start.down") == item["down"]) - & (pl.col("start.distance") == item["distance"]) - & (pl.col("start.yardsToEndzone") == item["yardsToEndzone"]) - )) == 0 + assert ( + len( + dupe_iu_play_base.filter( + (pl.col("text") == item["text"]) + & (pl.col("start.down") == item["down"]) + & (pl.col("start.distance") == item["distance"]) + & (pl.col("start.yardsToEndzone") == item["yardsToEndzone"]) + ) + ) + == 0 + ) print(f"Confirmed no values for play_text '{item}'") + @pytest.fixture() def iu_play_base_box(iu_play_base): yield iu_play_base.create_box_score(pl.DataFrame(iu_play_base.plays_json, infer_schema_length=400)) + def test_expected_turnovers(iu_play_base_box): defense_home = iu_play_base_box["defensive"][1] - def_home_team = defense_home.get('def_pos_team', 'NA') - away_pd = iu_play_base_box['turnover'][0].get("pass_breakups", 0) - away_off_int = iu_play_base_box['turnover'][0].get("Int", 0) - away_fum = iu_play_base_box['turnover'][0].get("total_fumbles", 0) + def_home_team = defense_home.get("def_pos_team", "NA") + away_pd = iu_play_base_box["turnover"][0].get("pass_breakups", 0) + away_off_int = iu_play_base_box["turnover"][0].get("Int", 0) + away_fum = iu_play_base_box["turnover"][0].get("total_fumbles", 0) away_exp_xTO = (0.22 * (away_pd + away_off_int)) + (0.5 * away_fum) - away_actual_xTO = iu_play_base_box['turnover'][0].get('expected_turnovers') - away_team = iu_play_base_box['turnover'][0].get('pos_team', "NA") + away_actual_xTO = iu_play_base_box["turnover"][0].get("expected_turnovers") + away_team = iu_play_base_box["turnover"][0].get("pos_team", "NA") defense_away = iu_play_base_box["defensive"][0] - def_away_team = defense_away.get('def_pos_team', 'NA') - home_pd = iu_play_base_box['turnover'][1].get("pass_breakups", 0) - home_off_int = iu_play_base_box['turnover'][1].get("Int", 0) - home_fum = iu_play_base_box['turnover'][1].get("total_fumbles", 0) + def_away_team = defense_away.get("def_pos_team", "NA") + home_pd = iu_play_base_box["turnover"][1].get("pass_breakups", 0) + home_off_int = iu_play_base_box["turnover"][1].get("Int", 0) + home_fum = iu_play_base_box["turnover"][1].get("total_fumbles", 0) home_exp_xTO = (0.22 * (home_pd + home_off_int)) + (0.5 * home_fum) - home_actual_xTO = iu_play_base_box['turnover'][1].get('expected_turnovers') - home_team = iu_play_base_box['turnover'][1].get('pos_team', "NA") + home_actual_xTO = iu_play_base_box["turnover"][1].get("expected_turnovers") + home_team = iu_play_base_box["turnover"][1].get("pos_team", "NA") - print(f"home team: {home_team} vs def {def_away_team} - fum: {home_fum}, int: {home_off_int}, pd: {home_pd} -> xTO: {home_exp_xTO}") - print(f"away off {away_team} vs def {def_home_team} - fum: {away_fum}, int: {away_off_int}, pd: {away_pd} -> xTO: {away_exp_xTO}") + print( + f"home team: {home_team} vs def {def_away_team} - fum: {home_fum}, int: {home_off_int}, pd: {home_pd} -> xTO: {home_exp_xTO}" + ) + print( + f"away off {away_team} vs def {def_home_team} - fum: {away_fum}, int: {away_off_int}, pd: {away_pd} -> xTO: {away_exp_xTO}" + ) assert round(away_exp_xTO, 4) == round(away_actual_xTO, 4) assert round(home_exp_xTO, 4) == round(home_actual_xTO, 4) - @pytest.fixture() def calendar_data(): yield espn_cfb_calendar(season=most_recent_cfb_season(), return_as_pandas=False) + def calendar_data_check(calendar_data): assert isinstance(calendar_data, pl.DataFrame) assert len(calendar_data) > 0 + @pytest.fixture() def calendar_ondays_data(): yield espn_cfb_calendar(season=2021, ondays=True, return_as_pandas=False) @@ -207,27 +240,32 @@ def calendar_ondays_data_check(calendar_ondays_data): assert isinstance(calendar_ondays_data, pl.DataFrame) assert len(calendar_ondays_data) > 0 + @pytest.fixture() def schedule_data(): yield espn_cfb_schedule(return_as_pandas=False) + def schedule_data_check(schedule_data): assert isinstance(schedule_data, pl.DataFrame) assert len(schedule_data) > 0 + @pytest.fixture() def schedule_data2(): yield espn_cfb_schedule(dates=20220901, return_as_pandas=False) + def schedule_data_check2(schedule_data2): assert isinstance(schedule_data, pl.DataFrame) assert len(schedule_data) > 0 + @pytest.fixture() def week_1_schedule(): - yield espn_cfb_schedule(dates = 2022, week = 1, return_as_pandas=False) + yield espn_cfb_schedule(dates=2022, week=1, return_as_pandas=False) def week_1_schedule_check(week_1_schedule): assert isinstance(week_1_schedule, pl.DataFrame) - assert len(week_1_schedule) > 0 \ No newline at end of file + assert len(week_1_schedule) > 0 diff --git a/tests/mbb/__init__.py b/tests/mbb/__init__.py index e4f21b6..6de0e6e 100755 --- a/tests/mbb/__init__.py +++ b/tests/mbb/__init__.py @@ -1 +1 @@ -from .test_pbp import * \ No newline at end of file +from .test_pbp import * diff --git a/tests/mbb/test_pbp.py b/tests/mbb/test_pbp.py index 65025c0..8931ecc 100755 --- a/tests/mbb/test_pbp.py +++ b/tests/mbb/test_pbp.py @@ -1,12 +1,14 @@ -from sportsdataverse.mbb import espn_mbb_pbp -import pandas as pd import pytest -import json + +from sportsdataverse.mbb import espn_mbb_pbp + + @pytest.fixture() def generated_data(): - test = espn_mbb_pbp(game_id = 401265031) + test = espn_mbb_pbp(game_id=401265031) yield test + def test_basic_pbp(generated_data): assert generated_data != None - assert len(generated_data.get('plays')) > 0 + assert len(generated_data.get("plays")) > 0 diff --git a/tests/nfl/__init__.py b/tests/nfl/__init__.py index e80d450..912d384 100755 --- a/tests/nfl/__init__.py +++ b/tests/nfl/__init__.py @@ -1 +1 @@ -from .test_espn_pbp import * \ No newline at end of file +from .test_espn_pbp import * diff --git a/tests/nfl/test_espn_pbp.py b/tests/nfl/test_espn_pbp.py index 1b036ce..df84418 100755 --- a/tests/nfl/test_espn_pbp.py +++ b/tests/nfl/test_espn_pbp.py @@ -1,19 +1,21 @@ -from sportsdataverse.nfl.nfl_pbp import NFLPlayProcess -import pandas as pd import polars as pl import pytest +from sportsdataverse.nfl.nfl_pbp import NFLPlayProcess + + @pytest.fixture() def generated_data(): - test = NFLPlayProcess(gameId = 401437777) + test = NFLPlayProcess(gameId=401437777) test.espn_nfl_pbp() test.run_processing_pipeline() yield test + def test_basic_pbp(generated_data): assert generated_data.json != None generated_data.run_processing_pipeline() assert len(generated_data.plays_json) > 0 assert generated_data.ran_pipeline == True - assert isinstance(generated_data.plays_json, pl.DataFrame) \ No newline at end of file + assert isinstance(generated_data.plays_json, pl.DataFrame) diff --git a/tests/nfl/test_roster.py b/tests/nfl/test_roster.py index a9916d7..7d8503b 100755 --- a/tests/nfl/test_roster.py +++ b/tests/nfl/test_roster.py @@ -1,4 +1,3 @@ - import sportsdataverse as sdv -nfl_schedules_df = sdv.nfl.load_nfl_schedule(seasons=range(2016,2017)) \ No newline at end of file +nfl_schedules_df = sdv.nfl.load_nfl_schedule(seasons=range(2016, 2017)) diff --git a/tests/test_dl_utils.py b/tests/test_dl_utils.py index 167a78a..05b6e9b 100644 --- a/tests/test_dl_utils.py +++ b/tests/test_dl_utils.py @@ -1,44 +1,46 @@ import pytest import requests + from sportsdataverse.dl_utils import download + class TestDownload: # Tests that the function can download a valid URL with default parameters def test_download_valid_url_default_params(self): - url = 'https://www.google.com' + url = "https://www.google.com" response = download(url) assert response.status_code == 200 # Tests that the function can download a valid URL with custom parameters def test_download_valid_url_custom_params(self): - url = 'https://jsonplaceholder.typicode.com/posts' - params = {'userId': 1} + url = "https://jsonplaceholder.typicode.com/posts" + params = {"userId": 1} response = download(url, params=params) assert response.status_code == 200 # Tests that the function can download a valid URL with custom headers def test_download_valid_url_custom_headers(self): - url = 'https://jsonplaceholder.typicode.com/posts' - headers = {'User-Agent': 'Mozilla/5.0'} + url = "https://jsonplaceholder.typicode.com/posts" + headers = {"User-Agent": "Mozilla/5.0"} response = download(url, headers=headers) assert response.status_code == 200 # Tests that the function can download a valid URL with a proxy def test_download_valid_url_with_proxy(self): - url = 'https://jsonplaceholder.typicode.com/posts' - proxy = {'https': 'https://localhost:8080'} + url = "https://jsonplaceholder.typicode.com/posts" + proxy = {"https": "https://localhost:8080"} response = download(url, proxy=proxy) assert response.status_code == 200 # Tests that the function can download a valid URL with a very short timeout def test_download_valid_url_with_short_timeout(self): - url = 'https://jsonplaceholder.typicode.com/posts' + url = "https://jsonplaceholder.typicode.com/posts" timeout = 0.001 with pytest.raises(requests.exceptions.Timeout): download(url, timeout=timeout) # Tests that the function handles an invalid URL def test_download_invalid_url(self): - url = 'https://thisisnotavalidurl.com' + url = "https://thisisnotavalidurl.com" with pytest.raises(requests.exceptions.RequestException): - download(url) \ No newline at end of file + download(url) From 164afb1a0abb1a72cc036b7bae649b1f98231e0b Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Thu, 27 Jul 2023 04:46:49 -0400 Subject: [PATCH 39/79] ep_final_names/wp_final_names were not imported --- .pre-commit-config.yaml | 8 ++++---- sportsdataverse/cfb/cfb_pbp.py | 2 ++ sportsdataverse/nfl/nfl_pbp.py | 25 +++++++++++++++++++++++-- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 79c80e4..17311d9 100755 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,10 +32,6 @@ repos: rev: '23.1.0' hooks: - id: black - # - repo: https://github.com/pycqa/flake8 - # rev: '3.9.2' - # hooks: - # - id: flake8 - repo: https://github.com/pycqa/isort rev: '5.12.0' hooks: @@ -45,6 +41,10 @@ repos: hooks: - id: pycln args: ['.', "--all"] + # - repo: https://github.com/pycqa/flake8 + # rev: '3.9.2' + # hooks: + # - id: flake8 # - repo: https://github.com/pycqa/pydocstyle # rev: '6.3.0' # hooks: diff --git a/sportsdataverse/cfb/cfb_pbp.py b/sportsdataverse/cfb/cfb_pbp.py index 7b5ee0d..52124fa 100755 --- a/sportsdataverse/cfb/cfb_pbp.py +++ b/sportsdataverse/cfb/cfb_pbp.py @@ -15,6 +15,7 @@ end_change_vec, ep_class_to_score_mapping, ep_end_columns, + ep_final_names, ep_start_columns, ep_start_touchback_columns, int_vec, @@ -28,6 +29,7 @@ scores_vec, turnover_vec, wp_end_columns, + wp_final_names, wp_start_columns, wp_start_touchback_columns, ) diff --git a/sportsdataverse/nfl/nfl_pbp.py b/sportsdataverse/nfl/nfl_pbp.py index 734c0e5..1d394ca 100755 --- a/sportsdataverse/nfl/nfl_pbp.py +++ b/sportsdataverse/nfl/nfl_pbp.py @@ -10,10 +10,31 @@ from numpy.core.fromnumeric import mean from xgboost import Booster, DMatrix +from sportsdataverse.cfb.model_vars import ( + defense_score_vec, + end_change_vec, + ep_class_to_score_mapping, + ep_end_columns, + ep_final_names, + ep_start_columns, + ep_start_touchback_columns, + int_vec, + kickoff_turnovers, + kickoff_vec, + normalplay, + offense_score_vec, + penalty, + punt_vec, + qbr_vars, + scores_vec, + turnover_vec, + wp_end_columns, + wp_final_names, + wp_start_columns, + wp_start_touchback_columns, +) from sportsdataverse.dl_utils import download, key_check -from .model_vars import * - # "td" : float(p[0]), # "opp_td" : float(p[1]), # "fg" : float(p[2]), From f7783d084f49800b89bc913c044e5aa477b036f0 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Thu, 27 Jul 2023 07:36:44 -0400 Subject: [PATCH 40/79] flake things --- .flake8 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.flake8 b/.flake8 index 055c73c..35346eb 100644 --- a/.flake8 +++ b/.flake8 @@ -2,4 +2,4 @@ max-line-length = 120 max-complexity = 18 select = B,C,E,F,W,T4,B9 -extend-ignore = E203, E266, E400, E501, W503, B905, B907 +extend-ignore = E203, E265, E266, E400, E501, W503, B905, B907 From 0a748af07503fb4e7587c95b207562040b496984 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Thu, 27 Jul 2023 09:35:41 -0400 Subject: [PATCH 41/79] Update .flake8 --- .flake8 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.flake8 b/.flake8 index 35346eb..d71e30a 100644 --- a/.flake8 +++ b/.flake8 @@ -2,4 +2,4 @@ max-line-length = 120 max-complexity = 18 select = B,C,E,F,W,T4,B9 -extend-ignore = E203, E265, E266, E400, E501, W503, B905, B907 +extend-ignore = E203, E265, E266, E400, E501, E712, W503, B905, B907 From 2eede28ec5beb05b4dda437366ae90ff682e47fb Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Sun, 30 Jul 2023 22:51:42 -0400 Subject: [PATCH 42/79] Update .flake8 --- .flake8 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.flake8 b/.flake8 index d71e30a..beb68e3 100644 --- a/.flake8 +++ b/.flake8 @@ -3,3 +3,6 @@ max-line-length = 120 max-complexity = 18 select = B,C,E,F,W,T4,B9 extend-ignore = E203, E265, E266, E400, E501, E712, W503, B905, B907 +enable-extensions = G +import-order-style = google +application-import-names = sportsdataverse,test From 8f05885c76b957b6f199e62c252f61fec4fab62f Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Sun, 30 Jul 2023 22:51:50 -0400 Subject: [PATCH 43/79] Update pyproject.toml --- pyproject.toml | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 5ea5ef7..6939426 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,3 +24,59 @@ exclude = ''' [tool.isort] profile = 'black' +[tool.semantic_release] +assets = [] +commit_message = "{version}\n\nAutomatically generated by python-semantic-release" +commit_parser = "angular" +logging_use_named_masks = false +major_on_zero = true +tag_format = "v{version}" + +[tool.semantic_release.branches.main] +match = "(main|master)" +prerelease = false +prerelease_token = "rc" + +[tool.semantic_release.changelog] +template_dir = "templates" +changelog_file = "CHANGELOG.md" +exclude_commit_patterns = [] + +[tool.semantic_release.changelog.environment] +block_start_string = "{%" +block_end_string = "%}" +variable_start_string = "{{" +variable_end_string = "}}" +comment_start_string = "{#" +comment_end_string = "#}" +trim_blocks = false +lstrip_blocks = false +newline_sequence = "\n" +keep_trailing_newline = false +extensions = [] +autoescape = true + +[tool.semantic_release.commit_author] +env = "GIT_COMMIT_AUTHOR" +default = "semantic-release " + +[tool.semantic_release.commit_parser_options] +allowed_tags = ["build", "chore", "ci", "docs", "feat", "fix", "perf", "style", "refactor", "test"] +minor_tags = ["feat"] +patch_tags = ["fix", "perf"] + +[tool.semantic_release.remote] +name = "origin" +type = "github" +ignore_token_for_push = false + +[tool.semantic_release.remote.token] +env = "GH_TOKEN" + +[tool.semantic_release.publish] +dist_glob_patterns = ["dist/*"] +upload_to_vcs_release = true + +[tool.zimports] +black-line-length = 119 +keep-unused-type-checking = true From 52fbbc1639924c6781ada3f6dcf883ecec75971a Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Sun, 30 Jul 2023 22:52:01 -0400 Subject: [PATCH 44/79] Update wnba_loaders.py --- sportsdataverse/wnba/wnba_loaders.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sportsdataverse/wnba/wnba_loaders.py b/sportsdataverse/wnba/wnba_loaders.py index 84b5c34..f50e87f 100755 --- a/sportsdataverse/wnba/wnba_loaders.py +++ b/sportsdataverse/wnba/wnba_loaders.py @@ -36,7 +36,7 @@ def load_wnba_pbp(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: for i in tqdm(seasons): if int(i) < 2002: raise SeasonNotFoundError("season cannot be less than 2002") - i_data = pd.read_parquet(WNBA_BASE_URL.format(season=i), use_pyarrow=True, columns=None) + i_data = pl.read_parquet(WNBA_BASE_URL.format(season=i), use_pyarrow=True, columns=None) data = pl.concat([data, i_data], how="vertical") return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data @@ -64,7 +64,7 @@ def load_wnba_team_boxscore(seasons: List[int], return_as_pandas=True) -> pd.Dat for i in tqdm(seasons): if int(i) < 2002: raise ValueError("season cannot be less than 2002") - i_data = pd.read_parquet(WNBA_TEAM_BOX_URL.format(season=i), use_pyarrow=True, columns=None) + i_data = pl.read_parquet(WNBA_TEAM_BOX_URL.format(season=i), use_pyarrow=True, columns=None) data = pl.concat([data, i_data], how="vertical") return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data @@ -92,7 +92,7 @@ def load_wnba_player_boxscore(seasons: List[int], return_as_pandas=True) -> pd.D for i in tqdm(seasons): if int(i) < 2002: raise ValueError("season cannot be less than 2002") - i_data = pd.read_parquet(WNBA_PLAYER_BOX_URL.format(season=i), use_pyarrow=True, columns=None) + i_data = pl.read_parquet(WNBA_PLAYER_BOX_URL.format(season=i), use_pyarrow=True, columns=None) data = pl.concat([data, i_data], how="vertical") return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data @@ -120,6 +120,6 @@ def load_wnba_schedule(seasons: List[int], return_as_pandas=True) -> pd.DataFram for i in tqdm(seasons): if int(i) < 2002: raise ValueError("season cannot be less than 2002") - i_data = pd.read_parquet(WNBA_TEAM_SCHEDULE_URL.format(season=i), use_pyarrow=True, columns=None) + i_data = pl.read_parquet(WNBA_TEAM_SCHEDULE_URL.format(season=i), use_pyarrow=True, columns=None) data = pl.concat([data, i_data], how="vertical") return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data From 32ca8f570b564ddb453d24dbc81524a463537e55 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Mon, 31 Jul 2023 01:09:15 -0400 Subject: [PATCH 45/79] return_as_pandas=False --- sportsdataverse/cfb/cfb_game_rosters.py | 2 +- sportsdataverse/cfb/cfb_loaders.py | 11 +++-- sportsdataverse/cfb/cfb_schedule.py | 6 +-- sportsdataverse/cfb/cfb_teams.py | 2 +- sportsdataverse/mbb/mbb_game_rosters.py | 2 +- sportsdataverse/mbb/mbb_loaders.py | 9 ++-- sportsdataverse/mbb/mbb_schedule.py | 6 +-- sportsdataverse/mbb/mbb_teams.py | 2 +- sportsdataverse/mlb/retrosheet.py | 12 ++--- sportsdataverse/mlb/retrosplits.py | 16 +++---- sportsdataverse/nba/nba_game_rosters.py | 2 +- sportsdataverse/nba/nba_loaders.py | 9 ++-- sportsdataverse/nba/nba_schedule.py | 4 +- sportsdataverse/nba/nba_teams.py | 2 +- sportsdataverse/nfl/nfl_game_rosters.py | 2 +- sportsdataverse/nfl/nfl_loaders.py | 55 +++++++++++------------ sportsdataverse/nfl/nfl_schedule.py | 6 +-- sportsdataverse/nfl/nfl_teams.py | 2 +- sportsdataverse/nhl/nhl_api.py | 2 +- sportsdataverse/nhl/nhl_game_rosters.py | 2 +- sportsdataverse/nhl/nhl_loaders.py | 11 +++-- sportsdataverse/nhl/nhl_schedule.py | 4 +- sportsdataverse/nhl/nhl_teams.py | 2 +- sportsdataverse/wbb/wbb_game_rosters.py | 2 +- sportsdataverse/wbb/wbb_loaders.py | 9 ++-- sportsdataverse/wbb/wbb_schedule.py | 6 +-- sportsdataverse/wbb/wbb_teams.py | 2 +- sportsdataverse/wnba/wnba_game_rosters.py | 2 +- sportsdataverse/wnba/wnba_loaders.py | 9 ++-- sportsdataverse/wnba/wnba_schedule.py | 4 +- sportsdataverse/wnba/wnba_teams.py | 2 +- 31 files changed, 100 insertions(+), 107 deletions(-) diff --git a/sportsdataverse/cfb/cfb_game_rosters.py b/sportsdataverse/cfb/cfb_game_rosters.py index 69af954..8ba3927 100644 --- a/sportsdataverse/cfb/cfb_game_rosters.py +++ b/sportsdataverse/cfb/cfb_game_rosters.py @@ -4,7 +4,7 @@ from sportsdataverse.dl_utils import download, underscore -def espn_cfb_game_rosters(game_id: int, raw=False, return_as_pandas=True, **kwargs) -> pd.DataFrame: +def espn_cfb_game_rosters(game_id: int, raw=False, return_as_pandas=False, **kwargs) -> pl.DataFrame: """espn_cfb_game_rosters() - Pull the game by id. Args: diff --git a/sportsdataverse/cfb/cfb_loaders.py b/sportsdataverse/cfb/cfb_loaders.py index c47c67a..6526cba 100755 --- a/sportsdataverse/cfb/cfb_loaders.py +++ b/sportsdataverse/cfb/cfb_loaders.py @@ -1,6 +1,5 @@ from typing import List -import pandas as pd import polars as pl from tqdm import tqdm @@ -14,7 +13,7 @@ from sportsdataverse.errors import SeasonNotFoundError -def load_cfb_pbp(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: +def load_cfb_pbp(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: """Load college football play by play data going back to 2003 Example: @@ -41,7 +40,7 @@ def load_cfb_pbp(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_cfb_schedule(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: +def load_cfb_schedule(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: """Load college football schedule data Example: @@ -69,7 +68,7 @@ def load_cfb_schedule(seasons: List[int], return_as_pandas=True) -> pd.DataFrame return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_cfb_rosters(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: +def load_cfb_rosters(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: """Load roster data Example: @@ -96,7 +95,7 @@ def load_cfb_rosters(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_cfb_team_info(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: +def load_cfb_team_info(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: """Load college football team info Example: @@ -126,7 +125,7 @@ def load_cfb_team_info(seasons: List[int], return_as_pandas=True) -> pd.DataFram return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def get_cfb_teams(return_as_pandas=True) -> pd.DataFrame: +def get_cfb_teams(return_as_pandas=False) -> pl.DataFrame: """Load college football team ID information and logos Example: diff --git a/sportsdataverse/cfb/cfb_schedule.py b/sportsdataverse/cfb/cfb_schedule.py index ffc3414..faf3d84 100755 --- a/sportsdataverse/cfb/cfb_schedule.py +++ b/sportsdataverse/cfb/cfb_schedule.py @@ -7,8 +7,8 @@ def espn_cfb_schedule( - dates=None, week=None, season_type=None, groups=None, limit=500, return_as_pandas=True, **kwargs -) -> pd.DataFrame: + dates=None, week=None, season_type=None, groups=None, limit=500, return_as_pandas=False, **kwargs +) -> pl.DataFrame: """espn_cfb_schedule - look up the college football schedule for a given season Args: @@ -121,7 +121,7 @@ def _extract_home_away(event, arg1, arg2): return event -def espn_cfb_calendar(season=None, groups=None, ondays=None, return_as_pandas=True, **kwargs) -> pd.DataFrame: +def espn_cfb_calendar(season=None, groups=None, ondays=None, return_as_pandas=False, **kwargs) -> pl.DataFrame: """espn_cfb_calendar - look up the men's college football calendar for a given season Args: diff --git a/sportsdataverse/cfb/cfb_teams.py b/sportsdataverse/cfb/cfb_teams.py index 5bfed0a..bc0dfdd 100755 --- a/sportsdataverse/cfb/cfb_teams.py +++ b/sportsdataverse/cfb/cfb_teams.py @@ -7,7 +7,7 @@ @lru_cache(maxsize=None) -def espn_cfb_teams(groups=None, return_as_pandas=True, **kwargs) -> pd.DataFrame: +def espn_cfb_teams(groups=None, return_as_pandas=False, **kwargs) -> pl.DataFrame: """espn_cfb_teams - look up the college football teams Args: diff --git a/sportsdataverse/mbb/mbb_game_rosters.py b/sportsdataverse/mbb/mbb_game_rosters.py index 6242c4f..3533d49 100755 --- a/sportsdataverse/mbb/mbb_game_rosters.py +++ b/sportsdataverse/mbb/mbb_game_rosters.py @@ -4,7 +4,7 @@ from sportsdataverse.dl_utils import download, underscore -def espn_mbb_game_rosters(game_id: int, raw=False, return_as_pandas=True, **kwargs) -> pd.DataFrame: +def espn_mbb_game_rosters(game_id: int, raw=False, return_as_pandas=False, **kwargs) -> pl.DataFrame: """espn_mbb_game_rosters() - Pull the game by id. Args: diff --git a/sportsdataverse/mbb/mbb_loaders.py b/sportsdataverse/mbb/mbb_loaders.py index ab7cba0..407141c 100755 --- a/sportsdataverse/mbb/mbb_loaders.py +++ b/sportsdataverse/mbb/mbb_loaders.py @@ -1,6 +1,5 @@ from typing import List -import pandas as pd import polars as pl from tqdm import tqdm @@ -13,7 +12,7 @@ from sportsdataverse.errors import SeasonNotFoundError -def load_mbb_pbp(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: +def load_mbb_pbp(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: """Load men's college basketball play by play data going back to 2002 Example: @@ -41,7 +40,7 @@ def load_mbb_pbp(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_mbb_team_boxscore(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: +def load_mbb_team_boxscore(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: """Load men's college basketball team boxscore data Example: @@ -69,7 +68,7 @@ def load_mbb_team_boxscore(seasons: List[int], return_as_pandas=True) -> pd.Data return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_mbb_player_boxscore(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: +def load_mbb_player_boxscore(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: """Load men's college basketball player boxscore data Example: @@ -97,7 +96,7 @@ def load_mbb_player_boxscore(seasons: List[int], return_as_pandas=True) -> pd.Da return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_mbb_schedule(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: +def load_mbb_schedule(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: """Load men's college basketball schedule data Example: diff --git a/sportsdataverse/mbb/mbb_schedule.py b/sportsdataverse/mbb/mbb_schedule.py index 4b2b8f5..0a9a3ef 100755 --- a/sportsdataverse/mbb/mbb_schedule.py +++ b/sportsdataverse/mbb/mbb_schedule.py @@ -8,8 +8,8 @@ def espn_mbb_schedule( - dates=None, groups=50, season_type=None, limit=500, return_as_pandas=True, **kwargs -) -> pd.DataFrame: + dates=None, groups=50, season_type=None, limit=500, return_as_pandas=False, **kwargs +) -> pl.DataFrame: """espn_mbb_schedule - look up the men's college basketball scheduler for a given season Args: @@ -115,7 +115,7 @@ def __extract_home_away(event, arg1, arg2): return event -def espn_mbb_calendar(season=None, ondays=None, return_as_pandas=True, **kwargs) -> pd.DataFrame: +def espn_mbb_calendar(season=None, ondays=None, return_as_pandas=False, **kwargs) -> pl.DataFrame: """espn_mbb_calendar - look up the men's college basketball calendar for a given season Args: diff --git a/sportsdataverse/mbb/mbb_teams.py b/sportsdataverse/mbb/mbb_teams.py index 1824f2c..a831423 100755 --- a/sportsdataverse/mbb/mbb_teams.py +++ b/sportsdataverse/mbb/mbb_teams.py @@ -7,7 +7,7 @@ @lru_cache(maxsize=None) -def espn_mbb_teams(groups=None, return_as_pandas=True, **kwargs) -> pd.DataFrame: +def espn_mbb_teams(groups=None, return_as_pandas=False, **kwargs) -> pl.DataFrame: """espn_mbb_teams - look up the men's college basketball teams Args: diff --git a/sportsdataverse/mlb/retrosheet.py b/sportsdataverse/mlb/retrosheet.py index c8c0b7e..9af94fa 100755 --- a/sportsdataverse/mlb/retrosheet.py +++ b/sportsdataverse/mlb/retrosheet.py @@ -15,7 +15,7 @@ from tqdm import tqdm -def retrosheet_ballparks() -> pd.DataFrame(): +def retrosheet_ballparks() -> pl.DataFrame(): """ Retrives the current TEAMABR.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. @@ -45,7 +45,7 @@ def retrosheet_ballparks() -> pd.DataFrame(): return pd.DataFrame() -def retrosheet_ejections() -> pd.DataFrame(): +def retrosheet_ejections() -> pl.DataFrame(): """ Retrives the current Ejecdata.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. @@ -77,7 +77,7 @@ def retrosheet_ejections() -> pd.DataFrame(): return pd.DataFrame() -def retrosheet_franchises() -> pd.DataFrame(): +def retrosheet_franchises() -> pl.DataFrame(): """ Retrives the current TEAMABR.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. @@ -98,7 +98,7 @@ def retrosheet_franchises() -> pd.DataFrame(): return pd.DataFrame() -def retrosheet_people() -> pd.DataFrame(): +def retrosheet_people() -> pl.DataFrame(): """ Retrives the current BioFile.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. @@ -156,7 +156,7 @@ def retrosheet_people() -> pd.DataFrame(): return pd.DataFrame() -def retrosheet_schedule(first_season: int, last_season=None, original_2020_schedule=False) -> pd.DataFrame(): +def retrosheet_schedule(first_season: int, last_season=None, original_2020_schedule=False) -> pl.DataFrame(): """ Retrives the scheduled games of an MLB season, or MLB seasons. @@ -235,7 +235,7 @@ def retrosheet_schedule(first_season: int, last_season=None, original_2020_sched def retrosheet_game_logs_team( first_season: int, last_season=None, game_type="regular", filter_out_seasons=True -) -> pd.DataFrame(): +) -> pl.DataFrame(): """ Retrives the team-level stats for MLB games in a season, or range of seasons. THIS DOES NOT GET PLAYER STATS! diff --git a/sportsdataverse/mlb/retrosplits.py b/sportsdataverse/mlb/retrosplits.py index 703bdc9..e10bf68 100755 --- a/sportsdataverse/mlb/retrosplits.py +++ b/sportsdataverse/mlb/retrosplits.py @@ -22,7 +22,7 @@ from sportsdataverse.errors import SeasonNotFoundError -def retrosplits_game_logs_player(first_season: int, last_season=None) -> pd.DataFrame(): +def retrosplits_game_logs_player(first_season: int, last_season=None) -> pl.DataFrame(): """ Retrives game-level player stats from the Retrosplits project. @@ -72,7 +72,7 @@ def retrosplits_game_logs_player(first_season: int, last_season=None) -> pd.Data return main_df -def retrosplits_game_logs_team(first_season: int, last_season=None) -> pd.DataFrame(): +def retrosplits_game_logs_team(first_season: int, last_season=None) -> pl.DataFrame(): """ Retrives game-level team stats from the Retrosplits project. @@ -124,7 +124,7 @@ def retrosplits_game_logs_team(first_season: int, last_season=None) -> pd.DataFr return main_df -def retrosplits_player_batting_by_position(first_season: int, last_season=None) -> pd.DataFrame(): +def retrosplits_player_batting_by_position(first_season: int, last_season=None) -> pl.DataFrame(): """ Retrives player-level, batting by position split stats from the Retrosplits project. The stats returned by this function are season-level stats, not game-level stats. @@ -187,7 +187,7 @@ def retrosplits_player_batting_by_position(first_season: int, last_season=None) return main_df -def retrosplits_player_batting_by_runners(first_season: int, last_season=None) -> pd.DataFrame(): +def retrosplits_player_batting_by_runners(first_season: int, last_season=None) -> pl.DataFrame(): """ Retrives player-level, batting by runners split stats from the Retrosplits project. The stats are batting stats, based off of how many runners are on base at the time of the at bat. @@ -251,7 +251,7 @@ def retrosplits_player_batting_by_runners(first_season: int, last_season=None) - return main_df -def retrosplits_player_batting_by_platoon(first_season: int, last_season=None) -> pd.DataFrame(): +def retrosplits_player_batting_by_platoon(first_season: int, last_season=None) -> pl.DataFrame(): """ Retrives player-level, batting by platoon (left/right hitting vs. left/right pitching) split stats from the Retrosplits project. The stats are batting stats, based off of the handedness of the batter vs the handedness of the pitcher. @@ -318,7 +318,7 @@ def retrosplits_player_batting_by_platoon(first_season: int, last_season=None) - return main_df -def retrosplits_player_head_to_head_stats(first_season: int, last_season=None) -> pd.DataFrame(): +def retrosplits_player_head_to_head_stats(first_season: int, last_season=None) -> pl.DataFrame(): """ Retrives batter vs. pitcher stats from the Retrosplits project. The stats are batting stats, based off of the preformance of that specific batter agianst a specific pitcher for the durration of that specific season. @@ -385,7 +385,7 @@ def retrosplits_player_head_to_head_stats(first_season: int, last_season=None) - return main_df -def retrosplits_player_pitching_by_runners(first_season: int, last_season=None) -> pd.DataFrame(): +def retrosplits_player_pitching_by_runners(first_season: int, last_season=None) -> pl.DataFrame(): """ Retrives player-level, pitching by runners split stats from the Retrosplits project. The stats are pitching stats, based off of how many runners are on base at the time of the at bat. @@ -450,7 +450,7 @@ def retrosplits_player_pitching_by_runners(first_season: int, last_season=None) return main_df -def retrosplits_player_pitching_by_platoon(first_season: int, last_season=None) -> pd.DataFrame(): +def retrosplits_player_pitching_by_platoon(first_season: int, last_season=None) -> pl.DataFrame(): """ Retrives player-level, pitching by platoon (left/right pitching vs. left/right hitting) split stats from the Retrosplits project. The stats are pitching stats, based off of the handedness of the pitcher vs the handedness of the batter. diff --git a/sportsdataverse/nba/nba_game_rosters.py b/sportsdataverse/nba/nba_game_rosters.py index c3c162b..915eb23 100644 --- a/sportsdataverse/nba/nba_game_rosters.py +++ b/sportsdataverse/nba/nba_game_rosters.py @@ -4,7 +4,7 @@ from sportsdataverse.dl_utils import download, underscore -def espn_nba_game_rosters(game_id: int, raw=False, return_as_pandas=True, **kwargs) -> pd.DataFrame: +def espn_nba_game_rosters(game_id: int, raw=False, return_as_pandas=False, **kwargs) -> pl.DataFrame: """espn_nba_game_rosters() - Pull the game by id. Args: diff --git a/sportsdataverse/nba/nba_loaders.py b/sportsdataverse/nba/nba_loaders.py index 52edb3f..2812066 100755 --- a/sportsdataverse/nba/nba_loaders.py +++ b/sportsdataverse/nba/nba_loaders.py @@ -1,6 +1,5 @@ from typing import List -import pandas as pd import polars as pl from tqdm import tqdm @@ -13,7 +12,7 @@ from sportsdataverse.errors import SeasonNotFoundError -def load_nba_pbp(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: +def load_nba_pbp(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: """Load NBA play by play data going back to 2002 Example: @@ -41,7 +40,7 @@ def load_nba_pbp(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_nba_team_boxscore(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: +def load_nba_team_boxscore(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: """Load NBA team boxscore data Example: @@ -69,7 +68,7 @@ def load_nba_team_boxscore(seasons: List[int], return_as_pandas=True) -> pd.Data return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_nba_player_boxscore(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: +def load_nba_player_boxscore(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: """Load NBA player boxscore data Example: @@ -97,7 +96,7 @@ def load_nba_player_boxscore(seasons: List[int], return_as_pandas=True) -> pd.Da return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_nba_schedule(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: +def load_nba_schedule(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: """Load NBA schedule data Example: diff --git a/sportsdataverse/nba/nba_schedule.py b/sportsdataverse/nba/nba_schedule.py index 281d448..15830fa 100755 --- a/sportsdataverse/nba/nba_schedule.py +++ b/sportsdataverse/nba/nba_schedule.py @@ -6,7 +6,7 @@ from sportsdataverse.dl_utils import download, underscore -def espn_nba_schedule(dates=None, season_type=None, limit=500, return_as_pandas=True, **kwargs) -> pd.DataFrame: +def espn_nba_schedule(dates=None, season_type=None, limit=500, return_as_pandas=False, **kwargs) -> pl.DataFrame: """espn_nba_schedule - look up the NBA schedule for a given date from ESPN Args: @@ -116,7 +116,7 @@ def __extract_home_away(event, arg1, arg2): return event -def espn_nba_calendar(season=None, ondays=None, return_as_pandas=True, **kwargs) -> pd.DataFrame: +def espn_nba_calendar(season=None, ondays=None, return_as_pandas=False, **kwargs) -> pl.DataFrame: """espn_nba_calendar - look up the NBA calendar for a given season from ESPN Args: diff --git a/sportsdataverse/nba/nba_teams.py b/sportsdataverse/nba/nba_teams.py index d8cf845..043da8e 100755 --- a/sportsdataverse/nba/nba_teams.py +++ b/sportsdataverse/nba/nba_teams.py @@ -7,7 +7,7 @@ @lru_cache(maxsize=None) -def espn_nba_teams(return_as_pandas=True, **kwargs) -> pd.DataFrame: +def espn_nba_teams(return_as_pandas=False, **kwargs) -> pl.DataFrame: """espn_nba_teams - look up NBA teams Args: diff --git a/sportsdataverse/nfl/nfl_game_rosters.py b/sportsdataverse/nfl/nfl_game_rosters.py index 81e9a52..3b7706c 100644 --- a/sportsdataverse/nfl/nfl_game_rosters.py +++ b/sportsdataverse/nfl/nfl_game_rosters.py @@ -4,7 +4,7 @@ from sportsdataverse.dl_utils import download, underscore -def espn_nfl_game_rosters(game_id: int, raw=False, return_as_pandas=True, **kwargs) -> pd.DataFrame: +def espn_nfl_game_rosters(game_id: int, raw=False, return_as_pandas=False, **kwargs) -> pl.DataFrame: """espn_nfl_game_rosters() - Pull the game by id. Args: diff --git a/sportsdataverse/nfl/nfl_loaders.py b/sportsdataverse/nfl/nfl_loaders.py index 1683895..c0c6090 100755 --- a/sportsdataverse/nfl/nfl_loaders.py +++ b/sportsdataverse/nfl/nfl_loaders.py @@ -1,7 +1,6 @@ import tempfile from typing import List -import pandas as pd import polars as pl from pyreadr import download_file, read_r from tqdm import tqdm @@ -38,7 +37,7 @@ from sportsdataverse.errors import season_not_found_error -def load_nfl_pbp(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: +def load_nfl_pbp(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: """Load NFL play by play data going back to 1999 Example: @@ -64,7 +63,7 @@ def load_nfl_pbp(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_nfl_schedule(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: +def load_nfl_schedule(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: """Load NFL schedule data Example: @@ -93,7 +92,7 @@ def load_nfl_schedule(seasons: List[int], return_as_pandas=True) -> pd.DataFrame return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_nfl_player_stats(kicking=False, return_as_pandas=True) -> pd.DataFrame: +def load_nfl_player_stats(kicking=False, return_as_pandas=False) -> pl.DataFrame: """Load NFL player stats data Example: @@ -114,7 +113,7 @@ def load_nfl_player_stats(kicking=False, return_as_pandas=True) -> pd.DataFrame: return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_nfl_ngs_passing(return_as_pandas=True) -> pd.DataFrame: +def load_nfl_ngs_passing(return_as_pandas=False) -> pl.DataFrame: """Load NFL NextGen Stats Passing data going back to 2016 Example: @@ -133,7 +132,7 @@ def load_nfl_ngs_passing(return_as_pandas=True) -> pd.DataFrame: ) -def load_nfl_ngs_rushing(return_as_pandas=True) -> pd.DataFrame: +def load_nfl_ngs_rushing(return_as_pandas=False) -> pl.DataFrame: """Load NFL NextGen Stats Rushing data going back to 2016 Example: @@ -152,7 +151,7 @@ def load_nfl_ngs_rushing(return_as_pandas=True) -> pd.DataFrame: ) -def load_nfl_ngs_receiving(return_as_pandas=True) -> pd.DataFrame: +def load_nfl_ngs_receiving(return_as_pandas=False) -> pl.DataFrame: """Load NFL NextGen Stats Receiving data going back to 2016 Example: @@ -171,7 +170,7 @@ def load_nfl_ngs_receiving(return_as_pandas=True) -> pd.DataFrame: ) -def load_nfl_pfr_pass(return_as_pandas=True) -> pd.DataFrame: +def load_nfl_pfr_pass(return_as_pandas=False) -> pl.DataFrame: """Load NFL Pro-Football Reference Advanced Passing data going back to 2018 Example: @@ -191,7 +190,7 @@ def load_nfl_pfr_pass(return_as_pandas=True) -> pd.DataFrame: ) -def load_nfl_pfr_weekly_pass(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: +def load_nfl_pfr_weekly_pass(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: """Load NFL Pro-Football Reference Weekly Advanced Passing data going back to 2018 Example: @@ -215,7 +214,7 @@ def load_nfl_pfr_weekly_pass(seasons: List[int], return_as_pandas=True) -> pd.Da return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_nfl_pfr_rush(return_as_pandas=True) -> pd.DataFrame: +def load_nfl_pfr_rush(return_as_pandas=False) -> pl.DataFrame: """Load NFL Pro-Football Reference Advanced Rushing data going back to 2018 Example: @@ -235,7 +234,7 @@ def load_nfl_pfr_rush(return_as_pandas=True) -> pd.DataFrame: ) -def load_nfl_pfr_weekly_rush(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: +def load_nfl_pfr_weekly_rush(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: """Load NFL Pro-Football Reference Weekly Advanced Rushing data going back to 2018 Example: @@ -259,7 +258,7 @@ def load_nfl_pfr_weekly_rush(seasons: List[int], return_as_pandas=True) -> pd.Da return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_nfl_pfr_rec(return_as_pandas=True) -> pd.DataFrame: +def load_nfl_pfr_rec(return_as_pandas=False) -> pl.DataFrame: """Load NFL Pro-Football Reference Advanced Receiving data going back to 2018 Example: @@ -279,7 +278,7 @@ def load_nfl_pfr_rec(return_as_pandas=True) -> pd.DataFrame: ) -def load_nfl_pfr_weekly_rec(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: +def load_nfl_pfr_weekly_rec(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: """Load NFL Pro-Football Reference Weekly Advanced Receiving data going back to 2018 Example: @@ -303,7 +302,7 @@ def load_nfl_pfr_weekly_rec(seasons: List[int], return_as_pandas=True) -> pd.Dat return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_nfl_pfr_def(return_as_pandas=True) -> pd.DataFrame: +def load_nfl_pfr_def(return_as_pandas=False) -> pl.DataFrame: """Load NFL Pro-Football Reference Advanced Defensive data going back to 2018 Example: @@ -323,7 +322,7 @@ def load_nfl_pfr_def(return_as_pandas=True) -> pd.DataFrame: ) -def load_nfl_pfr_weekly_def(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: +def load_nfl_pfr_weekly_def(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: """Load NFL Pro-Football Reference Weekly Advanced Defensive data going back to 2018 Example: @@ -347,7 +346,7 @@ def load_nfl_pfr_weekly_def(seasons: List[int], return_as_pandas=True) -> pd.Dat return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_nfl_rosters(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: +def load_nfl_rosters(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: """Load NFL roster data for all seasons Example: @@ -370,7 +369,7 @@ def load_nfl_rosters(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_nfl_weekly_rosters(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: +def load_nfl_weekly_rosters(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: """Load NFL weekly roster data for selected seasons Example: @@ -393,7 +392,7 @@ def load_nfl_weekly_rosters(seasons: List[int], return_as_pandas=True) -> pd.Dat return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_nfl_teams(return_as_pandas=True) -> pd.DataFrame: +def load_nfl_teams(return_as_pandas=False) -> pl.DataFrame: """Load NFL team ID information and logos Example: @@ -411,7 +410,7 @@ def load_nfl_teams(return_as_pandas=True) -> pd.DataFrame: ) -def load_nfl_players(return_as_pandas=True) -> pd.DataFrame: +def load_nfl_players(return_as_pandas=False) -> pl.DataFrame: """Load NFL Player ID information Example: @@ -429,7 +428,7 @@ def load_nfl_players(return_as_pandas=True) -> pd.DataFrame: ) -def load_nfl_snap_counts(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: +def load_nfl_snap_counts(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: """Load NFL snap counts data for selected seasons Example: @@ -452,7 +451,7 @@ def load_nfl_snap_counts(seasons: List[int], return_as_pandas=True) -> pd.DataFr return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_nfl_pbp_participation(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: +def load_nfl_pbp_participation(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: """Load NFL play-by-play participation data for selected seasons Example: @@ -475,7 +474,7 @@ def load_nfl_pbp_participation(seasons: List[int], return_as_pandas=True) -> pd. return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_nfl_injuries(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: +def load_nfl_injuries(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: """Load NFL injuries data for selected seasons Example: @@ -498,7 +497,7 @@ def load_nfl_injuries(seasons: List[int], return_as_pandas=True) -> pd.DataFrame return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_nfl_depth_charts(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: +def load_nfl_depth_charts(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: """Load NFL Depth Chart data for selected seasons Example: @@ -521,7 +520,7 @@ def load_nfl_depth_charts(seasons: List[int], return_as_pandas=True) -> pd.DataF return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_nfl_contracts(return_as_pandas=True) -> pd.DataFrame: +def load_nfl_contracts(return_as_pandas=False) -> pl.DataFrame: """Load NFL Historical contracts information Example: @@ -539,7 +538,7 @@ def load_nfl_contracts(return_as_pandas=True) -> pd.DataFrame: ) -def load_nfl_combine(return_as_pandas=True) -> pd.DataFrame: +def load_nfl_combine(return_as_pandas=False) -> pl.DataFrame: """Load NFL Combine information Example: @@ -557,7 +556,7 @@ def load_nfl_combine(return_as_pandas=True) -> pd.DataFrame: ) -def load_nfl_draft_picks(return_as_pandas=True) -> pd.DataFrame: +def load_nfl_draft_picks(return_as_pandas=False) -> pl.DataFrame: """Load NFL Draft picks information Example: @@ -577,7 +576,7 @@ def load_nfl_draft_picks(return_as_pandas=True) -> pd.DataFrame: ) -def load_nfl_officials(return_as_pandas=True) -> pd.DataFrame: +def load_nfl_officials(return_as_pandas=False) -> pl.DataFrame: """Load NFL Officials information Example: @@ -598,7 +597,7 @@ def load_nfl_officials(return_as_pandas=True) -> pd.DataFrame: ## Currently removed due to unsupported features of pyreadr's method. ## there is a list-column of nested tibbles within the data ## that is not supported by pyreadr -# def load_nfl_player_contracts_detail() -> pd.DataFrame: +# def load_nfl_player_contracts_detail() -> pl.DataFrame: # """Load NFL Player contracts detail information # Example: diff --git a/sportsdataverse/nfl/nfl_schedule.py b/sportsdataverse/nfl/nfl_schedule.py index e340937..df008d2 100755 --- a/sportsdataverse/nfl/nfl_schedule.py +++ b/sportsdataverse/nfl/nfl_schedule.py @@ -7,8 +7,8 @@ def espn_nfl_schedule( - dates=None, week=None, season_type=None, groups=None, limit=500, return_as_pandas=True, **kwargs -) -> pd.DataFrame: + dates=None, week=None, season_type=None, groups=None, limit=500, return_as_pandas=False, **kwargs +) -> pl.DataFrame: """espn_nfl_schedule - look up the NFL schedule for a given season Args: @@ -113,7 +113,7 @@ def _extract_home_away(event, arg1, arg2): return event -def espn_nfl_calendar(season=None, ondays=None, return_as_pandas=True, **kwargs) -> pd.DataFrame: +def espn_nfl_calendar(season=None, ondays=None, return_as_pandas=False, **kwargs) -> pl.DataFrame: """espn_nfl_calendar - look up the NFL calendar for a given season Args: diff --git a/sportsdataverse/nfl/nfl_teams.py b/sportsdataverse/nfl/nfl_teams.py index f250450..e14314c 100755 --- a/sportsdataverse/nfl/nfl_teams.py +++ b/sportsdataverse/nfl/nfl_teams.py @@ -7,7 +7,7 @@ @lru_cache(maxsize=None) -def espn_nfl_teams(return_as_pandas=True, **kwargs) -> pd.DataFrame: +def espn_nfl_teams(return_as_pandas=False, **kwargs) -> pl.DataFrame: """espn_nfl_teams - look up NFL teams Args: diff --git a/sportsdataverse/nhl/nhl_api.py b/sportsdataverse/nhl/nhl_api.py index 160ce38..edfc9e0 100755 --- a/sportsdataverse/nhl/nhl_api.py +++ b/sportsdataverse/nhl/nhl_api.py @@ -35,7 +35,7 @@ def nhl_api_pbp(game_id: int, **kwargs) -> Dict: return pbp_txt -def nhl_api_schedule(start_date: str, end_date: str, return_as_pandas=True) -> pd.DataFrame: +def nhl_api_schedule(start_date: str, end_date: str, return_as_pandas=False) -> pl.DataFrame: """nhl_api_schedule() - Pull the game by id. Data from API endpoints - `nhl/schedule` Args: diff --git a/sportsdataverse/nhl/nhl_game_rosters.py b/sportsdataverse/nhl/nhl_game_rosters.py index 798696a..97d6ef6 100644 --- a/sportsdataverse/nhl/nhl_game_rosters.py +++ b/sportsdataverse/nhl/nhl_game_rosters.py @@ -4,7 +4,7 @@ from sportsdataverse.dl_utils import download, underscore -def espn_nhl_game_rosters(game_id: int, raw=False, return_as_pandas=True, **kwargs) -> pd.DataFrame: +def espn_nhl_game_rosters(game_id: int, raw=False, return_as_pandas=False, **kwargs) -> pl.DataFrame: """espn_nhl_game_rosters() - Pull the game by id. Args: diff --git a/sportsdataverse/nhl/nhl_loaders.py b/sportsdataverse/nhl/nhl_loaders.py index 79777e3..5a39196 100755 --- a/sportsdataverse/nhl/nhl_loaders.py +++ b/sportsdataverse/nhl/nhl_loaders.py @@ -1,6 +1,5 @@ from typing import List -import pandas as pd import polars as pl from tqdm import tqdm @@ -14,7 +13,7 @@ from sportsdataverse.errors import SeasonNotFoundError -def load_nhl_pbp(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: +def load_nhl_pbp(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: """Load NHL play by play data going back to 2011 Example: @@ -41,7 +40,7 @@ def load_nhl_pbp(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_nhl_schedule(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: +def load_nhl_schedule(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: """Load NHL schedule data Example: @@ -68,7 +67,7 @@ def load_nhl_schedule(seasons: List[int], return_as_pandas=True) -> pd.DataFrame return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_nhl_team_boxscore(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: +def load_nhl_team_boxscore(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: """Load NHL team boxscore data Example: @@ -96,7 +95,7 @@ def load_nhl_team_boxscore(seasons: List[int], return_as_pandas=True) -> pd.Data return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_nhl_player_boxscore(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: +def load_nhl_player_boxscore(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: """Load NHL player boxscore data Example: @@ -124,7 +123,7 @@ def load_nhl_player_boxscore(seasons: List[int], return_as_pandas=True) -> pd.Da return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def nhl_teams(return_as_pandas=True) -> pd.DataFrame: +def nhl_teams(return_as_pandas=False) -> pl.DataFrame: """Load NHL team ID information and logos Example: diff --git a/sportsdataverse/nhl/nhl_schedule.py b/sportsdataverse/nhl/nhl_schedule.py index 606231b..b6f29e8 100755 --- a/sportsdataverse/nhl/nhl_schedule.py +++ b/sportsdataverse/nhl/nhl_schedule.py @@ -6,7 +6,7 @@ from sportsdataverse.dl_utils import download, underscore -def espn_nhl_schedule(dates=None, season_type=None, limit=500, return_as_pandas=True, **kwargs) -> pd.DataFrame: +def espn_nhl_schedule(dates=None, season_type=None, limit=500, return_as_pandas=False, **kwargs) -> pl.DataFrame: """espn_nhl_schedule - look up the NHL schedule for a given date Args: @@ -117,7 +117,7 @@ def __extract_home_away(event, arg1, arg2): return event -def espn_nhl_calendar(season=None, ondays=None, return_as_pandas=True, **kwargs) -> pd.DataFrame: +def espn_nhl_calendar(season=None, ondays=None, return_as_pandas=False, **kwargs) -> pl.DataFrame: """espn_nhl_calendar - look up the NHL calendar for a given season Args: diff --git a/sportsdataverse/nhl/nhl_teams.py b/sportsdataverse/nhl/nhl_teams.py index 672b112..cdc625f 100755 --- a/sportsdataverse/nhl/nhl_teams.py +++ b/sportsdataverse/nhl/nhl_teams.py @@ -7,7 +7,7 @@ @lru_cache(maxsize=None) -def espn_nhl_teams(return_as_pandas=True, **kwargs) -> pd.DataFrame: +def espn_nhl_teams(return_as_pandas=False, **kwargs) -> pl.DataFrame: """espn_nhl_teams - look up NHL teams Args: diff --git a/sportsdataverse/wbb/wbb_game_rosters.py b/sportsdataverse/wbb/wbb_game_rosters.py index 4ccc679..8bd4ea9 100644 --- a/sportsdataverse/wbb/wbb_game_rosters.py +++ b/sportsdataverse/wbb/wbb_game_rosters.py @@ -4,7 +4,7 @@ from sportsdataverse.dl_utils import download, underscore -def espn_wbb_game_rosters(game_id: int, raw=False, return_as_pandas=True, **kwargs) -> pd.DataFrame: +def espn_wbb_game_rosters(game_id: int, raw=False, return_as_pandas=False, **kwargs) -> pl.DataFrame: """espn_wbb_game_rosters() - Pull the game by id. Args: diff --git a/sportsdataverse/wbb/wbb_loaders.py b/sportsdataverse/wbb/wbb_loaders.py index 9ce487a..df2c40b 100755 --- a/sportsdataverse/wbb/wbb_loaders.py +++ b/sportsdataverse/wbb/wbb_loaders.py @@ -1,6 +1,5 @@ from typing import List -import pandas as pd import polars as pl from tqdm import tqdm @@ -13,7 +12,7 @@ from sportsdataverse.errors import SeasonNotFoundError -def load_wbb_pbp(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: +def load_wbb_pbp(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: """Load women's college basketball play by play data going back to 2002 Example: @@ -41,7 +40,7 @@ def load_wbb_pbp(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_wbb_team_boxscore(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: +def load_wbb_team_boxscore(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: """Load women's college basketball team boxscore data Example: @@ -69,7 +68,7 @@ def load_wbb_team_boxscore(seasons: List[int], return_as_pandas=True) -> pd.Data return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_wbb_player_boxscore(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: +def load_wbb_player_boxscore(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: """Load women's college basketball player boxscore data Example: @@ -97,7 +96,7 @@ def load_wbb_player_boxscore(seasons: List[int], return_as_pandas=True) -> pd.Da return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_wbb_schedule(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: +def load_wbb_schedule(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: """Load women's college basketball schedule data Example: diff --git a/sportsdataverse/wbb/wbb_schedule.py b/sportsdataverse/wbb/wbb_schedule.py index c1037b7..c33c2a5 100755 --- a/sportsdataverse/wbb/wbb_schedule.py +++ b/sportsdataverse/wbb/wbb_schedule.py @@ -8,8 +8,8 @@ def espn_wbb_schedule( - dates=None, groups=50, season_type=None, limit=500, return_as_pandas=True, **kwargs -) -> pd.DataFrame: + dates=None, groups=50, season_type=None, limit=500, return_as_pandas=False, **kwargs +) -> pl.DataFrame: """espn_wbb_schedule - look up the women's college basketball schedule for a given season Args: @@ -116,7 +116,7 @@ def __extract_home_away(event, arg1, arg2): return event -def espn_wbb_calendar(season=None, ondays=None, return_as_pandas=True, **kwargs) -> pd.DataFrame: +def espn_wbb_calendar(season=None, ondays=None, return_as_pandas=False, **kwargs) -> pl.DataFrame: """espn_wbb_calendar - look up the women's college basketball calendar for a given season Args: diff --git a/sportsdataverse/wbb/wbb_teams.py b/sportsdataverse/wbb/wbb_teams.py index 5c74a1c..f225e27 100755 --- a/sportsdataverse/wbb/wbb_teams.py +++ b/sportsdataverse/wbb/wbb_teams.py @@ -7,7 +7,7 @@ @lru_cache(maxsize=None) -def espn_wbb_teams(groups=None, return_as_pandas=True, **kwargs) -> pd.DataFrame: +def espn_wbb_teams(groups=None, return_as_pandas=False, **kwargs) -> pl.DataFrame: """espn_wbb_teams - look up the women's college basketball teams Args: diff --git a/sportsdataverse/wnba/wnba_game_rosters.py b/sportsdataverse/wnba/wnba_game_rosters.py index bbcd1fa..3e1a921 100644 --- a/sportsdataverse/wnba/wnba_game_rosters.py +++ b/sportsdataverse/wnba/wnba_game_rosters.py @@ -4,7 +4,7 @@ from sportsdataverse.dl_utils import download, underscore -def espn_wnba_game_rosters(game_id: int, raw=False, return_as_pandas=True, **kwargs) -> pd.DataFrame: +def espn_wnba_game_rosters(game_id: int, raw=False, return_as_pandas=False, **kwargs) -> pl.DataFrame: """espn_wnba_game_rosters() - Pull the game by id. Args: diff --git a/sportsdataverse/wnba/wnba_loaders.py b/sportsdataverse/wnba/wnba_loaders.py index f50e87f..3e2aa39 100755 --- a/sportsdataverse/wnba/wnba_loaders.py +++ b/sportsdataverse/wnba/wnba_loaders.py @@ -1,6 +1,5 @@ from typing import List -import pandas as pd import polars as pl from tqdm import tqdm @@ -13,7 +12,7 @@ from sportsdataverse.errors import SeasonNotFoundError -def load_wnba_pbp(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: +def load_wnba_pbp(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: """Load WNBA play by play data going back to 2002 Example: @@ -41,7 +40,7 @@ def load_wnba_pbp(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_wnba_team_boxscore(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: +def load_wnba_team_boxscore(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: """Load WNBA team boxscore data Example: @@ -69,7 +68,7 @@ def load_wnba_team_boxscore(seasons: List[int], return_as_pandas=True) -> pd.Dat return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_wnba_player_boxscore(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: +def load_wnba_player_boxscore(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: """Load WNBA player boxscore data Example: @@ -97,7 +96,7 @@ def load_wnba_player_boxscore(seasons: List[int], return_as_pandas=True) -> pd.D return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data -def load_wnba_schedule(seasons: List[int], return_as_pandas=True) -> pd.DataFrame: +def load_wnba_schedule(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: """Load WNBA schedule data Example: diff --git a/sportsdataverse/wnba/wnba_schedule.py b/sportsdataverse/wnba/wnba_schedule.py index 80e9eb8..c34183b 100755 --- a/sportsdataverse/wnba/wnba_schedule.py +++ b/sportsdataverse/wnba/wnba_schedule.py @@ -6,7 +6,7 @@ from sportsdataverse.dl_utils import download, underscore -def espn_wnba_schedule(dates=None, season_type=None, limit=500, return_as_pandas=True, **kwargs) -> pd.DataFrame: +def espn_wnba_schedule(dates=None, season_type=None, limit=500, return_as_pandas=False, **kwargs) -> pl.DataFrame: """espn_wnba_schedule - look up the WNBA schedule for a given season Args: @@ -113,7 +113,7 @@ def __extract_home_away(event, arg1, arg2): return event -def espn_wnba_calendar(season=None, ondays=None, return_as_pandas=True, **kwargs) -> pd.DataFrame: +def espn_wnba_calendar(season=None, ondays=None, return_as_pandas=False, **kwargs) -> pl.DataFrame: """espn_wnba_calendar - look up the WNBA calendar for a given season Args: diff --git a/sportsdataverse/wnba/wnba_teams.py b/sportsdataverse/wnba/wnba_teams.py index 7ef4b32..1397e7e 100755 --- a/sportsdataverse/wnba/wnba_teams.py +++ b/sportsdataverse/wnba/wnba_teams.py @@ -7,7 +7,7 @@ @lru_cache(maxsize=None) -def espn_wnba_teams(return_as_pandas=True, **kwargs) -> pd.DataFrame: +def espn_wnba_teams(return_as_pandas=False, **kwargs) -> pl.DataFrame: """espn_wnba_teams - look up WNBA teams Args: From 5302e8cda5408969e71997344b18501c141c2c6e Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Mon, 31 Jul 2023 01:09:40 -0400 Subject: [PATCH 46/79] docs --- .../_build/doctrees/environment.pickle | Bin 924089 -> 410014 bytes Sphinx-docs/_build/doctrees/setup.doctree | Bin 2329 -> 2328 bytes .../doctrees/sportsdataverse.cfb.doctree | Bin 102044 -> 102268 bytes .../_build/doctrees/sportsdataverse.doctree | Bin 57652 -> 50321 bytes .../doctrees/sportsdataverse.mbb.doctree | Bin 85581 -> 85780 bytes .../doctrees/sportsdataverse.mlb.doctree | Bin 167255 -> 93755 bytes .../doctrees/sportsdataverse.nba.doctree | Bin 85790 -> 4756 bytes .../doctrees/sportsdataverse.nfl.doctree | Bin 206450 -> 5520 bytes .../doctrees/sportsdataverse.nhl.doctree | Bin 102346 -> 5118 bytes .../doctrees/sportsdataverse.wbb.doctree | Bin 86056 -> 4756 bytes .../doctrees/sportsdataverse.wnba.doctree | Bin 83927 -> 4811 bytes Sphinx-docs/_build/doctrees/tests.cfb.doctree | Bin 3132 -> 3131 bytes Sphinx-docs/_build/doctrees/tests.mbb.doctree | Bin 3132 -> 3131 bytes .../_build/markdown/sportsdataverse.cfb.md | 18 +- .../_build/markdown/sportsdataverse.mbb.md | 16 +- .../_build/markdown/sportsdataverse.md | 16 - .../_build/markdown/sportsdataverse.mlb.md | 282 --------- .../_build/markdown/sportsdataverse.nba.md | 216 ------- .../_build/markdown/sportsdataverse.nfl.md | 589 ------------------ .../_build/markdown/sportsdataverse.nhl.md | 263 -------- .../_build/markdown/sportsdataverse.wbb.md | 215 ------- .../_build/markdown/sportsdataverse.wnba.md | 208 ------- docs/docs/cfb/index.md | 18 +- docs/docs/mbb/index.md | 16 +- docs/docs/mlb/index.md | 282 --------- docs/docs/nba/index.md | 216 ------- docs/docs/nfl/index.md | 589 ------------------ docs/docs/nhl/index.md | 263 -------- docs/docs/wbb/index.md | 215 ------- docs/docs/wnba/index.md | 208 ------- 30 files changed, 34 insertions(+), 3596 deletions(-) diff --git a/Sphinx-docs/_build/doctrees/environment.pickle b/Sphinx-docs/_build/doctrees/environment.pickle index 0fabdbe4fe5dadf701701c4ecc94ca1f9a3d865f..eb8e4602e58a07eeb23efb71aeaa51c01ccffa21 100755 GIT binary patch delta 50489 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 EKP0z}i2wiq literal 924089 zcmd443%FdzRVFIG^p-7Iem~;&u^qQ;sgLAWY}v=QY|D=vS@u{8BuGlTPxn5jyVa+= z-Md@Ti6Te{BoWhKLppG|JSW5ZkxRY+VR(i>NJ1ba5W*wF2a{YFxG*r`!i525=KgC{ z)vl^tRo#2HbmHjybh>+2)q2;ewN};M`)!xJ_r^6Btf7Cy%j0gn(LPm*+9w*lPJ20O z_eaC6j}02F+J58Gdqz9nJ~}fh4lk;7yQ71{jkQj--;1JJqc<8p@WkGuPc`~a4;FSk zQR!EX#YS9V)?5-R=#29vAM0~WoK#-5h(eY@wHttm!$d1fFFsRyTFE*A&!!43} z^?U8(q9Gx7L+3=)>osao4C;p)qf`B;9XC4dcytWRB+Q@cmnwsP2V{+`i$rG>Ln7Oi zn9%b?BRW~_)S}VwGUF zYb@1q@$$QN-*wl{!l6ndS}r`*z~BaYf|hy*g57LizqB7McUu)uRPI*#b+9npSWd2u zD#NYOscLIbi>SFD^(2>Th0uZcaDA}65SMBT_+EPAQ29{5(*vRF&twzl@5wBDfFAE3DprH|s_miAy7Hd9{g^uP|=^14{Sf*22pZ-v{)B+(56 zp}Qv=OC`Q^Nx!ntB1s*lPrkH~OkmeSt5ZFW@dp}lztLWzmcelCiArn07FVoaSHFIE z5hmB@;JZfk*VJ#SKaDTLZPF^rhAFH8`6L1Ec|-li`m@w?9qgA3c2qwghOX;Ht*8=5 z$%RWG<;qI(a|8b@FD~~-2YE#On)qsH`c*A@jD(>MEf;@@v<^u)>gYh zmT=YSUZ>MPQ#!5M&YXr=&y2*tjmwo77ETMGBmE@W8gtw6cUl?FWBh43FzaBVCM5D@$Q z(ZPdc$6LiU!vUW`3mP^rSK3|p%eXw~!S)4&_3cK#+o&F28J*=$m&)Je#!{PH)-f@5 zt(enFH{0bD$F#<`axLn@NzPxuAnOm+=LGD_F#YRgbXb7JR$47JdK+JlUV@yIlaSPG zQX?=w6#Eu2Qgt;qGeS$qc3b$;Dp!nA`8C(Ibp?yfZ21V|H~LQmiu8<-z_i-nW@#<>NFIh=y$5pelHS01?6;k&{YAU)t&tNyT#?L3={EH zwbx*$Ml$39q6Lu|e8>WVD`-!)Y~wK}dktDL$w6a**@%Fr@fLJOl)>ZcJL-2xpU80m z8=VR{M8oAqgJF5Xzlk=syy2B1^ePnK$p+-T5x&rp$~|RB1N{ARKSO^JAz9vk10#dq z>|l~J(9L{hv2vmT0mzH>J@vb|wSIT~9`WVQ`cBdRGVTxW*sIt)UMWCheJu~VHN;_9 z@_1=yG$Le*VUr6LOm{3Nd|3>_>IVsT+LabSk{PJx?F>`+ZPI3=%ASaaiZ)s}yAc8< z7{GCu!WA;mRgR@Z5D*np;D>++D`>Y@?ksSi%dxk~l>zA8;`$X#fWBU4DGe|D$j?d! zM098x8xtp0)onHU(hAQCfTDqdsj`SFV4gfeU}B>d4aAUYyo$-t*ONH&;!+B(wf7x3nLM0B`t+2$iA>tkb|Z0m}MQ-NDd_< zqA@-D1X>}iGMrP7eD?ufWTen35UmOS1hTYF1go?%Q8=LFd;@EWB5n@2X++fSs2HsN z-vvhkD=*dv=O{PmsYRE;-cu%@gNxE?5PJ0MO}InOriKsJ`~7abcXqZ~YnPgFEowDR z^h)ihKilpu&&s}!-aQ+|_s&8QyOx(*yQ)MOI!lRhY(Wsy0=ZP7l0KCd<_nJ&5j7p% zF^UBF$>3TeMw++6tM`RRk5H#$s#5|8)+7s5T?^}Ey8mm*|{r}`8fl4&4G zTPH8^LcdkDQ;NXO^4-msP%@wef3s>E(2{vJxqi92PN}raIc2bTUDBj|Qoqu}Rf z`g67F(;{aFN7#;$>rxtg?UQdfaG-E--?RG*5AA<;-!lgu-S@;3FYG^b=;2X<{7OAq zJ9sz!q5W;9&I&Bo!*i$Y%`_Pr!ae#Xr&vj@756H;);kbFB7e5 z)!=xlUKwr_%Snm)>9jtTtgl7bHNaxlA63}yHwsu{k>D#^#T67(V65zeWmtQ?Xds^> zzAYp5fs1SnmfJEQmXuMKQ}Ey_PQF(3?{~Vb=tR^i%LJFQD-niYAv&aeKhq;&8WF&S zZ7gV{^6g?6H_=*x7#UFoH(e^4kd$&-1n8Yl`?$;7q#axZ!b`Fp=yk1FsY z3B+w?4@5OsHRms2Tt-X3u^hoCOL-d&Cp*341Z@72^pHxsis%E5nH#RrSY(5ye|X`W zZlPZJ zEkUUII=Z-AIgZ6nluM|RfN!0ivcK&yOsy4(byYKMg=3dEOZtNNkMJPvyHeKwU9JG;Bhk1E>}3*iM3TVIo7Hg zyb~j#S_@>jUR4i-v0bSEw82}{uQXbi4FfqvXnbGEtXii?ZMg1^xm{m7bLWfF`EB7Y z?dT+NjOL36Z6xc$F>dEJ3dxeiI!PP}FN2o(thI=XROS@wc}V{I{UlMntXgi=$|weTjJy%dukfN>BiilvaRBm-T$B`DDMT|k-N;j~^s8Friu0>(6(JiCK)UHr@u)I*I z9>+ox*A+)McUMq-ZI{Wn4%#olhf^w9EB9Brs*EHHL|4MXVC+$=i`A}OlT{`py2A}g z3$Lczlb@<17S4msZ5N6K^$OjLvN?k3CCW~QmqhJdub=B-{~NA*P`AEgAxX+Ydut#T*rgvvJpRaKsK zG$J}&pLD>!7JLdS8HrOtip4-QKI{@8+3BLjpuH`VOVp5O!6qDg;dB)A!UaJ?*++o?$0D$rQpe0as&0r9)fHYsH^5i8}&8>6rfLVmn=Eg zsxI*J7<>Zz38FZk)R{J85N(?Y`^VzKHgvtZ%1!DA=X03YC#9jO>r1W90_>T0rYH?$ z5LcyItJn!?!|$v1D^|>H;qP$e0B?XBP_B*XBrFI?l9+ZLXt0&tF6<_B?Dh@fF7iiy+v>%tMi}-$t(5~Ba z+33mN9azgP8qct>fQdaGOdq-ToT!NDPGo=s-mO=|sH#BM zuS{JRAq*i(7p)l_T?`AVG;{LgN$gWod`5}9$YTjMWnw5`foeQ9_pIT)%Dmrv0?GD(3_p?I)hU)@-*V6&2QaO8W# zt9aFfRIoLK&<^DWax*pXx!v5@QQNWD9_KCN`bEP@JC(yNYR?3^K^?t*?Qk=LEUOKs z!Eim+2KM8u202BZ+V@9S{^Rfc`rq%@vqxO|=*Q;N#KMK$jlcMJn~_>$Xes;Eu)C((4nGSahOOyMz>2yS^@s@}Nywd!H z#B^(^G@Ekgvv9$)NGp3Cp3IEkc2752B4+6ik~FTSaL+r(k#cHjp@Yq{S*jaz@7H+X z)>>r>rep4Kv!_FQu{DeTZ5b~uq|e z%r@grd$w#zcj%KA2^Ie<%VnwnV=ZR(?(mR;c{Gdv6Wt4bo+W~ zRVkVIdMU&j)@wcFVJ^+$KZ^hLhz?*dAGdirYL4j;kzm2wPx5P8k+)xBmb6V2mY*Wf zkzWWX2(?Vu*H24OOUEZegw0RDN`_ZdK9Z=;n07rjgq!Csc^~Yyu3+tmNEI{s)R6Jt_TKa@!yr}_!)^!PKthm zGg3Jmhh3i*6b(v(Ubf||wb;JFBWhNbAk2qLNu|j(J10`Aknq@56bC!8XVAm4%%4GVD_b#X^_hSY zw?(N>;!1x?;ywW>U?$S*?5W7d>8OUpl>63QE28ijqsUu%{OvaIRDkkQZFv@XM^Hvu z_}bVC9(~p#1PZiMD2Vo1trB;l#aEiYDMK5d<5hxkX0k61qRw=NrByzG%06>5)eV^KuM{?Be zqfd3#^!fCpdY#jhn;7aUEK1ASCGRsr1^VLm0EfogU|{)$17r(UG?XXxs0I|nTwtgW8%D5 zn!hbAW~@t^C^p|b4#v$s%~NMf^FBAKAm*Q7KG&h3X+cd;s&AGmAl5APDMocPT+l1c zPf6kD4raXkIN7C_4%y3~W9ZeM?B{slv4(P<*DkNbec`y*T4cxk*5dPt6uV@IFQ}Vk z#}jO7oFcVX9a~~@izcr$e@7aYSFDpOM1%(npS(a?_+@>5HXw@@i5zR{j%3>y<-7jh0uC zYD<&nu@-c`1V7AP26zSgCGz&g+Nk+i=&Sh$_(SFR;T9YyMs|;bPA4lX@dzb&-UQ%k zKW_lMGF-om!(X(ID#!jl#ttu*Wqt;-mUoJHORV`D)Jl6wqLSUFBPg`3$#)?nTell} zm6P&RJNAFqSL?Wg7KgNOYO6WCh`=Al<}mFd-1^l~3+H*GLcdcGCpf$7sqT1Q7 zws>+{o8N^&%^$-b5a0Z6e1hoa_u>-(zh8X3Pkj7<`1m3I7&LzbpUoe|AJ9d+E)(d& zDL!nv6yzR^E$LL7py}rrbVAeAf@DGs8PdMZ1X{6aZSCdzsSfrM^#9f>(ND#h3G`o| zqMwcw_<0!HQ}FR;HXqI3$8XriABc}X5+9!zAAiCh(l-7SKVTbFddN#J((H`%nmq-* zf6HhSyU9&2W$U@=ZDpp{=qc#^w{xVIa>cy#Vgbu2U$dv6cjLvjugGf;lwRegcd?a~ zUZba=_nLF0m!kIE^wu-eYxETKzV;mHr64vhy@>2G%Gc~E=)LD0>7}@D0=+k+{TW4i zeuBq$6%;>qjug}Bh6xmJ=Hpuharnw!?Cd5%(epM%=eE{0zgak5I(9LEOq{-#C(iuR z^t20tt2V*SH{mxHg(KqQnD{7*j|zXtMM2U=hd{E?W;%E)l##TV?Si(KfX@tOa?&;! zZftdM;F%C*m&(mPEuuAqt-ZY0_fP+a^s~F2$Nh3sI9-6LRrf(`%CRD&^>$|By~v@pO^K*ZgQ z2zz~DVo~(mKsP*=aV%yz)Q>_*K2e9mXEOZWrtX$R)LU8VRl4=SV2L9+GZyN`A#48F zy!n5UL^*VQFQj5>dvfSXNsKrWjzeZ^LE@m-7>Rsiqd1VyVR!w2FxlY-&4obuN^X=A zl1F?3;x{qG<~QKOr?TcZqX7wlc&<=Lmf3@Mowab5wGM}7`Hrn)qvAnz^7NfJCFo=e zbWxrxeJ37>vsq??>h`r_^Or$r^TYJ#Blv?y{@{*!d9VJ(NP#NiHu&L&$Hf`+crR|} zq2tUrEx5McsNonGzHO0UlMCy`9XvP{IKO7NQOrznet5VM37{sQe=01^kAnm$`9yX& zzRAzc84d5w{7|K%d;(QGKIdY(328LHLFnx#3Bwe5)6klyrVT{7b1*#SBALko(l;_y z&9nIMTGPv+hEb@2r|IR-h+8oWIJs5ed)s4sW%G3q6#W%bq+{~)Hc5xuZ-H{?2GL!h zYL}!+t~EahtOEaE+@2ekqsXlcBJ68F0AYKl%{2a%c{2Q~*QZt- zN7{T4MB-sJUhjGEyt+&&pU;q${x1Fu@6GNP<-G;$8JnNQ;Q5QieA_v#n#j@x2%)Aa zCX1eLTh4_FRn_|P&9_t)%$jY#lFYVRim6{k3nK4d6CWQHA0HDRpWqMwY@?E$cec6l zP9--=VN}Clyz^#;7@UK};Mz5{Guw1%u%OO=+pCt36G;C_gN4mtPw zKqnA?eQv}O#OVaY*D;8oXqyw=TLyzlir#EQhI>Q!Xxzj^(W3l`EN$eE7=MB&e?R_& zg2mNvcRH39J}*ZsC^BZcRY-WebwYFB!(!JF&SWXdIpT=~g$lLn%M8nj1m7x*Aru-+ z9T_~!4-PEgI3wLjnhFCiOScLMeI~brvQDZkw}^#k7wcx^%&Jf2#_Y_hPfiQsH!)D~ zFLaS#tIc0aKnS-v1qe;Wk!cwrDzGN~vT_^Z7--kF=>j57AGj@JR5P;N0dVmXg*PC(qoAcCTuygFtu zsAP3yBeGXV=QdmH%wb_X=-zNAR!m<5?gI3VKutW*fII*EBXG~3w*ggtZt&mK>|e-V zAR!;7KVL!zPNI6%}%3Yv=H`+;|=5aAsN% zZ^b~t{VK`-#||BW)q#2CYUQ5XG&ow>GcAa;R`SP{ z%>kye5T?jQrvRa;xL{hpKvbaQzy}_`d?kmmMypCAUP76ZMm3IowobQ6|u5<*0vI89o zUc=9MSfiei;$09!V~_IKqv^52ZG14dD6(pCXTS#&JM`ara)s-de=0X&j+%dRS`e$Q z=G%j6mN&$B6&xCMRNY&vMhVsaL2mZ5s`mCts+C;0TK#NpnjEcuW?B%duGL$DTE#79 zN;l@hGz^V6O1Dp`5ki~)nw#yc+Pr0wHbIN4$$!pGkE6+doEF5YYqA*Bq`Iq6DaTT0 zXxP!@fktYW(CJNhIZ?jSWLBLPC+Sr2;VSlq+(bEwy>?mJ$qs-8rp7N&h}KJ6V-<{TL;= znt69_IvmY>&pvws&^PB>NrebN@)wQBKG`x8}y{h;_@f zAoN_>$RN)9QDiqMX(9J)A@@wB7p8p=L!$zFoUhBxe#S{K3@0Um4aJ4)hwsTvlcUu= z(}H-_v|3pfH3fggJ*3sjvZy8`qr{4OBscr#ORISDGe0-h)#@8_)8uIN4by^Hb*=75 zZ=+zI(p)92%Ez61GnDW+>|l#`|EO_7vtO561X(q^BUGnPSWy(Yx_xVIsvO;}Obg;w z)vbJ@ywZ{7iO1$kpxl=cdZh?e|OzV%2pU_N#07otueYwKN+Z zg~|fGze?GI(Cf!?v!7M3lh#;xW2VQ|>aXXf$;Ypq0TW9Ee(EUidUmq2MLwlotyddGD(F>nF&{?yK~dz=ycb#AXZ(cPKnQ& zj!vu4(pTqZC##mi1wh=$vYA?=c@B23Qmm@OYA@8WDZSe5PKjfy#(dw6`1+nT{bq=2-S~ZT6 zSiwo=dDOvPEbhQY%pN}JxpC)ebr$I;}K(}GxaO**GY5>2W@8{R}EG%R%Z zk5Sog2O}nbg+>>9t^J?av#;bWu8*JW`Gd7C-emF3tW?%BuAzH^D9=R&f_ge8u&k5IYEpaTTZw=w=}a(%y2nDkTs#4 zaBFVD95vrOEr?a0xo(7NsyW%3HrJ(LXpHx8(R^;!v#K>5)f#Gz-Pq~r+!Q(b+&?Xd zRoACm9#Z;D6?!atN+&L)f%4EF=BCKe z=kHGoV%7EO6o4$vIjv&a{qNlDWYtl)JcNhXOR?&s3ICRx4o5TpV_Fbt%}i0-(d8jg zNtgnJrs6}>rdYB(#P2zNHOoWBn~+T#Z0<_RL-${LesW`Cby9ig-rTqx)4Gd6d}Wr0 zzB)I1{u%6uazc6NaBjSgSpVI$AoN^0C^U1v%R_GXT1PFRj#@&c+wjIap%F*T5yI!5 z&dqjKjfKNkK|ayq`rcEy>2WkUm=?sUYtpR>DNWLSJUJhj3JV}B7+V$kzTBe7s@-r^ zNNSfjQcrPp)%@ML33Jr^ozsF?bv3*70Hx-9RRpEr)F?4=e=RrrSydaZB1qK+s|X*? zO_QV5Uz!%gs%tgmRB_Xpe*CE!Vz^H@Nv5nZLTK~9=4LyqHYd7aZx!MH&P|V_$^SAf zh*j65TSZXwQ)S=YRW&p$bjW1WWvdid4#QOhIYSj6uH#*wnC_1<_v*9?S9&NnJ6UxUt|H)# zcT%jnitv1HIvmYBJ1vN`W~L|s=qiFpIHv%ishFL1Es#}&Pb0MsX1u|NK}`KxXXj-d zcynPvzF*mTQ(g1Hq>3=itt-FT5v!A`2xoKSa!l);3?eAgSM_BbKc1VtjG~-SMfj22 zcpb6+;Itt0TzQ|+%=xY&IJqZmx`h8zok);l$L5~DlUo#7wHVAjC0P?rB>Yxx!W=b! zVpKdWkkxu;TXF!%iLxoL8=`roDnvFciNa?eDo zMkZ-#G&I7qgMIbZ^OL7ID_8gi2c=DK?s-LSdK^t|n-;{XYtl&~6HTg|EnDs>hlO&_ z2XhM`>kJL%o=T^R4>uydKQ~d1V(*<6#HuUS$vqRr=Eyx0ty*J5UQ^D^dRCPNb5Etx zK<;@oH${#<-!v_VRoADJU0UwdX%#wpM{ahq>L{3dLPzdQds}Wg9L=1W7DQSzQzVW$ z_Y`T$6d*Je@0fN~m$~O>zP!2T4e6(woy09H{^awSB>DWs+{*IX9q~FT`TS40aXGg2 zvkc-ZGx_{{ZuT;YazgU?hq>`OV*UMTLFmcyyFxSPJNcaT8Fh&)J4^Z*9z8Yz{olF8 zkyVqW(Bu1vBW7zt3i@xkNpqC_Kc)q->dJPW#g|BB{L@s!;{s{uE!)md;^KI{Vet8k z2`k|zsuprn<>>atX+f;IZrvzXS$udKG(5(Wj6Ri{^{kUN9OWt&Xd~B+rJl%5k)zLj z(}Gxaecmy~F{?z*n~;VFhEmewxrLBbvv*8($BGwMz4hE=IqHq31+nVtb?bV{*2f*J z4v+F~ioTqi{j7=&R|6Fr;&`~L)nRU$9Ic+67R0J+)k!|Hq^2Lt%}!P=-7s$YxvKf8 z+=Mu)`N?TPq*XITVySaek;O~_LR0aJ62~2HADtN$o8OAp1J(~T zzYQP9M$J*E;k{@dZjPh=AbHcXX>`r6PEygoN$KWIXaVFS;^Ub3D2tB@fB5Z;So5c% z@4+ZhZuzU+xTGn4O9J9ArUmit1jHAo1@R7m7%VhLQz>!k&FyXdDDKDfQhR?@p_wpt z_2qVaJ*_H=K$e z-&KX{5=$=8w{QlM>gY*3z9u)5S@m<>EllMu%(q?iZ5NV^U3j>|pcSdLQLF2#bCcny z>+rN7R$X0Fg)Lv3o1LtR*{Z^pYNy?g+Wq8Scdd@oxsf~SI5jPZv^sR0G1(1EDrkKX z3#D+4Q-IJ^Tskcya+UjzS7nvMyT|;i+^Ix1AIi;`-@=H&NvqsX=f>q2)CU;ESLQ1B zx!mk!6y=0f?)P%zb;SDVX+h{2^4mf)tF+3|n+`Lra;l?emHU_6OlH-OTIGOz@+$XF zxyf+U^$*j6Sao$xwaUF_`}v8q9UnBzu*zMZ8@Z#7Yo-N}R>u@elwRe8Yn%dvrsDn6 z+Lc)42E)txoocx~SS}-?=ry94lkDHX(lCEv^JDaJLG$DEaZ&RV^s&DAoAj}v`APAM zUdlJWsY%a@MH@YBYkp&s?uJDdI$wp0v|E7>%8vnz5`y`Qo22;piwDC?34I(@LBT@Q zBkbbRVq?j;v=zPjy(lU-+SS&eW>9$yC4pb2rSZe`=Og$NG11i;Y$km;0S^FIpHhS~Zpu z;e8o!NLsHj0rZQ6CNB{=x&u)_zet>~Fe&;MkwU*n#;-6<`WF$We-V}X7m=%f5#9P1 zF`$1DC&{nDaLZDwvjB!ztnujO(QuoPcT}^kZme~dD~)!o(ytKD!wahXv8mVT^vm(e z@dJR63ju=U>XapMtf=BLgz#@(!U>zTAh>leMzGR%fpvV zwW!*v^eX*Er;VTMYMp8uw`7hEHa`n79voiQh+l}zK4*s;%GElQ(Tm!nW251ga;4pA zKd!F~7KZCO3(csCQ2ETL`MscWe$8OGp<1uBmm<`Ln3Z+%r`ctFwShM^5h37N4#YTl zk*#!#`UR6%q$`8rMb&PX2fPID()^D=*X-e&Es*93Tsqi1Nq>hMnEB^=&vn7^rTu6b zuXXE3&qE5!F|KS@SN8Rm@VmLfL*K%GSt!lZ_k97#obDy2dRqG@4*6Rg`?qS@tYUTcvL@dun((P8$-j$4~MTg z)Qzf#ku@F~H2P7Y2#v;Zq0ttjcZ`PH8BEFQCZROn2l|J1U_jLCm#dXHD()y0J^c&I zQNP})0oqNT){+6hg1j3prPGVXLKgInhi`zf;FsvAZ#jklLUgB~-{_-00b;>^l)9Fe z8>eV`Ru+JzQYCL#ZZ&WnqFxT+OnAB1hixKQi;9_Ur58u#=u`!U8pVVXsuc#2X!r_& z$L=b?B(68HuGc8*N^k2S$Hfvb8r~?)EzmCPulciNIWr+xa2g%>l@6Ad;mrYfMxV4z zKhUG`weoVbTpq-gB{oaVcbYN{2?ly+xD~HEe!e*@Hm^! zPWO@mZ6SQH+KMXevMThDJ|fc8+R0U+gQ{9q#eVBPXuOh>>Pb^Ikw3 zyC&+Rl*VA;-%S?&LZaPdfewL%72g(WW20BFEG!R{u{bsQ%0=090 zpEYSM)IeZieegu3)u^#Fra(O2Jtl}X4h#2e6)vOIc*~?HAj@K-K-matos(^f0*SRT zVMZ1N3Y3LlZV^caHV{~T6gyJRbs{@IVHfWQY$4x$JX%547FCaDW^X3w4-!}wiai6Q zJG~geRd$}7yU!A1)W8%H4q$?TYt-v;pcx8WWxE-?Y(ErLFtHDI`cHO%Za+cr#13L) z$=BJ0_F&A=%55a*iv+LC1ILS7z=p4PW~IdH2WpAr>OlNG8eZiO<;3WSD@v4diZFg> z01O=fp#Szs<>8%v*#6$8oSx=5Bn@!9n0zEaXnWHZ>g!kX;!_gjICH)pwYpKSOhi@s z^%g=C&h1i}Ru*iaYA#E^QV69**waQYeohu;oYPU2O;%of7D=x+y7W3Ryx2L2>Y==% zGBH|YnUbQr(9VIV+>+Ax6iMu=Zl5%v)Gf-x*G`yAs-roL9;66>43_V?%Qk%^9G|;U zGe>se$RmNS*b*5sbz#gnppX}@6TheNn!qiz4<{|c1~Se9$q!g|!jB+oiNHetNP=FS z5YEf`o{+6e5=9w{WUeq+1U#A$Ak3}3*vU(Q6Lbb8kf2Z`utCdn5N`26!J%XEsvP7j zXl5tVkner`$;X~=RaTzwb*d4Hmb!Ks5*W}xpH7Z%ASl@|6&Gy3532-*#5V>-3Wm;; z?#@2QpDaBFO6e8?Bup`fW7z>}Dqfd;5U<_}pCyYYtGx(?@A5+D6s%4LZrK5AY97o! zsFcQOQS?}{T7%$eHIzp-CO(UjIa+X^spYZ}vj!EM?_extTP1wU%=uLE*4S*uRXN1(mxhc49e*6$y53I)V;E zVJ-O)DPmaVw@p$x$M&P)trPMC(alu$-bC5lmW@5`3N;6Vbnc*aY@ua_WFq5_5Wz&$ z67P5$O3u9&Z1j^XG3@Kd2x~OFE-WG;@`z8)WGHvgu{iwCSnlxlHZ>v29ch5$72*R5 zy*P=fub)s)8lN&dtK31Ia+EuK*g%z2?(p?fMVLzBsaMu=hYwq3S;<<4jn5)U zl{=`5S$vpEflj%@N4zO26Qf0zDJhzUS;`$gWYb7W<5MK@{0x>m6keIYDJ2*fv`;3QOmq*YJrEuE%cfI-)~RZlXnnvf^!FP*051<>r~DE? zoaT6+jk3Im0>l72sd;ca6__yY1ir=hBPJ1z>T!w&Cmp{ra6sL`c?5n<58h3nTLjz( zXjTbmvM&+(9&2VMP=$`?N-(!jOeK>&m|HkKO*0mOJ)u`|e>Qb0v=FG1Tci={Nu+Qn z7LDA3NKimmlLUpsBEiZnm=fL*%uR=rTR5UHa|?|~5yL7E<`xPVc~%_v3gs4dH&fXn zw;;3(A{%?$6>1JH#oR&bSWqX?GDE@mAC!dSe`;hXQIn3$>-aw$uCx&Uw-%D;_JqTg zR7X=rj{yRrvTCQh(rYZ$aa43-y&fY>ypYvBPRw9;q3#Y6%LpbhiP02i!wf=OP?D;o zqsin$0>7FI6AVGnt^k2Or{a~uCD~KpEyi|)l0gjvN4gQPfi>y;oLtlGYBIM^Fejj7 zS0DJ3h{2(|P9;K+og|M27R4qzHEBk&Im>iQBN$^wnpB$#Yw9r>PYYR~RMM-~%lM&q z%&KxN5;uoUQ>@hPAXAcIZ;-(NdQrF2!z~CI3_x`?4d4mE^g<8Yoj4SqV+upC5+J1S zhPnyEvXf-kMx>ZE&7hCUb6ly?{SlMGr8JK5W?&OyqETe9%rkhC7`Ja^@+P9ADevd3 z30JM)wxeFT-stzKSj*kBS{ru;$NK{Q6Z+vUw%PkID)w0m&-)1MHu|_1W?BlJ8ZuBQ z`zJ|yDYQ{qB-%2RXNSIwGW21z2()D=yW0%q)p9-=dQmD8vs$_EkQvrY*Okg;<(}Q; zW$ast`c77k85-<*%(B8=q}5nB6pL)hj|fYq&Qp@$ray_{nj{z)ECL?T1Z4JP3>Vxx zw|3|`lSZxXlM)Bqg2ib9fE_d_qaNYrv|w>M)z5TZ`}FwoLZ#KhA@t)JIMbs-YSldq z5;Y1-swNQ;-F}yCq0lJu5s{#BVP^J6otlNk@2MN zElSC)&Ju1Ug~d;vZF(q3aSL}Au_h^8vQF?=#iK$q?`@^p6_o zRXm-O05?Am5}+_B5+=JHw8de|85BeWAY$id}R?vv+8sYGj$Izgs5c+f^BnwQdZl&Qozkx;rri_RJG zXT~_&P|cLecd=T8`jnr9iPM|uG|Km&%|+^z+CNuPQ|f0Q94?`?#+A^}$i6n>$4%%G zTJVkmdgvlgGL6qty1HY4I_20x z_hAE7&KA0_pW0P$P+EkkjbZAQbqn2xEwilTPKu4sB1vtbQx{LT{9 zC2++Q$3S8Wom=dk?Q9cR@8Ua=c$fqmkwuQ!LML)mn3|UkC-)7~p)gnkh%Iy?fUO|g z7J3Q=6ynqsQ(d$|EHsJ~wzuFGx`IWUx4{GbW+&6m#1=YQv)frv*n4WDzsBf;}iojYQEnw=kR=wjCmD1PVFZTX6qg0U@`Js5-QN zZ+9~-P3+$j&kQ0PTig|b4aNc7K^vH$PNHRog2@Lc3Gd%iBSVS!A$L0Y2v&33i!CU3 z@=@bdKa5R^)@vj6Ppyk2@3H5gcFKEtihq~f%?GRsPZ>lQwXq$IIH7cZ6SVu5cfOB z@f?%fXq~M)7i|JrA!Yjso;*+hCMU*HwyL9PL*mwQ;;Y(OUcjx5+4QJ0Q#XUKvB_La zBT&q-9f>=;1$sOS76&{s8nGt8jr_s{NE8+cj}xdekx+}exa|~==ETXFykfXFU0P3R zM~lJ4ViEF~CZtzc?BmAH>ZE2fiYi%CqV9>+rjZ5TdN zv(P^#We|My*BCOAHwxqtgo)3hWWw{M#?WCtFdoWj1-_WFoXk@jqWGBL%@#!ehAU=SyXOtkW1`67$WapY!@yr!3Stx08ei zm2k$+_e3eBG+xxg`^M6%Cy_~b-#FdVbZBzl7_L?A>1K-`6k4ow!Rn#vqur^(p;#0J zOnznpu1OSGHm+Sk z1RX^}hI#Kd%bXD@VzO#! z**iufPnC$@jHdUF!+~ZU8-{i4e0CdS0!^cnITP=dBUOwu<~)jq0I}Te1Vg%6QA045 zih0iMRmhZGOfYy2(yIvFoM2Rd6*$W@}_4PJoDR;)93KL*99?cP8 z*4-KRAeY9M!lzhgi6=YbWFOfd5)M7FNK;g~JL8^y$#!oVrA1h>->az4vNP_%mRVM6 zXWYVPk(BI=ql++xOqQ+0RNF~_DFo=yo6m>5Wp2vJXK`5k6P@LxOqu%%Y8Ppa-+aSWRWB5 zOz5S2T3$Nb;2=bY!e9{~x6wcVTS3Mnzcvae#N9@-yJ%@xXe?4*N9zVPPpWg-pn=_a z*^}~Y${Z5N3LzW##I=@Iws28Ll968ilR9Nv+24L;7W!(pOQ1i`T2FLSboH5j*~OS0%dXrINQt-vUUquLEH&^i|^!& zF%gmVjIqW6xd$gMC^Y4+p<4vpM>dVFNx5cnOea_$Yi1@;g^uS+I9HTRCEg{RE2evz z%Yn=l!IR44vZ+&{g+Se0F@;c1B85Y-$fm4GP(W9c1ck#Q!A{j6j7+IoicN^ZP1RC; z>@&s*B8!-OXN*$_K!kgUEY+tp)fr<6gcpi2XN;|GrlrX|5j^KuYq%?Ta`QwJk2Wwt zokYtMD4ZwO$K{D=WGE58I$P(7!EEOwo)L`yR&o-)!O`2>l$OYL(g4R#3?ER)&WTHX z{e%e9_$<3r*$#Eek?r`ffhs54@%2;w*g{Zp6zF6-KH^PLnHVjyOi9sH$&&5(kWC{gjZcv@KHE`vCuTe8j^;EH*$#S44hmCX zz@p=HQe8xFErBbhjBJNn@?<+H_#nZ_c2Zpo5^O{kIU?I3a@6XV%?@?q=LAhbIur(r z0Fmtw0c-`?vYiwPNW;l?QeCt(EHsLg@yUmRg=j6De3+d~I}^zVTC>|(ASyI4XrD|p zndnsVK?sxlAe&AFS||B1(Wc!SWi}Rim3&OWuakTP5T`j-$%jOlBnNqzc9M@U?gYNY zw@N;kh>XdHfddvCoJZi-^x&c7gC4Ppt{IXK8?Tdm7|r}@0|iv^J3jePqm#)VOg@~R zrWuRKA6+sgA8B9^sFQr8`{+qDCix%|6ffB!zvHnW2k_UXB~4}j@`{vwut`;ErZA=X6_2!f=e-X&^i{>NwmyRF#ZQ6 z;e8@%WGGSdGuAVL={~|iD{^0Fa+2<@q(7+)U{~NRB!T{>#O8Su)qy!da$Y(hLO0;{ z_LQ=87%>Sus8$H-l_wSOVgzl@{=Z*3iHCIXDjVhrl_V+&6Il|15N%oIX39ioAr@iJ zntjY5eM*%+lHMlFAT|@NMVuuynn#(EgYzjhjZ_iFGdTEMU(IcjBZMC8>~=Nnd5*@H zsZWORE#X*4$4YV1T7@RWIh{9_5F49ClsE><5>abberlX! zu;ZvvXe=_G(z0RW$e1{IRP;c2sz~t}Dzb1Xf+j4F2~LV!gU2Ye1gM*7gAZtDkEX1` zMpw8`=O#)Ejp5PLNRz0V3^QvYVR8!OZd7e{uv~V6!Q%Wu(<|oY-MMeo<~;@5Li|Lk zF$wzfN#D&%g8~3|2@;Ex$1GB^rWPh1XkyHK17b@Pf&AhD5?BZYl}5n7NH?>wjO zm>g0i&S5AQgE3l9`kJ%raXq_K8jw}V3LXUP+}@#WP4zM+n?j_RWp8f~qK2$GAqtU2 z$kXPbF;-5NJr9>*h{Y{1C#IoL+o=Z`nZTw ztJt|SxE!;{a~DJNf=Y>;S-WAJjgN*hN%2~>5K+{8FXti6k@)xy{um5~7dGS3nNhL% zw(V=yoEZ(*2}E?Q`Mu~fzh*GJco{Le*=b9?5|^Ww27ITc82%gRd$##8{`+zM`w9O0 zoBa2a{FmOAa+V+Xc$S~Ma+c4~o#h>pvz#ZLWh&1yk!PDkZ#f3eZYy5C36- zG6Gq(apN0HZM-kr9sgtQ_#1urIoOH8g+=^Nx})#)jizhm<;JN9k8Z7?oYAdRBiyTA zZZ+sd75!-0g8EY~)GGv3+{PP6<>*vp8IPontwA58LF*R>ZGMip6vb8P$g0|DFE*Cc zKr*A|&k1atI?G|#rH9obylos$8piusr0#g=FJNf%P0*!KoU_X4VR)c0Ak@ms(Qpk^9P?keE4~5^d||iH65I)Y)xWK0F)LmE6Rgj%YyVNg0vyI`$MYMDK5Lv_|@UK zyAJPI!~Jy@%+Cfi;QoxDSZ3nf zFk;cAfap8W%z6Apm!^#1=p28oQMFn8i*o|kjIqF&)cutUZoCo_W{Enl-TjRV(plgA z@*|Mxnf7-swDHp{A@D?{)u@#nj{n{T7724NVr&dD~daF*Qv z*#{#$-52pFdUy=xhSoH!7mX7(RsZUPI1{w~ry zo0JQ#@Ce9RE~8#Az2;qvfySEWcxN%Bbcpu}4tDxa;x6Udeu86hv5xR>h)z}U03SS( zLpoE(2qiQTkeAz?etD7LmHD~_aHEf*%Ti*oMr^S;xi!_dq+aLFl>t$jOdKSIp3QAG z04LS(0T7WmTI^E1obL?w3LhsWV z3`{g7)ijILhh4Z%?S35G9$t|g(VNGyZEY+&O!Gb$b{?8Jl9q&9G(X|O4Vt2>sS-Ie z-1=_Ln5PJ$lRo4j*1Cb*1pRt1XlI{sjEw#Fftxs<_2S4wzI9)>3Ho_2XeVGEOa4hq z)`z?ho!a#{h!Nh(-7X_yvX|(u@1~3%XP_ScfS|rP!orf#^wx*ExVHG?o=aKA=I1RUoYDEo7jIw!VPS3BL6USp{) zULLQAVSpd=u@E8&)fHMgla+@d{H9HklP^e;)L2PU+GH5iZ~KUGs=FGXxUJtN=hHSh zt}_wR1vzrGBuG9YpkY)Qn5N(J(G-$<2&!c|4EA$Aux>CAAQqFNSFM-HN_;Z-BOmZF za_mLj4j!~_8ODFM<>)Sm32Ab)l;c7V^>%D$du;7bePoRhr41O5q;JP~Dm|*oHl<(i zfgNMQ{FGIT*FU#;-ItEOwZ-vR+hVGfUKI7p*6Zyp=zr;>&&edS;7Hy9>ay_HJ|y`V zk;V8hBY*3|k&h9HV+UM@F0RsYFHksD4V z^TC8l-{sAimQDPN52ka@K-)wbrac4xrw^KweiAhC#$0~CyvgF4D<_Lg4>h0|uYfn< zT?BYe9l@&zKUDorcN6MbA5^dZ@Iy7;(Qbm>-~;RCE0*+35pXuC-?wgp-s}VI#@|8E z@*V0X$jf{n-B>LM(!20p?gQ$bbV9QHHgLUOpN)6}zFY5O;B!9Uj^~<0 zyszHJ;2-sY&rLf=5PUD0G6)9#xDUAFu@&v9RFWY)2?&NsIazROtf=>qwu~YU1A4|s zinD%(!TIZb4Elf%v=e#-L8sR`hWVTirjyJ2g<4UDW<(P+XM*1IGk`VJy=^vy57eWE&C|S0n|%l z*9X+8_c@fB^*$1zUGHOZV;^Maz@7s+^~5Q;oHE1{K8Q{QQ9Y zTyqfYS)U->K*9%HxiEnD1`8a6WqJRcClFre$8g;scxA zy!1;bO>g%+LB@y5VL}^ZMn8cwwhS?H~9cyZLMe$ow`$ zljuM8fu4%!w21%22YD)@9ZCP04|H}HE-R$gmK9I7FZ$r-Ff^_k*ultOoEym0ad@Wk zuY4f0+ip5lVwiv9gPGmKnlJD(t?Tc6V1ogfZ6I|s_gkUaM6KI7Qx#gT`6-`RZzz1<5sIQKnjq?fIkGxRDi#9RUy zcfy{jzLs#lqG#d> zd&RU1R9&%jb(;@pa1JXF?wiC+{TF>OgSMK$w90vKHg-9WMR2DNawoDig`Y3hH#+mz+ETQ8*kZxw+ z2bpHF?E~iSQwD)K`;<)LOFpPUiz5LVTpLf%GnLu*!5pg;>$N{7sFOZWZm=Yo_S*kj z0Kza1H&{vy))tZumRMkCd=%vX>ZS2*KA>)3lcv-PY+%xMV8i6T!w1>jzeq#&?^80w zclsc@p^}7XML0bEEXEu92u2D#QY*s+H1rYThDvyYjcHLY+vuIhLOr0p{xGeIT7^AOJE=-!J)qInh8Em=g^!i68bs4O$6kQqceqc}^`c z%wO}t9IF(%#SXBoLK}nn4Id~c8cYhX0p7_>W|hvc-aLC1(tO?ZYq)2kv6d@c-5a-?__BD^ZzRK!)E8 z`+xdiyB-L#6tgfb&^34huAv;~oRemjRwqvO>5`+EivY)|dAsYUNrz?O>|o3#p1E7= zgX)~^vS~EuWt!U!K6q|~6{HgIoQ0kV-RuKA#&SrQ7NNw`d}QT=t58|OyWR)a&2kcDD2>tLPu=>bsaAUUqi+5GQmynHLEXCRnuK45OwZ@itq-MI>G@N- z_3>0IJrhc|KACEzCqU`eXLKt+;({LUq+4H1wbG-0bZeYyr6-VHu z>Cqax^#@X|^yCZO`op@F@3W?7Tjt#sqGZryxsqJ8FwZi>~dv#D0Pr%$)urCa$H1-fBRw?30(Nvz-AklfPo`SwW)j`{O{rG8wL!OjYpRv*JkYHp-O4+=baR1j{f<;C-8rCJ-=Auw zbL6`9S5vKY&{?p8H$aL$^q*`g0MYn!B)k;IE#rJAg6{cpOJvpy=g z>eh?lxTW{zRw};f)~i#kR14CrccogX7@%ACrCKT3)vZsbS}7ORt@FB-7fVVrb?ZW^ zl`;(7I!Lurh^t#)Nwrcuqg%f()k=%BZvCNDE3KbNt9~KLMLj$>U%xvgc~Qwlalb~_ zhnu>+&QgaTp3N^*k?&Yp+o~)?cq0RKS-%vu8+iQpsZn$H4TNR5MZV6Y)uAYAG~8A` zOixR}lgsh6l;?Y$Q{rVNd(eA$S-Dkt%SxFar-BXBE5_qhE&z=uoZ~I^mpsE5 z=w&C(doiwgAN_e9{kb21-ZgsXXyIL>;dBxnKUEA-+?@vDBIc{jQhQ)7plMbJ

9yA=LAN6m+Up!o=sh+jmjBpAP*rC$eSo3aCX4^0nG@H+$07xz204x5B{cMjTc zLBApA*oa?|gAaNa4L&H4!Ux3{_@Lz;AGF@!gO(0_kXOV9IVpV5+{Fh?7JT^M=`$Fv zZ7f58csI@_X-xDAkkN4KlZ{r?#u>jiv>PuyL9ax3&uGWnM=y^ShU;prMKt0F?lSlr zZh1mJZJ$QOqTBHD4jw$!XjfWg29I|PG{Ea{10GqY7t0j$OwH!k)%%LVb`8N57a8Sg51OH`2Kk_+W}f4Q}B4wqke?zzK-IlwH@ z`N2}H(IbHr%VlD}T;8#9{~Px``}~3Z<>&VuKD_^hgXr6fO1Q_Si}vEi0{-=8-u&X} zyYD(vkT1D8+AbbB8XrA$?2bov%uwsml5>d%-1^ox-*S4-&il?3?kJY-*m3L3PKJlK z5-i1Y!1CJ#eQ_Ezhf`=CP9O!6q%((%+*S(C3 zZM*NT%@l4c6eZ@Jg0LM{f54vup9vL%nJ>Ka+lY{(eNdtZp-&P7`qD>+-Ybo$Rby&) zCSrv8_&ret>upkktw*^wM-03Z4*`04eza};3;AB zZmD8hqceJ`o&r7u(ppTlQV+UpzNo6G^lzwy`Y9RnTcJ*$nJF#O?CKZWw!j23oS&xt zU22^w_n3KM&}Kdhr_U62;ZFiQVSZ;Jg(r^s^p+qYBSHZY@cXn3vUxPk zVo;%`ceB%I7n6Apl~bqL(`QQkQ+*fh4$a)jM!!xT2~MWe=|*jAyg)GoeP(#(N1+lI7H!joxKfruGb=T#7hz13p% z=GS8dpP!lEgHG4loeXWuZgvpqVWXI56H~#>-&Dzi=N>$=`X#Nol-eL_#P%7T1 zH-9s=4v5`U4~~)299X1L5S8v=vB(|5g5u`t8@|nNEqFiB0L#8@T=mA?ZNUw)SKJ0 zTh7UunVBclEAqs2p?)-TqJnxQU!l=2FyKPxMASpz)1~)B)DY-IRUp9A8igC!*ISCU zzZ01D!ok4{90oXXq0m|ML7>YP&V>kJAEWJJ%JTGjfgO*5f(YamaAA0&YJY%elql{Z z&Y(_)19~)(S&4ZP?aA~dS0+c6g~aZtfUTW{JMK7slD-+B$W`?lRd@vISpqVOaS=*U zbdx}&UgY;1LEn%rUBs?MO1B5~78hssi_RAuA?6fbt#wKx@p$z_Mx6v`&9zG=v|KpB9=S769<3g=JsgXzzpMZG^7=Z@<26; zW5`_0-xAV$x==*;g4Ygp8x$KPTA=q*;?7GXMCdO%mIVEHP3K~#xZ}v~V@YT3HQqo7 z9^L|&1zN4KxLBTl3|)(A)Gm^ppl3&6cDAr*_im9?kgk-m2=cUeQ^vBla9Tn>Q<#6u zumY*kUf)u%RK4_aQXgcr;{BQv6+&Gb{ls0QXP(vzOF5YiV`jVvR3Mh|lBRbps>wA5 zTEEkWH}hgfX9}lEYH_C%d>h>YVnPaVrM`uto44sTdKAic+IFN_44 zii*gR`l;&^BoMK3~tIjjhH)iC>wg`aLe*ZVJ^M zs-q0T@8{cDptrHguWxwa0DnuO0Jz=m3MC`$uBM1z?}>GrotS!wVi5>;QaXayVb4)@ z0O31Q5xj*^{(^%!wIaV0FP%Yu?(W^l%LaG`Zq)BJqIho4y*rVDK3DaO*4U!8`1l~}dvZHtso zC8k$;1}P`KPq`nx05x&KX~=g07a8BQW zVTKS<%8Zrg-rc(k56u;z+H~tfh5L4^z?A^gKT<>~ZbebIc$d}&wMeg(7hs_TqF%*^i6gkkp*dwuSU|%Rl zis)cY=@Fs(lj8lW6z^9GZRD;9vdEnlsIX#%_JcOwKYqLn_fdp`;C<=6VeGN!JBNlw zM2(?u-~j~;D72yz5$qgA{uTPYg91B3MraY4TL?)i^zW8}06={b7>Q0H*hY+Z%l9j7 zR52w%OvKItac_`9z-X#5xgt^*g`b4(5+5KerpC~?{vJ#y&2HgN5-0uwRzeE(qkp3K z*x$2LqytGi-m^P~SbCXz3ULEC6J;e}2nDg><-j&7>e|1{PkD@*T|*lvb)|oef>YF` z%aW(NT%OWtkm#vJWm|>_Ty80l%WbrgE72N^{WIv6GEZ$=NraDU$e+2Dc5|@F!M!Ov zX>fdnwG1w-5id4mS?Oe*DvgJG1H%s+7zGZ6F)=xb2Zeee*_dlqcx(MKvdgFyEyGIU zLa}n3rUy!pNWv>@VPbw!ISdzAi;1fA^n(uQN0B3p5^ar$mmDu(=R$@XMa{f0T>)6x zxTSE|CyKow0+UQ;@oXOmZQ3k5%_o)OEXotY6S~P;p~P8fuM~~RWzH(w0yUo{ zffdOSFCz*{0+~%r&sPbOr8W^{0wgs}7O)~7=SH>KBwkN?N6b?3D(GTop}A8y0J-r& zYMpC$OO;-)vQj*9OjUd(DD*#q(Z^{0#3xA;mrD2=7k9ucu+SQSBs%@l=;BGFvXWeK zqQw5QmE%xA8}_DNEsbI@Z^ux*l%%j+5PN5s5YeeF7XBEfpcILD3)?M;iM0Pzg<_Ar zd&N%z|M0^P8=w(%L?;^6=0+iQHa+{fkH#n^o?cvJ;2bJ%`R9Vx7I2B_hZ z{WRLg>GEDmruM$5DSwedTAH|xHmXvfzO%?UWJmRK@fQ)Z)jNZwI`t*h@`DxcNv>&WR=>u zJQgS-u1d=>u(*So*_Xx2_F}cGEC;lN;_D)Ug+8g`Mbg-diU}pR7qOegT8k608tQuk z>0axw@QjAWyFVUq$eU^dRI3tjY+eS8C!)m)wsF`z1WhIGtELMm_M4mzFOo1+&8SEA zzWJ@?9mNt&h*o8Jp;jp@?=38smU^8*x436Vi6ItuPBQU(-d8j`@VKwej9 zk@IQhfag0h{vH*G^<|lGTGvQk)mBt#i<38qA(>P<4S_uh{B+f*S~Qa}l^pS;8<+&w zjKpCVk_jA z1l&@MDG3WhP&kjEV!+CtIR+-=2+e_Ezl4|m87>PW*oH*|9Uu~xsjer)+)_A1hrYCo z3JWU~6_-jS4&Hgi73C?c;gA)MrSK%7bX7KrnkPmiPz`X991uh`%Ht8W&{&oJaMV60 z7nb8E_7;wxzycwi5>YO#QW2jPm*LPQ3Gf_LeVkJ^CMOTdjtO!-Yf?Ja6UeDS5-mdB z%CArrGU1k*ZP_Q?jLTMc#S&{)@KK@z30l8K4>swP!%@wKwB%7W?rk|iI^n_V=$sm5 zt(S8Kk%&tnKKwV}lD!6;Nsk`9kIoyRk6?)-uzY;Qq*quMmN6(nU9 zu_zT@f9Sb`oI+Ll%xpY|b!A}249Q&woFj=IUIYW`Yg&q+tlWTD&VHtj-yhsB?q=TSL z?5d!^C?7K*hr*-L@e0wtqwp}5Wcx*@8+$H38^b=wUhIWS^AWl@Bd!)s3j}A_p)p}p zu_u}+D<&_8#281ksoa8+%xO))8TdIqleXQ1Ku(Xf^Xo8at`Y##yvvETasE;(| zGnO#);0U4ekXE5^=P5+h#f1|7TMr?C`nO^Bs*_UKc%pniE_7>ZleF7uRS;#%~@sGSe}wvk<>k}bZH`r;t4=Cc}faTXcb{E1<-WJvRsMrxOpOt3ogbf zq{6O;L2Bs<81<7BuZWlfw&};S1N%qM4^UsGf*B#D@TpOgWf}Y6mZYgrgt6_|iye># zq81)hZlGL9RXNy0XQ7V`YdQ`?XYCWL>NZ*&?@ld%nwLUx0XyFS{mdel$`!X%ZxP(` zVRO996I*-KiBkcqmrxB&Y0*opwlAa)h*=UUkgz~5agXv2OF=wZfzcJ@;ojC@8OM_n z;%`qLAW^%0XCe7DXjng&Qop&lsW;?q7qv6mp(Y?mHTt|=O+OZ)mgI**R%^ucRCRe> z6BE;(f71J8M6diNAgaHa@1O$i9$#lXUA^r8bT67!~9 z#8m{_;+KDP5?9;vp`z@KllZmO?yO z#^L>yq`tuslYW9Fwpp;J#baboS;(59^DFV*S^2;N+(Lzo5tNsh5W1mi_QYMXZ5Bu8 z`wQ3GC^h_e+fMZ1y)Z0VznHGBHh8MG~TBivsEORz6=dU0}3MSd{5Jv0&V^*@y##{?#PiDZfVqLuL!$GQIEdVv6)e>oUE+S zkCTmd8<&qALw%xia&D#-E%tG})1FJlYHng-cLwhQK5h++Z9bCI)ht%(70Z{ZQ<>AH zSM;c%{y3frxt2)~xI%gs1e+w14{DloGYL)-F7BADi_3{G zmX$}1LVhbzSb_pBhb76x?6yj+MqmBhQNmtRzle*^wEK78CHEv1067*SIHIG{Vs!^k z*(y~qQa#0aVOh3NZT*gnM6vfwm6lX|D)*y1IK0zS>5d_BG22-0%H~Cg#VP=4pq)=9 zi1@FFz>OvmRv~!{keY=Q1=1O95Yw;E&G2vtln)qTchi`3a3(A>;u~M}PUx+OB*>I0 zqQH?eDiETbIBfAyVr5+$BSWj7kRV^e5YdF=N(ldor)My|_7;-a0z$CFC0FP> zE~Qq4jKOe%$+8%>=SFP~0tT1G1g~o|%&ycrlPohALyZEMp1#He5$f!Ax@4;~ z-;OXm65))Hxc)6vDDQLS9%??B*WlX+k$Hi)^J4P~Zl^eIUC{C|yJ7vHoqOvN0$S?= z;+@F4P+3BL6Za3-qeD0cQK`Mac4u`#F43-{*lbZL)+7@b1VYP}c?s^chdUf}6paIr zxRcYS0AlJ5u%fk>%#P{}bn==oHQ|AHm4~8}tWe&DLMDDHF>vYS+5C^`M2A2?ZOO8C z%22MkqLID`;Bf+|^sT{AGl43hsd#)Rm-mH{suno*lRz|a#uJqE%vLbu%ECt7M*LSi zq9lOwIK~kUwM5aOga)3PAU&)&B(J)w*={dd>Z)zV44CGpwq(9U@v3vtca7p#r=yBr zod$9j?nN2TIt^xA>ol6dtJCPlw@w#Wa0jtKitkjMYqlrxZu+8(d(F!f&{|W0g{pze zKv)iO>^;$><@S=0nH(Q2l`}Kg{?a)&3)1*ON!+hT?$IL|pTcEORjTzc88!XE zewGLc5K{UKD+dWeE*uKGyLN~`LcEMZ1b^`0;T}+d0krpYr$8EHFAX}C#*luej_3i5` z3pGX7AyuPd6}ze^S#4{9eO_Q6_Fvh5aN`&G8JUsiqpC)n= znmwTyBGxL_IAha(XF%&A+`=Fqg5n8F*3)(tTRGD3E*?_Uw`(m0RsmgbI=g}*lEhU@ zA_^TRo~WL%E+}-!$TLSQLVorDFAEb$T%VAO|8$Cmt*ALWa%53>8-*mpcy?wRV4#by zfF(xig~cxzyk7c}ff)TC?hyZ(avgM-!5e|LiX5YoE3;qCurG*S{CX{taHD9hE*uW8 z&7#ROzdABbNu=7&F*J&iIyTPzps$g;iOotep(HL7#DV=_Q3)8u(~{z~QE=Nmv(FV* z)P2s(pJc!A+qrN6Qpecsnu)m%lTu7Wn6miqXtUZClUX2=cpK!3LEVTwUZ&i4yYmtK z+0*fhTAu3;psiup*=*-xV45Yf-<=(T40N<&Ny}itk`Dk`uwF?`yIAyw#(oADA|I7? z%6+=&nCQ&RcrrSr`!euTnKPm`0TO?cvF7EZ=i$%(Hx_R)HnhmJeX;40-|d>yRgm5) zLtw8qf{}Q+TnAg)aX1M>mkj)`gS%zi`v7=|M8pKbfEd4qh&GU#Y<*e7DykSm3~Ont+~Zh`|1E+NJ4m+DFW>y% z1m}N`D)~W0p#Gn4{y!UlP{JFT#G1hH_tJ>?V;Rw`Z9G(byy$o!ODDHk3SDebW6#uO z7T@`AHz^Y&A32h&(aPAJynahAqQCKK4-w0!xdy=B$K)28WMV7&y>P}$ZoP9wS~dHnqo$_*yVo_=}nyWzvXJosKI{`UK?wBjdUeEsG9zf!I- z2EMDGJadeszdWp*zS}vQ6G?%Blylk!5-dNX+kMN`GdkWS)01xGnaKX)@$$EnmJN4gI@jX$F`#4{seF z?9$-`8x47kZ2}JH+7#dLp>u4TD-#GM4cdNk@B9|iu;Tu;<|7~PN!&ey?aR) zkzS1^lopWgrkHxy?^yehiCqBM&EYnp=V;uzk^~SI0Rgg0sSlv>dK6^kuhO7v7~)vTp3%1<(b6? z)3E*3Ox3`g)2Tphee=+c023U#Vp#o&nHs)eMk+_d3`{R-)C&e?caQ*N!l@0-^p4#@ z+K*mPn8a!)HVrOR*nqt|A%~p8kca1Q=yZ$@PY_LxU~Jx|Hkal7Bps;cmMJKq3$)y? za~YDYE$h0Y=6=|(kGiBdYYP0vm98Nl&{<_K?&kwiYDj#9hh@q3t%>V_!!kBA8xE8c zl3+Uyw{ANQe}%Tg(T{h0FT+?IUN>WLxFut8*wt9DhY-*C8(57SCX$JkelP*Ze>EHL z7?WXKUYIU?lj{!Voo!o@gFC-6D`H7^`F_X2 zo$F>w?rh1F-05mc!UALi8*+WkgIQS4D5=@D3`Dk&K^NpZp3FCMJ2FhY#klps@1S61 z%H+=R){r%I&EolR`F7*hZ^8x1ZpSt*V8pJ)2DZdrwe?pbq(OA)>i$Nnaf;u>O1!4D z?eppdUDPmcid&=pJbCNZ-;puAcBTq9Zm`(`@m=n0Wov~s;o*HYO31ZMYd5~l#%IxM zS*d`azHySG{jS3G8*Ky#+|4$?<>fm&_{FY}!zE_gozIrT7HRSn&iWt6Jv~MxfHgxtl zGN}x2L*qrko&Wan-~I=8ZwNt(?!Hh+(5xkVMEV+$@NrQiQHI~Mrd)4nmPU0O`{gM@ z4CWULB-wDcm*SgBytn<+DP6Il-Ah~HbNo>+ZdUid8&Q0~Po=GH8YbOxtEZGojcz{K z#zt9a+mv>W>fsU*+rwA&vDx>g1twicMXXFzMlpc6O+TbPC{vv>T4J-_HaSOvSr^jk z%jg)DuD00Su%vV~zc_w@&1CtVHu*xYFDWu=P6=soZwh&;mj;F__86OxQf!8r3e-j$ z7%iPC1j*Jpr$k_(r@tR9m_VjIk8l~jy8c7SYh}PxrVQAZDg!PY1eC9!`MbjZ^iTCU zo>?Kj+J_L%V<{p7Xyl2%@~L9_EF?lwNe4b=Aj^*(I3p}HAp>s?lH5`U9-i+3G~ebB zJUrinClc+ASbd>j-Pwu`%||#)z4LY#2?3GU1y@1qsc&Gi>~qE@S>^pL7(80BdN@x%J5$VSxmv{OI_k8S~RGf7_L_#Z**ZGFc9!Uj0 zkprF2(DUWzlyi{IS?=10B6#*iDiYkkCRUr$u8hInC7}Ko3=nO8TYn`XIIWzrz!LyR4Carpw?-FZkUxi< zGs=HUsF`2<)XpD^ z;;W~qSh{iQZeh};(R+^``ujt)+4t{uH6E__z(^mx!GHhV*jfxbuV!x)*i%5%ktcyD z0i!4kqSvKXILgi|itn=m9#OpP7xP9rRbZ(#XU%lZ77tgw*6KNf4w(ORCKvJ>;?gQs zJe1IDX)o@g;^XwvD+=#Z8p6-`zqDS!JuP?Pm6uG2oIvuFCEW@h7!ihr2cZ!;I5?XB z#IqWHH)&zGbU|moxM&iGO`)@D@MeEtU3;|cpHD5Hsv5@u3LIQ-#b@hJq$ zM@7uxf5k28DAvmK7Ot70UK*#*ZN~_%Cv3>cB1#~3nXfKU0N2sz zXhN}qe3=Q7{kKOMe5MHP0iluEX|k_Np!@6q8->kx>=JWo2p-+73zzU%am-aafOXgH z@cS{Y2MmuWQvn+m7#~i3jQ^0b{hm|p2SY@VhV+^d>Hg1w&XigIt%HQQ(Wb!AIBmmRzje<;#@Zg8es+kGo5`2*`dK621MGpW{)#jULH~Zrc$q zKG3b@8eB9AYURd*sJArEjou2PXGM}_bOli8k^JiHg)D_RA#%bl zWaD(kBWV$ghW`)(^`O-`Zz6V7o(0yzS$%e-PHcn2IyP3JH$E%RyYqOlDD-C+Em)2z z041|^Dq5k`TR(0S=nPHS#6|GrSWbOB>APR`oA(H6#Mf z=2CS*iW7zPMp`F|=vp7>DL6zkT1ZIkZnc;ccQ1qg5I8fK)vwu^qI+WJ106EQdY=q_ ze)*PsCaePkAhsV?fh`_ElC6mQI$_ZP-y78_O86xm_lcCF?7s5zi{e2?h7zxIZjzHC z{0W}r>5?#V`T!+f_DcP?D3Z>j0xy<#eAO^2P}O5imEFitj-T?thLksPO7}zfoPbBj zlRuA<(f}H_ElNvgODH@88xLfP;2)(xc3itaT3hnWdiAhk%H=5FdV`Y6pUzjvdtxI7 zd6L9anj>e{$vE4cxoUI*Ykfg!0L5Y%$ ziv`DCn(3IF+ZCaJi%T>;)H6Z|4_?u_#X}@B7B=mH^}x>LL>EUjYtVfgR6}Q$6y}1I zo#}VYUvbum0iMb-#TboP#q3u9OuG*=lI-AwModCawwDr%h9~rB-Ua!Zj#q}GL8Nly z>RD)q3LZvtU^G9Fryo=&6^_s|WXH>x?LUaW?~ZxN7Yqv$!^Xj6q!+Zcc>2IUp6F{NDwdP#X8;@nXXH^Ga3tzOtoOBmIe7v^cg;6N1i` zY8=R1CJ67#qk6c-M~5@?YG$t{d=;iZ#%I_PSs-r;6ZBi!C+z_Hm~M){Tw@qW7gYvN z{&v5yW(}H0YO6zVn#XA?b{?fdTd4Ao?99pJr@Ll;pf`syyJD_RQ0|aZItogaGuUGY ztZourM0Wa>Oly+GB$&KiC1l%UMsJ6id0))dNfFMOv2xhKJV_6SkRl-w$})`w7EUFpv{&TESxvLdh&O;uAh&KgV7r$30zaUa=oz_IWhZZ9H9R{^itdePKJ1 zBkdX8@XvZSwVu*7gYK|cpcPoJ>-uFA6ns~4FmI^=ES+DP2ixdcz2UF)N;3QlZc3pZ znH73nAd(0WA3p|+8CapM$4nc7+n#;M=>QB5Co4)>V7F}_j_F2oPJLvJ!me)GER?Iw z98^qiE=dEuy+Cpk_h}|SO?Vu5y?y+Zx>wM4{eL&2h3Kk`f%&e(aJ`gZ$nQD~hsQlm zOjyRleAi*Peo8RpcO8bq1N6K%Z6JuPphIyz6`>>*bSN+x=8(YA!$?Iar*`|JBYGn# zmq!fb!RQrP(3KagAXM%P0mz#M`1jlmsuBXef(po=Yq;ho6h#A@2-)@@^T7vG%?d4U zzh)}!I2I^{brmv%(K2p_)z*)8*KO^|9=I3UkqfN?30$Hr)Nw!ibE>>}O?Sq2_73Pg z!u2IuNxI4oTjmf4&v<3~)+G}u4=DAS>;SmAcI;T}3l~uKkIMUGX7(xdDiX$grrA+k zCd5ok^)6^G#uh?7#hZrol3acI=I-EZ^waS5WW0KzwWh@Lqvg)PpmKJWT3U&Zq=y^^ z+5%U?!QKQnh%70~i-k^4+;K*$$`-g+waMZMaoIOZ`4}tty;;%{ylk(1+2W9EZ}(y} zJ%Q#3c}`>k&xaoF?z8aH>pwv-Q~5|JAx1hKoeo`RMro*LQ2oW^WJQk(Q8ppm`zu%3 zqnoKcDcLlreafjcne9BIw;gY%JY>=3;r@{xPyL9VNsSax%EK;wmOS&cm)J7QK)S7g zv^u~mp#7(`KzmXjFCW*LGNAu9KGT`Rfr2$t;_zR$ONu7mz=9^P&jywpe0sHr+%8e% zJ(G>m_&y+D%iTe_gZ4r#jP!kaKVM{n*vkB+7YRpC_C z;UTpYH$r!N3p8gDc(1^clBSo0ijZmYohWt-C}K4MOUxX=AN(>*I;Wux8%lr<)v+hx zY$+K^$t~Z8jRZ&2`W3$IfUW>L7uHqVDa< zmB7ovDGXcO1x`3s4?sd$GX(Hs2;oFiI6NG6mvACz3wRZ(I|jTt99>y{81JHN<^yBQgQ^$F7;p@qM~Z^|5%7VT3AIvpT-r3LP6hKvRnBjoKy+FF~V*=whK8nm}Y zVfs6Kcy4AWyGFbjJBbR!?N6hLWy6^1jSqV015b$Bg(4lIAqg-UY3(a6JJ`=Mx3?z^ z1SC+N#YL=NoP^N6I!(-Ox(t1>dl9A$97u$E!kh9L#nB0VTIK=Qb4mp{kbKH_?Igxs zTe&R{+KNoEFL&j(oVqp=i-5@Iv052jHlaPJd~Wi5fI$cwPiBi0TRwujOln@sILgH6 zDs{JyUDRPab}qfMZGoC@xU6WR7h@lAq={mejD*DNMXH4_Kb>*AD z@QR1kieo;kt~|rA*5cU40?Esh+;WDYB+ewvBz~MBF$mDMDu$}Hr4ALb|3o5 z*rxb`(jBnPD1tEue>-f{0Q&)U*yb15jA4I8Z43ZTLCjjCSd9-*=zL^4e_b!gb)&l} z&2??ELQ49S9q{^NO$x&#Xgi8~1kCWWPieNL8*j?Pe+rqOKt{Sr1B2>EkFWdf9)yC@Jh$YyW|=HK zUysp6$-x2L`F}=HdxJy%<*k&pe970`=B>903PzNQISy>1&4}mpsLKJPDrYs{7$-0^ z77rD2@3z*wrBRF)=6tq=a(hBOKo10E=$y=jlhy9vG%WdwIiL2jqv<|W@K^^X=tDky zheQ4|o!|zPFKxt5_qN7lMK7)ylanED&gAund^t?E%DC+lSzdbx4Tn_XM;ZlFX8s!@ zeA{#l=7r*9oL>x$Zd(JO3s+yW7zl}>a4~m?-T?z%K^>@LL}yYTQGy?e=xOryyCVQe z+#(4`xO!565Lk}_8O3r+8NXMHO8KqG)nP^@BR{_#x#~@UjAA9Z(!#EOD{^%^qtb6p zu6k1-qgY9lH(sMIlsa$1F3Kcj?vxhc20@rQ5VpHCJiO8D0)Y z?$%)zcMDgXqU8fxuiqR_xnF}RNa!y-*t;ZtjDA$WQPgY$n?|I&&}toYR$GGa%TAv6 z9C>lwOGMw9iCDq}KSU%V?norNZ(N5#hy6^xfrH(x!`pkm~jyt}UrDxoRd?R$YoA_(qHnBrC)WIDR=hK%1+YG>KTclI2iv z&IQ9X&edOOEDRMz4T|Y%XvAXfYVNLrQ(~* zdS2q^HSW<7wP2BQPB;b@8d&z)5(ikpHNoYEPS zCnA{LreM2LogtJ7OHXhTyutICev<-nKzKAt+7?eIPBM5y$Z=n%t) zW2&vQ3XJbP>bNh7KFdqS{$*oq&sx*hy=_HxF#E;7C<%a#bV0SSsiQfSy~7 z-?IpPh_?o!rxI>dCMRh{je#q@Pv<3!-0~=Z+&;zNK~kFXXOx7Z`g3(4{cvnQyKJU3 zbn7-L&3}Fyo_KJ3*58|_;^>oUaAMQLA}M+z{Jd(Q^y2g!XtYQu^mFiBeFXt#DP%-# z#g?G(TL(+iZH^P*`J}O(!TA=-DD!tHdgAOJ(by8s4qOw?fucV5F3|$_oV;vd5aM;t z{Ruq@wsSHt;z+l*0+6>iD~JcZLuyh5)5loG==1!7$~;DIBSFc@7vK_3hx?xhlspXj zc5vrb@-^;N8&CipkDCu5!eZAdx4iSlI|*j!biFOW3F~hVEXqgxSrD_NPK+eMivLV% zhHgB%qH|vw+Py8jYXVS1HcZ0Mlpi%&xtQSO6&1h8!p_D+yb^~BzSQQNr|Wp)shH>3 zP5_1Q<15>g^aTL!Yu*m~2jaPz0)z|&vq2%7=HT6Pp7uq~?!eQAIcSZefNwT-)UQoI znD%Mg=$50WbSz|1BUzmHgzry@&sOk(slN4QaFF1z(*~15@7hsz@f=Ia2+WDE3tv4p zVcPY>@m3%jOH}5Y-l)EYU7|H)b&pc(SM==aY&AKV(ECX=k5b0a#bk6erHA2-VDgi9 zzLN$~4$?he(5+GuZj;&{od6{|3IQz!a^CF*nHzSOIVa;D5kD3;rU5iHJi~pOf}w`=!R(4v+0OPL3iUQ#VbI&AJ>JsvQ1)y>=m1KGeahyLPyw|+)_8R7G-fM zb!9I7Z>h5pKV5*g#pbh8hxQse^|+|_mlsDngD3Qk#m@dmyKqmQ;GG;tZ&a0>=tguz zRj_V#t1d<1;CcQ$RI3n%I1U=uxJ&~>%35XPM~ z38Cw-gAm45gAnEpUpE;8w6#T&479LEuuE4Xl9(FU0IPPL}euKgzhE06Q9`tF4aGeT+w$l=&XyuB2IH#|!h(csxXE^`%&nz zNwoJj7zDTd3))Ac_i6ae@00nIlAPi7S$odT+iRw|Typ%LmOMAgGubsSIvS#$(a8yBmgn%P_TGj-FUJdw*dy?p`7eAh zMCLzoHT~K* zF$n8z3S=w;`jwv zb+dZFAaBxnSA9GgO)I!T)ERe4RppPj55Y9AWB;rr;0u_L9%0wQtioDMkBOPV4qw&B zl>3Rp4oq&ipwQ+rLoTGNvG0S8mlhJu5i2W45S|1;>QVLdej2t7o8)VHv5yfJ!PXM8 z2JFTS3jA8|uIQYeS#n5J$1m#Rm&|bxnxC6r)Y13JvYO3T6;j#NW4Iwi72^c}nY}&F zCt!>pA_T(>NMj}^Gag3+x8u7Uoz$y0?LbUDZNx@`!ONWSC6r7XEkZexhHS`2s@G7wKgZ1o#yOHVx}pgKhEsf0u?|)WW57>da%~CngkB1{b_8iR zAY=)SYr5pmMzV**aS5co9hGz`>{)|C$ZCLGozIqZ z0_F*r4G4p3AI^NX2QWN88Ho1R3dMeqDs=SVOp(J%@<45t^Cd57mJ~Mw7*mj>L{%Y${2BR52vu?3h0zn z!S$CSc(gUIN7zQFqi7c-1I5##S#ffW$V;}MBZ<8)xh+s`W~$Y4%>#1=Aoy~d5X*Pe zRdSqb7~&Khfnz_KmhX5)c<{LiNvIVBt?y93nNNPfeAeZS+&V;;VL$;Wi8i?sL zH5O!0RkQHOW@vooB~89+;ZLy7p?QhnsG2fajEyS;1I=a4D?D3{<|G&Vz<{e(Fvn2hcAiUY)95$mAJyG8N4KIv6ln9%g~N( zxK1E__e-a(xx_v!k~g!U&!XAjN~s&kEb-QR$ zxN`=?Qz8C>-z6>5uS(zcnsO1K1Ca+IxG(X)gD1ShHprcVxR(2UU^Z{7$MrGdFyB-= z-=Ck;%i*&X#iX@j1Sky-B5{M5yrwIIJ!fvtG?*bfzP*`&So8d`(P`y2ZS> zm|-`U))XpN^3uGdmrIfL;u0yr4yFDg2GOjl88|e-&zkzst_uU0FUWO$&M}$o5@=d{ zF+V$>)_s~tlhL6cvy)jQi6IPW7DmuP3kMHpbfn#PoN*%}6NJ5QU$$8<9Wg|HI78%V zcQS)w{krKtyt4M@sstA4r98m|A1RrctZnY1@JkwUA-@iK-&#L#DJUF;xq(f8Q{YvM$zc7I!?P_Kcb3;gJ#)k~X6-Z<2z(G0q9 zx=5>jq>psyeP+V-qlRZ<5j9<=^ir0zBBUNH_rBb3o!eU@#5HI1)hT#kZM2qA-9Xe6 zC;gJ;{8|sX+a#mXIyXak$##|xn5)y}gY2tiyV-0Tz#-H(%`uyb;1G4dnhmw<@MpAw z=12qnH&Q6Nliy;P)vnmjp#OOG1fOurD}UVuXP%nKLImZ9@LDei1>@Nhl!*Mu)Mwi4lv)e}ThD)*8h7Qe3^On#z(ywU8m zMl=D%X)Tqiag60=JF6}b%pd+K!DEu~K$G&63=1vePx}R z8z&p?P7GZ%Njd|nW)9E@kJwcD$&`m*&Y*6YPrh&T!X?FMa`c9_xG*By`jTSXUSvTh zU*EHWJD!}Jcq9etM5T~aTp*^=Ne38>Isz7rN6JR8Va)1P7{>W(=|>?@$wrZUl?Z?b zUu%@n2a?b$how?E8h%8SMhf>LjK>ue>2Rj8B!fa5i;sMqn+f;J1RKPOUWnm)KWsf_ z=HARMt`#VcZ6)nu`Vx2ke7E|e93JZDCC0!zrA;WWRKNBE|C2WE7FucKRmRR?=f5IS zk&NsqLefmEKT+zCoHluh6fCbi+VO|tS;X~o{r9SVJykI_PaCPCAE!#S>!*szz`Lko z(u6Xm<)W0ANa0Fsyi)zzld794Cd67#6`O@BgzGv(7uT1nC<(nkUd+!aTu;s+6t5@Q zlVpu)S)vGF-W7?ggS~{9-XVky6aD(O(QpcFlVQD{tX>T5ZQndHAVZKv&CyPLB{ws@ zk$6kJkw}hYG(yFD&PJAX8jrGbr%|J%(_SQ@!3%s8k0#Fq4~7}7*4`z_r81$cM38~m zMX6aiMf8+1jH*|La7^IL4|J<&gK$(;McCZKjaEN)xVT2tHdt;Ob>MnN+o)LWS8%p{ z<)`pPcu^6mO4Xe05qe5}w+}I24#Osbwya7yzWG3!5z7bCJa9XZc-3x}Pw8MJ5n7CC zI%;K80h?D;%}W zjB(3M_xt3M5{`n=QrT3{y7xPL0R~pLl-82AIZ~0-xUji?C3Z~_lw7zSig1z^+9zq9ssT}Qw~fLrNw}FS<(m=Nm8k6FGsF@s}!*za;%B=>D-hBHz0i8 zU}&jrlz=10EuXiAAq>b!%CgDKxhcC*npg>y_2f9M!s(=<#k!{zrGqXKs~HGo&Txpt zKn^wC_d?=G;WR1|5KS(97id=&3`wIFfpt?^3etQ2epptHw%4z4lpOnF@a;0y-4jm7 zCyB(e=lFLu1BLrVTOW}(;iRG6mq-T_$&E%wWvscLWiUVTZcay-mVN;c?Tu^V?+l4o z19#UBd%A`sCUb?j7448{q?T|NYsTUQc}sv9{iWFt2^Ii6VO z_4?{0A_f_}Jup7=oeOWA71s(Z_WAS~##cBXaW-0_Gh9$$I3-tG4iCQ``WvY2gL1CN zwHQ*2%(?@TWXZS`cflVOIbl65Q9tmfz(A{W=6D>bu}I!B(l_;IX5@gDCLn&P_^XP0N)pQf*&{IW*_j;&tWH=z6Eh*(Il)k zN$xyyxLettqZ8R8l@GL4PBrp|HdB?suOPX3E2ICoK1&U0YQ7UB5S87J#4)-!2W6sr z_T-Y+vt?~PZ{*nm-UTf3F3hy1#Gr+^-Hh1ExnG2r(ucJ)QKgdZVbm?c_UjZI-9{_4 zWSif>uPAI%X&oj7XFnsRDD}6>xnL%6m5nm9;)urO#p5R@MoF+%y0o(B)bPnddxxA1~eo;|RzoBeH^OKYOBB|2iolhFfYb3Gg%wg|7q7^&o5tR~` z%H$bs`p)S$qY4U*jP-j29L<&9Yd#Nq_32DfRsKDl*H@aHo)95zD+-OJ02XU;3y=ve(QT8u}hl;r^IaZ69L1M-GGEuYT6oQ?lM z>2I{H^c>{+()K}aKuz=GCvQy(T=vEXlX^O?zQPmexQ5C8uF7=4bevTa>EQj2keywH zk&9DaVt1@^{I0Rb)zMUE!kHw|^bF`h=bV6fNkUTgVw+NNz1wv=wvpI<9bdqOp*McU zl<%Ln$9X570*H_3$-I(d!|@i8#AkxYiS-vX`Ig7^&VuiZdSXN}MiJ7!Slipkx6VXQ zk_#<+o_@$|o0SYZeFz}V3X7VGxdg9Dn*2sY;-I<`b?ChnAQiEOly28Vj&SSz;|F}s zunY|DEKp2O!CR07Kf%DZT}IAHiuTgQDzeJVhHA8D4=8vtBS#}IX+1FrB-GU6ox03k zk520uz2kITv6N#KS=ceUaf56PADCCk#>AO2v0eeo%8s2@mn0mmp`zA$4U04PCa9R3uHimPB2>m@IOd zhU9c1k0>kxel9^oquHVZNdt1%&rGn^&o35qJSFASB#*dceRmlu%8HH|>zYz+a$<=F ztIFXD6}@-_%OqCVH>)Fh;bF28K1PwQ*#}5)qj43TQ2_FW>lQY_k$gwqkcwMwB=VB? z)@j@*49GTv9^IxX3O={I1tV;!){K0ELU&lkR7k77%!1|BC4jx6-71__04UI zdr@%u<`}1M*Az9v!Ami_cJy}wnvty0JlWwXqd~L8=g$tO)V^dRo!nW&t|g{8SH zmiJ8jGoiUWrm_sJ+YzbV?K_I6sjcdAKl z8+W4xSt~_WS5}@Y(KsD!n5KSPk$`4OTnlAvk_A8G`8{@RhVC#uz6W$r#Y9_@cunyx zK$3#gpQ5QCVf;NIlFHS=17}6z>CAUd=iDi$+HcQ`njhSO2y7_H|3B_~oyfjgT z=0$V*uXz`WGAGhcM(y4%kmoG#bBhQCMC)MrpGWOz8WAS=bvT`~Fu-%dVP3+DL?;`4 z4#f(Bd#NI8aXlPXH$uy5`{q~ENE7?BV+Kf%QC#y8@?_P{NIN{9m*1bUi5J^CaL+#8 zSzY@PdX0{pC-zgn{AslqRgdPhLrLK*b2=`SyC%h@!qeCDJ#N-?sNpM7oYaio^Wu9e zrkiUj<{4UI=4+zfca!B3Z@a8PzpPWm2$)c6m7uQY49q3k#!lBkmvr;sd}!yp;drzf zU25iQr;R6cyxoAX_hj89Hz?Ds38jn(n%K^ihCsYQv!#(cga z!dUaRqBG1b5U0!jW$73r=yu1+bi1gB9gejP0!;>#Wp(zdDD$qu74g7}x{+xFu#hZ&HE; z|73JNZJBma%MuP@y1<%%g?fmf_eZ)NUD1v0I`XW%!_!N!5$_bX=IT0{v{=uU*}ByA2^vCpBFeJXWvf z*lu}4j-zgT-F3UAo)+`b@ryHh30t$_w#P3;wRJl)$x}euKBK+QDOb>Hi{lPl?t=>H zi{YWuW`m#4a*)v}n@cPE4 z>GnU8v0$LD%ov>%er5*!5OU@cqv9yE~^X z_&F&$1!`Y3Y5OBr?*Z3am*Xd+DM=1rXl@uAuQw=1-e{pkFG=^d=ngI~j+U#*>H_NA zO|zfGEw-ZBR=H?-c{XY>D^9Z>gu3ez*@@5!;SSAyuD}9+ z{62B@x8n}eCjR+(^H{&|Rkkq34iiN*!ccqNu8(PAdU?dxS&Ah6BGg&88z(x;+337M zL7mt<;cA<9_M)Di)hnDVtZ4S_(Cycz!56N3;Qe zw5VvvFQ-kOZsv{F`^^R8Cuk*vRN_sDwMqNPuLm32FDW2C$^ zCtalJiK(|~cW3jC7XCY1aorBR)WSD`qtsn;;U;Y&xn$z$vKqN+c=M`3QSZBYUva&4 zM@kOBY2C6CcpB=g+sz3jFsRN(%O-d00qT6mb=K|1$t8IMuPK)cZ>2QQ(AijW2)H^n zF4p(+D4K&dipUirSMQW0hz@b}Lmjukit2wA>Tf!Bh-*v7)@OpKZsd?Sa^t}IqSLfO z^NFds?kF)jvwSAZyE;M`s9isA#}x`#yfYLoW;4oOzg&(MZz?udGy0=J*8dvSe~dk0 zQ-57PJTeC|H>)$QOpE(jixw2zK@%c=Iz&n$Mva)2Olfj))A2}6oVvQ>seb6VR?&6>osD8Q z(55!BHf3l3qg@K1KF~I1LX^N?Xm4T z*xCR2*3H{*cPq|F$J=#;ul*MaKn9RWR0im>Mhy%M+o<}bB0%vU?stT&CxU`;NMS)q z6uNa8JWxmEBOe?&6V8~36}2f)c9IwIoFd-*qqcNAz`j=*i6+@rIz*l=t!yGQ;E!6M zIN;|*%1eyXr=lo(SF5Bl0{OV}c9+HD$K79$(~!{ng$9Vf7*%Lc8I~6h1H!|wKr9p* zu`U)EwMb4j`0>Y`LfssTNQB(j1#60W-6;TeG&VQYhseivI$ylu=*jYr);X%@SQML}*|Wy4sL}8m#GoAA zf|mGcXdT@=S_{tW8Hw^b=u^YaeLpXDkm~@(tCz~~I~>6`P4$2yTWCg%QwREFJ{g)h zBJCpQZE`L|&j$A9kiXyN1+Ekg<#FqYN^l68!Up$59u|{eP0>_Z!9!8gw2WRsdGUB$ zpToOzd0WUnD#ek=JWs3{7)BipSL42YwE#6Y;5hgqBwkzItnRH=^%*@i$j_RXy&Ok@ z+*sM%ZZtq|vNCDG2%5;mD2O|82~wCSP=tj)A+Toq?N{J)CJ^TAb_>^gT~d-}<;=Y1CFV&qlzbf5xOJr$tcNSO1O59Yxu2>h8;1LU_i`Z5g{5~~ zO<7J;_;&R0EcWvnV9CmOr8Zy5*x+;fl5Gn>S-Aj^!ibkcHw>@289gxDd8XzYD>Fnc zPJxedID)p)k&(~c{K+rc&1*MwUbPw(F4L&67OQ>xMlfAi>-0vj+pc#PFO7&AOOuU# z>l$f<(r+*n^6D>-=L<4Kd|SFfY*39}ir3l8yVsOZtd90{f-z3cc96`4lQ#c2rh~5C zbZg(19*>qQNCIsx-9A)A)O9&LrBe#iuP{64CAJ83Vl?xTB?N=+{?o_B>&dd-QBHIq z3MZ%C7OuX7Sag@G1?jaoP2m7;^O6ruM@T)8NdvmYk2eZ|o(;fMFu7pmC5^T=(5Ey> z5`oS}<64s+EEWb~z$W2KZgRlS6Hen`Y#?`PHUWwU(6#cmTw;#szJPn1` z^UhDzt|0;XOz#|QE@@i}>pRP5&D7J4O=h5p+g369N4m%qe5cZ0ycN@+| zWtV7*A34p$%5GkQM_>revu&^7<)92Y5{Tf5Ckl3pu88SC4#cp~bGeWczzmxX(mhv%y( z$C!pF8__{SPeC+7Eot;%B*?`c!YFLaF_Xt%!*m-AGn65l(jpIiW}uj3UEy!ev4)-^ zj`>>Vi1@G@+MWTc1u z_~}liyvTi(=)d^ZRgV?^vmz8CI08jW?xkCS3L%ZS>~~e3PR?q&>WKqZdc~Adn=K}k zUeNwUmkPcfO;7mr)r9X-&gQU}ol$NBpIB0$#Zy@xFD7Sa^d_rcgLRp4>GD*--8dxK z2*7*%IOCp^tk?w2IvnzPf;R#Ju)&vU*2i3HP%r)wZEVKD4y{7MLhQ12iF*kxur<}C zg$>qhsj43&Vu(^g;z7G?I4VW+W_ig5atl0^y)5=dY4fy#ncDfC^G|<=7(3ORi8SHd zy5pXY117sFy_06u1U*h>JI@-d18u`bdeyi#2eq7PX+6y@rqi9A zKWK&@{Yze&mlzHg_K}Qtq)LycO9wI^cA#_9;M>XEyzv1FHNk&J#0`@b&t}pZNlvwx z{RSOz;}%uuz_w{!+AI?RZ51}FC^1|Phgm>c$9sRn@q|{i^^CXp8YU=J@BqmRUp@;F z)S(?KHW=IVoAI)n<=TpwGL*Sd66Ad+?7W;Vq9=KWF7P80gJSzPY-2YqWpZeSKYIt& z?>%xrEYOz7pCPAgtYnQO&m@c%5tOhxFxQbaG^D^flFQN2a%Z>t^WExG*}oxTx?vJ; zImcd1HheUfdo4mEtkec`odL+PF>J`k!P3pbr1GIb;B#2lgeJPXT_Q%z zX7U_Ouan|Z&vc=hl678nuWq~}dhMFt(wv_ZMl4h}3V)AI8UAgun?bAiK%`{{cq`-s zyX`xz+Zjm;?kb1EGFae*n+sZ*aOI?5A7M-DE5n%1>>gi~l)II{jArFvG#+kjMYOpm z1fQt3s1LdTas~L}5R*4KHp4i8>40EKTv6aK8oJI` zFK{ogHi0%E9L_8elt@N7>rj@+zl>p31-f}yJw{-XQ(RfUg9r-NLn;pa4ZozW296GbE|V3R+K14aeqdpqPwOE{WJzDQXpq!WY;Kx7tv)T z^=c?$(rZ5rY^UHh$akG)4-$%P4apnxbY@Lh!n**MJ4a-EEU8SUArUd;x5$S~EU%f! zp5E^=>{j_Vy3dPoPBhak{x+d73;jAnPT(-7GFr+pWS?c;4MQ9Xu*R{zGS{R!O&cLR zyCTVs#0Jw~TDx0tra(~wjrJA`UaAeM@II;=eV1U1jx&e_wIS16rxu5C=jp{YNaDhz zUbsrDqoV38>k(y`rq6$_4sRXYmK>0O8Hyc#l9Wn;c3o}_uRq;2h$X$U_#djiju0tG zrq{Lta#;gO#dJqqS>Uq#w?6fjtxtZTyks_`zx$Q4>t$I{wV}U{Bd6%1kS_igkygHo zeZHfPIb$w%BH%jXXp5KEY8)0Qk1RjO@!hA+gw8r`6!-G&XpH{8rf zk@4P^*=-yV%Yv{w+SzwU%VCJH+|YPP&#x!;O0YpM=i+<`98c##a z6!|Q=F~=@@&}L&YTTh`la;StIF-+^WWnjQ>pf5$P?Y0C{*xSU}$lNVs^XwyS8JkOJ zloH8AxmW(ZMI<}2WppQn9s#BR_|6v7nI#;p*e*?9v-xUhqA@jCYO|zb+a{X9c^D6{ z)M>to$?@f|T!}*7tm$M@wv{SRu~MraOe_tFTex=@!eP>NLLp$+tEtA{OCNs2zTLW` z$NsP}+V2GglH(s?FmiDT=B3-xI-~@t_(5G9nTQ-@lFIV09unuyKtGT?3OmNKnlmtN zh2#luuA?zqSC;!7?Khb|Gi~Zp=%$E_L-v+dLSWuDb8SSQNyYL^>y_EqBRc-%XvTly zayq%JhC`P|xuz|aZXuB}iGu~%FsH+an9DYgKd=5m&)C?+LLNC(zz?P(z8h9AvdO<` znnGJ+Dl(Axc<@{k%}3?bCjC5{?UbD z{{PX;e5h{9e3dA{UucPC;n4{%AIgbj1;#%vOkjDlSYO>FCUaWlRLM)OSVC{#A_)ai zBZ=cO>)0Q?36dC>)2UJNA&H1!m?w+*8LbWQd;$6)so7M71FAOUg7#UB1Y3k1Vw6?c zMIRN!TA{r@S%KAC6Q_I*0eJk#y|!y7O_5oP1))JeloLfBCsMw+7Y7W~E8uF0fn};Y zuPD)BMQrqgTa2~uq!N(>x?{GXRQr~gcdu>*VFxld(mA2Uh@(x?P4(l=LI(<2iVg<= z%la*T0Y;Zveb3NXQwltjOpc)0CA(PyOO$x@TjeZGnCKcz{Wp58>Fj)3uS{rU7Y=jO zRNRb(LqFzc+@+dj&DY_h6K6p|NjCY{Z;7ur5#-vLlT>>R2BMGdEN{{89NEZ8y9!pT z#PWI#A&a&sjSj0#AnxIoyAV#cU!)0>^Boi0GsD%|1@32(t+*vGw91zLMWAF;5WV3i zCtqG-yt%>CFYkS)$VMx<0oPxurf8$JYGoSLTX)Nt6XfQeop$(if%QoF)ex#@wD_{U3Y*zUBa6Geco z`7`Bdfb(;D^=`HjFA_DqnpAkm!F8dJ7hi1#0*^i^DLGE?FOo13XA5*td1+o^bdrMu zGnz!^GW!NJ9e#;yDux%e6VH}RP4}5~hvFuG{PE`gNBx@kP|MC!I6A^*jh_!_yQuT? zYj(HCnZAC#Q7dOcdNqVajNQ|Iq;W@RUAz1pQ)WOpceF59DOd8Z7MC;;o_vQ<8a`bQ z9zJ{+c`li-%84iG(L$69QvZ{K`ts47Rq(i}{CQ4M4(`B#6caAMjF_k65! z8NzJ)mx630Dv7%H{^h&wm8&ODdsP1FUXRLu{kBKt2Yo7k|8)<@fBB+QElX24%_x&G-I0nnYHI9Vq{?LvRbm9JQDB0X^j<#*nFKIV;rlIK|9a z#(|(cUE}tvkP|ssV^Sr_K>D_VIAEwrAJu5ZuZ%aW*cIXgldN2Jko8;4ZzK%|kOsEB zQ6Z8JNjpmi8j-H$Vi+|(^zzjJIdb$oXk}e2XSS%597E6&j}RIzNOCX>8p($mW-Tn% z0!`cy!b@jGhjIo$=u?4 z+0)s_Q7WK1tlr5H6})}S_cYTpbBt_@W+)>A;N!>Gr)V!A+6++842NUpz}MA&GsbhD zUJh;4>;~t$_Q<}$G6DO%+%i zJ^ahV9?iac_~Z%x_vq2X@}fxT?u&chJ^K91Z%gYJt;P5Eo;>}w`r_W>@4tQcmlBE_ z$TV*BEFxby6A47A=NpkuilZr&<;FWQoV5RvXOn2spfmH`3aIkT@Cf&O$OUz=5!&rr zh1k4$=BWJ6)`^>tQR^qIXytJ1u1;vxa^_STl$plJW1zsKejVB=w9h2QH1Stf3f*lc zvi3pncFDCdz$GL6PQ^DOZ7HdjOnDZx6p)+vGRovEFwA06xmN9 z{=z>(ulYou6@0^OIwyz<*CL=SninZ%b zdPT&s3Sh~fQc&M&6crg(5WmuJ=yucxi(-*x8ULI$xyK?H0BC91(@}{LE?8)Vc46t~ zJlc$XMD%=v&uor8f^zftx;rFHnu1%RdIVjh28d|RHk%I-eI4y9d0*W~o#ol4V2tEy zP!#gB=sL2ZH_em;Gp$1Y4q zXGh}^9qdk*pYeYM_ym$F9*SoKdRcDLlv7ZKY-S-gkfA;st#*VwVAA>-2cy7IU}wUg zXn;I@L>|Co2= zn|)x%1Ys$%6Q|-b*9^(<`jT0p8ZQjm;E=C&?SeM2${ezCu^BGyXShuszXImm%11c= zjX!O$pmmy};-6?0FQyD`jHW%sH@`QRz&IZ_EkN}63=Fl=Os@wo9C4)6D1?L z(ZuWIqT^%7apkq%tL=!WLLg%OS3nXKQkCfS0(yR16q9nvZYyWMC25li2q7?$VCevups)9*5N zkem{9X0&b4OFAGJr4UJFoPot|m6EcGlr~?U8n^tx#o5ucCi)-G>1M{8rlpY$2Tbk< zxjv(Pj${kQw9^d7*3E>Qt*YI~txgM-HF8nkm^%Fx)egBA*U7i(dsg(O=l&xO%chfA z-P2g{85=ugoGFigb4z^wgF|hWqmz2od0)mj8f?&dVl$#k{QY|hkB{|Y3RvIjL>8oH zopywrj$fnRD+_N~zsn5T}6*X-b%nyf!Ar;Jfw?mXJ9bj^ghXtF?adia9edSi$} zFlJ_AEkKeys-Gg8)do|7*HyfIWHr@2nCg&kux18b&eWTADyhzLvF%yN44>ROXVyA; z=5RY8&Axp>&>2h-=E1h@A2MrAi$}emGC|8h@17cHdRxattYcW#3MD4GtQUh!4R7`jFz_qOQ9oKz zPdyuuaqicmcLTfN?_6*nDZ=z#5B=X1?z{r_b zfD)w(w6~07Juvj%WVFrL(R8nc$=^&X6{JTh_*ybervSUpJTS;nKbKOjN^6JlpIs^?0znKxRGc_Eah}_I4FxH5E+gaArlbf#2?q?n-_(ZL$ zyGe#3Ti)W!&Fa_u)=g8`4sRzuot)M52$-_OH5(Mpm8CS*i`71_YL)Y!siL>th@q*f-S*T`RN^;jmq}sN(c%rDNCJz9LI|^(TM*-x72} z|IUsn;xA_a1WF21%!G-nX=oY;1%=~TIz<0v6OEZFDqg?g=^Q924 zHmnb{?G;CZYju^Yj^qtaHm)7so@iJJseZQf6feQPbhY-bYdKdG?Cp-m|1reD%9Hwd`M5r#Qwu~Kc?6h6FlM_F=|9BqBhio(JTgY)4n{=xAPesppM!qtOsabsZ@`_zYo zKA$FVZykVN%*J%er6eonvm{9%f`8Dv1Kgh-uI9tjd8Yv!^c}$Rc)p;w_0<|@CZQ^*i zeH;nH`C@f3o2=e!kCu0~kLQcY>5D1-pWYGQ3dhN;e7b!sVZJ!31NuKYsVhj z|J;D*r;1hEushO(GF_aFi*-jqAxsyWhKu6dcF5PbawP|yqf_rD?QT%+gZ;zm_x?yE z<)3yene3f)M}tHzoyKB9ngdORoZ=k%vN62VVkXPNahr})Z|#3V)bzrhBU|m!p{+7T zDYd<5TE^&;{ZEvx8l@8}swLh0nT?0j`c*yMT=^Wo&^GJuM5#iW^}rND5=Gf+n)tmQbQFXL%E5;+Zo6BRlqyA8bnV zK@tPr3kRCthYB8oR*4S~8P$R)IkqH#J6KD#E8}8I1D?pV95W=-_-1Pf_3{O|JZ7F= zUK}6S%g(RPm{2ai>Y;F!!yGflPWYP1U^1%aE#!75*$_u^v+x;v6!Px#l14X{B|*i= zoAI>Is~UEn9sAO#3+wZC%NGgBXtk=(&R4k$q7r5;IW8TPMr; z{57pi(aZ1}>y8h&v=NKXZr`l#JXai#51Qv5Sc;-lHznFT)!RrwQ@y=LR<5&vmK3I%ul#SAO;KEM}t)nWk zf1WVHUT((R$gwD~EA+ziXpUnthf9Ydw7yIny-2M?M&I~qFa)`J67fnfB=2xGrhP!E zn%>+L4Tj`*W|ex%V;BFvv+2s+rOYg58j-hA+-7Ab^BRsbHks>M6ft%HI<(0oomOnW4N<3o{x)QND+6s8_NNcN?I`0> z;B58JcdNl8pY}}nI?HpT&li$O_d(W@x<0dQ9 z2(6i1615hGk?2}*ap+5gpd?V@MfN#*7A= zal4TlVlP>aU;{vOd3-w<*~`o3D)ZVPyNX;8zJ@!@;+FUd+$xr}W_=m4`vVdBcwpfVA!t9)wyy&};7?I$(9ZG_)K| zC!^)gK%#5ul^%rvwVj>Yf4FrkIxI3RabzR{Wp=BNE7aWGr}*RZom)5UU!uQyIy$GE z)5GP9(Sm|^WY~`+ON(I%J_qltIlB9S5K(65`}I%fJ9~E`0vY5JG_Vq9s$}{E0$5T- z5|wAQPey`fseIJ4@xH_I+Dk`_tS71D7RH##2HN$N475oYB70g-F4Q3uq(VW#%ftX5 zW9BEk4%mg?%C_lvG(EoH<#ByJBp6{v zpd3#z%TTZxg2L+I89l&6Zu_%aG7ZE>RcLw8-14B@^02w(VY%h)=9ah1E$=k9yi;!Z zNps6j$}RuU-0}~hd$!=RrH~g$g3SAA2fNYU znQLjxA%M~>Bef3t&ASf=nOoDTzcd!pb!lLhyUU|8J9TIM>@Y8i{%BoU8s6>ohS%I5 z?PQetyK@=+VLMr2pFPH2uSW!>=H#>z6As8_x4{NNep=>mVv%s6S{R+p&Q7rN4@JC< zq~{HUmtyom4h+ND628iEuM45ukoGK55uZ+0JAeG6b9Agv!*MfD=U>jo|CrC}eg8Dm zMxL3YlK0SB*eaKP%X+k&lQpXTOkv#zYA_p{vwu~#Ka$YF_4?&_@`jE-?ZZAvU{cZ+ zCd&F<7-Huih$pKiIRY2QFYqSVa6DR#C`*&g%k+#~g|r5ROI@7Jme0)`R)Vo_s}U@M zaWa`vY`aXrd4q!brYHN;`RQrB*q_WO*|zBlDC|$@9i(T^ZvWx=a~ja_KZw0M6un;0 zR(A);X0@RA4TvzFGUHWENtY5-sqg#Wv4dnG%V7q(e)%YEO8XCs)zTn;HXqm1eOgwH zC-l0d+EWuK0>T3w4pl#{)OR$vKo)|PamMoltiVsG{_dENkseG{sMLkbd9$u0W#|zh zH_JliY!v;=A%pPo#cI}ES@1GxxZ&36-7G)`GYsB?v(rviRhn?wu;Zla&2I+Wj7^%| z^LjCvW6R=Cw684t1F~kOrTHVLNIun~LutsG44+n84j%|OhF!v_3ZWkPxQypIB!rvB z6W6-ZaTt%Z3xgQN!%hRSwXX9(Zg(1pt#zFTa;MWkY%Mtush1rsL~Q(0edT+Bw3(NS z6MJ9p$P!3&wlPe;nf5Chq+9r}j@q~4RTnxpdzyikjNlm}n#xjPlCm0`b0>l-+HQHL zERKMaS*7_EO(@!|J9X8|!2JSC_UVgdzmh~fWtFqd3Lah_F^YX{ixL{c^G*v&*m1eF zZ6OWZi+oP%L|Ig!G;`ct)wSGq-?-b#3G~H}S5)gyG12 z`S!tqMKPA;$T4RX^9_tlNdOc-dJ1&&jgGMR`Hbq(oKAh|DP?l>!;oE__+L@qyu8`g z;_2)8o`L@brAw|COM1+qQ&(!k&VAZ3Jwn)N%b2JmTl+Dtwzky%oF8u(TTilO^iI>PA^K z{rW4GO?sUIJ3CX{T{#{t>mBpLO7z|*!9};H^dt+7fWx7)?j5Z(>F?ab3mTY`G90ia zrSuZZmAvYW>VADv&z6%{b#-{Ys)CSb<7nH{el{q98faH&3`%>7nB1tI;Kj>{@)C~0 zAwvy=Ryk;Lp^tfpwLM5QXGa{=qyhpk&#Ul|VzVINEh_s?XHH~5%uDl+^ICR=6qaYOk&E537| zQ?WmN;Z9v1b&bu);DaLg>1}`E&Hh+=H!>$jUSbLKulbi)05gQ!CPiiH$zkx;)7~Fc zpXo;RJtZufEw9Y#(32VHE}kqijKQMoZuN6U<|T&ds?iZtznb~>*Ft&R0V!6%$#}yK$bu|6GdpN~{SD98yL{^?XNvzA| zOs;EEysPVdT_)FdGvdj+#ko}>?lqlT)v49#_|y9Z_4b}Xq#kBfr;I#@!BcB#W=(gG z^5!jJT+hUY5^q42m%7TELH7MuyM5E>(U$2Tk+3^VWi*ttuN&$rhIhHvAc?jjH9KZy zq|{vi70}v5YJk?TKT)Dqv_JPTw%zPrQoFM3cZ{!q0kh*0TX&A9abx zs6z>F`w?xot@oJ4W^ZZ})@}yAZJqLL z5Vh{B4-e>K%o6XnS5Ge%vpIHjs@03ps-nvMEY8SU=MP&qX+y()9__jgk6(;t zlr+O^=FJyFUmeG%bZx*Q5}g)z){ktavG{Xm+v1?J=Vo)7(ENS1r_K;f#BHOj>m=UP zHj}3tlB3g7@gt4qIvA~peowo|ecJI4%hr0MN8e4(-&mQz#*(8 zjv)affI4PNM8RWa&{Ww$2{|hT6nQ2FFs_Z~QfBH=%PU&0`Q%I37ih_e_>#`A9+?oy z?hZr~@tET(N;yUH5=mr1i40~VJvS5^=8C7V6Plcs&W!Oalw-(?57*i;E(Efth6aqg z%u$hT>oBGuA7ytv9aHy_82c*4TFOC-f&KE)y)6wwaY>Ec^UCA0(IW=JOgzS*$w`Wa zG^82IoX~m$A_v8+HQT)LwPq(1D*f07dt6$CPom`B*_q7f()LgBh4>O$5l5Lt(Y@$) zBs0m!a6RT_WYl(YB>AwP`OFm0IonKOj$w+6%oRDPSbe=;ms2P&k#>p*RHj)pKTth8 zAF2G#BNQ6}Z(`!;RDxzIRwV19+#~sh2P!oaM7&Y$?d{RDbB|7)DN)_xO?5(sd5O}! zjDb{*4i0QayG&30mo(R1N>9QjzN1%2GL6oV-NN_dMGmErrzl0LERXGs24dNu6RXj` zb0fkE;A8^Pdk=EcKqqAE@N{y@o}?pLp9^&1dpbu9iSNR*BJD${Byo_~gmkp~YPxbB zf8id3F^|5VMymUM4)!VSdx_A6jskrRpQMUNVC|FFa5kUef_y?R#}df2|0?DjMHV7w zp@H=tTp?l2rE#d4aIh_6wCm`yWNfaPO#;-8-pOP^H}?2e$uXr}S=GUR%DTdEyf79G zo!U&ACz?uZ2soxBWlI1($tLWsmXmM*qFX<6h7KvSfR~pTW3ZM=03bMHRz z89>o2LjF?u0&A9~+w|IBLN#u6m6&!{=EO>GFqa8q*-DwjUT)AY(cTZYG&hU#j%zYI z?_+N>NU2+I%3X~T`$f|Z{Fi8EIv+U~4UV$IJC?nEXvZzz1=jtsy=p8Wz5TFE+s4)YB!`ZTbyrLhP_WZyLn_1^j16HnB(=`hGY9S`zX%)${S=E)zdAbhK*1~NIL1{ zGQ0$vbuXBHayqmxw?rHiFI3=iZt0ub3l4kzx}GY-=xKkNmbWLTnH1>5Qu^#R$$F_2~~PsdPgB!_oyx|P;pVM< zB@i~*BF`Yw7BE1{ZLO>pWfb2pzhz8!PJ>KfIQZ$*@uyun{$p|9xS?Z5@oXrx`=z4G zf}xWzSW7L01l!71SeBF|%`?$^E+|dpNRm4hwdPX^-8x1#NHcSiKP{W(jIgF#$xvg9 zE)18|%}!@Y@Z42JPL_GnV)f~PJzs4&iG#a-4>WJ>A7Esm3+J0P^0H%By;wOqUQy-)KDdQ#iH?owv4~swTryg{ znAY^Z7!oRaW8@lw8T1?^d@cG1RctibeANi2JG3aR}F&FYp~SHS~JuS zzqNZXuNyKi^9el#kS)l%Eb8jjXfYWbO=}a8>*wvtrFJAXCa3o{#|LEvSts`gkyp4Z z`Le}%e_g_)a8P9uevc<7Cq6zdMA^A;l+JNfxAt$T3BN>Y+%JDRe}XjQ_tB}(!I|cZ zg-p5O`ia%4WFBlI=hvJrVL2CvvIMa}cleQ0LT3t*tq);^Ex{*pTP{KshmbyEF!7TR zpl4&G9{CU{b_~`YL+8XHJ*c1{Q{;WhaoM)TWAP$E+n4lGXGx~$L5ZYh_Ot*+4ISV@ zN9*eiq7MXi4j+%mP)8!1Et-?SAqZH4P7IWU=oBc0}((ZHZKE>ggB zH2&eHfgF2b07^%~xli=p;45Z{MtnV+3*Ac`BU#|Lv0MaoaY^W0j(3Kou-cbQOI1qC z1lCuMERiBOY(>f3w7Jt?qS)lT;4JsRMrRZx;BLD za^LpK2>H?79W_Db0&XtK zR^Z11Y%N-25r|%I4fxdv(n1SWH0--w|CL zCW0!a3Sp$l2YrkvV&#A!uCwJpU6{iN>?|RTs$qDqXTQyu&p zhKIjeES$iqqgUKzOC%nVdjBA|^Bxtdzk$uhJrNJ;?m@Jj;7d1JFw0|+Y@3jkQHCyK zJHkz7Xx;Vc$p~M`Q|4<P@0wzZ_uVvKa+@aT~ii4=tcLMj~-7ud-;{{p_e- z805-d_s>Q@?c~G&Nc`wu^it_ps|#gZ5y%LUmza%=1NESt#UXh;byZN@7M?7$^B9QiF1Jzf-5KAd^nduoY>8{Rs&W)Gsk)yXnZzVn3tR4%c z=RYs-9SJE4f@nl91gtRkJebZ$tJ@o`^bKGZ*vV{M|KwFKy>Y*Pd~vqCIHMO^kdAOC z$o=L+b?0EfBIMNYlk4(4@^zAcnG{RE6Cf!LmPls@eMOXfI0GG(Mqz5Zl?)UxDIEUP z8WDTW6vA{`uXyun z_cL#!CMUK+HJOo-qAk$zi93x5wOBOF$7mLMJz2eQXBv*FrZ;ON;x29Mll>k>S5KF9 zua3~uUnonVi>AsTmgr?tegt|+LPa#WQN0+^^GU0DwYZp3;?uf1pPbiInwlyz^H=B8 zKiz?*Z<8evE~X~GyS*4uWUXsM>q1^n%2Aqn7gTRCKd#M{YS)BzJ8MBWeS? z6-g69@&eevFd!{`GhW)IFO5O{h``$P?X>|%L(A!v%Z970DhFpLRZ*r?#l@PWCag_T zM6;e!q=19}s(^$^)bK~ShAhhM_2IPN=u=zMVwdh_G8wCIqq?mlcj-Q@=kGZtG+X?` z#M>mrHb81f?obM8xw@doLdJx4CP~W`yHDCD`Y>OtE@qR}8}0K8x`Q;uJtXbp@&z@e z4|IvRK<=o9yA7BO=p4_vHNuE$6kvdqP01&dDJ>5|&@0=va6nc4OLR=)()y)Qd0}p5y=?A)sk3$mH!3%V zeK9HR(>SKH(GHz15CvvLjN|&QJYqvbQhS445_5o=hCVP9bN?hertnMXqc?Y-4RBhu zsz+zY!*2nD0q!nKw6=yPv_CpMl|7>L3px<=%a>?Mrp(S*w@4vGgL^B!no0i44tE-E zJC|DgFy{3Mw-EFq2eZ)0BhcdMeUBK^nV}BrS^jz;?`S_hq)K1eT>wwNmzm z>s?QAf8~|Z#aa;^g!_i9LUdsy{h|p!I`wyaJ$zR}n<$|JE-d#VK9{;;3gP~(@5+^k zuDGz=mH1rhiswPml`9clabdYD@wwC$xkP;DHw?-Yh>lRH+)w4fn4KQiLCJS` zKRuoY7iWH$(E_wcN2)*kcD@XrqisYR~ZMox6(YWk39>!tVW(jN@lkWi|o$SjIqRK zVzCLK6Dt~1rCHIQdh~zYtBc=|uKC#pYwaN)Z5wS%RK^}d=x%Q`bC;(KUOy6;uwI4& zmBmv#$Pp(=B}WP!;Y5vcxs?D_A5Ce;H%VOL zVKQ$v;Gp3Mkdnu1ow;HWjQQa=9E)bkkIH9?Uk;;G@n)~aj{JZQ-;U?AS2~w5{sw5n z1*M<&xVefENX?j0)SYaA>>*eJSzhK|tlD&(>zP$lY_L3O zgqPR5H#{e|8`MZzXrkcyLe?dJ@({O^?@~06QUD<($+5_wHQ}V=<4c=y@ztb;fUf=% zK`8al>moYd6xu~=rG_TN+jVS=F;HtZW}B8%S{gTPjMvo!&;>p|h|p*9WbSYwHF8HE zBE6BmpV!9|3dewRv1C1pbe@8Enx=4*l$6aoBn=2QSC6_9TG!O_1TV-B@q&CnEIJ@- z?9n6=^$5~ISg|_J;gaF|qH-$?8`Z>U_(-qVQG6H}MiiGecV}yg9^ILp+Ua!_et(ii zI>6_M`F5^#X{{oJ-90u_$PiKw zzPq%wW(qNSbc~bp@Ulm$t#T(8sZ@Lwob0$M%Kv!@k~l>N`b|qnjeC2{Y906 z=3^c~Jz*srIDwO>(Z@P9QAze&!10<|Cyc9oA1`%!(gnCTQOPJT$+H2{bmvpZ3Caqj zhj;{i+Dw5JI1-Ogp+#{MQoXzL6Tjidd%7$T#%y+24+b|_v8rAwuF=E=Gl3!c$sslm z%?3dh^=E_osW&1YewZZ*S z?;+T~L+`e7>?X(81={^%0^n{CcpRcKGPyGn55<1O7ELsWaSMVmYVSu9in=u8iRyT?ScEeP0iZ<5+){W1@=s+%`dAM|Jg(pL z608?VWtS~;G2uYmz0b6cNsC>nOf9^_CHhQgc!@qatY9M9EPjNj7amY8sYBAENnwJw zn8ocG_SFO!d1^B6k*;xgvY<3w`9;1=IAqi%f5vo56QA;B$?~Lq5Zlubo7PI$R&A@K ze0LEY$|rQ|SBji_zn~j5lm)R$`4u@K%gO2jY5AEKw18injizn;axQS9xcc>i0yW>{ zAPlRNbd3;|mCbEbNxBo!hxbO^B###VXyZ>XtoGjIW?QEl9KA%;9xndjZC_N}orPrp z?}yQpT%`Nf3uLWrJZ1KbG(qjikzri_+q?1nSJcB~=GYP=8TAc$*#zT#WGAMX&fG-h z4VQ3RVkIzHpOa|)tFx{&yLV>fI*#k*=p-;%pONg+h+Sl9q2EW1G`R#m?aWJ`d*Gc- zmJS2YM^{juBV7$XG?uDpjEuUVfLG>Q=DasjU2F-_aIHP6R9MC`ZqawYShQW+6?FJI zXHt?1eT41iD-w3$BZDLE#*?S1DHkQ3uLi)U65cA}gXDNl`~pN3*SndfID4}jO2L$+nJfVF|wKA@IwX1ak+Pp&)1FF)UnAX7Q**ioW}xcgpM|& zIhWSiiU1N5Z3noHy8VDR+M$3n8@>k2o3NGNIswVC3P7ynYQucbU12)bC-4bHuIsif!CBNNrrI_x&CVZBxLW3{Ej&VDgwr z)&QZJzGKd6{Qi%ouN)oHRDsuftrgfoXs^|VtJ(4YvJkX~89&k83;6 zys*vTa?y(>*2^t4;WYJ0S~8QmzKB@VL{3Yto#s$OHcZCHFey+X{9}4b!W5SY0UlAB zaX+ZWl;cj;ScVOHcup+j6&4k!Qw`axn2Tb`^FxaZBUR}foq+ZfXzRPQli%cY#-#O(E`#f6)TFAP^_@ikUX z-NRn%{8bF4llaDx2y8r6oG^8eM6{D);u>o*@?P^f@>__>&p>ZC;oq$IFJZ^hGEgeq z?y&UISdBFo;BTNIu9ftq0oX@CR{IgGQ9gdef_RIe31wHaI#D|^5{Pl3Y znAG$xXPPU+H>+U#a^ z7`?6Mdda}=k7!{+uc7?>wxB+Y#Isw^pACqWgXc6*Vq=ab)!3(Z4tKcL&FWT^aX;(x z*+DC`gAAHyD8GaZRib1fWETjbyaR`f&^UT!=vI)l7z~Z55e!D2H@51~*itBtgrQ;m zY(Qzh>SHo#L-%>fG^Zz-J|Tyjk+%KrRv#r7a&GM(9#W;9cxa%|N2Z;n84Wg2A!0f| zjNL+4$Cd`V#$ZZxbzm~Oa^ohtx|Y4@3UO^=3z5i3$UF8c7yFLUD(dj5pbnO-RQS)@%bA(>!`L znoLJ<4`u!tw^d}GG%qaQVnC-ON~@D*vEd5)Em=55uaKY=DR9Wuo8a2G_x&Um+By)1 z7lbrdB)Gn8y>FhHp1i6bp&EISB`}O8ANgot+#pJ=xlLFt@Spn=yd*dq(&Mjxzo@kr z|G1jjf*i&aEbiV(Vn$Do8A!B;&u`Ex3=8T5n!N|kyM3toXRl1an_qSsd=@V{*%#s^ zWQz|kck9iA%>oh@)@Cu<6Ow&~M&d>5wVK3k4a|^QLBet=9QxtqPfA8p3C@3jx$Xy( z94gA%Pvatp^t-qB32~h4UsSk(aeY9w`MQgdP z?~$J$oex((Vfn!=hQk;26qmA&PmYH8W)S8#)A!*#N^Rpd#!TTOL9y`Yr82fk(J>mN z9A9lkDgLYir<5M$;g|?UuB_?NlqAp4aa{xcGlO-&Kyr&~F6$AYMo>L!w&b}|`E^~t zjH|w&=kh2m2u=C?MQg%1(wlT~Ghe1PfXqKk8iaoEXj8r7>4N+!^3@eAG2YMLJfilexy5RJ@UhCT6YiG~*G99|iZ(0MlSWm*G^XT%yu2W8NF zj7u>j%P`t!gHWi!pECE*fZp(;Y*wQ;JANGQ+w)y|#1zk+YSY+YH94y(GmPldmuX$X z`Fu(Ye!?#;sy!!Fnh}5`c~~C#s8mBDe7@)0pqOadXAKOt>;5`}{6`!Lg<6yI$??l$ z?1`dHPW0$=^RVq}^$>BLeWF4$tk?9C^kRNAI-1ad( zoL7@^J=bB_PsR3ch3~*tII_R^HLn1DUpnjSC5cO;#W+kRO&|CUFdNyE>Ywjc14^Du z20@^MKJuAEE+S9|KS{m)^T~N_=7!;LG#+z+m|2I&naQ*!e=u!;0S{Mi&TD?cYT2wAbb{qb*QT@jHJ_O>s$Ci_>(#|MdHLpfE-cJC4vh(NOffX4 z^M!fWt4nvFJ(dom)0$qxns#l1&fr~iY;i({q;qA$$UI;>goW-fda??~H66#&6Tdxb zzwT6Q?jz>61WofPNoSube+eF*tRZbtIGa7#5)w9)w0=kuSQU|x1ko_X1f z-BuP(qfcxJSm_JBg$fQ(D8WPEf2;w-PFSd_C^f`*x9g;LA$A zMDj=+jQ%QcuaA$&SOu3bLMs%O@N)j3=C-(R#rYT=*@rcTP zwvS3nbO=>8@f~?w!Bvb<2!$mKA}3KJMfACy48l#+pot3Y@DfK+gP2rlhZ#AH8ocOp zJFE?SO0Z1&l_+5o+v;1kO+XSc%UNLCraMi1zrwpx0uR)O5S_hFah!!8Mw z!~P8V%$#9IBr=Lkv*w`B3>a;QMBp1|zk!?GG~?sGHCmqDv)&+?&a|GBD$NLlNvr#8 z$6lw|TG?c#8-xihqIf7&X1PH$g=s^NJ~t0rOkNr!QK1=DGHq$lk}A#cqG?Nm6@6}o zv%!oeXR|Q*m|f}*yClwj{r~K}U5s2wb|%(6l138e2RkE;MzXZ4JK5@~tm$G_71=%0 z&7Ep(u}Mx-#p)(l?CEKjxY=2ix2m$4m6?-2ES9*0VZc~Hi+M1N)|0ieY#DwrAZul7 zAG`}05a0(}7W`zuFbvo*{IY-n!;khFhQIHe_`Nr7R@NVz?5RXID>HshM4UKr;>3v) z=Uiw>JSNPywA$#WK}%9Tzsth$M|~><`IL*pzfoaa9RAG(P-4^L!a9~HBNe>l5^SzM zv8o*_wxo6*n>Tgak;dIcPtdkYt7^I+x1@SdC~|a1P9omO2zg?n#C@S)isr47@fBpqj`^_V&Y3cR9_`>>s!Q#_<=59_+t$+T7^}l7USEn_?1ui!X zrqX}^h4o+jnPku6%Q92^OFy%|#NxkIYJ+7iV)p!Oz{p7F7Q?E$ogxt4O;J^LCOD|2~$&y{9vzWvvm2P3vB|v@`7Qms)9eo0D;;3stv) zix@S0V7QP?Xl6t41#MiSrJuu>?U9;CuqUIH_h3`8knx(*eAO#X;ngNZ-fi2G-Ohez zyz<7<5`to>;=BdBVi|)xU0le%^PPtWc>giFG@o658-r@x0P1crUYRPwYqeUbM=&Mt z_WKXB$sil=nsPB)rTHS7DdoHT`Rc*=1;1ozGaV zWgm?4HeUVF5|ek^?as~)o(|%fl-3_LvMWnVS9oQ>rs?6NlaIz(%bGixat(JGO=MZ% zAM$&DN2=OR`jUz{Teh+z?argl2sYj5%9~5<$gS%e3)!2?^7Wk!4=@#ZyIG5(sl zhDLMRU&-GfU9o>3HbTe@rY|U>M1pKXEx6CJ-$q%lKMroPYHu#pf|@dMs3?Cm-P~YJ zh72ZG+Z&Gu*A^FH%ADhIWT(M(*MJS@B7Te{dGXPeMP*=%cCjs*)pyw?2s7F0!$hYh zivYsw0O0AiQU0%n)^TG2nA6PTBI`6E=q6OJ2}9~c7lu0-U#o+KPuS zzo**ATC>^y5RWUI)huOIynPt-2lX1vZXS;Cu8Zv0tIduJ**@qx8Zt(IT;TR0Dpd}v z2dzW(1|s(_1*1HHj46Y}Hy*aO$4ypg@>P}25Z<3IfH;w8rjG|oevYoVjEV#lROF=j z$(Ric+b@8bts>hmbi9oq*K+9$HwCrbyno%>bh0q)hG|p9$*qlYyzqCf$&5pk0%fIAnLfMuw!Twm1o0+ z1brC|_&n5X&UbMF7(k@J8_S4v8xVeb^_XBdz(yY#!-LK!cMTLeQ0gGJ(dK8_!9oY3 z4*F3G4Q+0Y8eGsK|KsS~%N}zTBXQF0g0?1jEH=-l29Fzid1rTT{NRB@o0VR1sDJ{m zzL`H-$R0m14uRU8)@~o>@<6jx$R9z>rVdC#bo&!|eS2Qo-c~u%7BWxUjG&6jFoD*S z(lkLee}n`6E7|?`Z@iz~gRwrw1qYs^3^SN#I190jk4s1Gk3}a?Et=uz4j~8sTAwSj z?{C)Zs^>XX_W4bg;am4RsLlQ#wUsjss?2^1ynYJ=UJqG;q9krPk|^^Odl6s z>2-QNzSle52n`f%@)Z~{;4SE3U57Ip9=9h$s$1^0dg>bCOu7T0IB~ZHXuw2GTi~m2 zy>*;Edj3_A9!`O=04Bt!CpD<>k?`ms*l$J7$zF(B2~t)A$zRW(a3 zCJ}?}#Gn@ixu;O<^Lm6Giat&~3Z+d`OUuo>k+ZGXYVcUrtSFju?TuDyon1KUM_-bIFvr(&rtU#u(}@|kr;8{H ziuGxlI8u`DatN&2+|?W zF7+pJwL%L46_d;XIhsS48BA;q?jCg_`TnOc*qIJeQTL2sFe4jOKB}B=NA6ojyl{~# zW1Q}Wg?pF8ZHwUziCY(;-O=I`9qZFY>J>(dR~9YrSX4%R#GQ=2j#VUqF9^R&GJq04 zQ|=}5n=Cn_s=%5jGjrq*h`1Fb&j>Gf01e6Oyr|Uxsoa-qY=55v4-;DjXbaBP&r`gnN#0XYTU51YV1xPcR z0wdpOuMO?(Lu=CnUjnz3U04aAZdg1U0jB-VUiD=8(^B4zCt*FJ_l z%Gb-v36tMmJ4C*(ZdZcI{zq$nitKMBm#!)DpRD}_iu_6%!=}t%uKmm3|5OZLQZ0!M ziO;Z-)2E+EMkQo5CMlV5SYr|g1{NiMx5}b~+L95>N$&sLOD~C0$j+pZ#?(kb1j#9?e&2GWMI+TciV%o6}aNozoX5Fkr>AOIgerfCED{M21lw$GG#gWHvql*=X#is#_e zP(DW}Cj3@x?};%>n>7_}DKkUk1M`AFAj2VoRe6pO5T3JhJV#tRvj}Ucc))MQjG=Vh zEU+mUlnxV;<1c#z7m{Xm;v7MR;yfznTyE*;%&wyi%e=^|aA%~ZPr?iGw3MFb=Jb=V z#)NFPgkM~H_4hv=rLxJ_M@O-2F!kRmE}H$Vtp@&wOH0^B46)ktRS%X3Sk>Qrr^4*md^cHgN8{H^aTXs63}ee3%V(1NDs6}k$S|kL7GL79B3=*wYCu4=TWD{`&?FMA92;I zGi`p@i84Qn^K6ofkf11pSAL-hjDSDl5`Ib~v(Q;g?+|YW;G{{3Vhl4c_D@rXK$gsm z{^a1p@o6ev4uo;;o*eTKM|+XBTe2$P@9bw*9t<;lX)AJhRu$rfU_P&Zgi-TrRhVM~7C;^d)A zF2i%MdRqgjnhFl?=|DVV!iCK{+_+ z!|q!|82=&=_u25+DRoTAW+0KysIo!)A(&Lr9(~a&ZE#sLx@=AzmxFwj`4*o%ehR&g zcfuMa@Vd4ENoyb01dfItub8n)ipq7i$3@xHreh>U#k@raWOYRh)CsX&Elh&Nvf23P zbtsqgx~Zk@3n3T@1yWg1Llc`?NB&)re<9bIW~pomz8Cvh03H5pe1dtb3%E^q$=5YW zSr$g=xR%Eku}l#wPW-U<-BvBy&372+1UGrEt{)A&Jn|e9j6~rRC)%VPN31<#DMUzD zVGRJv{nZlf9b*?#*$cy;XdV*u!g4J|Xw=$D*j5pZzDE|WVlRRacZog2P0vcrwb=%` zhBSJSWXF_^%E~ukzQqbVyFwvY!!xQCA$>GZV}vDCZ2=hB1}LZ!^a~{;3E$?;q~SG% zjeae_xb|D--O{|uS~3ObTH&NnKFFAAe7?Z<66T^qsso|1F`J}@2OOZlnUU6h3nGQk zHUAnrp?;S~w5Xmj7K+lQ;4mtu7?dPb(JZHaht(Xc6NJw26>AE6olD_&Bo($xmt9OZ zI{CPz$b!$b<028*cFQ!QL?drMw9Dmb)XIMO1`9-|69$js{F8`s%+_Gs|F#EapJLZj zaM@GCa|PT`cxl>RJq9Vbo1;0uhvV@RreYi6TdgQb9|HKA5PWEW>JwTFf-oB2-!smf z@98_8um}zny=|C+MZMHLDJ*6-E|a9RK};I<2-4>fMV@XM#WEEGFqtzHY1lGltl9&A zL?H;^**y<=9Lz@ew1Ydi(=^x#@?e-vP-5wt)GUA#AE2QlmnfkWm!gI4$7-+MECv=V zdq1#yN^sRC(_u@7E85Q!b0J2C2$v*^lcmH|8pX>}K`UOCGAJXK6OFi8$_T~IQf4Sy zmNLuYXQ>G2s*fGIfQo_`8sz z!JK~qPZA;LI?*=BGIuv5UXoyZ8INfvQQ;n2dio%|nrG{vSlbC#W)onvt07%76+G8< zPAsHY7f*;bYr5@%Pe)weN@A03DV6}33UZ%;pL2+z znhwY+b9CLYL(wu4Dg&0H9K$qEfl55^%g*EFMsx8f+r1G6lyjsw+}P-jxm4zmPVMY^es_PI!%Yd?c5<(0P z0p@Dg*@fVpM-_`SjfGrpM!6+u+t!Y$JOO%&&bLrH2?{6)Ta8w^WGIPpjblz+wE9p! z#cmH_j@u%ry-n;A2}ogeyU?g9>Wg>g&cOJyqMh?LnJh|;!aTkh3i7LWyz%6 zYL2RGx-*M1GVO@;z=~_>wQXe#2?&xOEJoXq4~K13Rrk&6{-dbU%@Ajk^tRVZnWaE?8g zY|aAYkBHOQEIi3jTC!}W_~fKW|Iu$8GASQ=J(GXXE~E-(gQK5mAr@LC-<$+(fmm9V z5+cQ0qj z3G_Pk$=8jk3}}shAdH>N9lok@-d5CaMD;zZ+$N5H+=f`FW;Xm@;gc-6`EWng>nR55e>PI*4HLt(_qd5Kk&DHga)QxxEy?Nt`{qZf{%ZT;3*`J9dQMJR7jj`_@UCW4>O(@X8W{Nf~a@||~u;C8iW%26;uMr1d zgWVbKcjYe((eQ);`rTdk(uF=)^y2rJHwLL;tf*}?VJ=)*x`NlA#vEleaucI=b)&4# z6GQOk;LJS56fyWbS_Q!b$nET$oXcB>m-!doTAT+Q$`mP6p^>?(KckllLKdjOwjFvT8f{H&(@5q}jWH$gxhd2cKGlL%LS( z4Q#D5e^h}W&SRXECE6{otKpXhcD3dx>=eV)v57vq(=NZOa7Ko7n9KpF76R6-QT?j- z5*d$U`s88t?rmoR3uWHNRraYQrL9B$`|(0`DP!7})Sa2ZoXvxb)jJ`f`F~QW^ za%oF6;%-oEP;uwyqQfJ_9!vO>SiPIH53yW zam(nv6*mWNmBf;TWn=sTKM5lPK6$G5J}0;P_BUbrYJX-g$zG+sShcv!viWVf!d-b{ z#{E>^E9za6DN-{3u<0i=>F&YS93&#%XraFw$ZH7RN_rym1nJYDBZUomct`E$-LP3hbxNX@^~Yz zXo)XoIspygTG~#YdQ(>JW-%UeMs1ZUYU~$?zRMu?UZMXAl-}}oKsdM>?Hy322l3-} zeK5e=5#C7U`-AbJmO0q#Z13Ui2-v>_4`^qMWrDkqz8nKk!6BQRO13CCi&y4EH9M~# zB+_svxi)?0SQs7GsKokqqQQ;EI(=Zi{n(}$%Gf>dzz^3K7K~jprK>)9>e@_DrZ_|2 z^ia)}fo)xN>oX5x+@l$HJFm7zef#A370Xr z06Hw}_)y(pfV5|R;8LnoMMgrg%+}CC#>5t6BjV7g#szD@n3W+uO>B}l?KL}raZ?k% zyb2!Cm!vE4_zK_WJs79BHDurxhKE_T;k$xq)Va3(=6GXDHLy0#NVsYfw)Fv?V7Cg- zF8v97!n1H}tV0ySh8N!R?{jZzgJ`oSwPhIAY~r%7c16BC^;;3)Y8}3nnyrpgYzeNq?Jaq@ z;eK;4Mtpwm=#kj{rbTOVi@umAnGPeqV z>|n)u#ubh4Dt5D5tw;SK6lg&OZgM_3ZozJV+D)m14G6yRhk==^-zz;BdoqB`zlGPn zvYYx5!PI^syS$uzrz%%^p`w%Peh|CXAt^|cDz9YkVl*PNtYROAqZ4RqIy-RPNcB|q zs5R`gwqQggg(3Dh;6|uPk*23s_5Cf+g3t;ZAVwGh3O~zY4de$sG^e3TrCHZUsuSb} zfx96)SGU`Op0O46j_8IZTR%93yIF(1vUw6g>BfRzZ^6GiB~rKvJarTDo-WbId(;M= zO%v`dU!s{+asF{e4Mng05{+?8tKz{C4l75Kt$J+(ZZYbO*XF4K+~7lci(?j5>L=!B zBT{*ze^PSdgS7%Vc{%nJnlu)-$Jm9WBw&V{gM z=ToQJ_WS~I5f#z|a-gafFVvim@@di^{;jZRl(4U4dZ8kbydUxbAdrEmAubjkU?~t9eYB8 zP!>KgfR0pyvPVi%(TGOfV;hLPl)6{&2cRQ+X=68;CgZ-4*}9X@%%EHiikrd6Vwr%j zkI_#Xuk;N1X+c%>)AwrCy_wPLC!rg599PwOcLy5y-Q9fH=-@`NzEe{&gZkD3c&ef{ z!Yt`&7?~jrA-g|8n(N6^gRU4=9vhPck_4NbQmg@=%xbH!J*dY`Ne+H#IXxFu$#GmGn^KQvf{ z+-6MzIzqV<^-#0#<7wGUFyz5<5e5lUMF9&V-u}6v7MitHxR0eP>8@E(P50V#MY}au zRMjmtUDfWu6;;iqo3872vIi7dLNcHJxuR$$k0jnV|jH`w@rOtY|jw{Bs69pn5o;yuh^_IAm`?G;4 zU}@At3DwT6#zW?^*6^g_3ISe7e6MiP9WCF64_7KG+hh&D;(g^Q-M28*DGMTl?r*{3 zr04;q0pGN(QN30u<=f&gjt6S5)rH%?G=yy&!5=n^4}>2|gd7ltc7eAY$EqhmR0iVZ z>KX>d$IrZ7Me<~OT&D>^-fi^CL|&N+ggE)UgF#tf4iw^W>5Kc;XN zwT|t~k#&UVU7rYu9~V}aTS*~K2om#il01HW& z9L#99e~=I1<0m)X`$90T9B@mg>7zO(#V}{Pg58Dyu@#W&0scc!4LB`I_m!pM{wBI6 zNyq9%JU?MqPTd$;HJPp*@HsZ+Hlt_JnBRhqXw9!;i`O_M{-umM!N5IZmQzBZmf<2o zXKLv34!kNeN_C8HOy>){jO_OR*)F5%Uc|Vz)6KspUPiXDXRwTHQ_o}>*~XsBWh9-e zT1I~5P1zQ^t=*{^u zP&w-|T-cy9C+uq^90aoRnSe7qT=75=eL5EW=%&!2MbA;lHAI40w*o1&BFC8wA@S*5 zPq#7aKy|oIArV_7xp^ro3Z|QKme0=B<%?!d${-jazT7IV6PHDOAC&?yuacg2@ zE&!vZiNcd;ySjih}s3rlLt3f$_oK{Bqej@DYR{+=e1cyldCo@7_` zSTYM}V%lW-OMSfLF9ig(FZrD>g8GmC>^I$We&A3d7rSTrXZ|MNd*MBui%d5ib|A`R zIRN#-w~(2rwr?OFwO%GkTs0HrR^p4Uys?B#rPj*WiI=MVYL+@t2zp!Ta?^HtD9;e# za1(wBdZw3pZ5zA8lwr9C{Uf}c$2-R}^M!f*h`Vnr(aqzH&Z7?95N5X*vNx9Fl-r9w z_x5cc;W3yAJDw|ogkior>9&Rj2@Ba(NrfNn@nx;Eki9NR{hb|?;-SiHT6nr&)1`n0 zjTsSHI>`$}S_=`;uVt8eU%yBK)jkhenOPEKHxeN-?c-q3$6!X{n?Os+zHqPT)L?eE zHCN+rHu^+x_oYlo2P|^D8`O;z?+Ch(K(vfVeM{aHLFtjP zd2fKsOqd^yWn2+6Hz?EqZA@9S9N=E3Xd%N+p*6i=G-J(W z)a218I5x*NK{LW(4+?N*tjQh_2|*8{aMZhb|N0OwkQsGCo?O#R1JM^4-im%kH0A=n zb;c8U6vJfrZ9vdPDWT8g+~Oy5lK1kq5l?xp!&TIWND%c>NU3QPk9XpX!zG)0L7i|^ zW2Pz;FP2ioj3l1maG_Ro+QFfT;gUmxG~b1r;+nEa{`RnE8IzrcQ;*95ns<+a=W2+? z8k+R6d!Nl<-HX~LB87scjyYVN6$gYA33E?3rv~PU23rCxR-|V$#je8=rTJ{f+=JHF z;~}ER8K(IBMy*?i8#%kEo*PJf{7hRFT3J^63F?JATCvr~y3J1yV=}F4sjwyl`gn>; zT~4jjjOq~1r*NsG^3sZKNjgiK1iJ(=Di%|^bS*voR998jS7?x%2K2kw2q^N6N+v^4 z)HCok>o(gcH0%s$lWjKIyO7=oaNZCqj5*1AgAz}?q?c7jg$}^X$P0?B!u{|w?O>q; zQ3pM<7h2fd9Kj6(-Y9Z^q?ddKY~0C^I6*W$)jNvEi^mO(!2Up=6my(b619%x^G6HW zW9vb_7;q+k#G67oqz4IB1-|ixOw1c2bRr!(eLdyW;m)7OsFcu|KQeavQ)H&ra2KKn z{=H)C_hk+FXQ?7|a`)Y28CvX4TVzV&;X0mYy;y|wB6wvpn>C?k;kc)0Pl4zhPaoH& zi-CN)5t>NK+T<%)tKH5t$;Im6C1)03h||~Z=o=rz4yB8 zQ((@FV*#f05RY^?v*B@jGDN(j@jV1_Eo_*l=njD5Ox+fs!J7`#7PgwV-a1YnJ-a-+ zE(S0m+&rm05<^4yI(%<&wf5A!8xHBhS2$p=j{4o0g6m1i$~Gy zxFyKZFhZve+-~7$j|iW}y|Ym{!cr36D9w)|Pc?BwB7dXd@n26+q2E#4$gDa?SDw4i z(JDukE38iKgR|B-Dl$~hmbc(zE3-`)#CkQC>$ui&!fX+R=b^Nd#_pY1Yv=mv#^8sQ z*4}A@3~m!d0~E7j2@rKHySd+TDlxY=hPu|0N+d_I>HTx z5}iNl?@+37YD?-?w6sLFvIsG~3pH;d?qFZ=kW`I%4(>pm9`mhRbGK1)-F`EffmhR`&Q>_IUH! zG87cU92dpgdA)WSTM`cL2nzam9NfWj2-5%*%k!*IS^jm%mNEE!SR?OMm4D;f_evGO z?W&5u$pIA`K>u5);60KN|JKQ^)fLCmzi64o%KjRAo5+NcnRqGW$T8_!#y^D|86_&o z-$EV+B;XE9>Ej&n<4jYSMxU?RXAY#e8X~PShpyX7s%TVo%7Kzf?b`Y2`pp{gx-9nf zHD{d1r8Vd@xMAbh{^Fx6 zCT}rtB5W4!Vx==-iQhL`v`k}>r{K}zl|{S07qzR3uo)Ri1rrgm=LRrr5BkJ5c#1F| zfX0;;mQF-dTn`!9?|y4#(_F_GQOZ@&FEL4(d345~rX_g`FzR-|fS`w7UNVelDk#Sq zpEYGxQDtnOpnW%lwGUkdtVHRgTQ(@&39OS>9;1M9vYLkb((518=NrlwVSce(zQA!s z-50GRtoOBSSr8Q81q9+xsx{VBgNm;?antUx#%kfx+9pgJekpM*HwFIT+6V=FeM_$< z`_bB;(0U@dU7I3*y7p%%@+)banKJ)u?Jte(M5-k*Y490-2$E3=S*;$9wrYUs!{4n6 zLCUM95yBWe{y7tXbZ63o!}Wd_Dj5wT+8s`s+bt}eXJQ9&>C!)N4c76ZYt;F@+yoND zZFT0RS^8Z!Rt*%jjx=4E!E!->&U%E9!YTWNXK6w30*L(*tvz)|uUm=<&3RIf{YnQm zZt6K-^-E*LTmIT{Z|F1J7Vme)kdT&_mX=O>B~N(+j2LWtz@tz3#38#Rw9!RDTuGMI zmE6l$^U5m%8NDpED<-^jWA8Sx?{3yqOF+3C`jCe(<#t=27;1pyP~9sa{IP}|I^wF- z_F0b-tPcSoc|~eR2Bm?xk?zrZdKavuE;;^!LV6}?Gbo9pa?T5?j?U~<+OW(^N=*!z zB)nFU((~NGe)83rvzcw<7uR0>{ZB`!Z1VomQEWXKd%BD0HQg;dy0o#U|2|m1k6M$q zL~D1OlX1sy3f6YS$_w4C);^T>gTrBGcW>MjzagX6qx=@!e6NUN)AHvq#T^@!4D9N? z(HV&~zu5FRprDzZzBBb%zjJ+co#9ms|5%M8CX-TZ^Iok2;#6NC%(kD~xN|Ta{aa|ynyA_in$-j(NEt3fZKdx9}XRoxO+B^M8w=EkJ?&*36 z@?zsrpd0zHmya9S@AmFuZ-G(BPUkUJ8z{Up8clK*M5t=|ndMi6d)CZBYx`kqH*XjM z${%m%gK>6K{?POi1QB%m{JR0-Q11Q9h4~T&Xn6P+*XERmR}}j1gp-#ekrnv#6kd)c z!|QX3@!$B;OE0|wzhH9rtPYJ3pW*Nl(pD#9yxqT+{Z4 z=Oq%KyIF+o0rsjk?19^pR#%)#**IQ_T}t#O=4K&Uu3Sm{+=EeT2XU}qYdgeXv2W4*L^{0W%&RlKo=g?4TPm3OCBaW@WLL(W?R~!6YyPBFjsk9eH1@88D)- zBM5j;SJrcsKr!mU1bP-($POpF1nvqE^7 z#E1~>{`+eP+{2-%jntet{UeKgYS9$$_;#D!%eNn9;6X!W1Hw~vrLuAZ6vjCwv5=p} zYoFX1>l^M}a3pHm{hes`IF7LIdkZPk5zo|~3u!~+2okfqSVI($l0nH#+35GPX@fe?dnUUWOhEF*4DjFaZhl4|)=2 z%0V!TpZPq&fK2djU5+ngGSTtHL9(&bSOOkvTJ4e4AOM{`Y{nkp0EW8>HEsiZ&b2hh z+MBfs{sn{V8lJTb5AR8BzKpMp`{P!31Nsd30Fu6_C|udnlJ0<@iKq*(&-PV3hh}%Y z%yz;q;`OlWTy%e}RJT)mkMEF(59$WFxruTJYXwV-L4zRh;3;rEynTb`g!_=qS_qQ5 zvjdr~H#Uo2>TKl*Y)g(Jz%28yhZWs-RL7!Hx)mgieEyZS9~vD?F?+gX?xb!==HDu* z*HwB)865;=^Z)%@jKty-HSFr1v~$(Eah{oJPfDbK%D17YMvZL2<-m$q=4#1_TP z237_VNfDB=j>Nx#vk>|BwvlmJ32Ii^!ekpjOA9VV7(WiJ3dyNxEFg;=I|BH}HR5!z zAKSvFRa`|ungV9Wx~+^5DYdf$$E`cuXS0JA)%U*2< zo-BlsS2jdNnHv-^So^5P#%mwpCbmM955W~XNtJ8?;Cl-2y}pHaGPx9I0JGGxgFT#m zGfImj2E^0Et(Kk+bz27=Lq0LRx00)3NM+MwN~BuVESDx`Q)JPi@M&Te50c_B=gM>D zlV7=bcF(Mv$wwB(I7&2t#E5{Q_6*qC!8_vac?icXrm&hoQr zcICMZ0d1vp$hooy%bICtHA8YS7oqn5Z4d(O1u}Y|{`|6c9Fk7x~ z*tY3wx>6ov11vL}Oy){ch5^`0xy%%tB97^(g2g+NAV#=Ykt&QBNiPz!Sip)vPr_nP z-8_mov;f^G9!+y5w3O^veF{PDu{9R8!z1C6AUb62f-sdTXBvhGYn~Vgiyy<1s^!7i zf%~(Jf-Z11`;vn#E?9;Kqqxp4bca00xAVth73|@}r%>sxD7Y&mOG~=3FbUPwEW#wA z8!p^vGTB`2!Rn8+UcxuB{Rxyui0a&(w4p?YA6L)@ff2|=MX?z zXVk3!hF2zl(lkP_+|Xzpz2?*Jw{M6-)~jH#SqIuz(}+|5MN%NVQ;5q4cdEv)uxsDG zq10Vgn%X9htv21EmeSRRJZH}ude;}~EGgJ!2mRqgBM!_id_b^NU7|%TN6qNn)Dqhb zM`gJVPB}VQgCHDsgb=LoF@5XT$vTl}qV0JFSy3qYwk^`))v6q@BiQ=i0A2{GZnQy(<<#5VPG?$s;b zJN?cR++sTB>-gHy{(xmS4#tlkEW=w_e_ zV3Yh9nz@154CXjyf4*D=OBe1nU0E;#e-9>+YH!QuQ~XZsHQ<*@)e`0g3giY^qrm+H zXv_kI_d)FmqR$#wpsqIMycdz&yLO79cR~hvm8$6&P`g> z0z31pxViL7&X}HOn>m2bK)KU2KP!~nH+eG;qQi^2j&q`uUB?am7EZ6wIE;F8lpyef z>Nu+l5rPymUaAY+)@D_4qKx+ah_vfS#-TvBF#ny5TPp;bZF5NzGYP{;HsWIo`by?t zR~I*WJ;&_9M8K>tKFrod^MdYdIwS_J3HP`ca)XnJ`mA<1C83?#8Jxzr*zM;S8)xP4 zpYNU1j?{=|X25EY|EorsF6%N>8XK+qm)CxWX7^gkQZnRZlfAz7V`P6l$`&k{{PEg< zZ0+fzGTdZ8{e+79bY;nz(l_Q$W>M!)SNh|18Xwm{>Hn|39x44z#tD`FVqaq~FP{UG z9E4u!ji?-hkpV17pys)OTX%CG0Oh*-<&T90!HBy`2!+ZEEAs&OZcdvIju;j+BsU&1 z)<4*|Zx3$S_yE5Dm~T#2VD;`4hXXDTaB9r61u>ZvbVU%YW+@gsa#A7?ae z<`xtw`D2(*bfJE`fp;(ru9IC|9>Kch%F>l(z@=>)U|>Qa!b6FmCMe5S7OyTZvPh*s z=J0d^xd^9MUwzi&t6iqy-_>?oELpgbL!W7c}s=M4%|TTD#Xq{ zPPVV^FTUDdeD$O7P&TEi!FsylsEy`K!+bEeBdyh5{pi*GS3@T_SUGrupsl3JIr$!P z8eqPSvR?T>a?}mn6|)ptyHCqB-r7Oiq;{oZY780G*r;&PDsyp$s4xQL6`P3gQc$oX zr3k6tZG#aY*Ib_g0g@MJ%GkWuQu0@*x^TVZRW|12P<$hr!Lu*$GIQBsO%q%lAfWE* z;pyEWZn56Zt_GL*Y|e;G@SHVc+zH;1p2$dh)8f8Kf;jBk@_OpNu zDtke`cp)Iz6jG#M-+pTDZ3utHO1V@LloT}`X5cdII2gyX&hr4=Bk?k2?vZ#M0=Fjt zAb}d%3Eslm13|QOsN3PXsWKR0-$nle5+}h=HyUxJsZBv~{z)2+uwc{=Q*+9VMS(j! zXYw?rek|tr*lA(zG2{IPM{Pf|NYpdn;Dr>biGvDy_jdM(zDFi6EG;832P%8#W=^&C zdb&0$Eb%Kw-(4#;sNs>TQ+HMw!f{ce(v8^KV^mmSq->t8Zx-4#NB9u&JjdW=i^!#s z?EQP9iXe8g5zLxYQeYp#4@`E!l(xEXgTaUc(1HhLLvMN^Xa60+H|j4+Y~& z;SPPE@%Sp|QES-gPevIfM4GBvLw~z^Jj+U%ZiHLjF-=Qw>TSAi`D>inC;ts zMrI+c(MUz)q&2WKK~Yo9fuT5s!cvIAq8Ew8 zfD0tDku$kngLconhl{zGDe>Og9~k{!MX6B57q@Vv3ePI%roX78xJm`yUoJ)pJW2kX zsPBy0RT)%suOsJ0*jZuVQ!Y7St$~ehrZ;y6B=t*TFN((DI_Mm(+%A1~hSfpuKG-IM1H&+XeiH6Vc5sOpO<7V9(8aH<`W0+BPQ@B zMxeFle#T`+lLruv$)7*eQiCU3v)$kwYV2Cns6Ahl2a3*&17qY1ID43)PgWhGKUTe{ zLko|a7}GHk_qOYlka|;=`RyRkWyjY^HH^hrmB~CtIAicIt`8N7Jb3dYPGe6 z>ukJf3UvJ8rAB!bRu5VSa`sj$;^?XTNRwR??fg0D+lzbEPvyaQ79>2AW3gQ!v4Q^# zuSMP7YDn7Jv4q-lL$$gvnu88rj^S?Y#Al^gscylY!8u7~qOzXdBIn|mbm8K}(K~sg z{am;>Im*S!@mHx)TAce`fU;oQzvhc|XtDy&#S!PH+~Z0hzg`@1j37>J`}5^q#+}~I z|AQHqsOpJ~xLM)%#Ar_p$26Bi$>`rbMp1 z-+3O-Eq3hALKOY!6@EDCVoc8hL{UnT(?m5 zesLE0e4a%xwN4RMp;x~UI-QSprzbjJ{dvrQQW96m+aVco(HNR(=xAsje;hXldec;T z1j@_s^EX;o>HBCze1@XuDGbaKdUP{XCut{bj;#DOtL$l^RjJW0gilza!_diE?CBGr z3(wr-9P*j_JbLEVrIsDFIG(9_Pht6A2%Q%~=jYeu_=V63?=FF(!o*w2XRqljaG(pR z)9w!`OuU$4C!(s=fATGuiaICUO=G@ctxHAfbWaj?@Fc;+U7BX?Vaj*b{wtJ^!5gAV zf&c6EFTGrXUm!xYCQ^&N|Dyg@CO@eP zVFdgurl&u+Q1>qNA9a2?dT=4hpOXg{GL|@g5p0fzaDKVk?QXyg!#%{^#micam^bwy zu9B#{M-S0G5w$RHTUn%e9J*VOb8yQY|j06-PmW(aO3Dm#g zlTp}F!U?9h6QSoiI+8Gs{}4}*ZlCcpny+(L0+V3qO2qI+5yg!;yt^`+*1tJl;mN#*uP%g`}6jvLzN zp4Utr>5Io@gbK?%#<>04@b0S_>$n~Ko_YY;p6pK;NC0kY6t>|&R=8EO2ImV{9E|NS zfN8{lLbql~vcMv_H)9YcV zXumzm_UNfc!5~NhI}NT%H zVY=a^j&XWf=!u)yGw8=I{6Etrgm}RVhX~g=@u7(!CQcMWK|C+=1ZIhg@tPF8Y&(K> z&(n$TJ-FuMlN?Mw7~awhY#zrMj~2W=^poWepD|QdT%fbnpb}aw=erc-2C$@lK^$@K zeZy_ym|lSl=aLnVqn0@JWB60Et1t?5TO$G+suUlq>Onnh)_)gI+{BDIAo8f;5rvST z7@^N{!*#JjgC_5boFPN^@QV%trR(C-n%{2UIOUEgp-wI@TgQIqv?qr6{%P4Aj~erf zu}_MU_)DrJ=7_w9nj28p{*q=yV!F}){$TAP3K;*@f(u=HCcC%x9~tFs8iwdrQ?(@{ z?huovYFD-6(60VH(cFya!+@BRC?-??eRecwkLq3#V` zY-i#T7GVss{uaFxB4r2BGq8ILTiV`h4beOC<43QC*jKmvgTrBGcaNSzw+?CkX?oFs zn>~6)%Av7sOKti&=|Ifbho?UYjlGSq-L8BF)n4u3;GhxU8xda-+mM5(4(B=kaeLpo z`sR1z)X_s}GBg9)emJ7**tZMuwRJ1AYD9Ji)1@&F?IG&^&DHhQ>-QRC1c%z`bj|oV zMM3Wm@~u&43~$)P3-TCaOrnI#jJTLT18K`{DoiXns{bWm#xW7uzf0%=4YhlsRb9Ee*?5Mj;9u#0_J_Sn(2 z#l`aMEV3BGgcCWctBg)e0%g13o$UAM$9Gf4Vu`sXxr_&vNTv!enbAUk;L6NxESO|1 z?8)OaJlUf?B?U`Tw70pKLweM18KC z#DaWYSEL=XtaqM;9zrRv*H1R9u>M@~42v*CNMaFsP$jDKSt03p;_~_Jokrz4rXixl z^>Z}NC0r1}MIR$z#$Gg^iY^nWUX&B|t(Z={9?2LIIVJ~J_mR4@K|D*f5Dv5A} zJaxNa&CP&NXjZfPNmkR1EUWc8+YgmUGYyy7uufLbmg$)1I`th<&ivS2dnkQ9!Ua0nTELk|(Dlgv9H&-@kSH&dr8f2obFsFePe@ zHWS~L7pb^?SYgQ(_GIbcR&QL!j;Xvo$l^+E4id69DS!Hv>iQ!4+g4{d-b-zTlF>{_ zteTS)SxqH4Oau;t-AJ0^aGLFWYl5JCrj)vu^YB|Ndyq^;n4}yh z;?lg?^dvF3%;YFs?sj@0E}!R-VlS1LQe@7w=JsAm3IbkI$$R~s0JfKmUdj9-WLW6b zw&6+$#m;ziS%r?DM-Xr+s4BMQc&!#oPCud+O3@izo6;aJr#!=0-|=;22+-F;IyBKD zgn~6?MYq*HG$e>~-)$oB)l$>eHCAlL(n;95ZS|(lqlCG!(FP>~Do=3w)4JP_&Dz2~ zr|vsa*GRXqJxDxn(Pt=hxxsEBgb;Xd$^L29`Rf$t(tyS)g*V4dng12 z4`MW!WU9JanW*GB@OX&ifC1)H%y- z>*4qWrUw?>IHl=9jr$6ok^_l?LDQ)^8s^)ub0P-VDq;0HQ1p;-XNDo(f+%$47;WDR z;GqZH;T_;-KSB^th31et6o}b~og&&;u3#W^41%KGP?*T}Uf!PIUNsi{EN-E1g5+0F z_mE(e+#I8;EEmav;o@C^+HzbEQdF!f8fXOt&=IdK1@DNmiXLI3Ol3^wj?RfqR!L@~ zgh7XFSDa%~vi_cg-1KR%0Ka84V`6IQ3Bhki1M!1R zHBBP`-`|bY9BzEu{D7vZL$bX zZPdYw``9+~;i6?K3=X>V{=M5j@W(|q8OF#)pM>2`@3ydSKl#t3-}i3*u-NU;?DpLI z@TBu}W9{Y*?Smr&i`r24L z#Z9|9&8h|^U{MOHWTc_GI+ajq`@wOr)#5mCaXTJBA+`_dMeHb%eEZP4#Xa9gaL5Tx zvhj)3pMIj3FU3kT>`m@3bpa>A}CbRxspbm=vVgV(@v8#Mk;&k4B_x471xVck?zHgA!kqN{@h_lkkO9t-h!4xd4k-|-CPsKfK*!<%kus1zGy`qX z=%1k$L1(f^tWd&kTD=8(#t+0QxaC0QE)FPi_MZ%BM1w=)!~F1|HH6(M>Cq37Y6%=I zclzBfw04c`9jLIO8-jKL-GlW5Lfa0rI*+51Twc*X(54x*dxs)<^@N{lL z{3u4_4hMu7>cD2SIk3~00mZEUYq*#`a*(D@YqVRtmaW?Nc`0<5l3acLot+Vi-_@@w z$_8iKGTTXN8F0S$!}qgnVs+NoCAy!jxfT^I3FyA^xJpBywKBf+Q(k4>`L+9YZnpOX z;}Xb?b8N&_%)}z84U}*<_8<2L^@4@qC2e`*&Xc7 zdfU+;D@0&3QLz3i4c=frl|{TThVON>#ib+ii6fP&iOKI23ZQf3#t(7Wnlq8 zFoC4|Vc*b^cNzS-g6M=H#6%}a5m87=OmvbI6P?zJP$)?SqLZ|k=rk!JIt3OJoutJC zCP@XNleBOybVhlBkp3Bx;=+G&~h5%sMNV) zZlRr8SD;r^E@T$#j-*Q!bqkq=x_*4wyPffCU|O*0;StD>OT-r2eY)r=jV)E8+|CAH zKM{4pFUpg;aW^%eNG*Uee63hoX0cov0~G1wK14RR8{C1MG8yjSzd9ZK)uX|`J`Mcq)4{)fH262Bfq!E< z_&1IQ|J`ZezdIfLcaH}Dy=maTHy!-E` zA_&E$SIC?SKE3oJ2*ssW$eap3z4Rgo#idusoC-d@^dbnwrB}$D3O>E`A_&E$SIC?S zKDqR2t#;dJB1+nU*7lDl9o?kDya%eMys(hy0tMl9&V_{pRATm~FSJ1M*GtcVg#;!| z#86(ZHK5tghq#G?J%H1+Q%zl2qUOy3PGxFA?V-_0hvpVcRt##Uzl&s!^or_XeN}lT zMiYbykc&UKgPZ@@@_BG=x6olndF>iMM+|SFLxplYST^)Zx~X#e4OoC+ zKI>0Z<$SF+8V_qv%K*zYH*1E=lN2$S`Kx{Jq?$oBdzod>t2qxp5D=&}DtK;BqofOQg5X`*kTj615soB8EFni$U z*_!d$Yxf|-UAqTq)34nLIkgmoShCGs5TEq65sUjCwZBQx?YH;);B#C`0iew}uA#XH4J@AW+KE8K z6!p39iwPraDk1*L8se>!|67h8BX9*;9R)ThwsGPh!6Tka!+}lt==e@Ji^iw8t{H^ zwAUG&XS;U_qlFnpm#&uJ337Cv?S5F``S9NFZq&=OypW}4VIfOnX@SNhiBWid6U(i( z?SRm@pd15JGV8d-$;{Fo#l$M%=1jM%8MxcDz7)+VSk``~3Ri1D7PNCod~5wOGOS}1>vhFC+4}n0W%$qv zOE`xR#*Nm}+B!;nBY@%G&6^^3*S6@0D=4DRAWe3@_D9J6r6AiH#hL>D!P>t z*fFSKtSRQ8zrAy)=jqH&oRxa24PCGNZWZLNTZTSuO zBJ1tmeqW)L{hUc7#V*OSXxE_)v6+9bKfXn&Q@l1shVB){-JxEz!pwbkyLGY)Vse{E>bMNuBGyx=S0yo%pA%n!$X;O|j=}_e z>k$7ea(BUodHIPRE}k~vSJCTk*kF96;QD8eWz!G4zVZy`*b?lj!>c78e;Givo!KY} z#oPcKrR?{;i&&#QX_`cYz5vLkK2oUG<3qgV>l{;Qs>DF$V)RD&Eae5!TwX{E(4_@W zdcrNJj?;5N_w!slH#@nsP@MAflodoR7O|}mx2Jq;b0I8TSXkh3j_yVN_Q~@53t_?I zgj=)Qm!6TZKtA(%kQ9Vp9!pMemjOvZ!OcT^XTb^prw|lWkbw(!c@);%I&>DKXDc*l zJE~?_5ZiW|phHb%twl3=$Uc+mf#irQ15;LIA?U{dEOt1>C&s$G!zcN0qKXH`L}QqG zq0k^!&F4iFQ49rFlN1DWTFNOQ7StKZ2%JQQi3=cpd~sF6*S#p)ppRb_=!4~m{Diep zw1~q)#(4`jp^h*^_|Uh$_qylk!xr31X9?|Ny>{Ka4=t@t_<_TM;^Jww5!k82CJl4e#+@A-qQfLhOi-J4hHj$;l2{GST(#Q_r#?i50K{21oRc%SaD|N85X!e)OTZdL+sgO>i^D4XLdiDINv1Zvc zc>`2w!WvktMjM?~YXxIJA8D1b&hduv840VVwG+##+(0ePWe1=r%&<=I5<{;!d;J4! z6g{}y(P7Dsi?CBH#P~~`3%Rj0zGaaFaO(W?LL&aWdnP`n(ysEzWr-^|ae4o-BIjJl z!}c~ll7}P2olqPuNSu){To#2-+v0^J43~sBbA(uTl6*>PGK`h`mU=9c?Jul;;X;_6 z*(7#uh!*R0mlDtE5sQ=dg$rR&?nzw;JGtvzC1JyzK3{@x#Bzgk`Vn$4>7Jj@MhwPY zKkoN?&qxe5t!4FYzB68w=%VmUuWrR2!Ixu@EdE)b>YYC~nVIvl2b;&d-D7-l!@wzJ zgH1isNTKC?<2U9)bcW{2ZFZmEpdM!>I$O&3>N_R0K%aaeKu6}RkpS(`r^ckuN}sIE zQ8~~F#P$mz`a+0~4NpSBw{)Dih{GX9?Js^Dj)D*!+ee;_ZDc{YZ1`h^K<)3;UQ``( z$cR4^(Ha5oBo2(5+V+lORjmlD^q#P&#MIzvM3XVUo8!Lz6)E6sWo{?8HmqwyizfzG zqpS;2#(MqtuQ7P?xQ|SgOO8@A6U<=9=wnbeZzDt*qlZD4JR0QNjD#aPYDmwm-A)VV zaD+5-IYw@li(i6iPlm%5y&{)^=)LfTgnc1l7s8l{<&Qm|mFOH-;-EL#LL^28O$<^& z1xfPs?0z9{C**B8`Ua?dHuClvwR$0MU&z}R^7c<(+&d?frA&;^RNkf(H}3P_&z-2P zN>3}M1y{UIWSe5l!9A6qGf-lmTGFw$0LPTS9lQ%W z4<1KwVx`yz9|ji@s71Dw2;1oh)W=m;n_wfAlzSmjlc$N>n!phG7$(tot1x^5Wiq=m zqe=8xHhUpcU&z!KGWAcrOns(Ov=wn+6aD$I1hIpfwwqG%`2tGO=62y(Nztac$V$Xv zw_qk>tVd2~CE|dmL~hPmr5Q-Iorvn=FO=99O6-IZyZQuaXOY6#) zhj#-p+ad7$HP>g^?1h+pA!c8Q*+2DS_Boo0SRv=K;gB!50{edGC0~4FW_TVg@d@on z{k64s;kftTNSCDB5L0e#?H{4s zx5IJ@C*`%n)cLp8{srp1I=zlLw3#Y@wf3)%u40JJRQdnb-+sBGPT5XI>#VQ7y#8;q zQffSQ#sihVsrB!?y#Cj-)QbAI{$Ibm{zdEb_=p)1PsCH_LP{KgQP;c_P7RUg2`Iut zM=PIxG7D%oS(SxTD6i~Ci0Y$z9hQzdGWp4G79HH~cefe=-F*rd&A(MF)7xq_@V~AM ztP_~(kKm{IpZ@wwFLm|@{UIFe!J@wf_38*dH7?n-ez!H$wC*K!x?2QXT(C!X&Wbb!`8vdtyXuGFVMXMT<$enSj5t`6$UL?$iDNPhX<|U z?x^H$S0CrYKqcJ{%4c^ch?v83DH|o1>$H<@XiT3nbtad6(Chpd!9+|dA{DgoKo~|` zTjLfSOtjT+uWQwM3$M@X$%Fdm;#ji!R=?k!zn0zJkytEtw7|8U2>42F!FBn5-e92! zCsInM#+OpFoO?y5w4sBPYZ=|4pt~GBOwa@iN(>nRWe1({US>jAd0z)}t?^zrM;zx) zdu}0fzb5g|-NJ7MMwwf1`@x)A@G_zwfr!Xvf9zmdB+OUFvLu=_f1t-h%pbqf%Z|)(yVGvM@s7w35v+(Rf!|L6)R2W)C3c#u*9tRlet^VK zXEg3?kKB|}_%K+OuXZuX53{i3yV9kHC5heswwp0Yuu~*Is_-nC7NJoMU1G^BFo%AG zTy4V9zsb+$I-_RGtP}l#B~C<#$b^IjyIw=p@}u1vK6ELD-_FTsZ$3s0oo*1c>SOLC z;^3pWC2_BF+*p8^4bSd1E3YptUtRj%8_TcHe=NXh)u`8mqsjh$YY1Qdz4o9F=TlVnsamC!WVE22;{6wXU|k z8v+q{jcwiV*KIlseuAwu91u#|Laf~Q1p0-#0_}|R{Sor_@-8@y-^wN3g4Uu!76$Ec z4NYun9r<@f{zY6p#XT{)5_~WAvj95$+4ux=Ru^!an>EQ{#QUt)A3_%47YQ)S(Bk~T z*zQPv0B?Tq1F}-%D@vsIdn>N4AI(UKEf~APCr-3UI}RtI2wGW+(U(Nz0xHV=)e`hB zIK3BU8!pU%+nl8%QEMw@kCE?P5K z=y!>SsGf~+%ua`}DsWh+OV%h!XkN31IX4W1C<Y#3G?HU1`{?TnfLVZ-xENWf#*; z6vVith+<4TD?l)?P_Lc%0FE)B5oG{uVj%=@-(fpS>GVFC zFh*TiYP%2jeJ)8?v9cz0>eX%VEOU!T8M8a_&gZeCe8c`_T|Dq-8G-;6yZ!KSFzew~ z$Eha3hXiN|!GmEoK{b$7f4Gn_1h6tHn*)x2FbgXnl;V=NFwB-C)ZT-LSKqW4K!hRG zV;gqwdXx_#QMS5Zy}`EA-U(N`Ct9h^)mccQ+*wLYrBMzo6|{0_DTBZmDwbvIApkNq=G-YzDdkz zW^OJ$*wig$dL`F&Uvt^ll=f^}L9ZI^Ay$p=e`UE7@_sHK!}@LOW$v$HLMVnt0%*V% zbk!3QPxlx{7l$_^WV#*bfzDWX+R)XX;!@*%PN{|Hxr(Hij;Td{ji-SpSX`N zv4A{Y8Sj_a4+YLfay@nHwGWKlq0q8xJY%H5I zjfI>F%X1bPCurN&jtMpax-JhF-!=5C*&@~B3xCSQ&=!9HI^hok?ijDqfJ~76TatG zgJr<$(&DBR;*hK?{5|sg%qD{^03}Z5vdNr*V0v7;HsA2B;O8+Vw4ml7EvcCjI-S$` zTnH@1$xOcGL!^Njxmcm5=U{~lf~??Pg_1%1bXhni+mLO8VlokYuK+MU*Jzvl(Y zBB1Cun6(@T-5fq~-<0I#P-xl`))VKOQ==^qOPm5hBvTwUZIObcDrKmO=IPRwk>6eh zlroWE8I+CEeVwP$&tP*mu!+hT0}+3;JqUQ6Pm0+c7-N~!*;X!n`iH;z^Dn=|&(lxq zKlw&dt!eIte}3&MZh60vR8*KE-(7pVP((Q}*`_J7x^{pf-wdFHl9DO(duxAyLcbam za-$1C{bcQ*nfSL={r+2ooSgWWnX{J0JIP%mW};G0U9U*pc<0@lH$J#y0L`bberFA=N+gN0c(667 zo7e@{GGb;E8g}rTqW(>8daLhu?*Lwgfk|WuW2bx}vAy zW8NO5dc2}XFv488vUCNC$&VHGDlVh_o@Zk-)Okw41@8xRHjTctb3DU?_m0laP7Y=X z*B|mI4ZiybF=PAz+)Lx;stZL39DqD(b-FDEe9&^w-&OKmwHM&Mv0f;HAbB4yj}G^8 z$KNRr;=KZ`rw(y(?Z!npfe^*vEt=pVy;U! zgyJ8Sxb*hEWcci#Ztq2D4T@iG5SAjL2}Y3M-ncxK*mliic?qZ^CpfH5SKZeGV8E$& z<`D#h9r8Ks(3C(R+6v7JAgf!D>0f1yX4Q5nJDbDWG5B9y&cF29{|0IEg^ z-#4&+#j^%HcDO+efgoe`?rmq#IzkRjXBSC02(5>IKVHxjW6YLRYng5y>qP{o%@SVp z7@~Dh{r*8zLMTAYY44d5jv)OWVjFf*e2dWrreJYI8(zF{pax--kMRcFD3yxOBLuO6 zUSEl0RlN~)`q{%AdaKJ>jmfv=(v(QUjh+~g;u3DFwbj9Wub7nD!-Ux6iKQ05Z4IHa zq*-Hl517>+b=o-%NwOLtUO&8xe>eJ*;WmtA#B8qDhBi$KIIAzDtKyKR{*uM;bo~iI z1CFy)=C7jFS8z=Tlh`(muux@~6KAxR$>izGhj;l+M%&xz^Sica9KRk@mFjLR0%D(H ze@#SKm9D4|QLQi=}Br1>Btui>CvZ7p-V%SzAS9Cqb25Z_%4P<-9E*v zOP>If#10rkSjMh7UkDfGcAfb|ZoRjK{CyfpnTx}xn1vCfI1<$~RTy)9m1nKyCo6N4 z6feAJZMwn0+ShRw+k=o@XrrQGEw}1BU&2<^e3wyHK>-2t@y(pgWY0DqF%c^ss9nZ> zva%u%?Yu7j72G_?vjAoAVq;5|P0B0GTVf~fkDvv3uG23ti zORn3SwO)Q8+ahdC2erU(*nk!@lAYRYa%|FxUug#8JTtcMCE{*u`L*dkLhI z8G)A1*&YUkaEZ=H2c#P_&mw^IL5lVMsKHkP`bLA_Y{>=0MtA#<7|7t|07Y4mJ$E|c zn<+C0kO?yzJRa0tvB(O+lq{QCC5NSyg}XoQ(;*HVN2nWs+KBmq`=nA8*-0t3*&13% z>w#)SR2tQI{brz;g^_`ayCg9_G+c6CO%cO4Rke9oxN0)FU?h3XXWv@MYCRZq*ZhJp zO%+CmHNKPMfh6|%U=~$i3A-H=s)4m>M#4p#uqCt06I26Jb}QnsaBNyb48m5ozn|ad zmemH$LQ-0W8O$cG>1tQx%TvEY5w6zZE2!D(C<8Mj?whv~@iq4{+OZrnwrh4m++cYz zNag}g=KI8lSm7pr4M($v>|d4$Hj70b%mb1b;)ZbCjv6%V(jEj28qx@gnUt`Q?gc=g zn=z9T77Dh)RJVidbeGNMnyPEE()CJo8+5aj7k1c-J3$Sf7#6h>M^Mlvg~jZyn;L`7 zOAjKFI8xQ40%;g-Z3Mk^X<;`5Sv1vDkezO*B*>(olnSL`$>A$iUoz5lyf7Gy)F*~R zHIizo0h5((!W|5P7F=4`fD^}3O}NZ-BSvIR)ihb9dQZ#yP}M%6Jlm~UPYL9l%E)}C z74n4#E8hk$RpA=%7@{rX0k#a`Z?3xSEg5*ZvdxX_l$U0@cByIQpwUiM=#FJ{MuRZx zm^R%x#8!-ZOhoIWu~lokBd{jbhe~4pwP$5o^Td=#Ic2g>0 z1393hBmYX;Zdh$NcLS|3I+AJFqj(Dxus zs=SiDi$RLOvWiU@UPxdN#xPLwia&eQ8g^*pbwkGH{v8#l?vDZy8@Yp zXe!GzQwd&S$WTt&333B7$CN$nT-~PTf@V^ugS*KWsOI%T2!6c<10hPIqw(<6O~~`L zL?iD}QFt~@xTkZ8W>&>1#~C#gy}C;@#xbpSi8(kh7)`e7wGFtds5f4lrzUWN@8j(h zvZzu&F+bZpQs_J}Iq_{-ft3vIfX@)oGQ*sQ#NKi**Ohiky)MxVR^XcMp$7J=R{ax6X!};VMFIaShMpf z*(+xkq2j&QFA&+m!+q&g+n$G8Bqz&~&y`QvM$VZ}*=C*zpR$e3z^B5ZMjchsy2a+E5saI)k{Zq(ez zyISJND8{KsW=5c%bgHjFn}`Z&3jw&jX&okHs-Q7kRq|@0KG0%L?2;nV7ibfC?O*MF z#?*m4=i!=1XbEus2vy7!1e3ePz7AY6v*etv&)}vw`SGeb)r0qJK{j& z$I^Pxw$2k=AHB3-PvV<3dWOSkq3|j@Zd^;>$IFnJK*}3K92BEL+qHdLo;q{fF&p5* zn%)Zwf#hxb5yQ~MRMD_5!E{BtVk@fZN=sL@3$voCS&QkqZlM+`+eKT%l}|qkSGHss zS78pI#FJz*Q@D!rDyyb&Wvf=;%4R2VRmd#0>va<`j(s|DMO{W=tRyF^_K2&7IaAtm zx}UhVZ`c)xfOu9mg+*9eSM?1Wfa<2_I6&0Hd3%xTA(IC*aR2IS_+D8jS}em)qmP2a z1H3G&i(`?pqk@WuXD{vMp$Tip#qJ&tI)ky;dmY+hX^Q2kOUTG*bc) z;cgr9`oo5K+bl2W&@re)z+2_9-l#sTU4!2+sE5SEtRJvoOR!p7x>n0xbA3yJEL8JI zwgZ}>lEF`^q#vViXod+%fwI#;y>J3~vgM@eMeJQLD1kof#9o}oODW!R0n3x^alO~v z>Ezuuf5KK^_i&|V9O2R-9Q@uEt4C`tV*=lZ@N;A90IJW6-nzTwkb>iu{9?F;J7;?J^HW-`WxF#& zEg|-(0|e(-2RG*ieZ?w?*EZ-o_&%oo{Rxs9^g`>U8`iWRi&CvyYJkOr2r$px{y{#3 zZJ%{RWCH_^4@_adGlHex@gZj5^$ zH!3^eb8N~9JI|sqFX9~0nqNv5r*TUBOE)(@*Da)bKAU3RhLgzds`dl#z?oSNc{;{7 zrt<|}Mpk6{Y?o1WFJfHV>E_=PFC*L7GgwBpsb{i`Y-7*mGLp_!EhE1!1JcJ*V!4PhcQjl z#G|tCMI|tABS1_qldh1u3N@Ue=LB%87O2SR`=JG;zH59qX6qE?QcV!8EIbl~Lq6>)WOu$8G z;KZB|2Rv~s`x)~?zp{*ep3&H&qmZsNG;7?;u}OZ+vU~mYb8hx(YP2uNL<`@UO6-1c zQ#hbq1xGF-!ws;!?Ye`;^H{4+Sc|q(MWyLRs!GQ^FCIx1deSXamG&(tGL0i&r_c-( zyw=*tBq_?_(*^FT5jLbQhMD5$5l2kXMl9IH=AgxfQI2QCI!5X#r<;>%dAoB@FJH92 z(%qECD&6Z7{xb7+E;=r<$~cfb|(qT487>pR|(??NpQY zN_MwD%6RRF=Zmo7L69%n$lt{O=5o>z>`}Z0?37-jg2k9RG`a;SERHZO{}ByialfZV z!&Na7rz&d5_o!YAu4VK0c(5@a0&3besB;QLX+674>p}1j2 zIB=;&9(5VK{D?lhy^_5Vj7VF)y0jFu>~#kg$wzFlL0uyR9#O&XXJ{cFXcc_~ZRlYL zQdy7|10lA!Tc=6J=R|C8Hn2`sJSHI?bBd2im|jwyiWArQV^ZodJO7!`XO(moAS58p zs?ujbnOEL?b2??ZSvV01@e~w{sZ96|`~?k>%i3eyEIh7C{toaR&`?$Bh=u|kNgTY$ z9$%D)TkU|6>|@iEgi%WQ^_<#}yprivsX7&vFkvE?!c?3QBFwIC-nTg*u-t_c{`u_f zY}uQlgqN$ZN~x~;LnluXwzie7m|uI$3Jqw7L=<&8|YwUv7stqAETf41>za> z(}Jq%r|*@!rpMTMqu>rSaHNoTcW`g9yPFRi9Xx%n@6;6II01S9Pu2b#*Twoo1DPQW zA-g|8n(N6^gRU4=9v=E}rSXRAhO3A4<+qlCoUoN*=RLS^d~zwG?~>DR7j<}e;OFc6 zWgWcaxaLVF$PYUN7~#*Lln3p)7B%Qnwa(K^>K!O_n_-;xd}VDDK7GC%z8y9>|4_XB zd^LP{>wF5e*ZwhT{c@1U9*G-QQ~M7ja`iWZ666Bb8qO5@tM%VwH0aP5w_mziX8q~Q z>z|^~gr3U391lR;166cF@=BeT)ap)2c2#dAvw$e3O{TjSg8QMY%Keb_V*?*V|NQSc zA4GVkj4+L`$~-qmLf+rM*&iHUZI9~gg!=DgSb`QFc1xU9@#u>XL!JH3c;$_yr3yFO zK>!9i8j75u$*K$;4%s$wp6vGf53|WY93RjZx67glWGmZ+yQ3bR=;Z3e=jO)U_jD=7 z?y{xx&a2Q_`^pZ`VcXCo@%#*>m^zCY=9e{jA-f_e!+dvw$kzs)P}6HU#Bluo*?YI& zNRKQ}Y&C1FF^%!Y`)$W+nL)iDCnUkn3nL73eTS*K^^-?@NpEu9mWB)Zbd6YGY4*6aUJ# zGwY0L+4Fm@e(Ey9Q09^ASb#H@S@q)J%ijtqbM--<`Exd+`OMph?yFp~*_0$2=3SiV z?Yu=H8xw6lWDsmo;Kr_^ZokC~MndQKA|>rUfsDECFvs}>uPSxerbEx)hLl||Ac0j& zx6x?W5(kCqev@k%no|wo5CdZ5S+EUT5(5qhE$t$&BPQ~w@Ciy$M3JxC_E^C_x~W`Y zRaGR!+6|omeCD4AhdZ8ai?wvm=Uw%a&-~lLZZakYmg7w)bvMI#L4`SoJ>IJ7!QNaRXP95w%?btQUr3RaTmQC$yrr09f z8A^~EmUQzeP7n*nOo)yS0Y^5N;sp3YqwbXh$pwb-xTnd;T&>e`^l3IdAqx$Q0_^}_ zXwT@uR)K|7dgpS7J+6*4Y+#`TipoJ4LbfxI)E1EvL-zHI!%^iFAWl?cl19r=z$q~n z*Dd()$uNuMOQq85uh`VV&AlE*pf78V&Sle9cQ1x7WxMfnXv(J9Q|d=?kNSjY8*z}x zZJ2y3wS%Z`szh?sp=dlxMtPfKn#4N6K%Si|ozUr_(xg9Ts!pv`aaJ`OBkL046D_GZ z4CR&%pC^&;=?O-J0D}KRict6A)b`CG7$FcK=teEPaCkUvQ(y=K^I;mkZG2MTBZ9{D zIlrW;df9$j_D`SAzHvNntWNWGOZohI@zOln62I$KK4*xRd0gD>H-ksjT_ydroHj7= z9S7m_-%>B1>+Nfeebk+t!py*bcXjeJSwopDEQhSaOLujPOtH8x9X8o6G#7=@>bp5? z2{()OyFh;mh??Vh9U>dnQ0EW1K#v-V>hfCA?ez-v$v%(8n6<*A5c#V&!ds0Hv_<(6 z(GB(VvH5XS#u1^eB9^~>Vy2UfSIidqUNNPG>-}Q74(^JqNGW#UyfMYA*eQ1H^+3VPU+n(bbfUJRd2=RQ+SU@@VQMT^R~Pwhos$9^iSfP z-N9@WH7`h6TiWX8jC6sfdfob`D^J#HOk^c<+#@=~HfSo5v# zS0YKm-M{jiT`-@ub)5Y|XtejO;y|6znw?g4ngaLPNyBcum7#4b z{!N}mKo>F5ft?F!N`jz(|3oFS3HYSoKT(N{5{=~Fs0;%VaIcc@vl4HQ>aH-2u3Y(1 ziA~J;f%lO^cM};`4gRj#F3~;PT8$%FuLu7MEXl}GIy?hS6e8KkJ`S2#Q@N2qP!h$L z0aJR<$GGV@g=LQ{UPx!(=CKj}I?HFY*9-H8WVTn&3*0aQ;0E`4(;K@t%v!yn&{7We zhC-o2AczfdhrFq`q@*n5gD@{Z;}itTAm9)3wj4H{b@vXnQ(xO9wt@O3WDh-O_C|~D zN$y2X2Ym<=ct>}(wGgQVWtY}va#DU|92n$DNgwRZf1wBD|8>uP{Fk~0zntQc=*p5A&|9Wbd~?|;lDtY zzn^DVUFTmu{BKa__c##84Z7^VefZz&_<|f3a_sL4jBWK-zm|f^UZphYPsIz&j|V`| zz`tD;G{7^dDIx~`i+@GG4SsSy#7irq0sJ}Yeec2KymQ<=#&gM+6G8A#Y=;|X-Ivq; zZ%X~}FSF8H#t^(WVgrBijgi3dc14U6kZ-w2fVjIWxSvqSXHK(~{?z-GWgo~H~b&37sx4k=8T)x9Aus} z%x-c-!NcV%5BRW}Ip?-IRPji9)~(i*w^c|t4{BV_2s0jjjN!1Qyan7Iz%TL)myB$W z+|;rueX0V&NoTQ6>kkZAg{{<%hQGmf8IQ&r&#p%&*#ev9OHT&f8B%==u;ezR#GL{N zX7a6pCpM^ObKtAdX_c!wmU*FCaIOv6H`1K)&n~3)CUwJK{$3~nw9x(Chkx)N{?#;B zOc^b}^MD8dT|PsROIJlm#4$gRn#_wv0y`!TUYUk&?+qA`ipuJgHgz7;<@n!%ro2chK2 zH#L*!)o8)s0XFfdczm&O1f>Kg(1Q~R3N@p}qqu8z-Q3|eS6n@egyJv@#SG#Dg1Z{K zY<)K2*66!<`(#kg%1*zBvvT{_dHCmP^fxy#@4C(CyE+LEx+IVRo@SE+U;ZI`F&@46 z;B^WmUCGLlckxWj#5H{(Coybi>bo!Ml*TVJUmUdVsdP&7yfZrK9Q7x&r))7DoS*XF zUt^t=ulO5y=iCKo3Mev;+@drvU7~b>f&!aoC}}?4S20| zB5imgj6oy5Wm29Hi2Z_nB@0LWL1!!iIDP6yZN0^WDEX14mi&P; zhG+JR7TE6!VKU|*#B%S1TUl|^V6}JgXu!0jN)$&;C)SNdE%D*R-QDe*+wa3nq$Ogk z#O0qs6w9)q$LM4Z~ zE!4-3Y9s&MlmEhu)7{cs_I{6j_7Q0RPxYvmfIC!)$g%opy2|@>{KWgkE7zmA!kcTs^1QODWC}3aXk(D>TQqmmJcMJphXd2u z)4i572SKOhc(2tl88_vtzn=jK+_}vkz<3}lQ#&F4N6r~X1MbLzb~@N3_y$A|oiQIo zsRZ>D1;-P5_ksd7Gx74tHZFAS)bAk>}x!ry^(q%m!A{QbLJPeJNL z-H&xjn|@v9!v{lVOAMXAf^s^)s|V<_@-KPW_=zxN00jGr z;qg;sdi6U~%F6!&f(**jpc10ACVU&&QQ&w)A6@VXOJH&iiwZfPyo&wdug{~v$QHuE zhf^9G(29Otc_F+9Bu(dvV>yjlXa@%$ikY}l5D2E%iB=Y}ioCZf2?CRB9XT&ORxl12 zDn|Ygpu|TS?r=LT786vpdjt9bFRRWM9E1`J2m{@X)r6}V3gA=}?bwIWbTE@(sCG@# zRJFKGbt7yPq2jM0(+gm(W;e(3iI#5iG53v@Z&J}%zDWUvOZrh;!byQ|5hsPd*_#xm zmT^)A{LZJoqj5(TvR`g3<@lqvnElT*g##WJqB`K(gw{g6r!wfaUDch{2dd#JUn9OR}i536=>0$-!UU+~J+3rNTcFbf;#8Lmp-jR@;8)ilb) z@NMD13ihOpqOR_qibrBPL_Ik}S0CSseWI-w)T|JGGwZj-fIKIe^q~=hB@O^BZb`nK zeRo5BwHuI^p_M+{o1H0UHM=7Hi&Ec0= z?LqHyg#z*G13CXsv(R+r1}t?*6mh_ar-pW*av&g)u}r_;IEEFB)G~|Rci44lzu-*4 zOC<5(P0YVguE81>@CqfpkbTV3#cVf4VmY!GyY2HhTrFf%mqN;}=Aa24;?nL}CCEJ5 z7&8*Y$*gT-exTGdP7_NoYhV#)Ukwo>|L=^Ahv$ZGDMbB>c?&hMKE;asd=wY}u^Dar zZGo<%Qu66T?d1RM$usjAVgV$vGRS-;b-n4aJY~wY1I7DuLBr2$InEn^x`vw0dO7C< z%Skd_ZsZVoprI6tS~>|96%b^F^onZgJtvicEF4|TSpeGxlExI_)`H?CBWB_|7BQgU zZ=I#z1>B>_T?(+BSNtkr0?F)Uh`L8~FpCKz* zCyB1|;lp1%CHF8$`NRBPRb=Xmx z=$JcrDBcFf=-dHQt-RY4g% zL4yYbwZClv1^SbAXkU1E$TtLu;p=S#9|O$Rfy!La!gF83fEJ&Zh@}r*~-#p&x?czT>JDBgI-S>N2y`2xb zAMb46{OLy@-Bc%=oP+2b5<=8SGsJ_17hX((TH}ET5Il75If_ALIe3R@_^9r;#A4k= z6x?{ebA!Xsvv@;tmcd-o!MO&QHjJ$VVdQ86* zZ)xawbfteBtR(OIZXVhnzhCkFX8!xZXa+y6w^-T5L9{WGZ#oV56Z?ij+(=h|3Av5@5^LN&-;*(qe=Ay2Nl9O4vT_m4vT`B z4vT`x4vLcb)}=8C&F8vVkLtt||DuHH41M6u^(&m%e9oLK(YI;5JbDx8vp^To=VCsn z?PCv8Jzd3e2CqhgcT*!4DOv_+;c%SMYaA8C?qq_@0~FDLV34cjA*U2AgQ0$$ReO;c z&Dq6Si^|&QoYvaKqotBE^yp=fBb~fRa*Y90>gm?m#G6_wA3o!SdsC4@6PFF1ndFWX zS>e?MehHd>`yDN8m5C#zFB_XTboG#Q>sU$Kct&+iWN&wS7g+<;AIi*bJ-K@yrtFKd ze71)ot!T_`UlY+;qrYtzIR&nIGO?KIFDqd@Gz#9hK}e;p&kX<~;ep!qg0{A8LNpCD z{w@WVXbRZDT8ub#8f)a{^vKH{$KJab0wTK)=6nYOj(GeKiTp5cTm-kQE@>lPJ4UcYSpgK_u|yO4q*@Y?`fa>-XVs z`qx?H+1k4B?=q(UFMmJY5a`l(AD*G~@7Pi!BXsfj;a_VfRR)0gkaA6>AVo($#{*El zru1G_;6j3v>EF#oP!esrB;Znko9X}9w{9l**~WM(rp&pTK4-R*#&@v9S2cdcl-3jD ztTHZoytX>PwvlC>iSFvpdpNaJH@;^0{YUC^j657CCw=SuQvTpC?tR|*^w$2}4(=N( zj^~%#Uq1Tu!57KLFAkpEx_z)8*1dE0@xf=E+n?Qfl9W8Tb?>0__`&UmQIiK>{Ci(L zd2l!S`o;bI&f{DA`wzajANIh4CwFh(eefswqObNpOA5ZcckjXN2e&?t`gr{5WAyRl z0A-JF9X$B*i?FB1_nvh2@7~(S_d9p*RhALU%0ggTT?SOs!e8FK`!L_1l;nGpqHJHP zB<<<*Tfe&dr1KEdL2B(E+=}Pxv8qvB67a|qSQ&s9eB1xx)?+ZwS6{@S5ANQ2)cO3& z`(K9j<@5gS&+gv&>hp-(IEv4I)$x1T%(HblnLyMU{?|&I(d;J-l0EoK9FQ7@XW71H;iP^)KC-#VPj4 zr#2(c&c`2rtc>Rx_es27A!x++8DqSs0!|3?I_;-TbEhwK1u05Qiw(v^co|sWB*jjq z==D$NXgy?c_>_w+NW*XQPj!e&VPsMcU$hN!E_t7mgoAtKd2&y!NvVUpJWOdWFUwp`1_4 z&KMhG1S;vPyEs#2o;Q8vm$3Qc#t#Jq>u}?cU#{zZ&5>pldZ*10*7P#Kd0jFOZaJhA zr6apvy;8Z4jc=dH=8uqKDPZglL&24Z0s~aWeX%ufKpFhr^uo{Y9RtZ&mo7;bn;BE) zma}H@E5G^}ZG&(bW$d*N@XKfd_xT;1;qZhGIWLMXgeboE`q(y{`AM1PEL^S^MTUlp zR~q0PqIW(hJMb@K8FSetiZxlkc56)t*E}K6ONGE3Xy}vjpd6k;uk5{x6QL1y@Ofdt z-mJh;Hta@V*=`sH;b^Wi<%CDb?{3iemKvRJ``@)0MvVj`JO@yMk>L4by9gqI)K@3@ zSo7*iC#oyH0gNr{bmJL-QIO!Ok;`lzy70}eO{sMabKA!s6=Q4nH5ol`Fh9)-7 z(m%YIy7kdD%=~0|W}|6YgT1w@HCr1_)HCD4wQ^=zyjPw#>7ET02>y8nm96ZAX2!y@ zwZzg|1-=3ip&)XAZg*{*pvgI2a$gxgJyFos$4>^`weVAb{e}7IiRVXo8~AAvd~5g# z!B_p~E958xZ;d=02p1u%| z9JS)BIWCJ^z2}VO=*>}uA6-lpTI^TAVflerok1(CYX+sQ1}r&4WD88oW}hs5jUvadYHnwfLoUcZpn@8J~8Oz##l+wf<2;Y~UZ2$+&>wm?!Px z3udm1#h9FKn~~Z4yb%v2Deak@bskDCm*AWvadmtjT$L2av?bT3P6Asp*;%bXoe1`q ztWMY`OuKcc6I!2gnDrK^lg2pDRVOl;Z$O=_olCVk@yGYuq)zG<%x{@GF^lE{|mt$xJJ9Sl}hw4S)ZUwJ*wRDxS+qw1TfAM z&Tsl?$a8l!MR~41;at7}eR2t0s`ZICz~3f)Qomq+%k+s}E#FIhqSI)sbP+39w(J6S!8+qt@7L6UG-7V_UG(Lj2~io^ zc07G9w+(`8LYv;sWL}(_$8hNd02vd*41(~qe4iCt3kk(tei9!``)C?3HhC(Pb!xY) ztC>Cxb>Rv0uZ-xxS6&QTVDGtx8TOG1?*q=mE6)YfVV4Qc@l*{m+^bhiR}xA=xCoag zTxXrGR(obpx2khz<*IY%GE}EeweN@C+-a&eqidGmORdT53Ru9nPT$8f5YT!nU2>Fj z=1L%+A9UTUYXw%0$F0K;S8c-Xe0T{a>^xF_PuAv#o{3Q#j+DD${=HnGCROrE7p4Ka zu4QQ88>H>(+g|Zq>fP<_CU>bXYHyqID0+QA*4lQ{%gKHoiSOkS>mm2&)6VfzD5%w% zoOcMgxJ|(8T4_1l-D=xewXQ=F)%lqVF1p%eOohzH_iDlIG*oVJu9xrSqC=Jf z6*3o{jh^Gcw0B+>vr%zAoSu*Qgfl*!Am8sfa)L8QyG{)F?zA{7k$k_`{t=)sV=vfwra6udw5nXXYqmC*Z18ZM!ozMbb2+;5A(`ota8O_@iqD zB5&{{p2?o3GLu+Cq@!|MBJT}y)(6=*(2_qmt^C}iN_(u%LTqDzW?KA^e^oP^D^2}L zNPorzFqMy6&&Tl3exy;-=OqfVJ@9fJ1;eP#YNXCgJWEdxD3*x|HVP4xZO2U#jFG zy5+U|j0-LC8iPE@*u}c(BYznf3N<}iBd-xMxGMk~eCAMRE$a_4gU4k6Q=e4gal*g) z&;FCY^*7|_yT97_%fIb7>}aghm2N)#C(LGQOH2YIU3~lDHz@vlwpg)?O6#Mm{Mo|^ zs{EU_ikFB)*EoOpA0##OT^(KHKYI9|qsHG4U?3lr%59+Q{Pn|sfjSOTbIV&7|I3H} z1B(Bl1;Jt+cENkuy4L^r@PAE!5EWhgzdiiFtAKd5{+~yG^Bq+C{h%+PtAITETkkyj z2Q2>wLAeP5vDN<3JCAOpU?8eAnB8|C-L3@Vs@;3%(P0Qihd^mCfBMd&NeISZqJn93 zwexo#{l~2KdlqRq4A-T9`Oc%i4m(kdpvn5^pTG0yzhb%L0`JUD!}zb?dGtTA&Ofp} zB&Q0x=KuW8qyL>X-CP?-p|0?MzVqn+Wd+BVzQ0CS`v2Z}^t-=B|Mvp7mg z)exny0x>De5ie>KtW+thT!EREb_GkB8-X`|DT$UFso^Nag)HnY-`%j>`p-0w z)tG6ZR2d%e4P4Ml1^7O^+5%%bmhjmvjO6eqr=z%fX$9;GZ< z>GIW4B^DDbKA)dd8i}X>aV{4uO&=_HB%S`h9{tU~q4Pb<+h+Rr(bQy^o|NV{rdn5d zb5G8PNKeT3*>!a}_v|9c9^M!^KQ3*0>f!m>_!Uza4#x({xI66O1O7Me*>BHInXZ0x zKEW$ostaj}2f8L@n<=dEX9+i$>F=!U>Sx$qo*s=LJ59l-{G*&)FYY|*;!WC@75S@s zgUF?ZrMJ=!%&0H^RCMa{1X~}ty!;C;EgKCv)RTNFDY+c z=hxAA+zCr|bjiu7Uw_xDAC|nYOQzkE5|34Recbfgq@^F|(%}eM4M8@)<%f9qwH$ZU zP?+?aDO!D+)1VRROG zKbov`2o|30!4e7d(VVpLZEHAt%5mYvhMV7QN`}i}e^v+`Yy*{DS-x!;(g-b)c?=_w zg2q5i10Sd*hk9BT5DEI^TETcPdak>nXpRH&FIPX0jCin#ot#X!@^3 zNslf1a*yHHVG$mZJO!1evxBFl*A{Q>u?)cYB-BVMzQA^LGCJxW_3=zO9#O%wj??k* zmyf+SOB8&e$dJ#am}hJB5@WIToJd{$pL*X(Bs&zr_ik?Q?Cx&g+`-Q=hIML1uSoQ z`s|fubh)A82#!}(@g9~%(7dgq(j&XXn(R!y`iHk%`8H&mPxWw^Fk#O4y6aRv-*~=z zL+RBUCpsY;^AO+AaDO4sb?<%a$))f9@LQh}`iVgH=Y*8O3QBy8@fl;re=IPmjCy`E z^FAN}^C$lv7aF-2&soh|IhIE9T}7BjoTiqZFj`pKJR}x=wJRt5T)8ZL?hl)|`HV42 z7gN}hk66ZS;&m0aF!Ce(svp{;oJimqVG^})Lc|uh%+wVRzxjg9Oo5hodVL#GN3z0T z?F%p0Tg9Vr7B89d#iz>?YayuB&&opdcLPU;TCL(U5883m*RbOmO(3+nap02+^7fq~ zJ>FU$kD)-BHVPTsXRlP97f<`}FY>i!=xotGM~p-lqd`Cn$}nyEc!G<4qnBon z=dm(>n17lm>V=7b3S@Lvy%$D1E(&~mY1+L1(JwT@jq>mry8Ym|gvYsBSB*Pmy4gb37&%dAOKsVLN4 ziC7?~lII(jSKV+Tj044pc8AYR&7NKhvhm-0=G3H{nbxp;A;#78>1f=_LvD{pWBttJ zb=d?tKq5c%0@j@3I9ao}t>bKY?V;)5#ocGIEidNm2%=?UG;MP$(Ec@Sbg3nOn%$DT zdEna;qUG_-0lhCoN%nkMQbE(Rg6tGGY*jZZQq^LO%4THI+O<{Oz&d#2ZGZ=F%~M1j zrIGXwPQ&pwE}@a7%iW85?2rbgvW3I%)}jtS)AYXAeU7jyJXap@ssnV(T%Qap7_0q3 zFaP~f7b|y`FZ%*m=1XoJ9ZkyT`7*Y;HHbi(#}Z>S%=a7`rYuFnl;qGbWeFOaG$6S( zj^}g3Orhg){pGO5edDx68$39=b8K}DN6e?ASs&Llpfu%RnXlmTbh>LaEcB#)5Trn$ z3l!%$o`RoB17Rh3IZHh-PUg7A<5a#RIdniFtDg5q=jwU#V|8d=C_co#Lmr*PgH^?g zr+6&@&&gCS^MVSV zB7sLyeJ0I<4d!O?V|YP^e975@_tp!0(nwTIQOAS;gv0P!hP=oEw>0X?DwYiaR4hgo zRWC?WdVs5|F|+$+IfnPkQ>OPzbBynoC(O@6{+QK~L(P;`p=QeSsF~6#)J%C6HMgEp zgT<~zy%C5u#GcIsg;^uDQx>8$aJ-@WaUXIFRRnTE??pCZN~^|=@i&F;h3U_n|ZURds;K3l_>c4 zLgU!9Zv~GEo3H%01*3`Nc~VZ#2eZgpG!Aswkq@A(~d`9|xbeMgU*&PLuv(oxz zUJKoQv&zFpAiH8BP%n;#RS2~Bw0!Jcu*1bku*wkjIOBt`wctXkAz81S9#8sXK_X9t zmQSr(`YWg0-9dLeEyWwFsrDz`!FdghvHO`|O5wi1dEiJI;%x8;uF=YA$uwam@&*k^ z(9f|&Z&3AkFgkulycQhB43833gNHLPM=p&mX6**m#tg$>37%K)#&xU+yW3 zshwGx>Iv4pmWt7eDmcYPhRusY6Ymw5;vmS$wtNtnHK*dX6otyaY;9PEgVG7Nfo%j_ z^Ls5N&S1IKLh&Q?VvyA>4=L-Ysp$n5tDBnhL9ea_Yv9af(PnywEjGFuvXbiBL8xt2 zT}%J<3NO6YjQg;qsvpRrbF`cfSeUnNkVZk{Jck&&KPxBUuw^u?ZsY!}X=4B@k&d3G zv&n}4u?|VoL(tg&Sl8HeqO1xTrQf&uTnE5(6aff7*8wmXC`%>5Kz*(QV7iI`grDmG z7z{{2?1d8)^!GXtrn4AC^t}#*QaEj+Z5$t4nSftJDcL$W1sV@tZ4A0+N4;+G@)KFV zFE{IYF=N$dd*tfjxQ(6Yq!flTj*DV%uQ-^T(|iO7*6<*HgzMQ!Z& zw6Ud9L(2K|NePz1w@1}vz;A6i)vh7tgWe~_x2+w7#T-JNwsz&0??SWCEU$TWzq38W zO7XBIHb?vHOwg_>4y~>>L}VLVfIdRAy3oy^)M+Q%Lw|TaE5*~Rx}0KzGK4|>ji(H~ zweIkuEx?!2H)>h7J2;Uc*yWs#B@!RS&-RL4kamYQ;BMU*T7&Ls=c(x-1+0qNc*5jl zw&7jTO>5GZ-WbwWEhw?O+XpzW-!G4+Pf9g|cKCM6bGBS6O8a>GbK`L5dLaRFY-4^t z-rg?WD?Z$Y)Jw;sXGzcPH|_3}Mg`w5-$bLT`gZF5`XOsyW`A{jrKsHP{koH(G;BqL zZH2q`bT^$+A!bAwQXX1a0oYESm6&{q|5sSa0KL~#X~%MBoOZMwo1x-pvPhaDK(Vk8y&9L#x^jj-n3E z#{z!Jm=oOC><;D1C;|(4?)Jqk1^Jj(W*LT~%w?H6BEpN*UW9}}eM2*0HRE#j?6HX6 zn-{`V0UM8|9UoutVl$1J@F}8cklF$l7?6wTGC`V#9)$1uSsvw631Bsuuts-RDHbTnif)rfM zL^_CdD@g%B2b~O$DsMp`52{cITKgPA|3ccR85z=c%}AfNo8Mm@ktjY$qG$;>B2OrK zyruJfAxtpyS^XH1U}0C5A%C-;f{B_dyHi$)B=!)tM7Z6)O6$S!HQkZ%=}$T|vwGs% zb-bSu5LcTdRfDXT$BU4J{AhV`VPXWWmGSKK>1u_-`ne-nnpc?|>*ox$So+frDSuU@ zhIsb_p(VHIn%M`O;AO_MHZH%=Xkjq!%twA_>F%b5DFFWL=-AkKZ( z+lfADTz5Ad3(olJh7)aGF2q?PTiznhGO}>ZoMpOMD`%Nb*34O^o6F%WcW`!S)cAwm zH-|XGp~wm)Ol;`|;s?^@_%I`Xva(Q}1#jlP2C*}rr%IdQMV=s`(WVMFvDjqH%&JlD zUY$|?e3gSVJha-W6sWQAd4p3KjB;gDdGS#$Je8Jp?Fy@6ShXyhRBOH^t?ElrbZ%d$ zwp3ri>{X8|u6FUhs>c=dwa~b9bn{`02B{IG2WqXT8zk@JF2ALvuCB*or=KRA3%=QK z(zI%dNH&Pamr4SPJjL%>0qUe8dw~r%A36y&A3C``GI6>u`_nz|0Jg%n-Nt3M4YVy( zeTQL8Cq-5>J8L1U0d6Jc{DF&Ja!A*aHGqDF$f`DyS$@sWL0Yv*kcR5U5Fwbqd^c&; z=G~+y!jQ>FakwR5*Kh7j55WXA(`=iXcoyu~5d4z2ZTz?w)Z4-3Mz=vG2Ss57DS4`< z%z<&ZR<%ya`#mI4I><#xl;xr-dZv||Ybnb<3oVkW%1iW1jm0JD2R&5{G^p2PNfLN2 z)~Bk2q;Atr0?ghfZf4MoV@!&Gls3r}xdwHMy^8o3ITo&xj}9Xafj^#sVR?KdA~HI) zJbeE{P*8%8qLS+K%q(Tx5bB7kRv!HF2Z*f*R9Rlfq_b%u_y>LSPA?sDq-_<@R1h_5 zcQIKBIzPTJJ$RmGPdik|%G*g3^43MzL$_Zga}ea>tRai!MfX+6=#HW4Rtei)kS#Qb zRWrq<(LzF@K0}Q2LlrnfGCo-FB%nO-V=*NO_S6^A=-d$EFg>Fd%8* zm?$sJj64eK)-hZSwJj=Dbu2Y)O{=ifR$9D-RfYEUObr-COZsw`jL6UR1Rbx zBRR_eDJor0oP(?!)e)4zk!!109y8G_?@GHvP-H)XcVNnFu*0r8d}V&>Q}gCSwA@pA zxEc3(InN!2c79DaniRIFAF^zPTcx_Wn%G5`cCPbzOU1NV%K$Ty`fXzoeS30aJI0}hiaPc8Q2KE-5 z%J=%^pjX@{OFLF0ApUrzsKSIKcJ-JS=4EdSqEt7kW*@sc3yQd^Uv4^XTDbC&k-?Q#mu4R`UcY$}usL zf;}*994h3pKg?(f&rr(7sUd@0aViY!*-Vkd4krFle=3=J?BucM%#hqY7e`QsZf%3N zgmOpB^%Gf}-y_PMh`bXO?%UID4wq)RCWQM`5x1z6y0skiMpUF89!5s zrsR|<!$7uU%!z(CIZiYkU62#~ZmKxZbai1)^gFBLMAKnIPV}pLoLG}_ zxkmA;tb|MRB}8=~Feo@KhcBrlswJeM(S`Za?`DmB>36eszVtg=KVOn1nn_4e^35$e zqwST^fW@{rCmQ`GZwjtQLYgiv%!$4<$#J6T=n^EP>FUxXr0KAsg!HR>5>oHk8AobwYVKIx68<;to-j@YWi!1+JCEet>hsKD+{m( z@zOV`%)kDtY{(&rR8BuHcEa6-Kc%pqbf1~#-+t#_%rKMorFs+*MXJGxe)EL}Wpvam zClbgHjaOd11Bi-#cD~6TNQ4|a7h6D)bo^7*ZHg1&Sx#>Z&bG*mGQ`j>1;FMVT02*) zmYOkPQ=yuNGT zRMf->can20 zOa$XyY;8?T-dFdU70;022cz!f`c*lb;FGK(X>EXk($dLamLf*-g z&6;3HY}9ju+XP&P@lmV zc0Sxep`)2aViBUn2UW?4GL$3s{L$zLuLP*hK~(p#(BwKn%AT(dE$q1Ga?x-k$t-OH zJDG;$y22afAuSmfn;OR=oEy9F%y?X}U3;v@pDWBkmqZE64s-bjYRT713Hzw>H_1Jd zmR6m7eQzZ!q{sADa?oW`qd6DBeCg0~u`LolGBw^LC$)@~wGu9}BYG?O=<;Y`c_NfW zd^t|*C}oBoozIHlXl6f>g$f!mut+V8gs=KT+_}=(m#b|~+n(y-GVF-I@H%m^N?{y& zVto9C9bMLXUCt5L^g5yC~9 zrd(FGa@zn=s)X~p6WP2zr`hUe3ac7uBqfD&nDMPuOh)IYPq~*JcM&5uDI&^wmkt~m zhy;J?;9>~s%|3V`2J(*lsvr|l9!8dv6QuZND0#StSQ=%(nvA-~PtOpPuqv2vMrtx7 zXs?0P>+1uV-OJN1f~qE?x-)q?dsT3U4cs^JB4tRt1;} zf;T$@Q)OiY$EDHnm}c7=&|SIUJBB2Ec7*@03|k}Za4&qka-cXm#^W|TrID(9*^m-&oH$iW+n$1u%XF{jw^qFmxUxZ$SUJj~5A7sSsy)kb9*}Y|xd*0#{Ci zv(ZXNZ~1}hh{|+@JOR-IHwUybk4#ra2xO1Ix&JDq&I#^S;C-m+>Lmj-g*+1_TcVXM z2kbf#ur50J6!V8;%oU1Y!@cB%b-;x{>z%Jqpb<}H;S*bbN4_$SJ0A|q<8nIfPVmSV zJrzUzu`1|~)nV}U;;Sb(98S#7I$QF5I345dxanDUIOyYbvsIIJf`MzIj#e!jurV8O z1CN*4Uf}g}1Xuj|c^_}375sNh=f|oDEDj<_HC&SZ^YiY&UG!LFJsOv%^>*=$oSrTo zj7QTJB@@cTbAE|=He@cG=F3V|uL9XfpxAlM29Ts;Gld~n49BzqTs-`^$9A(*DYJ+tA|Cb6||rT;V<1n+QdImMb)a0MCi(Nb#7h=UIXGVU>s# zn60Rpn4^IDt94d6tVw~oJn>~^M~pM*yp$+;Lr zdmdbMtXFTSMExHo)}^G&z=P)#V}$4ngfw@id$?Kn;Qkk1KDm4Q*8W|_i%0O9kLOiy zYQkS@Pbrrhjyb_3wdAPL46C)#{&xHN``>LcncVa>u)zOmgVKtEJ$SKU8ke%B>R~G$ zWxhZ#G}rrEg`j3|dR+j9|A+Y<$oet(dB_Odtm2czfUBo;Qr*Q9wk?&h|4>hOO>4(f3BRc){OirvzfzcS>iL((ao81vy8d|d ze(!qGoAjTT9sWk|5Pq@9l72st!>Q}!{@LhsRBT}PCK=lhKP~l{xEpt9%1M9ZTKZAp zdXlgN%yb=(#;-PdWjWrHO`Gk{l;gtU;U`#DD%Z1VrY^3VmwitLqwdU6(==V|3HR%8 zQ=sK>S%FAzG8%V!=i>oW65vsIo@1%#abMzWl6)4DP2~-IM zJ0t9lKiFS~aZmi{sQ-(6ukUjTqeNW7D*2o^fAkw(Mf&)#gE+rw1AR%J8Y42JNnTjg`PP@MFeBu zkjjp>IDT4Uv&%6P;b|f7-FxNzsRZ*flPMh5)OFZB$k;f5Ttx4z*Abx6aH-C}iFBxC zxJkH1yaj0w;_ur`8cq&8yJI-*^XW0*80I{YW%?tCFld4}FEf)Q4*Ns|H zbX--eC#}m8aUR;`qA5&OstVhI9hfPd9WAynjajU>Be6IUG0HHTg%+p7FI@y}gDFv` zp~P9uvAG>uR%B=>e{iXIMQrD>WmM9fAq_P~Lph=%M~0iVS~B1g#BhrZU#s@`2a&Nn z>I!yc4duUjR8Xmx{4`bb-Dsp3Zvo^jFvBDmjXHKx@tv=ODA0^s*7HUsu*{H?0{i$u zQsNw1PJ|FO$ITZZ5M!4+61yx5bOAa|T5PMLB3|oX5w<{QfJXuC?W{20Rx<`#k4W^p zAd|{&Fv<^XoU>~~Hz!du2HK>>1^8O8!sRGxSEyObp0O5Avu~m}<3Vi%4qGPG?_=z0 zfQj+hBmJS9J@TV-)>a7ZWkyrgMOzKV3TnG*Tmz-#h;@aM<2KD`C9%Fer4y5Zb@gsY zse94st}{z)smQ+svi+P@7J|T1tHr{V?woQ}!?48Qw0MKcFq+!tTu_s`nQs#xJ1L>O z61oMb&9{qWct(c9s?Uut0Lc+s;^c^))MkrCB|O`BGA;|VQqr?=wX|nxmBeS6W4ZLN zHP1t&Dk`R#}^C5jBXH-x~O7USPoWFDAEcm z5I{PM&c(>2!!M~aJvABYuoYlSEhQIov^g|)N^>f~09ZsbRnEw26hgfNcmfYcXN(Y* zOKpUR2DHw#%cz;s@yF_YTFh&$@o5A&uPNA zTjvT4=a3zueSi|b@yE${hles3v(?!oJow`m|EIOD9Esr)@N4k*);)2MBaY^nIniDf z8()sch-$&vT4o#Ox2|dF5n$ooUksP%7>$ zs56`u!i)SQOIdo1K;sUsLS!Y3by=J#k;CA)a!eooJU)}!w?}8=0d9{ps~#@GIXoy% z&N+36!#q4Cyl#;(`3D>hyv5B(Ny;<_zu0h^uBmWUa2!<< zv|5yFNtl8nIA`ou?6Iey!h$}Hw5RmTWe;Nv7RF5znft}OhAHmpm&HeuDjtgd6tbS9 zL|E}Ve`>{TWh=Ivv;vyEmcRODeU#>Jqzd7xCJWSpo40#qm;9;S#i}avcC)5d-LBTu z$x}4_ntoaPoYqcV>&mRVET?*MF{K=754lpwF0})%Y(=O)@GPldep1S>h-aJ3ZtWyq zW$v{Geo8(P_6No_>=pid^@Z*HiGAyNw#mY*6ri=EuWUswm?L(7ung2vsW!o_4vjY5 zS4af?Fo`Rl#M%Y74i&k)*F+s{a>rY7v`KT+kA`b>ZkAmT<`kHn@${GfdU7p31m>(% znl+VD%aOjZKam~P`xIFt4)vUWb-r-1=UyYV~&7hqR(-!#)QqknPM3^C1+<0JBYll?* znFpV9lZzQ;$G#=H*2s4TqZj*(bDJ4Zr!JR}#=%!@aKB_#K|RqwRw&z1LXaNh?2_<&RffcJS0fX{AeChPT2PC_hV z1h*UwqOMbF)`0cL?1A)9PQeY!S;vAL&(uMJoe^82$KXLFqET@Q{j*9ERNxv>CWTl{ zRBxC?gr`@OLi90N=05He3mXR!k0Ff6%__PnkmzzDQfNBFDI78ue8XE;JN|+8VJjll z&-aRtBe_aRVGIrb0-5X84vtm%v=4%P6iiDGQJ|Nj+W~c;pzyh zxm+F9zZzT}BQ>9^qZW&DwZ~E(??wvCEAi^j<7%&J4Oh2$@p>a(V!Qzx>g(%8_ho;& zw=?Hzbqpvlj=Xg?N6&jT{WEwgse@&YJ>jssv5A|3h?BO9HK^i0jvmzTh~%4b-sd@( zdNnICwRNg7wN z;pM%iD4VP3W^QELQ@N3G6!Y*iAS~ID6v9q^rk)+GU$1gN%glqpXa$4HBAXdhcJP&h zB4RX3%1*kUs+dP_7ock72xG|}tW`sX<~+d_ZdxtH5s_%A;D<<%xLO&)v2Je2zKFEL z4<=|OgPG3ACsg2D{hWmE+wNlech7%)IeMnlLQckFA zFk-bhh)yL~Hp-yJL?pR3$UtU&JQ}xrP7)tLl$Rrgu$!3&*2=F#i?=FwoA3-1W`?Gz zo`2W>TGMzfvb=4u%aNxc&G=iO5bmOyURdaF)K9iATf3^-TV2*7(* zyVn7jJH;K5oyPQ7a2shZ&~Zf=*7yA$ZG#%!lQ~jF+iud!TGn~sI77*;KR?lp;q(Z2 zq=U_#-s0%d9}WR{ZR37ig$NX+up|<&$UjBoHl%vbK2D4===9FhRZ=KChzAG2DDxfQ@>MD_Ozj?=0g= zuCw|xl41Em?%Q^lF=fqpfpGM}k;{9FHFrYn`9`x}EY~873{O|~`HsXE&tN5AqY6~0 zI0N?b+*I01R>+*l%?chFTLEYLrnvHbu;!R6cedy1wZmB>STDa=2(yjUH9|e>damqT zu_SxA^NO)$>i!07-Et`8}&WXQVy|-yP zXy!F1QsvQfjtn_+X+G!Gs1iAm`m?7d(N=-n;7AXpxQeNiD9e!Zmx+~-TU9aeWiJJD zh3ki<&^rlo!{p{R#{;Wf3hSMYOIiw6^wDw>sjtlu&EvQpu0lEPC~L~rUyi<6`_saV z^O5`oBA4zx2GQFKA-*K+3*bXa$d zF~>}fciq~|)tk?B4aoxUOHELk$7>g&OhoQFYZN(wC8bQV)`8|5qh?bo1=?O&*N5uT zrgv9XU*q)e!>Ug;$%BxovO8k}^)AB5APp@~D9c~6mLH7n4tspN)<3*koWI|~$?tIM@{;M2_(wg z7u{IMLC*~{ zSFe%*@|A(|J(D;h41%u4y`sSz9YmsKtI{=BK zqH4L>&asePDUWHNWp4hjYu)6M>xhjA&2R2$O9tqLMXwtsMMf(-rd90{=xw~A*2C0I z*$M@XaY9z;CWg*mD5Zfj?J9au1|P1>`$^sG)pwVy%}dO-Xo{=FvYIEV)rzAe@r|32 zi>{8-a)^gcjtk*XuC2(uaP1o05pv3bhvlJOMk6?(X*ifX4bpOjnp-Iw+U!p2k=@;V zW?lsf(tXG;RQx$F&Bh;k08i_@+qeak}Bn?3DM zQsQY53jeP=Z^?nJv=%(M0VwPhrG zv_`3%E<_$Y;;5)YBFO1oftMG!^n)$<_2TG!R`hXz?alH!2pII53O?Hk^Ag3>CWu*Cn$u|3*J1Cha1s14hh(+rrfBTtTE`sH(%&i`jh=G` zhR8wh_8OtxO{Kw8LyAS2cJ^|p6n9t%%%DEg4l{zt3 z*&r})_)=@V?FbZ`H_{pt-+#AXl_ET$4 z#SS7K=f;;O627!qOzsq>(R5RfS7~t$n zHED%RXyZJGZM>> z;rUihQ8+l<68Rk9?i?Jp7((Ke#Vf(~w>p2cH87OzENPJYhymSrrsC;w(->Td|Ed=m zHf3988|9|Ck|)w!r@7{awzBOBf*N?bM*WB?r;x3D~ZT4uSK(b=cj6mC|sc!tb|LIg?kv$D?-FNd2yowiSrRB4AIeAdug@EV9e zLjA%=v9dmc**C)t>`+m2cVEsXT|6+`hy1@k8ucO*!x=ninmdj>crn_N))(?vTPVft z(GV{)NKm$*;~1(+;kg>kr+*3JJnAD_{3?-0Bjkp^2!SRl5>zh}^%yuTt)kQE_SB}6 zZ~F1b{neZ|QAFA@HgSO;OMWA&c^!rAZGm0Cbn#PH{QKj+V_s8Ee%`2{?EYPx1?=B0P5816!^ELhA zi=D{INGR{u&5EdtpAr$#JcncjtK&BHxOH^aZ-%`V#1ryFsyy0Vc~8Bndw5Z~*?p;- zdKS2wF|~qyr*731&BGRSj4b8473xDWj#d}|na>^zdRCK#jm|nV^`@u&mXQc^0oyUF zk%}?r&h)3(i(9iJdq^rhKq^bO{J=n#`(RR9OF)g+>@}k)SPFB6` z+b1M{pQF5GRe2k^N;rh2V%eRfh`gPw;{RmUXK2~^mHE4ZUlSqYH)yPh(>^bYxT#0r z2g^vN@a>QYL^aPU&O~|}<=J9Zk(CHe_=rL9gFM4^`&730BI*ZumffmB2zrL@91^B7 z^al&4N%S@OFtTv?=?8h1$}_RjjwWUI+4(ry%w>#E+n(fX9Zg2h%3*OnE{fp!Nr6&b-*b(N1uv2^8+d(RRohsvbzgzH-h!5!6aoN$B68m)brWmr zk5sIJPPM|%5xd3k!UoLElA;H9hL^vk0no5P1yJOY!OH@gCW2)?F#O|suO9Q8R_XJ1HV0;EulQOoaQ+V zA;Koz)v}jc~TL}ld}Eb{uf_9xqJK8{@u-X zcS_VYP6ngy%rTz(&Gu$+>YR4_Lyeea7hS6@9c$s@9d!i^aJ~2f(WjXnPVj6)xQ))e z-eVE+NjjZP+L-GZP@8VFybHn0-)!;^g*{4NyqA#U7T`fUauDjQ+Y6rLNDs&y5!Cru zp36>7=X02CNo6R3I)>eX5|)DhzW%!R?e_KE@4lY?V)IwM*Wur|-Co?gAAS@>!%w^g zj|4E~AorSUhK)m#I@S_|k52l7!G_8yzInJEx)kQ}zi5;Nj)*EwgJ#{ytQ}wB_YZah zQu;IPEeWNx!@=6BT%R(VTgA>#E9xkD8>8VWJIYzG`6i2;YdOiBhXr{kN!0@iaOR*w z{=(wnuXagkrlTWNB8gmE+f;SSw24ep87C7|uga*Tg-K{M(YW&XGgEH`{*hNR^%L%F zZ@W~bM$M8zK8KKReG@JZ$0(!=e(9BHBl)K?(<^Z)Nx$zJ4;75-7)1ao6&$|2b;C(f}R?UMc&&`9{ z`h4C!o!esZS6o? zgUi7P&dgenO`us=zR?&I7!mKUxX9hHjQK=xGTaKZYn0Am>pPIxdQVh;Ft>m1QuV9#8sbXUGfWb`}-58=6kX6g!#dZYb4Yl892xfRnnc zpQ;gl<&PszwxLwH8%J)1^bt7vKP|s*Hf|`W?V_>h6p4goMNK*=>S~&K%p(~ZlkCC? zM)@mjw!qw9$nMOzLB#ff|El1!-+0$pL+@Tka@xFn2;#!s1B7!J&j2R-8D>f7_vBcsxcWhq9eKzDzZoTol z1-gboXV;$Il_zBpWr$mbI{ch+uh-U1tDcX;)MFZa4B{yH+yk#@$eeM);R=QncwC zbKblPz7O)=hXe^Fl}0zpCGYAuH0;1aa6WyT#XoLzkEVu9UcZP?+AQ$$+()iU(t$sY ztmZ+*x@6i>if8~4^ODNSNT@n)t`BMEmYRm&@n}$;$lt71ct6X@Yy(dSwCE1vaSFy<>2USx zEELmcNyQHATm;l44ZP(^2WoW1Z*o4A6dc1*L60)9^fGSD%Rvc(&vOnKw_a15qYx>%mhItM7v=LY87dBOm_CY=p+eD5X}N z>t(r4A`xgdgpA{GA|l=Qwd4JWyflYkHDFN@10q`zTWzUb3HGO9kXs^i>o+F)rc;SD zX@U6`1vpV!)&iQeNc`M(B0drzD6@aLD%cD1YTpr2!Ob}F=1(Q=^#p2FQ~K@dqatMS7QZ7jK8cdgw`ad^9s+#B>amVRB&gqU^p!F) zCM7UGE;sas0kX z&9u;WQ(RqJ*-U@5W!52Z^)`a3qz#1HTN8dvU8OuT$W*x`3k}8HF=^_JjF}+siL7%C zY1|Iwgwu}P$>aHM2t$G<=2ujADHALkIUHnRHoeS_tb*}$5AsKKr+Do7=f&>!&ihdS z0GZP4zgN`}ZV)ZilVxTi#UIaD4A1wY^LkY~sBc-%Zc77`?e$5wj|X_Vv*V}c*Yne- z6g`vjw0yZI=kMMjN1a)cC)>IQg9PYXqm!+p(fP19ea&>}h#pmcEjEf|_8AF;=f;ew z!rP!`K^sOR^_VE_*d57ftzcDDSQ3>@yatgP7*Us#ug(Ei0*xuY+AcpXw7+JM}?TtO$T?1;q zF(j1tq-riIl}r-MvlYK}kQ#&?Ck)!q+{rAE%%`~$+tB+;IYwfWOd%;RhrR`PF21!V zb2Z!EOU;@Z}ku9jt4m zS%hOs=H*=43ZKhcyp}2p(A~BPD5?F9%{W_E?(|CJQyGw`JDWdH)XEvXe^M_Z)Kb*W z1?P`HW_5=YHWwWqE0D#V@^~OAcHA}M0yprdC9K+viD!VyzCeL!4@WcR)NmUF)xWR( z^@|Q_l1+oaE9nE&IXZj^7hL{WYfm_OvrBvNj*?mMb|jf0)>QXnvv3{W(A50v;3AQR zPXBKNe{E&&wteyAyj4%z?eAx@Dq$_e!s94|H zbG2&7xcFE0`6%v%0bhKS%6eEaA`zd=v&OXbs3$1*d`gLPfd#J`k}2fkZAqWIGU&)$ z{4uQH+2(3a&=1R+HKF`jpgJk7t23&4E=FGcodxUfHmMK(E+@z3c!r?O1%@G`)+tLD zXok!~+|hv1#wee>BULmZ}ysTv}v*M*z~ru z`P$J9;LkoT)eA|@FtpoeeRX;PQhEZRGwhx*;nCbR7)gc3?YKxROEqrgt@x$eEW7j@ zcbHlKjoZdTV3CFaPcG8X7YB{o1u|iwR^d8btm)FI=0rr}uFa}ft9RjMOHPc6Qd+Rj zoY-iLaE0s$+uIGwwQI#)mA_WZg%X!$AT*|~ym<0y^ofJ%^iI@soklXv4}WT#J!^dY zq~W>C5XO0Hkx=H0WH@azzv&0x6`Q=7`Fg|tsS}^-i*H_m^cww*qt8|Ve6t-VDh*9e zN>KO3?LI3>VaDW1V{y-#fW1kpkO34tq)?ajUgHaH7D8UP@PBszwVK|(-q=8_>(>kw zi@{q;Tr2*#a;|+VaQ<9}n|04|5$?gvemhE`?kMeBv58mK;_cXjSQaOf(OI$4{>d*k zi?27-%)h@4j*&wAjgVip9YAnQ@F&!?SKZ$BDHmiG%M=04+@LZO8q`E6qhTNKd%!-y z&?j=|4v|J~ROVNXsBi;GCf~wI&#=`LHecQir`-4&mYZ@lQcf_ZQEpK?x_ZnKA@Ay} zfc6fDgq$mYK52c8p=H;pVe>qHHQ*Wuov*KFkhWY<2|1sit;Vr}br{Vsgg}m~l?TjC zkT;2`LTGW;xhDAUFADz(BQG^)p{$b?Ku}`iBO-j6%Ge9fUE0(4Vj{2Op6@bKH=kv5 z%*Ln`PXlxAfWW$@Omhc^xCVycVy@QdbNOxFiY9G%zQXe->Tk2w+bqypK~xfG+8=Lh z%I^a_)Pfe$G)n_tv8*3utEYT%(+>igvK(WT-Y8KN@0P@)o68F^$dR zNF2WPB5gK;t&-JcvC*0GeDhHof?6ZWMt1Yspip8RgY)cj&p_ARRot-35)PXU@g`eu zL(7x)7h{Ne(j44LsS_O+Bmm6*1?tM|R?Nc;wDldxD?$OYwnO8bmfAi~(wtR8PImET zY4zMqx(l`TUNKAH*yWJOI`2qN9q%Sp*(422Fo0?vzxEIc`P8)KzcuY6IcHkUOJ&S@ z&VI@);*5X_2^R5eC|d|#bMb-;4eFGKJa$=nR%lcb>|c-#%WQOgiCVMiUACjIzrNmn zPwU?`cZJQC66H$UueUd-e;VPNSQYqT^8}&qIagog9i^lRug?4UkfvOGzY_2?XAOGTmST%Z{NTE zoj*V7!kH&MLfKpa*i}jmh-Jl1@rSi|Sw&QVQd9AH>knZuyQ%cYAKmV|U`NINK~+Z* zZLz8W758slPCs9LemVWzyQF@;{OoeZ^TqAU=qD0UK3}hv-M;?%e0%%&z;?v*w&VEicauQO;23VODwDE~aBDL{?xxTDe2h)w%^eSNA+bEC6RFSM=LZCR>12 z82P*=LfSv%v8`8yPCPNSy|1d%dY_aXqb9Tp)YQHtaW+-F1LF3l+~z2+l!$|1Gpf%$ zk;LAw;m3uF^(sCsY3=ksr_9X0-CWsSyxLpgut+-!7umJ9+1L!xb(k6)8DdnBDOd-R z_``ExifoxHHka*;9q}0(pPAOyS<#&pIa#(?hdbg5Y(&Z~o|L*6DR*o9j&&$F3)$ij z?bqvCTiaNs2M}tf$1x$fKJ)|R*fJMC*W>`$bHYfh?OJ_9&N*U^A+50CDB;Qyt_ePB zl4ees?2r*Bpc!JJHE-LykV8lHi_V{ zt}3 zutY8 zYPmhaV}J^bmI{%YI70At4dWRQOS@U{;=pS{Qi5532H1?v-qcDDE+f=S?}QpvgbE}! zV(8S^t-MM6aJdz|Txalw!R&|y+!{Mn51&WgnKkyuNL3Ec#;P{-Yh#DLG)2`u3fppo z&HD;=jl)EszHHvY4boaW^m8iJv1(pSgg0}Vc%;@KD=ccX@A$*psU{q1QY-iL`j5O6KZem*ULoNWaN?)F+^|?DHW2~!KsiQVfD}< z$+Zxdhe8YcI&W*&l<{JizV~8opTX>z1;E?9)!b;Ctb+y?I48>BGiO2CHkXFbRiTf| zsfU$vMUa9v#GZGqHoUw^Q|kF@VMOv!xLut&Gr`GMW92urI6Nk@zXtZ9zDCM#xQ@LF zV>*BA<`e_vmPR3S-avU=nCyYR9((P5)oUlC$70Q82hCpjvmJc#0;gGM&=RHK-^_K` zS4mL6c1Ej(#eq@_5W@Ynhybs}VHZn_MO=u>y#laJ*d~8s=;Zk=?-I7 z&je6FZsWPmj_|&Qo4w4yYPm;EyFTZvQEx7+X6^>i-`Xn;wC&<}{R6P*olZpGSX8NO z_+u1~^)ETsl%7!xxOs;O{MvgGiZ;u4u-Mwsy;U^wR!cBtO&3kjs8YX0wW`&yEO#R) zV1<0^;m0{ECo1OJ61;wWMg1oGPw6S%rPg3~bum*T^;D9R43|bL9bu3W6>8DhWv9Gv zxkM@}=b!|cOQVMEh=bQ}V;$V#3@OX9*uQn}?!m9#?39H_I`)2ZWMj{2r{_307p@d$ z(y7rf*0!luFb|O}ZFF){Wl>lqVPcVN=+UNm2g`;SZ5E^9*0kR%Tk(#lA~itp_E0&g zmAjcS+tI~kffQ>QtX><07ou@0M)Oy2EHc%kUXaoOf678iJ-BexI;XA*z&<(vSE6SQ4Sgx%|xgI;le)EzXIVir3Rnm<;o6`OvX0}Gnc)4X3Q>k{ksSMD{;N_Q#IvpKx8Z(^PEQiv!Vq+2^J8#~TfmM$> zq-WyB$&fNPg-dtnj4Wg5q&LWy5>>c71juR=U+ne*wtFt^4DI+jzUi&xM%KKd7 z21xq)cVw!~2S>cuXMN=T?6uO(zMc7MFltC>>)47o=$0ApIH||{DEa!Bmw-sFR)ydX zYA4eem4@@UEWbuTBasu8hFz+{WwAoZ)tygVfFvlP`UiL_O0vCL}XD&>zI$^~azV^Vjt zxbnFs1?FAD&CIi(#8r4YFNG~-xND6c!E34Q5hm5wIrYv|C(HuEd*G98aj~iUsgjre zSRDKurTv=GN)C%ck)^jEOD*IOZXmP5qOeO#+Uod}RSpnuJS;33L-ZSu4EFA2b;Z~! zK7DgZ{n=$k)^4eX`e5Dz3GJghxbs>rxJX^35>QBlZKqKz(z76^_( z#@4yRTp8s%EP3^11vTLCyR7b#=eT6g2-C>;-y3O^OV_?p%+jeBZ3yLa&ZGj$~ zO7j@V!MFNSQ2^kLTcvu3ks+DuWg#Nl`PB0iO+z4nLixz$V^OZo-C^qNmut+7f>w zh^G^ns$28^^i}>a5dIKMaGD$fgG-nHn=kRM%IG31G-mFcmS^} zh|~%UxYaMqD~Xgo-KsM--weOkyUE`OPC8lgDt1t-=dMdI(pI~Zh)Lj0sr_}{>u}cg zWik$%1YSKg$9+anZZ*Fsb^UIL?aEf=D`<6~yeHN48A~;&Fgbk&FA%{`Uq26^m(4K) z%qvCO!TM2pbp?`29cYQ*6{DmcR3;Hh2DwbFd4O#qE|8sV89V{euh9TtaD<+R7*v@D(uDMJmN9mDoi$h`VTGpP0vu|f2KD9vXYhaB`~>Aq zfCa?QV42)DXaqDEQx(*pQX{a6QG#|=z%y0nbrE>!GjN0QhQI>9P&Le;T0_vWqNs)* z*Ni%dWqrFh8I8jWLg}L{EnnU{zq|M#@gH1#2qk!?iike7=%N@`LTP!j7B7%nByA5~ zIi4L(%X5-7Q!OVaGu6U{m8pvL`Xs^%4#bt44RLyD$6Gy`8X1e+W)IF8Ab~tYs$Wu2VxYmeY@?u+RNGAH|r0JouoL6ih9jS!`+m3>FkL0 z*ADVwf=0vVbi1mwzfmNN!Wnl?rsZtLM=DjzemTXJ#*zE0=#|Hehv>;?lV%f+Iy$R{Iq$Dz2RRSvs@*{f(U;-pD?%QaNP^JvhhJ2PU{PIVV@ZyoV8(w1(; z45>$}h?n4Vt(cbO*|Zq+pOr;_R=gOUBdY$Wgu@EwQ0Ig0MEG501xGHC@dyI7&{J@| zn2rZ%1XdFMONsRTm=X$&bZkBYOlRHMuMkAjJuNYdKQl4{8Q*8!$*kS$9Bwe1Q5KT9KAgSikpL>pigiW*ZXE<@d3LgV|oT4@VVeG}wx=)38>Big)>8w_$by^6$0chuucu ztVOfA33b*FyA9urH#7=&vkM5?Rcru%*ll!rCvG$T_LB6iup;Jjn^_d0;F#YoGG``2YcM<8+L(+x+vzuJ zZO87B1*#TX!Oq2b2~!w36ms7x(OPj|5qYaND;c8Z0^`s6z22bgd_-SKMRdM1Ru#d! zy4=G90M8+c&o(haVMJNPA&z_bId6n*2thXhPJZ zX^@&ME%eUF2(&{)%Y%xVFcm2~I;gqw#oQ=r`v>pPiqa;10s%oDOA= z?1s{99`o808~<(?Zrh?6VuE^fM1zR)1~S7CR?*e&+^*2kXfit=_Hk5mv5|qD;18ZN zvZwvirvv;S4+dO_jOgs6xHEemV;O=xFK6%MCv}`4^S0RLzR|q?yx55s2c_YnypaHj zg^OmrZj0@(E@ z5O&_`fPYYlIHco+QQz5#1ZFf*sg~Vo?-oC?GaNIp6Rgoxw&&N}Zhx>4;lf5kG*ob6!Pzhge_Uv+PKrC(VP#5ow%;=>eb`C~r6NSg(H02QQYY67 z;)fTMP?3VO`Z--nmN_y_l*!@ibffI&t#CV}WH{iq_VWBTeG0v;wA=Lj__&-le11MU z@P&Zhbw(QJ!1?g(dP@BhUM0^pL6g#sH@5R5Nw|#Z=#9BMo0Vr{q^QYRVri&=46|}* z^Q@Nf=mo--#FAJStu4xOh@+x}4&T3C-2A38McI{`9*<1f>9LxDY-U!Q9k|{4#B*h9 zWJpszr9o%QFk9K34B#_T@gttO9{}Dk!;nCe&W;r?1+rc0rJ#y-ZBbxxOpho1voqMH zvMI84Lg};&`h)JQoQu7|(2bQCq{41(Sh|ioj6tnh&ZZ1DSwKD4+)-}M4#YK)1zpu6 z7dy*V(4(D+rG{u*Hy)vFvD5%<>&E>-&<}gqUrE;gP;{OD!nevfJyOL@Knh>MwS8eg=o ziCvn`uyQ9{-V~$1OP%9Pf6??Ex`|cRP*mQ0dJpb>ZiMP$vl@nM|9G!xJytQqtw82a z$7Jk`j&fn6bG!~%;>hCOd0rkX3D2QND=r$nN1do-A-dEm08KX?@EwQV`1&eIj*>qOwgi z?H)sz%>xM*)tbv3H8&-Cz0PzzDZ9PS3|{;B*Co_;Ydo5EW@YzGx8-meo)4Io?S4-Y z!EWT9j?T&hz%=XpA88F*=3w-L2~)v(GzcZ_t+o$1r=gvSAU z#m2__A8v0u2ZvGHbnFDj9Gk_Bf>k%$xC$}efcsSbva&wv$)maS6BQdOIr{#D^etbG zH@0s2gUsTc8DBZ!vq z3!UTc;276buyHKM9q@GLd3T^C&sldi>A&>uX=y0oeJoY$@Dn;xu^Is7ct9yX8+E2Q zoGUwL-SHT<-{JN*b^%htzgM`HJM}Ghs$1^Xx7@96`F?%N_p4jptZ#XfB4NeV1v%DF+m#$?I?${Jzka{ zO_04DZgC5&H!E>vHbMk_-+3fjOG6Gv>Ga{<$8l5a(yYG&b8qvCFWV4y?h9Cg#^f+?%RPXL{5VI(qlzb@p$-+; zIXcq<_S)K2k?K?tXzAyeZw_0gPS7A*4H%e8_|;Y+&x0y0{_8yrn&2jFnO6WWSepTd58hG^Tzw+=E@ob5_7L@v$k_Q$&+)Xe0$3Xb26 z*t0y`*d>0gxQn$kIw?Nu4o;G(3Oy{um_HUNZ>y6uWkpH6H(&tEhQpNw=0_^HH!;A? zMCBAY2#}T4&!u1g&{NWR+8aV5l3K*#hq0Ks@c+%v%Tut(_hnvIFc!BE4e72Gci`>X z9nT7;P|~8RKSe&CX@Lj=VYY+|oxSNTCmn><0=#lK?LRLCsuO@X4T)q2aky6O^BE-M zVoG~SzOX?}-|*X^=TiGl>1|6F6+yUYJe;Arhuie3j~L0m(&Vpg_fAHpi|i;BRgfm@ z4!pPfDgyE3{Qqa~U4JA?uKX}+wbF_sE+I;*T^W?LxjEE!7FnHD)$<^yv9;VLXGR>< z^l&gUT#~LGB(o~Bs&YE3vY46GkL_*CpG?c5B@3Db3_*Sg`d~no*RX&!3>$t3GA#Im z>@NoV*^vJa|93v)Cr+F=abnsjN&ZT|%W~j(Qz9@( zHlvgpdc<5y@qZ_XtraAncaMgX>3H;ei*NqkMzJY5Y^}Dmn(%K5;6FJ;|1{8i54znJ zG>eYvGoLtfhqYVO9bL1@QG8y2kBP5fEkLA5D1Rs?LRtxJuY2o`W?x$h7P?3D2k^yl9u76>=nm7*Bd#=(rq4|w3 zgP%gWe^Se!5&r&gH153lH>=aBlCnbCT7$Ml&;%ctFihdv!Yg-XgqSIBa>1x`JR43k zJz~wx89`U|4W>euti|}oyLT)mg{$m3%N;AqVqLz$JDuCN@d)Lz^UioY8dq1J7n@Y{ z0@rkYvqHc+5D5DZYoYAT(}BJf2KrXI=rj*B&NfSpN0T%#X3T$C>l}ym+FiLG`4#@U zw59^(D_wUC9d&=j;?TX?A{6alLPKr!v)cJsgj!^qdAFU0R&A;{LrliX9*+Y^gPPd= zDwHk@jR^CI??k6rWB7vD2}CV+|5{YE(CKO?112rHQ<@TQWTsh&mihQL91mXZq-Uh$ zT>cd)$fKF6mA5#Aaf43aT~Ct=EPs8FD8rl?_0mkSmv7Y|aCcMeV=y^z_izQ020-5q zwj;t^)$WxkTWqI0Y9bOlf*JuS`|28SoZGu(U>!)C-?a$yoqqYec-h+fH4{=}GKI$RQ|3};b@G<6;E z(;B*7zbH6z`JOCtgK_33Up>#uLNIU+-Oi4uYSUh=E`WZ9=%T7PjeIa>C1YO1=4I6E5HU4$4+znXnHcqr4p1mwpaV#CFkU_ykF)SjiTH3$^vf-nqzdgMXrTph<^ye`co^}} zt*#hFKr2@)_srT|eG8*-07m~fzMKM2ARbh|D_(0n{YuvJGD*lsc3ZEecjbQ6P%gl@ z_KupgmgOk4Q$!@V%;tBUJQIe|4zS8NV=0vB=BI~=jDniwAF&DC9yOFC3fN+c_pG^f zuk1z4!C~zhXJm7eBny4u%@5x9C1qlENVtdU0K|_R2@A`}kyxM{LGmwvjc`$oA{oB~ zxJ6x^oZ)iZuV(ycoEN3 z(26-&Su+wkO%=n~pj1Q6So)!)m%14jWxcdjv2806w8A6+G%T1p5z_04lIn@_B8-Fh z>mU`U+=$dMf1IUJiX6LS=N%gLbQ!rXL5KM@=BM|cEwoEpY_yqHQu3GvYufZ*2to&L+& zwg*mB5s>V@b{WV{{~H_m&O^?JeLqOSOrfeF-P!DnHv>x@4KyTECWX8Pa0zObF3pFg zvf(NdK&WWV<&jKiUyQ8fN^tVwf)@PR z)vuvx3>}<72=tTAM^8crGuC4Jp_1wHd6ZH1k$)xJqc#g%>$}bBhDDPcuwJjkFoE1+ z0EhtpR`tQ8OOTUEOQ8=|uo}GLlj(yGVzkE4`#P5zd0+d+2Hv+2@JmMHs^jkA7i3@3 z(4~jnw{Sx}fat0QqAD%InN&&UR{|s;8VepzjhZ#mBoNo8JkXI_OPVl^GheX60f3VTD*eW+ey_Z+!S3=*UJ2OIBVWeq$BH*Vcgu4D#|B z&G)zauQJy8YyLm4W+iM?qGR2{x=GWM;{?WUlrag?vk=lTBHVsA@DMASL8@TN6F?D1 zt9bANr+|entI>S+a{^DtpZDJxzEkK8wX=BQWX&*J5mk$aZ@&#}QJU(zU<>u-@WwL- z^`LDHqnB!1^gZ|bKFScJGcO}gn^7F2X-C{oxN6q8@n3W6{WlYaG5 z`=}u%n;t>o9e7MTtRfGG^U3gFI#xfeu)0`$Aq>|-4yUSviLL5<*?;5l`TGpU_xj}M zC_cw-7^)yav!Auq zj;_avp}#hXsz=Ak>JpJ9J@gG|=4*4Zf8dG~PSX?6bdPXH=P$mL);oy2qNXCfKV%bkRY*x_lA&eF~?yJ_@=8&l$6}c zF3M!}Dq%pu>>)nlx;P5^W-Y|?82MMk$4{qK;%0cX9M5q@M?-}+NE@CMWjL;}7`)hN z^2q%8aYm(BNY3BXTjpIq)w8oM6C>F?VsbOduF;9L4Z>kQ8pc7WR>Yb1F#(%P)OB2q za^lqjPNeQy7>fY*bH=(D59AWDkNhCED4zTOKoqXLV;kCoip4mVj9ym=V@doP$ln6H zD|V*MZJ*0^Cgzi5IFmuIUqqu~%{099`kL9`xMCrx%57e}XX~fd_o~v*Z2f9vC9>tJ zUSC(OZ1`3w8bb6iMuqhnC6eVW(kRl^IxnbeweL5xi^@W1(=EwC*QZOG7e1n=1l1&u z9EVzYiAof?yg(K{6d=}$;hIKT?O!IkT!sT#&l|Jr--BI5Y%46?olNw>A=8C=w1Ey|P+#x0q z?MaS+bJX>KPB}a2ODIz4_3+W~N!IO4ApaZB75N}9b+eL)OUT=Th-7MIVq4KDxF&{q zEm3&sX^pm{No|;f0m4+&BEYC5+>lv1&zSN>8PBW2i=L~ZWOu&@7n-W43*X^JKSYQ2RnlK~ca@7PNB4H*Na@OG=zw zoE~834LNW+eC#3Q`SA2DwdlwoG9@y^k%iz_S=dbeZ1rKqGvk~-?sf_37jbnoD4tWA zrLP+^_s~)ku~Q2lL}0M+Re8n_=+$wJg)N|^>&T}I;Q_+=kKr=E7~J{JAUfps=W{kb z_HWoErqoTgeht*EOt{He4_*Y}TARD%=AyA?x7-RhA<0jr`Qyj)Mp|q-M&6g`!`N{T za1t;X$svsM{ArL3_6i3z18zVmXc}qJh^s7aa@{^!9i@ zyFf6C(6%HTJb}0t5PJEfin79&HC#|%!6D6yeR!>quOEgn=IHLE?++?c3Xbvp-=3U7 z^rlrjL#!$sR_U|~OgXdjb9Y53&Fas`i;F2-mC{&VoA`5&d3?9?xPN$Yy0|!n0`2ilNdz5GZR$)G_nP4G;j55N z(RwAGj;4-8#^NeM6fI&&uNiYTiqUa|0QED$+I*x9r#w8FEOW=4;S=&8F}`-X9(5w% z-*=Z{6W}5S)&r@i8o}kki-zfVBLv4>T>cO{9dK|kd#t__A}^yS7$--%HJ%UQpffoiPa$#0gmRj8H(_YmLeDQyP9Fi3 z`}nR(^e(`Q9lYo}?kr~=lttv9amT}TK8)uVQ#5moJjPT%^NmOYGOgsfF25ItY>{Da z%(pp_35QA@I_V6YOK}-$TB0pnxWycgReDw0w7j;ko{Sc5Qv))Uk<5y$1(|M6dO@^; zsepC)ZDE=Nnle&6ewwomoWZgNF@loJw$M0=WzuQRI&f;S5>_F~ZK0Xitq@)^+Cnjn zsshEy*y1u-DeRuNt`3`Iw8aI5Ysq7IZDCOtuBDLAYzvL6Sv_xt+;!m9a~0vO$62fq z-xjJ2P*PwWEaS_A+Gy0FqBeZF zwr!j)g?c|eSPZ6kVjD6ds^xx0Y}a4Tq#W$mzI7Oy zI(n@Y8&-C*w?GKZqs`7EIQK=Sb7IP~#l`-@?v~mNNhipQx-d*<$H(J&e*)LD-Xfq% zFtEZ;$7pAR0Jc-JJM*1&f7|02C4vI5x6g4vK|B@l#qQ`xo)BN4Ih=$k?jJ^-Bq;{G z2VRzINkbKh8DG!MyPkHey|_(UEL2O{l-FNujH9EvKzHk(V*u-_KX|E#q!>OUpk%_R zUy(oV5|78o_~ z1r>#FKi7sY6>H$ja;fm`=i2b4Vhw!gtBf81^0;4XLub7j;0s)l9*#(Qx#w(7Mo?Lq zj+WxHVsW+K{xA~Hf%^-S-gqFIhNI;efjhxK;8VMtV8!;ySGcLYz`W&r=O2^wj2;#E zQF7ZwdZ^){b+JolOQLfHI36?$dd=Z{)IS~LKr3BxLlN?$3J_3ae6)m{7?*ej5y@8& zOzv&V!jD-s7t5CvP9|JZ%%c<$iVWegOR*AO!L+)B9^#Vhs#`hDY+T>pqxWg zY340}C0egbyI?f96a_9eVcH-WHBBip6bCY9KzM95uwS7xZ{@eBaSoM-bnr#% zrRIR)to%WYR!Gwtdy>=!L&WV?z%*cyW8l9VTlb{~a|Ew<$ooW8t!YsdPWVb`C4SYH z#TczMO>}atTT3QlOutl?75cIE1jB7wF@82m@v3ZEoIj7*bUr@B23T%>EQEDsMx8f~ z{(?3SJrT9&Z%wz?oXm;H#W1aAu^=I&zKco=$tv(#`ntj*FRo#GB-N+MGBbBzO($I<*iZaq@&vBGbZ*0+*vf3s(Is1HOGX~j8 zp}lrkBzcXPBqcpIDJ9yhI~kJ|_Ijym7Z!KU7H;`2FwWEvDS}+f*7X~bRDy!e z+*%sOl1))g=V<-#kwRfAIKN1=7Ioa5B}>W}7xy9v8^R)-!Vp#fr8pSzOE`ovkL=-@ zcGibK>LB=yiqycu)}mDAWkEdom)BekJ1fx=F7F$uJS?CaGFw7h;PN=aG)%`2$L`iu z0cy2SOXxFNT8<{;>8KXtyhsh?FnKRGZAD~-iCXY#X)jlAUR}2dBtG8Y``FjJ#PH<5 zq>fFtwBm|c=5D!4d%+8Qb$hO?E}9Mq)Ev5&tY|Uv2}i5Zm#bdhrdUst^4fZm)CL1T z2hqO&-KzDdw6+~3tktqtZUO+2Mzyj%OmZL{K3bF)Gt1#{KBs*wsaZ2n)tQfq)x_GS z^VyBsc~Y@%kln`XS%McZS{Zu${)Vvmms_PPkL6TqRNXBa#7B$TT#e!@6n>|a z)Xmh3=O>3U&J;ER#xk0fRTKkS;HGs)lp(wZM{@+FqQ_7gaU?7TZphQS-;&Lh2TMgE zXCYit%1G=&WT};fXpmS{sE}{Ue<4dac~!pZQ049^fVk&@ZGIGi^H*9L#Mi#4d?A+&Y;(+U??n`tiJr=Q;&+G-zSGqekuL>I1mNWO>0b;WCKW zP|i<>Q}DrhR=U-Ws+|2bht8Kiz6&ZdmGD)%a#Y3^?I>y0T#EJEgkogOwdvKys`-i1 zRLowGIw^(9T0&Gts$C~_Ima$RT~=$hf_<{oC4QHnu1K87AQf1?aUH1TUA@rxjY|+X zTm|(DXhG9|{zNvsKwSG-RC_3ntU$uFoX%xgkW`6BLYS?%6tfCvhqa_bUW-s%G(M+8 zdOb=>rS&DEp=RRqIw!Bf{)W(ZC66Wg7YSF@6=iI~*0NHgq$^tgB5BZ~ULE5mRl{me zbgy%9Go(!lJEp7`zj+1an3md9cTuG*h_5V7UPGz=Dz;dqL*dqZ990QtD`+ZwLyudVe7ibyrZxo?-X==H4i{-*X-h2m&YuIl1(lz4qIRq4?i*FUwVi@ zc7$Wg+Jo=veF9@2x113;&T2LCafQ;r$NA0U9+%9p%EzgmA!8snHtNxF6h=KgLri%? z517*9xU>WjZpx>)YRVUtXJrAA>8nevYRs89n*g~YD;u$rqM*S!p5BoBJ97+d0sn zkNGN;uQZ+Rxvq8J_Ux)(XsU|fmAt9I=9L$9l5GiR& zt$0HSB^1Vgn@X5I$#_fTug0=uas?^-soJsVx)aXlf%WqWH>hr_T$Zw}00!gnnTD@+R+RJ15tl>d z_;d9%lG3=EpoHcaUmN2WYyuTi(#=i|JU=FE>u-iN@^H*+EC`k`UMnxfc?IJOt5x6! znX<#f2L#?2v&a`Tpi$`>xWsvIfVd$K@CXUu$KOTv4=fpe75Sl>xGrN+Hry;@v4!YB z@Mak+9(gDBY|vM;3M<&CTCKv6hA|1h(t&V9ttPe`ooIQ+(CiBszG6eqD3v0KT*%Yd zdZ4mH0)Q^(8^!wZN|^dbF*icDLJ&0T5UA}sClvo&IZ+x=jc+s}bk%UQ=otjW=wfPb zHlK{KFmw_8U^X8ux(NK_W$yA~e#V=3tz0IrLsN)plf{biNU?k6fH7`S9599nB*UtR zCIn>8An&ICagJ9rrdagZ3Ety*ofb0-T3m-kq}^92{Y zBG!9~2z8!0k|C)~Q_T1$c#f#^waydhF~f6nZIm~P@X$!Fo7Bg^;{m~h*4nYCL3u!o zYY?tJVQQhtN{9A$KI{wdc&Vb2xgEwk1_KSP2xi-X0S~2>t1WKh%~f0{93L*B8d|o# z+<`ckuR!feI=TUhq;8Yj{X2J1su$zM?tQHjrvTH?g@`Ef8r%w^SZ^AL5`(EDiqWOD)jooX8meVR4u`0MC z3k^>3AkRc!8%PDWQM&LGl(eZdSU^D3lsK>d^9EAv;m;(*Db*3sltA~XZ7d=ov9fAy6y#a55bpOx-<9XoqG;;d6|2hM*Y2@esE--K9vY-fzD3HGFvNu2P~S z6JCw5ite??qV5W#5wv)p<^m5t^$F~jhF!TEZ`DMDlFe6x9v&>=0)_k4ZUTiVx`kT< ziVKXFL5jlVwTHzEJt-W2#ixFEk@d=3&4<$zl+WN2)E*~jZ3q?ywgxo(_Ul*Htch5Z zvY&NDl`m4{VqR&})n~dkQKhVV5t2eL+8nL%(B%2S`C$3ji{ZiGWIW}3KRcQZn5W3< zCZ1cqFuBNAt~M;2A_uTAJQ|O6dGuK)h~7;MgEE zt=kL-Z9~5jh#P({QUOvTo}Uh$crK-c7N#!WKa0@W@A|Y-1&3#W-i1!fsUi;b7ooSf zh)b6ONi4zV>+a(@D|pWF{jj_b8<}T0IUVEPW74KiD>X^&^aBj=`x+9v)N)p&83A&V zhvkuvQcFmL?0crZ#6&A)m%z}H_B}+%pU0I$VyVjcuz6upZ$tkooLjcrcmb)w&`xWJQ|c9n4{XJ;W>1Wz3@NW;hQgqw%Z^!)4Xhe^!;r zPRNo)__JDo^IcD$DYQrZ*P)ER$xGKg&sI_dF9uNpQ$4(NA04o#tKLdL?D&Asr%Q0!MP+?i4=rkHdXmRb zw^xNGT9(LF#=DQ&oFfAwi~t*oQ+N-IIb zjhYFGb?umXTT(zRuU5Ta!~E-dRX1wer`0u`>9-{evLdSy2`X!>i$s9F7 zI_qFC?|5`RGx-#ZP3x?8!d;)4e5%Hy_G?==TeM2+P*$XIE$T*#RtZ*=-3;dn>sh*% z`Q?YUoX<=?T+u%>`J{JDg1W$M^=Bp@wS8WKORm9M^Jy~qd}h~>1JwQQYDJGbhP9PF zsdQz9PoR8e*B}|mnFd`*BBz|Vs5Mo`I4k-M01wD}U zQ(cfL?esy~H3~+D6$Xw!-2CRB`W*j0e)e(ir@!{jn{U4hd+WRN+2MGx=nrroak3l? zKK|$*{ngKXZm+-h@=reg>6f!YY0^%d2$^!@Mt!u`L?;?0=(^Ke(|Kl+9H z|CqI2tTa+ROLJBJ$uHdh+kd)iCq&PK7F(y67Z_<#K3{oOxHfd?bEwcMRv_`5Ht5R#%RhJj-}>_?pEEqpr@hYS zihlXe-~a1hs0AGz=DG5nFWi5b<^M(nmBZom@IvCGjL-Qn8P0{LuF|Vtxc^O7`m2>n zb|i*#K3DeM7w-RezuZ_B$7inWfB(z(|KH}aIQesB|IIJofA&SdtezRFW2pq%u^BwCuK&ZB1B~ zzWC{v7ktfN!Lxt33oXWdG|&Ij8S&#KW>Q`L;t)@JT};Ox_kQqie&chWo179(2XNs1 zb~=9H1($FR2e09xga4h6eA;X}oNL+1^HoeK<$V z&jz^vsD0SwIl^Y=t6zQa2)Az*FKl#f|90YtqgMZJw_Cvo7g(mV*@Mo-d1rYtrf@-Ol$G;}LF-(Pfq=9HYt65$;Ul zx|-Hs^gDO9x9@Z&3u%qvOyI`7?0Da3SFZ05MMxh5{(b(Q9Z6L~GL($SXBF;97v|$* z$h5fwz1ew5QsMu;ysdRMJ1Zk=ts_y6Y%sxd9mkL$=uqCQ=(CWV+9aivMMEhal=p-uqH&2GvF>)> zRc80KI(V^ZiTeYLWn{FJDY6h!kQe4-xYjRlKQI{-Fp)NRK7@-Gqa-}FgAR_>W~Ylb z#&kZDfAOtE{s2Bu;&VVg5++BMP#3B4uXAp`nDdQ>?#Xg_es^mN?%QMBR}#Li<37ghY6jSvsuLock!cMr&O)=1zwk8d64 zk*^><+H=%C96KRsM3fZn>@@LAHv%5Mf9tb&d|HB}# z9L!5~wgP(N`i9seJUD~T-u3pG5Ca`N_mKlz@bk^}T9*yqbE4ROrfX%H?U$$*uV#6x zk9CQsp6^uY9@HkT=kB$r$?uPK5vY*>xTZNi-|Y?<6oo|QAAvsZKbkC0=!<)CdbUV! z6Vh>PFhLvJPzfLkN1t91Kk3T|@0bY~;GF{3KfZj6d1p`rwicmh=#IHXP&T0}P# z^;K2$WmRQc64zk@h>uVj+Mue!l_q8Blm&#>br!qm(4!NmI@VRV7cMVUW+NB|L$saq z5~^FA--S+o4wVA*@YEzl`%p*EdMU$5`V1NKN0S9^8aP^J2viW{GCKGq5zG)s5DdK* z9@^i>4OcLWXyn*$o`JP6i+SyJIt6cCAR@#DuQ)vJpNuERC(94RQf%5Q)O^^ldi&#t zo1Mq@-r8s~Jf6X`9%L3ej~|i|$*I%|!Zx;dxFKyHsvc>DKGa%HQAK6k>d?JQlcG9) z=yt{RdKNaX(3tRF3-2Op<$l+uFgQhTuI;l7gJLu-rsgQW!qv&i*%>cNUaf}?iZ=OT2R5>fCUp)FdX0P6Q|th-6V!Wwpb*(> zG{a+w5VoE1V_bY}rjH*PQujlS3L^qU*ayjXg^_abHgzwo-DyY1!;3ks5AO}n^d{=H zpaY=T;f@4oBoSR(5R$LGwhoR#7-g!!6(|J5TL)ovN0_Q1FO<+Y;*5WIQ5)Cl z+(*9m&n$i|41U%~!Qkh!+8O*5OS6exyTwoYt#&fFJV!=FM?AK_yhi64eDRT9wmt&ppGH2&?Rg;w`KnOB7vvar(a17pl!>&~oRUyaU-!3SRU!~HCtM!w?(uRX z^6Fm2Ob> zmM6Pik4F*K=@_u~@o2}vc#1XAEkXE!Ex>#UVvLyzxhFqsTtNtAfcx%8lgGQxjx5`m zLAMz{SA(0Dx}sxFp2co=a*VL;z@^v;x4Elkh9NZ46h_>1?<5Om=V?FQaAJfuOfknf z7p!ZdnAgEgfTg=kw|wY70mHP$YDK?TKI|-#Z17g{4J#9OEImCF;PJ8Q9d7%WjG@m| zgw>FTP>t-l6ZI+VR@;oEitRYs$uSjQD!Pv30Aj%t2swqs-e;a2L==u6oNUlz2wBV* zTTh*g+Tuogv2|z5#Ko2h*A<5pcBrRxfhZ6WSDf<|5vBr5bRd~!Ztsq1PWk0I9uRSG zMKKXeDL{jD180G!oX1+n=~g;Iu9DVHIgzEm_ukvw{)>gfwb)-Q=l#WW0xKj;+B@5k z(Zu6W0_QPh!#uvZmeZkc3Rb&a1l5c21l|#C+z#3Cqax?H>J-+B0VE>ipe&F&&OIl* zmoum&peY;Sv2)-5IGa6kr$uhdFrf7YMWsrgAJSpVJ;g~KgGOt%d`bR`9V?+NX+C~S zuEW@hNE6^5`&OX8z1w+BR&JjzjViyS@F|?d_+qIX&yjnu%XD zNxSAVp`s63e+F-1<{oMq!hJqMQw+>!Na zfm34?g?0seb$IkqLK^eXnW!!4Xq5Ri>FNhy0i22;i~+&yzU27IkD)jMbix5M4l*6# zxW4azM-d4c`}MOYQgYEj#OyJKZg|cUcRWdR2s6=SjgxXCpKy6gz+gYP9Gaj1PwwIPF-T0Mkb& zvnkH~(Ev7*;tX)YBjyuvv_MR7G56uH`|Jq}aV0cQhmU(P=8zM~l5jdX%kuBs*?2)5 z2)|VEZcu-lz0dFc5Z*R_v1)j7`MX0j8wa;-d~W71tJXu<_vG zW%JkSyliMcTfoKUKmEneeNG%@jxNp)mvC-?6-hmArt^!z;Sejxb8@-)9p7LNT(p4g zUUttLsV|P;-OIO#>u;z9PPCh*>x`tSb@X21B$GGyY21-&hqxt4H=?T4^tD@WJRY8& zPotgQkLZy#Vp!`Eqxc!ARNwfKLb3hmmA8okRyC+-cW7=Uh}*cWdqXVTYx;mt)o9eV zM6D|MfrwONbtP!k7eomaZ6K>#O6^xr(d9I7udk1y04U+a4IIWkTg3yU=M0e7?N^Wr!?@M_Rgs@+OUjE z>H?=)Ot!U>(y}=G^ouDcI-&ddy|4V_g5RzSY47{(J_o3l#qm>Tl7L`QQov`=d=w!K#m=_$O}ar*hF@ z=8cIG&Q(+xoRDsI4q?SQ7a!OJ<>u%6?|Z(OWZ)F z>;BOD*hJwvkUL~t+QssN6N}Lo*&x*I28Cv4Zy`h@ez0Q__|e04PI?7#4eFJ}bzt0> zX?E{y-`UxQ*XGw>gY>3|FyZ|opNizjdL|G76Z(OZJcQJgI+`A9lj@?s@ zsc&O(TwR|_sH^SP7zZSiqh8|3eo(ehV20&Q^ukz)7`wf*ef!Q!VZ=h>H?B># z1-oH`Xfp&Wnkz*c&uN`>qd52GaoWzs zD;WgNO4&$v??gCJ5W>WmNI@|!(8DA#GwQUbX39ELT>T_gARh)Dma|&x#|HGO63U8n znTO35_tNCBf>pV)yJHEK=zbX4A6=s{j%L13vNlDDsIk|l0z_f8bB;;cP;17YH_3oJ zudcpG3xZ`B7rETu-1{wOK}f6qpS&|vsKoTrEt{YR@RrS2D%(1jPl^uauz_NAf~Nbw zf7#!(arYQ*{9+9RLX1SF=FW$Yc9Y8!wfAM}g|luN@^6>3B)^M>rw-uZ#5aIQ!U z2rOQ(Jw&uwM4Haiur4eUC`kqS3C^VscQWWSZaCn8lt6p}nvJpGn=Y`V!>BB06APA& z(u_cIf>%>-cz`E3A5MmJ?__o22+vO)Y4>3RgX()M$n{&PO)MZgu^T$t~TNM-{p61O%?;(%8_}ZA9PZICKMBM zIy;_u637SqsUG+gCxvfGZ`a%tdud!+Yj$7W-nsMg*LPld>Dm;?;D#1d$<;^DCo^2i zd9vjXpkawo1LfA!$IrIR#S5V=JBqhnNL+kP5*1UKAr*7iAM7a)J%Bm;)i&j%S|nOm>aAqi+oZZh2Y@l>xSMS*gS3MQBM>;X`)S9 zKT?MPmRgt^1^`ujwZ!OXlP8F~ut3#d;e-wq^au+RMM}`5wcWU_oQz>`46afyfsmp^ z%;2c8+x2bov8Op5+a=jcTHCIx?g@NdW2p~<<6rt9;B^L54l5Tegue}dQ&<49&PJV; zOpMc(*UC@D6?RsJh~kR3jJxr?Do`>6KrJ(JSGPZ|TDAj{>L6%(VMS77+dN3%uZnfQ z0zEW&!V)Ykc2GkA5_ zOZE6RITvDoW@r2oc2mqID7hb3Jfp+H@CXg9lo*7PO9q1;`f3IdqW~U0Mm@G+OI+l? zD73bcv49DKAhkJ(vm~mlmBdmSRoF^FQ(-F^R7|UhxXfJ4h;`jctyrP0WL9YsW7!4vqUy}1>4qB+#udW=n$ygUek`U zRIwrNk^?avFuUwIZ}1Fx3ckUDD6DXSrP(+b)^PA;_Q_yG9#M;kHJVk~m zj56yQD|ysIOE$D)@4Dd8*(|>Nu5FEYCp1i^{BtsGG?^yXxA7W@B*-F**a(13vHL&YR_R z64Esm3%QnBRMfWBPPx1Qy1{uOD_sNylEkehlUy>CMX54lO!8~Vp2$^3vDG2Y@hb$a z#I{I8w@*3Rad{vYf2I@2x9v9qelGcx^dj}EFJsK2PGMAfUYpGOCg7C`-Qu4H-QLK4%ED@yzw$skL{ zR0c`B+^}ShKr}qQUCVbCjp%btg)FLhURKOp2_4R6*%SjSNgtPQvV%O(Qu1UqgQ&F)!}J+ghE>4k(Y1huDq>h7 zX?sa4kSdr`LKM*mMy8lb85*MFcH}!~v{EKf7bAE{dZ_bEdQ_GGDFqVgZ5@epjF40j zj8#eJ3NQ5d@h=zcp6;mem-b$vM|9C@;fj1??{~5ylrE~TxFX-)dxj#v7D0)X8dvDw z-TS*J^jD%nzEc#uuGHV(`(tnx^Y*TdC~G173ZzLUU%Ng z*kyXu`qf`4Iu*Xn>-s*B;i}+<;!3yt@d&D%dTaA{O~^w`-lK_kUQ|QzJKuSG@U46A zzcIjl8D(9jy!-aOAHMP4;9KAQL7M)-8~5(lq`v*#zxmxl;rGA&oi|?p-rElTMmZLv zUgEPJye}Sg@648O@)SpI$GT;kw4=M2IJP-Qle--fY#+XhkUsgT>I$cww*jw%{z9_h zk`EjsG!z1Bo;s8;-qlSMVQ$hZZ^PWFQ=dxLzfO;Oesq1h zDJJ92&KfZpb7Fl`N6<|%4e49NLAibKrB`;g=?4*~ig&m6CF7RLG zGU0AgFA&`icP7r9A1;~2h7gOd`>6{w^&q(~%*pDjhwU=}15R(U(fKXh{@&RMIfOdr zC34)+VvvDkmh_x6D*bJtw9(}@zD2&X6J3a#(En(ti~1br$?&h4BTnS97YhVCQc~o1 zCs4z2Luhc0xX9SDySo<9R2Xu`-O(U|{0q=cWnju?N82?;Au=?vl2h1OGV-^Hn&j1= z@!0~#!htxYy7^t}NKQz(?C{#LfOuv9xfB$l{n>l1HXE-+S`c!_AYX&XC}Qg6hN>9| zMptwxc2e+9yqe*+pUL2Srf)nCeIwph{9K9>s;+n!!lwMS3KNBWo#Lbh+s_2bT9+ie zK*No+?!AueU-j9EuIS4A&AlJdIP1$~3xF$ixc3Jr^?5(m zF8Ocm{T*ks&9D=5Y6DQoFX1g2wHQloj0F=eRvTPll&!tx&AU0hAN+qW`MWQS9c?z) z*}{I!$7;WX)u-+jI6}XiKjC9EXXjSy+V9aEA$kqWAOi_-4F>?kF@;<8CNDw{;0|^8 zz?nR}=^x!Q$4Pg2Ji1lmU6v~M$dmeYw`D%NNREpuv7gS*(?#-x1LVvKr&4vk^6v5* zY>H3y=qqy1MxusdYyB3C^Mr@A*>Z5cDd*0+{wnzmUK*!?R$jn}&f+N)<8jVz{(E)L z#}NoA#ef6daKk=2(zIXzp>pn(nH1*39pS_=f7U$8q1e~Z0s%5d_xtb0^;5Ue$kow} z?fwH^J;0nVZm zJiaEb%Mx)x9EHX3?OcY>Mi9ifoE|J-m&6ni@r5uEes~cZ#D!<_$?QCC0oG>W4cAhi z@h2>x8Z`s{A<&Dz0Q*3V{)p{0L)5EddA0?H>~py%1ad|^W*fwKOpG*P+yaS0S~_w> zp_V__;l}G>J`24O&~+g%LV_RakYw>VLP~WCE}^#y2dvHNYWIo>&4>@P3opyvs3B70 z5YD%|^2}FYvlqeVa$iKlkBDpJBbuC{Ps1PL{2V^V)DgxAmG(r-JUW>io*{^M%?qo5?>2f(&4+R zTRJv;2p6E^!v6W!o~MA3Rw8#+tH?6UgDtCpLp8v4(fu&&mcB1>aFRGSAM%+AJh*g* zSCTrv37aq9^iaxcX^2w3kh2r{@&=N`c%za%jvI}Sf-;2Ik(6`twm^)6^Sxu0;0L6O z0{wuT4ie3Fl?l&B0`m0@~eL|IzBBP z9Ix_~TJV|WM%?OHs_C*S^`2ElVV$T_Y3PN4UwElpx@9C}n&Qe#pK6n_@Y;*4Hol-? zTdr)mX`SlqDA$g!|%}!Np1VI@0De%n1Ust-$#J;;pm>Z#|UgL z3>vknl3NF$SkJ{K^i<^q**Av|XLDE|q6%qp+5+=Fz*s6GQVF{Qyec&xhbzm9ZwkhA zFKAT%u2Zth?At9}ee};Q`!hq%c-p54?dj1Gnl!J951Rt*`;2!uL|~89i*-4ysgY&r8O) z0SQWjDE=hjajf1Nf$`~y)DDe0xcl_sb*}K#2@L%GAs$nrLWKBRS8hUXe-#>e2UFvk zn($ZB3eDV;I2qT}P;zOl&=~u4PeQ#TnEm47px1pL5hQy3uWfL@`aZ9x8;x01xu2A8 zH;!`i{j%)Di&q&t5#E*ADZtIy$-rNko#G~PcCtpU$WCz+C3doAuFOtxLv`$AjkRE> zxTvyIjbh&@{Rl-wZaY4#lP4l_$QjCT^6m z;)ZU7vD(Y2D=m9rf_Qc^nRz943UG5dW#F&OPH_`CJ6R)FWT&`^5<6Kl&qYpIW9-w~ z%U)0^bqNyrV-Vj`lUrIw$0sSLNvi6J0%`z=Qc{u$g>p)#q@*Sl%DQeQgy7E$I~MifRB&I^FBRgdPiMlv%4L!ChNyD zRkiV^sqTi68S@abXBSBGm^^djN=X$E0v>nzuL%1R4POuGJFjg=IdLmF@&R1jKZ&BL z?mcA-tMfw{Z)+%bOujfF$^UVGrMMS=@4C&$|5awBc>yA>r z5*@$=OG=*UIjZI|KADYU<3d|BgxU&&$vW@{@Q-$5m!I6m$G!c?+{w~vZ?ga+9>=h0 zFoWEFmoXcmCuflh(#Ey)2e^aP3Z&fWg}unrf+&$sRWmRcsW!m%wHXV8z*VjcL)X-r zhIJ(_SG0w@rm8Nu<*K$m*Hm>&a=ETwud~XwcE2UzPsk%mtB}5mR#vh~t2hTx(m}G! z60Op_+Nvd5S=Aa^S$2_DS!Si(Am~yWhjh}4Ce5s`vEWRr#yJh`c@dh{4h{PP36LPl z8bpNgx~>jv02W-RSnh$q1j7xp+y=;;aDCkA3^f8)IFwB$W`Bl)^GCSr*c1D%wUN{a zY!WIrpl>i37I!x4>pN|!(0b_(MrAq~E_&UpRA`IcSP#^XmvaPKO8N%?3GEKC&w9{z zu>^isUfkS}mlkn>lJBO#-?w`g$F{H%%Ss+P9H`pez8k-TSOS?<3q-aKwW#FiQz{wy z7#eCZASF;X4EhpB031`lR4<{jzEycnSw-rlc|4KhIWg?d7l+H<+2Ck0o{snl$Bg4A zyImKe8rMzF@PUReq_hKAD4r$lBQB8bLa6b!{+fv8m4GdK)gNR+;gOIRQ# zA71Oj%#E73I6E6J&_P!RP<=ns=6!ONhSpM8>)wR-!xGaW^r~9R&}pjG3YqEfc%bu* zLPa9{WYe#|IGG$Rdpj9EJZ5fJ^LAXd6;i!dw{fs!_@pDU5xqC6Ii40?fZ))4zU*s1 z@TjGX97188Vm8A!DCQSO*+d{@{pZAYZz!@f|5TLTd3k#q$XpWFge6jmR6qc4G-}iO zUqxUC&k8*+1n}Z!6@hE%dXR+I0?r8sWdW{P%?uE_)ndl3;wv{*!0;wR;as*3AIkh| zM!nod+m1SKCBFj*TbMja$$@YKh_Z?Z_&xCjk|^wu$$X<0HVAv#G_Q-jTb@)yUCi-h z?nyt0lrZ~nQtR-Zn`y>4=dRv$NQyM*BFge6oEX) zrRkS=e3?;-MjCCU?|=%eXMJlXrFC=rLH3U+8$Y@D34r!*__P&&#@Zi2Sv+-Vgr$zwYm0 zfrj!DU;BVqEL2H32z!(KtTkwp&t{F7#4`DDld#fe^dK>Ww8W?~bi@sO*g3|PuQSGu0N&Jy z(%?~ovVW~e9`K$yxAV?ua&!c*tFxuWC+Z`}&|0BjA|hBDnk4a%upSGJIO4di$(x-! zk}@A3UrdK{*X|}BW2Z|yLPaip!I!mr)9G!hADf+55>)OEpo zPnOH`OWnKTtT>n<5PRi;JK--F?&V(?r?q`rVv(pHGDnbQuQ7$Qa83 z<9*HXI>3N+1~j?q5N&oKZMA0TY|ENEs7XkXkt98gYYB%KWN=#6BnIpdFz{2niI{L2 z;!_PWq2$yk9P{`VYuLwC74Kv%B8MO;(PHLb1&8P5+a7Caeu`*xkU0F!zW}>UpLq0x zD97qVag}~~KpXzulUrT9!WExS6TMHSv;LOH052^+7%$fxMjaIogx3<`yWkxV9L!5~ zwgOsQBCcnMJ;L%j-}UzS3?`L82hV*ihxlr}#_dkI#R(A=E^o5s?(ul`t>WwA%rq8_ zuk1>#g?l|b=|vZh$XlDyYYNh>>*%BNUoX&guB%7S(s5 zQ=dZ>z=^ykHG8gRmsClR^s4D4LjcEffg@OjYv16LL@+}jK```Mcwv8k(Wk%=24;}f z4j%V8>L3xvI?k}T!Ml-<`}!j2hk@r!>$EtgdedIUk}JboAlaSwaT-l2PT zOZ7i1%LA^O^YSHaRb)l{abrbA^a&!gCxqfWMN61bKt;I|j1kdDn zS(Gj0S3oC%3617yS6-sU4zl~s*1hgCck?ACdZ=*2gbxkfRBw~sts=fNYg`{L22d19 z;Y=m77e$!Hxk2o3h&?jiT5{nFo&5{P(Flyd1`H>J+5ZT!PLi&xCv980`gGTHXjSgdMlW)+79&HG28U;b~p5Wa;_~SBGQR`yvaF^)b@&*gVXkSnlM$`RVYyHyxfH zjE0@ZcRP>w@9w}1<3jZGy0_JV8?P%N^o9xm(uOda0OaNAt65wAW#~#N_)|DEoz#_o zQ*z&uq*~R0tb5+C=lwKFmTzA_x>~b1r+rb zSL7e>{Z}aRt7V3DW&UXIpQFqd*^$5vF7J=`{%7YgP{KkE@y3nR`)y>Po5Tt#rlta(LIiJba8gLoXx_~{d9gYIOMa>&&em?cYK4r)8XUA znHy{)OXGV-5&+51UddyMHo&e#f+BVnQcs!^qL(X^f+sM1bd^y!jRbf-Js z@N+hSmyDhHZ1Qn5zKk|I;H4TUCh~$J8??o*xAE!o5_+9G+9No}0P!Q~3tnXBYs~U` z5Ny%dP`iiN!t}i5pV%DXAl!f9h1B?yuc5}@+_&5c`_5bL4h))RsVSme%V{n@Wb4Iq zmAHQV8fpv&VRYxA>Z9@G_+-g?Rz9TY5lozCDP9cT8Xild)XE35qrt&szC2-##q{Es z|9%9gzVQ?OqC6XG4-Xk9J;rG`1qsgx0}r4N7Na>U2rPP-AC3oylcfNO(|GchK#4QO zNqsJLc-)Il!+8ad6%(*eK(jFxj0HTOjL*;>Rk#JqMrlSMF^<>N8%p@8i5wTRx;q`z zk#!$7P|?X@49z9ykWT1Pt~`Mr@FOh|hD0(mWHG$+;n9+DT*aGyiERh*)6_X1_za)y ze2kG6!aaOMO2&B=3laz((((P8WBiy7+GyHnf)_$Ta~4csIRuOdtB z3I5nPG@TEhNS!o*y(p9|4MYdS1za=^hSTYmQ^d9&!Q<;}aT|2yW9+x15~}rWJ$?La z%MJl~JewrY^Uq$f@m%zH*Y#FXRI@XjbM4g9MMF(Z&PEWBSxE!S$l^SHFdbnY@ivN& zT0;f`n}xEV^UP=8rDg4V(vOs^EVUQ}QdIHP z5+l51e}d;!yR}$236+h2$t^`BX>B)dD<@;X$lxmV5(p_u#0+OEyU8=`ojI3qN%msd zY>=Xx5MS3=YE-m23JYaJpa~rT1K_k9fUHyXvyzF9Z+WdO%C4|ee4}w!yg3&<&#Sg2 zLjcs!p?I%=lTHa99=0-$yLnXrCq{gAcX_hgmFnQX#rS-;JFtuJk{WMuK!R{|^A#u+ z!q@!QIhV|)+@vM-oQFx0)9h0nXFAkrOVnxMnj?=3%N7u`(wm{J-Ei;ttux+LPEEc7usV!dS%y(cL=!}Z6Z=!P zO#Ti2eO;xv7ruVW98zpPMQ(Ln+|szf3l^c!F|?lZ*d4En_rS5^j*0~O#Hm?@gEh~OagCIa>Z$)_=jYhl#aSivj1{M2Sn`BXIMZYe-t z%^S=pfFnrMV;i=nL~mRB)rkxRFnc&z;nSFAZ zrGIi@!u0z5<5cNg?k-PNfU*fe)%7pe>EpbDUY}LhE;Q>WF~n#S zpw?R)mcmSd>BP$ibt2MrIhJym;cfJmsBNpAa(MxCFjFQTwrxRyByrI?7Udkrm}IfY z!WdN<#g>OS$1f4I+Z9#`4p&?r$i<&F(e&y))hlqH*g3=?!UWx?+qr&RIfs?>)Jp}g z#(O=>r@(4|2-6=!4?xpGWH;BF7CM=LS7Nz|9Aj3U%nkdR%0#xZ6jFXRb4dNhYv1o# zRFEkmf`O@PIx+IOZ8`>WBPCEzN21kga`w@M&I(UP(TXdpPP{Cox)cY#dp@7tTb@kE zaO#Mlmo>qPmM7ZU+p|Og$kkOdYb9$P1z1c&u1@iHB+o1vVgV%a`kEz^y3z1>VpzVD zUD4;7+FexhysYK95;~mCaxMl|l4LI55K-MrD)qQ0G}Bijp2zD3HiU!yuA%A*mu5tCG$YUZ^w4UoP4$ z-QnRc?Y%^YkfP1O6?t{g6N~LYi&d;3xk8WkegZguC2Godid$n> z>Oa`~Bj-L+hxh-5Q_UtYl3&*&$*f1TQaA1@GcQ^mmf}?P*Go2J(uR+`VPA~{)sw&I zoKC=Ne7tXRN>{+n+q}Hd`ZZrUPh9?Z1hr7B^Y;touoO@ejwZexz2WiwZ+&aWkdCS2_x{%Z22Zn zX5L_)Tc71^Xi^JtJe)`M;;v9;A0~8&nc{&jj%{lqUc3#Wcf=%qYr&ICnig=S!>s0z zZ1DL#c%uK}ln?Iyu+<;?TOW*Ap@+X~o*I-ova1_1!rZ*GeFtWwAC(T4)yr;yXY;@i zk-xt_=XecqHF6q$3hx8(sY<^taFA{vJh{z(TRxpGb+74mfUrg8=c>{dPBvUFj8PG+ zftUW4VRL3fiAC7`)PN#)430)z{E+L0&)_e#nTBk6Hdd~Tr&IeH5 z=zoBxqY$M8XQl!t@`s+lh#>CkSls17csL=!fnnK;1%n+0De}7$sN)xl@!ZCLTrt2 z7Pr5!w*kA{ulk&1Q{l>N?!7_VoiCH=2Cme1_J%0+C11*z)MbzMo>?qeUxZOwKN723 z18~VNft8F}%++4E(hLVv-Iwc3bwscCga74CfA4|rRc4u+E$kO!xvCW|G2^{HISghz z+^s%;(itMEz`51B_B#~icca{{*IR*UP!nVHWK_LI;XMjzNMi@o;9wzR%^+8IJ)-kE zO$kDad+)yGJzf{MVD_VWe^heQ!4v-XM`~znDC->BiuN+st1Dl`m=7P-Tf#)vq5wHT zd&-o!xBbprM;h8$+-n`b0qqlXs z5Ag&!omuoLux~wi7hv6U@JPc+;n@flQYqkF#o(cb%geJ1MvFr|IQa0&bJ|MoBF2&$ zOOB_KV-koCIufM#`|mQBJ$~!#XvW{F@3?@oYuRG^r75sm@3&1dnn_q_gqh>6$~_I; z#AAEU^qlANp57tU(wY%Lf&sh1-P=u>Z_1(VuDm6PLo|0GTJ4wS1f4s4SbD+P!)4(l z%L+yQ8$zzh8NAEEAriU}_PTSq0f7q9@pjWp0I%)p_M6WGI4XS|$((aUX5Ag+kE`3yEBvbt;53@--()NO6-SN(s|a zN~!gkQ>|%qDtK3stfBc0lD8cNQufg9ilMmL-$W=H^QA&3;vpOm+u#Ke>ctWTloyQJ zDlgz-y+DuwTFHX}|3#?ma$7(pxVY&(qCOAi$9eksIqr+_1ZsSGzI>u(9-T}MPjFuZ z;(vT3mkFo18U~lJnQQ@2!LtBml`K)XORVCFW_2Dn$mQAc+ziOEFc6orZhjZvg=INw zYX1Sl`nl5h@VKy08a06_{7b7oedgONa3&$Z5zIq%M+62!E$OX0*rsy?jjPeM{i^mU zsnM=G(jSjD;wO(E$=V4|*7MkJ*?|^M6as$$TOY;`Np}0iiR-*EB0oKJ}(dG)eZi#lADZa;Z(0@;$wpg1=Ms2 z2JeeaBc69?fK8Z**KUWSo-dX-GPsfs-&NgG#^vLX038?ha>zg7EYxz)aR^nZB0Zs6 zW;L{sbE*Z~=)rNkVZNl@V zP>H%baHZMD;a>NS{CF0YBEr==45-?!j!Li>jg9x^UgqK!k+IHLlK1?4dUlQ&tY>#+ zmbe=qZkB`&nAR*iAtPZFc-9T}UEhpL_>iZP6jI4^nz>SN7rVW_tUAU_5@9pe$?+!V zawa~(ajRY}iW>H59|l^Q4j&bBJQo+zH3j2fw4x^`RjjyBw2+t3j^nxk2GUng|HF zX`Sk{isWch)4F0Li&ch>2`OhMXxpcKuY+5~6k`%vp8$^D5(d(`judn200^n_dM^A- zpdePWH-`^rb9gyM6}ZTS^X%0BRnZQ4%_>%Rt7M(Gl0&I$C;~`!PQ3I zBd*C6EL3&!=+EFy^F6*Z72zJGNtG8n-$8FgXSs)U7!Rw%P%}B297;@w&coq+GCY9O zkQ9d6MXy0-Vnhg*YTuC9Bs%i~x{irdj#PG;urS`mJcm*UV4WbZ3 zHdb#jOdR~Q!s^_JMjhOJUQTs)c&ObNczrz_O)wnXW$CA=V_(1yzI zFU?N8qL#5!&||N_P62MtP6qzU>=ZYVvy(M)MRtmtD6x|@b7gjl8>(X`Ypex3#YL5! zYPL*`0)kL3%~-rOc@vBk;NBQx#ZBB0W5rF}C}YJ9-3Vi~ms3|-_QC{_6`ZY_%)HXF z7vSdXWZ-X9PFW*2ET^oQ=OU-9u@-VFE~;`WsFWW#^9vRsEB2NegwrxQJ|d^AoF=KN z2aA|$T!crIl9Eg)lv6q-B{iu~)^#%}?9!srCXs1SLXW~{p-j{xD_7}}W%wS&2?dx@ zkE~>+N0t%yC`%K_k*QkZVsktSmd&cMQa6$>c63vrR{sf)j}>8kMA9ieYI2M$2y$(j z@8|6VGa?7vwjQ8rebuMW!gFuNCEp#5XqrZrwsmAXBq(L^g#y?}7qAHoM~RVCUad;d zV;gwVN3Ev;M~bSjrCW)mJqqNSOmQJBzw>Dg%GaQ}8TBmH3D(5Zo};`5JPlMGo`HIP zyNtf`_(}MNh~uz&JUxO1{`h!2?@w^J-aG0lo56e=06bGw8~zNs8%Ac#L&%<8AkAa) z%#kZ4Re%RP?(|>rX!v?a-+6gE%86Uakq_YF{=GTG36UNHmF>LRJ=j4*{m7K+Q9%L! zJIap{LXr40DiuI`)RG2$s@5qOl{hC?8TB&*6Ae0x!Rbszr8^_znG!Rw!L@nrE&gG$ zF`{tUZwOcKhffMytEjQQo>KPCwV2<1Nf8m-I9F)C813p{o&~rrxPJj$u%zUfo}<#x zDL&aID?A@8R0@X*pGlwrgmemn$vP;`7zDW5)Z;EcDU7HDS!uP#Spy!&uxT)J-M+rx zhp-k2hMN5Wu9CHaAy<@h7-URU1q?>I{BuFgnzbri$I6v-)vT$eYi+rrEzLDmbxAE( zwH3Igs#|o+b^VH*RkqdnEzbo?oRD52j%8Je6X(=cD-p*k))2?COThDzi5!B^>JO)r z;iA{gN`lY#%R}z-n@HPUkH`OIdoX4 z1;@#=UU&O${9aE9WL7N@**esslA}+lWawjPsKtPkK-n@hsz!vEXLCjKj8^<{A9Q5!e_(vf^UuH)e|zDA~yc&&Zk6X9U_Rv zUDyuAV1bZa+B3NPUGQ}vEa{rCKnTDy>>Lnl%=e{#afX*>@rugFjSH1^_y(Ku3;6SBEG*kATB{Cmnv#C$;#OGnf}AZ0ZzoBF z&(!7n+lW5R2cT1WV>o|?r;({lpX@ZM$0DWmwM-*x>^V#$YwEd7BWvtaIgJFlx@i>V zYlB*HGMpY&lv~l^I==;*3*5}8ZS8DCI%>X7Qn4Le5l$;yr^Dj`P$Rmc5(%39#mVGo z2@F%7rsGv@;oFtcfNVVw(EOmrQV+Yncmlmu$9k`BcfRVE%S=nbBYf83R59A{>O0)m z=F9$%FYsRkS#+8TqT}u0xP**xgRVAi#X!kbC z$Y!hlu3di6F5k5)4cd#H zcV~+ZPa1Ih_@f#5IR_O~zE{JF95okzbQj*F@eA=JVvwI_)fCl88C$ zN?!wK-g$WiXZlgN6bo?|6!odf_#XUcJVbeI8|OAMu1^1s=pFG;UFn2}A{$BcCPL-Z z&#VL5QtET|!pDF3=D+-${PBVLFT|lRTzH;e-ut%^z2^(@)nJ$N;ogss^TqVysmnXu z`}dIdD^Z?wi5F;HiNC$~KcGY$kUKbmxg!75-v5mvzZL{R|>XKUDo@CcyESVXZn0C<|MhIbsimyuY9+?l6)ce8z`0GB_5NJ5&KB|2v z@N&rQ(R6UJoP-DM?I{I>@Z}w$1s;ZT@r&{>NNt04`{A&T=(dU-SV%PB&5pi8w)@I9 zUq2Y~W;jpJT;_CgI$0tQnaZ|<;Q~+9osEWz>>cM9>LYwtkceKj$GY4U{M*o$_y~yn zjmJmN9Ga!L+|n-}WoUt*J_x{YZ9%z}QY*oaCJ!eJf4t~-UfE{6>DOIC=dI1oD?4e* zTU#Obt+&E+)6q~^gLi}o^YQV;bU1fJfF)5<@ubJ{w$|C~yevtxqa&9RpvrI>JT(z8 z){#Czh#=H6H_DqN+VEn!yo)Yuhx$1W)CXf4c3coG${TsEx*UAze1^^}BxsVB(qVls z54Y4l7@ubuWd=Iik;(EXeA+2{3)Vf$bh2EY-`(0;;Bp~6=q3`E2!`;}Eqp8_dF$bw zEuD;8?Lz!;4j;^h^HEo}1_af^eLa zw*(ob!+s&48RCmG5QPL8=yYG%jswvMD*&$is}R*rlSdi?X?7*bkfOe;|HKcxNXNzx zxrwX>E70cyU38{A5Rqo0VkqSvn8G|qq$*u7(Wxm;NVJUXD7S-~q^+i-I7|ZRP~lZN zzUCMT>SPfCPKQiwCBT<&Mapg&a&AXqA}pDg8iQ;0k{25{Bq&JNJr(4lf~ekt&=2h6 z8j#JMdNpM9eqG?^xJ(n;Jzka)I_H-{iMl|>kPR5PaW{F(Yn5!yRhZP@jaZLv}o~SiQdpNxQuyr(`mHXuZ zZTNsCx4L+jJ-%*0^gfx+`e+{myj%{RTC=-4DjoEhLWG$Z&IdRcO&n4|EbTKP20A1h*>x^(!S6Ge?G#s2cIvCotZI@>Q% zFJ95|Rv&}*SmvAW4m6=wdSs12AJcrisGq5g{{r`{Re3oyYc?P5%7d_#qu#E>6#=lh-}N z7Yex-_?*&-bQ}ljMyJ>C#Lf7jyHnVpGY#j*P(AS9J1)SR%%KpsLzGSl+`%%uc-b_` zl*QwDys^&~oSat`Qe<6oGYC{eW|39q3hgNn9phCEA~f!qi;r@J79A9A^2N?@gg3?n zuzxpGT|?9E7z6lOgEu3{P5e+cbX6m)?|{prx0F!N>p6U?xIc2_k0LhZXAT z{n*s7#oC>AbUeJ6C!E<4w-Rt~(tYF``ONUx!tQ8|w6{C@8eD%?+Z|Q2FvHsSuG{da zv(lGC*r5ja_4dNeF)tuTkxdDH9eyN>CbpmI+E2#{jU6= zAK<%Tghkjab78Z8M7_qFE@@rS(gU7C7(pC%vF%NkC~TYqBsFE;NVsxu%lxK{%&c|>>IUM)8x3Mf?^NwfAA1@>42zH?fKwh5vh1&8jL%&SHpW>q3 zNnQC@?tZ;e0TQpS_&3<0yaC`pM1^2SCjBkypxKLqsaSH^XSo4)m)I6EofSIEk*(6* z4*$t=q?f29|7LmUkbrwqNneifqOEHR!x-|_0!>u&`H)za*>zo7N=1{Z4Vy|Dw!1fJ z@prQ%ShR?3a5d*(kPe|I$#R)?qt!kmsp7XBtpth6k63mrco4!@j~Cb>A=+DV9#T97 zaZ|VH!2>y9Tx^-(*y2ulv2|z5=J=K(t0ZeFOlky)s6VfR#TP{@%a3?vyL;J zKeAIb6nn>9r?6V!5(Hlok%davgPz;Lazo_=PT&7Hn?34n^i?{-U2Ug&l;ex~A6iEQ z@Ygy!LZb9kAX0-U-tyL1Q=eS_l2<%!t#v*OFYNsu93B2rVT0}p4EB~N5b7Jpy6i`L z{{?+1ip#bu@=y4H`cHqgOfy&Jzuo&|=Sfj&NgNzPM(BcM)IwGth}Aw0aD@14xuXHp zk3LlpS`7Fw`XBrm_b~d=#Tj0MnFZ&C>HGqU3MS8U@rroih2QZF_D+Y77n9#tpM?zx zqwj}V@b2lE=(X)91ZexsgcL8iE4ZUtTnuh}MEqJD3VsGQ03D?(w_K^duM3NNiD|8u zg%0EhUu2VVg1zT{;|KrJTz=-?)e(p4W2f{h&g(lkJB9}iD(q};Z(sI+o_Ys_F+}${ zy-%vU4_PZ=o6c2oWm;C(p{X9!tGtM`^{W1^ln7dmopfUL-S4W+fEqfiBM)#V#4mut z2J~TvnNC22Ee;W4cEiD`;jEhq)^~^Tl><}j89Yqn!HY9S;GaUA^du)7R|sa3*1}|* zl=BY>Xz$!=r47rtEHCDGE!{k;)L$bzWRcPvT*ZF+#Zuq0j`{CLm@4BZ{6)z&Rs!IK!G}rThP=UcMi`UPVz3y^SwUd&9^c`3 zaESK-^E4hMCQy-u(~_$)RA@{Hc-(~XbjCI=a5c#iAQ+?J>W-LA`FRSX2@cA=fk5VA5@Zy zNsR15{4^C_b7wL#`pM467-=fvG2($d9Kf+2f#3$X?-%cbGaa_3f8ldj+~A)eKW5FaOG< zSfQ?9Z$WpqRE67m`uN$F9cuE(I7#41rM;Q#8R7A+qn4zonifAC2`ZUide72foXCyP zE{in1IE5e46W9e7=b}=%yRvYq70*|@OyCz<$uQW8fbfPRJV3@(G zVVX{&-#Mlk?D$jPP!n+b8uJ}fi^{1y2go1Z*O4D~RBg@$)A12sK$xBFO1ya14c%EN z$HiTdrVAZOjpZ!07zAQe5!Mo;BSwUMwA-yk!^s)CI>cz0_)$!f=62)8azX}_46xEK zfso=v)Nrz~+x2adymNYRNV1o%W~Wn?69~J;Qp20$TpHdOl+AsH_+f9IG3J<#fdN?U z?va&Dly=K&Wsh}*ov|z?xnjx*;rU**CK&^CH`D&Bn-j<2yq5v<>m=#{z zyZ1t5;S0Xpo^tYCa=N{cfOYuB_IP))w#Vlg@7XN=7=cjuM7|*L9VEmTJ|gi)@E`H} z)k}Bv^Yk+wzwIUAR=OKM{Zd`sU0q#WT~*D474muPD(ac(OOXadyKH8Payo>PgesZ^ z;{?cUgO?26iraZ1G+k`iwNeWI25yawFJ(8=FLbWUmWrr#)=tvZdb@3+5q@ZAH33+e zN3AXMC`G~&(fNcO9s>!8bx~rueI4@r?Wf?J;wvhe?89ChmJC@ic5OmPIf1nYECn;1 zJU}rOhjjWE6-jdZ4mBae&@M8AYCc}}u@GKS1m}aJ-Dl%zfu-(D5fTREVDz*ov^P4Q zJ^Su6_^KBGVPJkVr8Gd3`-saI@Te$K#z#LnRom-~@$Q8BNH3;D$Qd(MTEn6Y%XtBzG}pXC z&wX`!I3EFAD54(Q@cj#qQ$e;IGB%|QqL+-UBag1s=JISLQ37ox*3u}EwhFpL+R7le zXeCAowUrsg&{k$!Qf(Chnew^wM)&fGt(Bi7*I0>(Cc(yJ3aw6=oGWF38Mqw>txAh_ z)6^8qEs4dxBg(yZDIbTdO!Fi(P(4=L)6sy=HV%KQuZSXERi<2Kf-YNSwn$59xKcJ! z!NL=6VsOMvn4RN23+_7eD<#oB*W_C#R#V#ZwgOhmdnd4g&iHqhD@KrjAh#{BJ#f;Z zCJ*9WWrI)=AWEx+oEkWX4zIP({X$F|WLL?esNF`2_+Zk7G6pi)qi5^0L!}vZUQo6EiE25p#F$q(EHtWX7v`Ik zM~LBMq556iKbqeSycxPUp>;VjGpyi6D*Dfix0y?WP2fHRzxhA_&x(dn&*^743 zD5YM^$#Us^p694dD838CoU{{AzgyW1I9;(kkc*$b(lj6h#Whf$=s!eJ!UR3gBlfGT z!dSTptLSOsCFxv{7G_F;UO)66k^ikX3oK#*uPE6{j+j+eb2_z%#7DA72eZ~n_OfJC zX*m~Fw$dp`Sy>ny7yoo+G#9ta63E?3L*!nDIiV&J2?B8+VnY0`$YyvIc|-NMv0z=Y zY)OqG{e3VgHu|Rc>-w%@vm*zKW60GLzXL<_Wbg$HNs`MvnWGz~Ck>G28}kaESCryH z&EvW>XC-tx=QFwptj5YL-x(2UU_mz4sG0drBZDA2*jJ;Zoe2|vux!o(gzJLUcouvd zl$M;&G(9q833_P&D!#$3<vzNcQb$=>FE{@YrG7t_N_BBzFEa3>>pKC(sBul5c)i|w%^%!@^~9o zj7rIp^w9?!fA#p|?RS3g<3`3Ok2gN-N`CU)?>&C^haVWM`*kc9je5d?+Lj?hv>G#- zp03I8ATHw!oTquKDtE-)o&+BNV2_;xTZ5VI&bj{L?hn2J6VAZSaQ8XmXCO$|BRxD{ z`P!k1^|~I(2zKew>LVyKKdaq4tKr?WH{hWm_y^ot?33nzeOcGgA4%+)94`c_N_o(P za{!~E#O*mx4|dKU@ORo9z8H=V_-MB1V@l&98#=Qtx+QM{p^S!gsd8V~dvLpOs)W}d z-5Y25bmmiuP1yd_jhgb%SdI`_rm1?a2f%8+upA|Y{Xb&hy?=s zhS{KC1%AV%*sqTvke^OR+sEg_6KDd=8UrE=oi4>BPX~q#IQT3(FlEUac8MXVsaVNG zca{w9n4w7qsu&$HB+oWb7@pf=@3g!jBoqGgniQtKGdUAiMj!s$;VA^&bI>vdH|8K! zY`@|qi$@WPyrh_)ub7E5lfR^6-`czn z72CIRR&i>uLQ9*EsfWvj8sdNz``+dd#lD$~Ii*_u-sXkr>6)mB{%jt6DNuzhIof@dcf6Zba`1INKJN|TfphG& zF4fZ2HDTS6gX>R*+i^DQcc~Z?z4Cak?Lm?34|@;ZM)cZU(@wz9wFJ=R8PR)}iZRhI zZhZ894*@=op(LA4;WGCb)p9GOp`CO7|Fb1aGJCeFcFPdGl2?VO|C|WvP$8@xc0^BE))015ox`@r&(H`_#QXpAuxr={}G?`xX zi4EmQ-UQV&-C~*qqGZ!2qdiP9EnRvtzd!nje{sy;KiZ%0Z{1n?*VIasG-Go)$BLSx z>nc{6w0nitRgyry=B@S;!8H#@lT+A@;RT`#y+^seDfSKGW{|LrioYYbp6f5Ivlbx3 zZIYYD5}sZ$l&~yZgL$D~`=IkNK7zeB$V5VvLe+IFZUwx3FE+?6sVUbRAdZg`~8ptaoA|5K8#*5Waq0(GKsWpGY^txDiIj6Pi`nFti5m*_H zcMl3{GGB_h5bn2q`Q=3Jkh&^z8tI#BCa2hX2`Pz2HBB|TTz2hQ3zH>=6f#NWdJJQV zwe6kB3@5EvuHDoPZB#U%?N&4Lt|D!y@oc>N459l*7HWR4cL4t^*k4UV zLPv!(y5w6FAFw=MDyyPSjH-R{Wv#E}jGQRY_O+}rgk^3|9&v&B{?D7*dd^^hRvI4( z7ZxnTDAcRIyrA>2`r5WBR-$TeWvHf&_tuq1E!;tGXkO=x_jmTI-KV0)V%?EZN@yd< z@aPNKhha8)+z+_WLiNUtqt~Xq^JVMkC&0qlQafOaAH>>}@4%f!|ruTkUx$ zg`4kfcpQ*&%nVZ`lL?4%FqtmX_Fr8u_?MSl>H#6&fyAN({s5Fd#myOS=LM>_Zh%mj ziC3(LdxKQ0FdMLv72~LC$CP&Y&L3bG!;rYbz(8s<_JKQ}b{**q1v9UrrP9Tkae7{o z85mm~^8T48N%U9SB`g+f^q301>|}+7R3;%u5mxsI0R_`KGPuqDg?CcIOsd=x@h-{L zfTbrM0kvYL&V`yvroDyhytIdDzpvQ+N~14+t;1NYpK!Co^M3Q|C>t|E&&|8Ij8F%V zBw>mZc0=7_)^Zm#;Zt7UaZ?083#A)d{TfbB%d(3(6-d<0C=K9=`PoFwXCs^>numr{ z+NYsGqm59^Qeq+95g>D>d9ajNC~Sx|v_p0sW!qwFbuFu2uSK{4%~oDSI7l}&#QN$kUMJCivo2RX>9~7Q(8nboR?}%h3tAmtrJ!aN~=&EmdLub z`ucW2mAJD=7XHtuy4P`T&rnWt$G)gy{K!a zP_CVpY$gS=PUU1d(`9|~)s*V2pb`>Tc7#;W(XChZ+_Fx4ZPsfS`;`QRmaVVYt5hUQ z=s2e{{lcPs+DgX}uUKNtfqnuy8}4nmjg5F|=q^&t?E+A(XRaZs8}z?@Z}?(z0>_9@ zp-e7YU>>(JMN=wa2f@>NC!-vqIq#Gax*iT7_U}v)q-l&e(78u2W?$+(c!*c!J8}h! zLa5p5Phh+9L%!`5a1UuxQk zI1v#cr>8rE{u6|x7%czked-CH(EHuTm_?QPIr59mBc;x3lM{Wy8swCv*c*^jhT9}3 z2Y+L7icK`h$s4&LImISw5qs1~6UaeZEy15D5#Ur;E5PU_eAczb<#UJfk2nxS`eKIxO|3cX37YYy_G?GQZlpTpswC)b0YX;2zY=!O&tyE8mw3JCEHO|ms-85ugs3BwixSm(@Jer_Er17#AG0i=vd|S%f`w} zw#O>wv?j?jYhzXBbyls7l~?T;E6=WuRU@<2ZdOi|W1Q1DR#aCs2ASmKdXKVd%$d_( zQvQ^+bHk}X0VK1sIV{3yUG+C?0J@u_!~qHn<<(kV=w`^|0s4xbxFjD8q7Em#2O{P2 zt9qi@sn*DAIX5x~eTSjZ5%7TB;SSV4&E>s3JQxqBgMOn_Zj0z$4b?R0R`U(*)~-y7nUncQI+EjY;>4f?BV{qU+q4P>KQ9%S#( z0+nohY9(_Yb3+SENDY)v0~N!S!GqV@pkJ$3uy=j;;h4ONqgUqfT8j5v80FL5+2Ck< ze>^(a<0oteo}RDwE!r)d)%ez7vwATMhX`tYdFRVPW*s7k#?6^tF_7~aFpGeh>}UbSmEIt$bi zVlNONxWqcRIWIFHtUbc{gaLv-!PNiY6iLerHJhay*7Scqq*}Mt0E-C`V4e>qUyM!= zpu_|7I0*kOmm4O=_BZExX`jHQJRl9i95@L6E+gVf9arX-_*PK6Iu_*&u@V1w%B*~^{2G;@cwdM*3ErV4%0V;vk8Lojp&LITYF>$PToNhVtO&Rk;8otwh}eHC1~ zh>ROxdHcG9#`8+6PFRb!(?zBAM!HIuJg@9Y*YMO^=qjCC(807Ay-A@N6ufF=nl#Hg z6L=_+spAkn)(MI5PLIs+jm6lP7XSDT{^NmB>y1MT2P4g?=O>yi*%qE)N z_=$+xQQXKr6lHck^CZ-5AQH~YFm(nu9;9PmMdWoq06X;g{57;Ox)BFjhAAr0-rM%S)CY1i-CjVA40 z@1x1I$NL6&H^Phu0py_Z=sm`N@2kg3b4lS|s+rWm&ar^nhsLx3g~f51I{y*1ZF9e; zMk7@*iBlCdrtawF2yy6sJkuVkd!^_4bA%8_my(02yGG8Tqc1dS}NCI(o zmA(baeDua8l$mDXS|r3%Q0P;e@Dup2Xo&pU56+A5xGwoSjCY`+uF?e!1szEURgP;8 zKfdT1aqU3);I{3!HLH}i3!NH~-0SIGsrsYJQ*t5=!&sbeI~YU_ogX|~mU@Vuf4}#3 z?_st@p{(pT!+HC!RDF4IAMZJ6hD+){*y9^5aI7#lyL~s@hiOR=mVs7(Dn11LU>MWU z#=}pm9vR%Qx*>=Qpfy?ILRv`%`ImMW$Ox!ZgI3U?XQinsVP64HAI)39(?NCN znXyXu^yoXU6w)aQAr(dk`?$Dx`gC-%JVs=Q!G2#y97nng;6g3HdrmaSj5LJo$tltj zCKonb)csl!RKsHIytet*h{5w(;Y4g%pKgAJtZx(!xG4_M z?&eRC`8zR_SV^!~tN#z1|BWI=6l`)4w~4b-|MTJbuc25GN;~rxE%f2v{o05Bj8dl( zmZ;GU3Fy;>h-9|AAjxwull)GUB)2(aOgybTm|TPeLeL;^EnFB+>5&Y-p6N*91!>SA zk356OLBIJ27OWfPM%1f#ylwwpJY@G(g8}6z{+`r|qRf){I zghy!b{P_BttE(L`v%|Xz3>H*GHIqeK1P$^=ao{|dOrG~nk0nBaMAlAuG?GMEZ9D*a zWC-aR-8gv~-h2?WQ$(OC9#clRgfiU%xa546rw z?+r^64Xq9u z2N(w(G=|{Kc=Bb>dZUQ?bEft((6ia>cx`0`57LfcS~^}P1>w@?a0MSzNnUyJXhj#o z%Fb~5d^Fp^J@Sggd6HnsI}^Os))!(xyl6a`0`B~>FQ|ai$uD-zY*&fIF#O1pVMD6? zy1O}^vJX#v0NHg3pQA0T>rWs~C85y^RKjudU98ynT9Q6dXAy;BgdJ|#nu&_Jl7<-P>3@aJPJdIdYqe^-U<64B+5(fg|ccQJ-r=>|P|WSm7Y96T2j+L*F!#{NFR z>qFbDvrSFgKw4eufum{7EZ8qt6Bl!RLYgb+Nh_CQ(13FV4A>!HkR+pdgNgVhe9ci9 zdqcOJTI?Vns|qc=kG_~RmxC6G|1vl{V5}yrrFmEuLTpZH8TKN5q9He4SXXC=d>wH0 zekDNn)0<#_(#ONtQJ^Q(FQ>C9R>U?QR|b>4;xy{4Bp^JN;jowH)L>q#vn|kahP<6A zP6%%wiBT99)Z$azLagjFA?9&N)VJUiw}4-%*L$M_iNS^bD+%-QJH34w;!b* zO@eicC=tZ_q+UxU^6sk2k{b=mjwjTD2cjY3yP6%pe*HZ=)jeT*Z#3OK86PvifNiR{ z8=4&>&{4eAWH(d|tRb%W?f-%$i8s(d0MVz)5#ip4TT+nomxN#gfdaw22?;H1ZB3WS zFob~lVhf)M-tQq1M%s3-F43XRmYn|3J)^^8XbBLH6;!s5#|-hNAvkSsbjHWMxY|1LO<_MY#)k~A z#Kv;;^VWIujx0gLmpnpD4DG~BCW&Z_VCBb2_|NWJX{)L6G zC`LWkxZ{z}4c%3Di|$=E`le>MJzN~1h*d(GI?jn1A#P1e?}X>YaWOnRv6lc$%>@4v zgjBYV=Aqc0j4Gm17s?N5yHGTFD>X$^%3Dm)q?@#BZGL#$nkMbJKBqPsBD`bIo3LRN z1&M4s3308XN57Ao5v_}nnX48`@NyG%wUCVOExn8eo@QOf!)Ih{6eY3{xX8p5v z*V6i&pJQyq6ZJd78NrY;C{l&iS$!w;(}tvI~r6BY#us8)EC3D!{PDZV0gH*H|(9Q_0G1|9^!m{GJ;jj z?r6||pchei3i)gnEIkNg9)M&jkhMT@F)&MlhE;GK9B)qu{mxE2AJ8l6#N5^l-SoWeRtb z7*YlOqof#{#pK;#Es2ld^k5AB0^RAuRpTpjK_$-jN(rwtpil&jb`Ei@g+B&^&+C?= z?A!xr&nC)}*kv+}W&R;oWb$=1`NADAZFiOm%fctIv+0+zK^oafd~HjWGBD253*2wE zZ{q9{^=q43xI_Kz%E`GPLe{BAz z-P2YZldIE^D$;H#iYoWDtI#Vyt=t@Xe?<8c4JTi2X^clmI2b@jSem)!70m4p4GDm2U9 z08L&~y3ChDN+|6sOkG?-@NA2s+ONE$N7Z#-kAM_5aoBwhUya1@<&v4U<h{{xSmsy_e# diff --git a/Sphinx-docs/_build/doctrees/setup.doctree b/Sphinx-docs/_build/doctrees/setup.doctree index bd9edbacd4734d042c1d1bb69eec7bda5ecab05d..9a8de415341d074559b2b0af7dbeb33ffc2ca1e1 100755 GIT binary patch delta 22 ecmbO!G((7`fpseHMiyq4$!C~3HVd$DvH}1{odtXV delta 26 hcmbOsG*gJBfpse1MiyokcDCdKLnBk8&Acq!tN>T_1-t+N diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.cfb.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.cfb.doctree index 1d610387293c6c9ebf5934ab1b4627b943a35163..2fb40523624dc532f97d6ab0de93ed96f3120a92 100755 GIT binary patch delta 1730 zcmb8vQAiVU9KdmZwatmvHYC%CwUW$@W;g;}P>5Ml;x>DU(0SguJCCMw+pPx8iDwW! z#FG6tFQOJb#293wt~z?D_7FX&uk{>3Als;%nwN^45(4PAvZwVTKFN?L4JMO2i2ElPqrrU6;GLet{Y-}o58#v^pF(V;nWt1& z_kb0>SDO^uQ~0c!Q0SWcWZ;J1gW(M3#XT;ZK@0T<(e$7dE$!pc=P#{1yFUxsG9PS0 zW3EQ=fG=%8%i1CFKt8SKFX&dS)=Lv)02`+WEI{H@O#8opvxS}t6(RoP00G@1b#YRR zu64%fS4K7#j!`!n03&E%#L^-qxQWL`|yM)>;q!P<#H3QvfM7dlkC#M7@C`JMr2C=AF}zTdpDWS1fNTB zS3^ba(;2zkm2d?vHv3frI5IqJL5_$7muDkuyQ#(1U65YgSZQ_Ra=D4c3fxZaj|Pb7 z>qDp~DW&x#b0Y2cl%zJgRMb*06bYGAuqN}G;p!|?5i+~1OF<;mOVmpcR8U3~VbnuKAyHWOt{#^D+}(XU+{5pF?)P^%zeY8e zE^Ab$)Ct{$>XRCR_(g3kKfp(PX2xU+^tgF{m}P3VGa+}d!yT^mcJb#qFbGFA_Qg<- z`5IH6)GU0}dQ^Hn?>iphgD!X2^|yPQ^a>~AEtpy+=v4Bd@y`6Vc&A}NE`*cIe?mdo ze+RRfHQp=k&a4Hi?qdTqqPeA_L_Un-!YQXuc-`WGtyIjv@84wmLGtWr)CXNTJa*43Jq zCDj)m8u5x(nfR__9uWuIONFR!b1@vk*y)w{IAUCgnhv?|L|`sA)SH#dL}}djQDyGh zK05b)-?Jic;PAy#oF2>0JvG1B= z?9i@j-5JpI82GPe&2_~%E!S7$%HY*WI`~eKYsKHw8xy-Byd=>xxG4SL#D^`Z?=Z3k zddb-;Xja}5(x4O)dm}hRz8XRRyU2wqsF%lEtS~`=nm*gax(+B;Aad*Bhtj@ah+u(v zKP6f=K&%KxNOC81dW`C;e~>z)afwV3flqQrP9>Tr=ztXO4gu=zbCkP-E{umW%yukYe|A U!R?ZKjW<+h*<=w{odHMTA7Xk`umAu6 diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.doctree index 3b3e087c5719440e2e6aae6b0e99fc82c2c0e070..338e95e0454730aa85ac2cbe335545ece29ce513 100755 GIT binary patch literal 50321 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 Ca}6K> literal 57652 zcmc(I3zQ^RdEUO}z3Z;paJw3BJ+McRg_wm2(cmMl-*T@Hs{P`CCU%02$@GG^|`C_?TtCzi2Cmb)= zOYIe}7IdEK9QpCi+dKJiqFKDys<)dZuM=)TjFMlemYZI!bG8%irszSX+M1W?$68(~ zsMKpZxj*8M`eSE1d4D1t2`WL=Gd_ivw;J_k&?*;$;svkS@^X!0>3nh7>rfJ;*zusZ zN~EMmd~b(SiDC|fqm3fy3OfG2aH60Y@wbPglKzEoN3~M(It%T^VmMh0f@Wp09e{7* zb9<%SBA&)d)ncoKKs=AsiYwx2yxeX?FN+|v=`B&5b~qLkm+4c--{VjFJN>Kt?fy)7 z^3D^F+*1kO&|aLo6YM?GDqol@dl&fM#@Zuixa8+RW$Tfc5aydL@No6%ZS6&WN+=ybjr6k!WrLUB?7n3;xx_y?+f@_OFGIugAX|@b57G9RWN3D>)l; zBDy~dLPkKy1QcIiY1G@bvY|ksTYn2=P%i~d&s47bmR68IP3C85AF_w~niVeb{6csj z_ImfaxaQPyl`iwR;sqk-cqG1 zc=LbQKjS~-zsY~I|FD17FL>ejN^!O9H3GkLdIwCf*xX(R#awwm^w_dgZJB|j&!oRj+T zw2{Xn&fAJCngAO&dlGxh?DK}*UmIguD%P)F$bmTv+i|3hfqm zS-ABKCK7lkFirSI;jQs}^G9;|N=@py%W@-vj&#CTrrXJUJljs9=!ACw2-pE{u+)VB zd3@PTUka@XT%MraG#mWzP`r@*QoB|X*`uI*-PvvOL#-pGY)>yND$2@bP!gZ0^E|)s^ar7JMv397mFt*Sz3y59zxo@qAJbz{O@cyvTOzLziLT3(D8~~ilZ(ViE;8@Ip_?>+q2gUE zp&Qo;_o&B0u~zcyO+#M2fO-PmnxNQR_JU4$eL)~_>u9Wg9*tlJeI%r)lsjxcXbea3 zf~L0PFOc?s$Rbg3B(&9m+38jNE$!OIbxPcMf;*m<&fao} zDOEn))>s36kU4XoKE_+QzEZ5zI;=(F8w3BrTEpwGfL?hOGv3E)b>W&lPCWe$H`i(Z zIe$jt_F&EN3*iX~7eQsk6RlZ>R%bR{DXP_p@^v;lFWTNj-v2ul5MJI*q1m?lqmJlo zCS<3<8OKJ2CrQVtsE}u)_|C@gV`_*W=nYNQy39m>kI(d+TM&tsV8K!Rif^1 z>1U08zC}N8)lYB3(~#kLo~m&(10*}T2_mmSVR)~gL<~5cJbq01_8W%3liOT1wSW8_ z)%k>#&A?wSCk*0tVJ^nNp-Y9c^3BH9D>bd_#@2{!b_M0yjja<}mz*XWU}GQi%!1ib zkyP=bUTtQMI+<}o?JJR#`)K=kPl3GpI>sbv+{18a!>uP31$eRxf336daBIEWr$0$a zrkN~zA{?#M_>_UsGud|hm@1JT?IQ1Y7l{taQn6hP3Kxpiwr3X)XTIH_5%EH6rPvG} zZP#gPg0O@wm{i$dW%gJtzk|u1#$lxGzr~V`Pom*$PUABTdXpQUh^Vf{XDbp7v+*G! zRO2IR4sU$6BRIA3p((Bj>Z2}W@Z2{|^o~2#fL$)|i$RVspoK>-r+NjsN|2*5_g1cs ziLhMLYk0-LE9bmbk0#jaH9g`zl)SXfvY#Z5rrCQXW2!6RoFHQFq1ZE^jpO0H2W0&Np<5*U?dC`Mr(@clUz)0 zQ-4MjB_x`Tyi~x4DC&$^z)$l6*w5mR7WA$MJdB&xU~+nZE6+Y+#u!&Ilo+FcNCbaG zg-x@s(4W3VU|R}?O4%PEJ-UUzZj_&79Z?UgyGOOq zhziO3&xi53Agn%6vqs&sSMkWk$eNmBa?W7UWRPP9tGh!|eWbt_j;P$_&`d(aEp|j* z1;T{5@l2=LRS;g^6@*qkuq$;6J|%q^oDDXv_G~Db>z)HGR|Qj@@S3zqE)~O?r!j~N z{fzw>nPKlaE*0^vGxsmN;lT&*r?n`U7adRvFNCuyqgj$VX@mUw>3ktdLl}Y9mAm)9i<} z#UypuOq8%4^!q5?G&`-6Z>mlKXMWUgzp`r-S4my*?ucQFS0q;%m(bPvhpZ9ntS>x)=hcOrL!SF97~#Dvc|uE0E;i!u2<>Q+ZV zs4Gv&qEtVHW|bRWtqkwGu!^}S!^;i#B$WZDc&v&(K(kmi;P&g{*pll2J zSgc^%PsxWncUcCszEVP@|Tt2Nb%qzIb>7}D+uWsxa zeO_g454&orwJ|#yu6?R(BQw}*-k#AJk4h5890l0s{B%`1LZ-;#kkP^LGC0z>I4(X4qNg746ao zwlQS49a&YRc(7fcW+D0XZdRuG;JnT%ytrw266d*lq6Ev|`5F;15Npqq#J*XxB=$G> z7g2yM!aFqINs#26DS0iDy4(r7+ac!#Zp)NN&I?4!VJC=QE~icKe#6UCY`J5&O60eQ zB}Xixv&$vE9&`^AoLPkgDc2YYp>TM-f#kEEiE5Pr;8DvuYGe#}92Mx2gpKb25n`J9 zo%F=9kw0w^HhQf_t$>x14a``0{Q#pzc3@+Cnc2J=Cee=9yp?|{G@@5dhUppzIHc_< zmL>cq8xMDNM?fu(h1ym5gHrWF@UA*KtFXEmX!wBCotV3h+(Pz#5Hiea6Q*fERi$4h z&V_sbq!dD;r(yRmUsJ4BikL-P(mhOPgR9t$I-nCe1EULS1fsX`tk#JD7xoMAgtYrP zuezNX3T96mIk3-C{=$F1p|4CLO8I!yb20iNEppIcpFm;#_EdiddAXVv1AeWZ>hB|6 zmYyp69Ef7ShE(kH^hBLg_5~uePZ1FKi%u)mevZJTC(|!a?@Q_=&mf5aZChw&kAO;& zL;cDy0nx%-t$u(3(QXg+1!9{oCi-i7;wa6ZQczmq+hF6;FN8PyUa^e*mRSM%h|?Nr ztax7LoC?jN#<-AquSRB_S9eEu%=|{fM(W)L;vFMGokIZPEjp{Pu^B*YOOyLqnR~eh zG|fparD@Z)hbRZ8BbM2I%87`G91!*rY;~wDLbv8+TX0Dv&LQNVG#(@jZT8d0RT%&w z5@gsnLcALC7YOkhdg2JdpHdJ)R3A$cKnXdyT4kTvRjTDn+ zX`)dzgKWIG-&tWYbz|SCvkF_Ah8HHUgF!|?lRXe*L|n7CP);$(xCdcFg^WZiP^fW< zU!Ben;g~q zRi4OZ#BA@VUum-v{|aWzAe){4JUBbQH!OB`nzc|W&Kh#1`A@2*s{ZJAo0o44&db+? zlyApMFWv8VtV92nyQCjNHeLIT4HG#3*|Tln=90$$pniPEP0vvG6Y42V|={aF@)XDpnL=SFPuzI)^aQ|KD|1msGgy zlKU^;)`G}9LjNL?y3on?zvT%1U-Lv8>Ay^ki%OqtB~FiQ3`vK=k#_y?&R5yHnz*u2Up?Ibb40!keuB zM$$=J0c|hay!{j%j1+ z-Xk-73xOhFUS+5@g4>I}$JJ+LSy6zt5x&;;C!7Y7X~odHbyl0)B1gP(D6?ARZBjm| zOIGYQPImDkR#=C)x#^>pYBL;s;K@3^d)`b=fS5$0Vhk_JOB@7iU1j!r_;@fVc z@}&Q+18lVTs#JHfXO;;q{#a*q3D82wc35Z;vBLg}xVnUB!5K?Ni-^iDwD>Zp9x7UV z9~mmN5F+7d@n5CR2Sy8(0nkEAIxAXSHlfhsAU$!k;7=Qa7AL7HHRZ^{0EppUQE0gX zs+4u0#to2Kg^{`MRVu$@R z;%w7UgwJIsGnb4c5v5&7aswzIDw3Q*h6+i9SU8d#mbxDpNmK?v5-shlNaBMC;dfN% zi6aSr+889ch3XonRR#c&u1kuN#|>ox>%b4bDblhfPH+-*jOo&Z&!OLKz<9UR^blN* zcj&A#UwX2>@mS62u&^THggrq_Z5mcw*3~}HJYOzNfWTVC(OT`ZXHNK*=+MM$|5@kFr;E0%Ee@T388gSTySB5H+u_L0h z3p>69x`&D#FCjyP9YQD^JN{7WePHZR82~%9tg~Xr-bsZW`{;>d2Y+&6hh0j1lvj?` zsyTa*r`HPE341P=yINCsji!!2bx}8z<#I<$xN3`r*a_Dhcgo4e+IbeI$|?q5oK6h+ zE8K9+ozAH|^5SMjEQ_N%M@w`L+foIW;1;T0fRp-8KGQw%@6iOWyEhr}&hDmMyAk*LKGZB7U2B!vs7S{@ z*|Uk}7tH>sSDVMPP9B}Jp>IY~*UXJQ_ATdkAL5DZ`Q2%UWlNT=9ZZLk^rD7i8G*ZQ{N|)BPrrQzMD> zWHVO3or2JkorQ>`E@$B=60P4^P^;gF1m!6Rkl>!eajDKkPa(GYZRU=84L4*2SWiv2 z`QjFhdhQNt*6u*Fq33~n#M0KpewT`#X8%!ApqedvRLwL5kgu-4*zg0mHE7GlT%U{N zWZm5`dDm)jrGZI6=+WXx+9Z^@8qu!;_dKcdc@hB*fl+QSS0816{{Op zfHo68)B6pu-hX58=Om~ZC9e@nUiQ+5cb=e&AWoc9GCBuo)pCm-E*Pn|m;Hdwvcid0 zMI4(B!^f#=bhed9(yq}dJ~-9iJ2yL7tcnD5F-gRbqRaeEIoj&l-)#>G3cCt|kP6!wSx%SAuBK?iQQ6Com8;66sVqMX zl}$!G(w)sNrQJGk?J-ojj!Y61obFXbEs$w|5gN0>2nsUMk2{ycj~^o=s=3= zjsS*F)%o*bg{c`+*>(P|9$L43DH|wvSwKlQ7%f%2YB~9SCwv~^pN;kgPz_9?yO(6j z_(jl@_p2MH^_5y?kj+ZLSr_7d!C8tu&Hg$4nUTGq)2ILuF^l!`S{Jc)bMOnvu77TB zE>~H?+Bn=ER?m@x44~1yd7Mp4PEl*p@QNOy&h#DHHOigRsd-~+CT}$B<#x$yO;+m{ ztu%Lv+v>QNMP&*Hi(dn;8hn&kuPqx)g=xJLi9KnbZOO0pEqM;*t%g^sELBRK$Kv=+ z)#ZXvOcQ^gjYHu`?l0c;R4%H!ynUyyChj5LWF+w@vK0z-ccsJ4WI-jUdYHAQV<>94 zKHj`hRkj56)67TGQB&nTl_miBcg3z;nKd1N(+rM-Z3n`>#W$n}NThJp2)B1v(U zU5)mJudO?6ARQRK<_!k6qUq^2sy@7=X;$x{S85?v$c#kO-D{K8kBX=d^^xlt5motS zs;0WBmeUJrP(*tY*5RARVFBaRYvT7-Sv@DQA^&FRLd>1Mnx6Rl2Y=e28HRHGVy#*) zmWMFIpt4%W48vKKM;^26@+7xb;^A$mPpfOOgWP`L*eu-wie?-`OtPT)Sr^ z&TN2L@Gex|9{$(6p6uxh9SlE^Kii@RoT6|}Tr`l>)s)#?FgbBi<%w*{r}*iIqRvRp ztci+I@m=*B)vL`&I5;DZ37Yzyi1=A|Nv??qS&}^wVcsN^bkN$rahEs1M1&wCy@lvo zbHh4l?elIHT}L;4Hk^dec4&18)9YT6;2^)}D5koelr$KoI^Wk z%uvxVf!sJ6k<_(d;%+1w-hv4t0pv(9miJd$P}*LXQkWRGi%$yy*;wZ=5I$=nf5p`avZl|zDm-~0C&Lm%7WV1QE(w#+( zYO&<4c(veUvDqxH7G%<7 z)`am@%w`!Ox;F7eMPqwVD;$d{r~5zsIbxrUQm);O-F!Bs%a+t~^R^=k9&Y=0I+$jv zD2mPHjFU^JM|UNXX7=a~@Xea? ze+Q3Nn9b|GgY$Yy2)tj%^`j%`E5*?*}hQU+@@H{l6 z+G>$oc%5r(J_hQ9=kalR;+_Y8O7T2oJ3gq_aokwM-S0l$>r>s1D6>?*BV-%h+L}c4 z8sgd;Mb2NWH_IF9x;zJkx|;9NJ~sycj?QQEg=r60nY$v?;>t*(vDWbqG}*#tZFfG? zadnXd*q12RZtrCWX;ZY|mP=xUTdiLdj%H81*l7QkPU@Xr$=8uIvsdzbrbH6f6XmiQ zu>M~_Z8EG!Bz3|1Zz9p~V4X-%ur7>(!}`BRaO&(vY&+=P_ZH7%uUjn_*>IUw>0uiK z?R%s1f>&}&&H4)MipzPcji%RXVI&-b-#kR@m(cr0hn1Z+M0WXJFf+{#(x23RKzyaO z>{9a4AwYD;Id^K)G8{5%oWn`Pj)R~B$EZU+u8Q(!UiwLxzlj6zknf>j|1iHPvAF2#}_ZJ^xcwNjE z>_z(hZ7V!Yd5CVXYg^&Hv7+K_$tSi0kG3_|I$_V9pr?8$-+(o1C+M4yF7rbDeMn^P z1T`B_fS1}s~u2niMG&k}~a_s39v?ZA??h zk0IT<6_U6$H%T2gWv_yUwgyR^piH}a;Rb#(CIhG}w9U9fQ(8mDR5s>*sE3yArKvXN zPFbwDg&>E8l^cN0nf4g%g#C2RejP-M&eRdahR9kZ^*glEjc2C{O(-n1IofnytQH%cJvmO7)S=yQGV*|OrQ&dHN77hMkpvRQj z$)z^5?o|QdOpK0sezAN@Wi3_RBbHVz`vT+w?R<{@q{2bWQ2jkf@mZaMQ+;&ck@RQ7 ztA$u@%O$hc31>uvu5&mA>#?qDL|FK6wyvwlZob#8L8?^vW^spX_KtQdfV=6fmb?b7 zxoGh_D`I@bbd@bpq`I#O)%~efL{c{`raEtc7}TcOA~< zU9@Gr6W$n;r>p|=Ns;ZVJPq5ZZ=h`?Q_^E>gZ(`z676=LS+{~tYGcA4UfHZ0LE0X; zXS{V^zxCM0Yjr!Qwx#eA$tT)Rb$N=Gl@L4^iwb_e>(*VFMhCvqoB)nco4;@n*d zx5dusJXAR*qZrPh4g$!%KBmTaA1XOx`z)P%DGyPz(W93wM^qlK*!RWCN$)_7<0u_o zDm;=K;ATXwhB`~zU$5%_nD?>RySu?OKs=V{(qT z&Wl?0+1yH{RuIqK5MlJ(19=~UMLWcn&@QOHyXegKGc~O;K+B}-lC6?{ zMn9(ZtA=SOJ@JN#KczHGaZ%;jSJ6UtjX2DShNOGkqsO3wbJT;z4;-T&d+z0fEOf>W z_J9QX@UE_KU3u6Hc!q;EO@A_tY+RvJ@R5zz4BUTxHoQD#WTO)vN*>aPHMS8c;g4i% zb<~~0y+$+8vd}O_WL$Z6Gx!hpNcyzEqv0+2Dbj^gu@GOBc^GfC=NiS*`QozI;Wd(Q zrYlM=iW0f9eAH~a&@0!{HWzNArKq=?IEl-jrP{hR+>JX#aW+>_kl_pAP8CwE)V$6@ zIF7T{0y<*06YeY5OMIkn9#e^Aeq~(Q@gv1*+iOKfd+k{WCzidMhpS03M1hww?kL6n znw17#rbIC~$F6oBuaokXF8^2vcLnuQVG&}WuPAN=bTu&%vVW;w58&W=#>dlJ!(A<} z6wrRuRhr%GAY?a9V>Sw<`U-UF-WE>!I8Ri)j)eQXRa^lMZi03z3LPnXi|u9PxBFtTS%a()qv;h_5Hb-o zvFMbT>O2Se_0gcI#xY3`OeT;SRXud+W1Kepf(Yvrnkp&ovycPZ$E zd+P1L&Siw>g2BPqmsl3;Jel)uj#&l`qov02n{K`d1lV6YU2zbSrF%ZNk>dzARgh+6!2-Gf_OTg643Ri~*OQ#Hb$4R?Z>7V1EeA1?&6k4O;Tgm@{pGIT!GEMC;Fu+e=~tzbH}WmsahvQh~;_wGF$?rwOE=%74e zW+#O#qE}9LHFd(A9|VooiK9m^Uc3k>!z5~LzTR9uiuvTDh}k;Y2@g^VP6$#I0uN^) zqw00SafHv^aO_o(>+a@KDOL=HM8#aCiV5oVYHPmY1xvgbKUk@vh>4)Mh)Prr{Lbm| za0bG)LJM}q#g%*&?>pULrZLQUU;%dRKEc;dbgZb{L(lLqQ&ejTc+!Y>eqguBh zrUU$4ycTeFa#0F)rsU(C$`aHrsvBg?1&Gw=+YJ=48<-@H85A(^IBch0s??TX0SXu3 zZkK{qd(n=!*IQZiXzSk^%JMxgKyC+1SP6c<;H?xZ)k3jcrezYGzU^M^LI)kya9kh+ zsw9jH`)w*P_n?67gSecDo5DU61Y-#RlrV&{x76yq9T;YX(qWuD7`ZL!mr*{P#KXrZ zuu`VyJi80^mc5?Z;#I%xKk5H#|1_Yt{cEXcUg}PiI)_r<=6~An_@BZto}+@_ z2=qtd9^I#OGp2@J!jVb~O=;&b8h7{vDD!8sPD_Z&*{dO+mvvYSpnzgN4#Gv{Kj%Ns zofs_dlsWoi%Bt44C-4RTPJtxTbzv+L6Jw`(-@1vis+we0=KV0v0 z-3XtYb1}yw^u=ExBY#PSeky(9iSqbNvqdmxM<+-16WWXIaF^LKaai3| zs=#$4Tn5yN5OP0MJb~Y4h=_*<9xd6*V!f&xJ0A72SfGwQVdXfXO67_6Tlw>i=j6K{ zjOQi4Y{5CZW%wIya(z1CRH@6)-+hw>W^=0yCCM6W1pw5E8p!xp~!+>=m diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.mbb.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.mbb.doctree index b80fbada4ffb891b88ac1b341e64c8e3f4e71965..e5ceb7a537bf3100a0c833a49d63c3dd2c490522 100755 GIT binary patch delta 1501 zcmb8v-%C?r7zgm4+Sw1RafWRbs4!U|A4@|(3X**ch=6j2+lcU=e>P@eV&)^^A@J4Y>v`o zs7ZLQ*`y$eKdFv}PBxMVHrp9y@Gi>@BpU6Gc6)3%!g2{`fK9R?jvc~$v5thaz5Bt9 zD|$v#+Cnz;H#H^;7fhzOXg{0ikFn7Jo4|>hBk1mNP@iTLsihR0e<-E^&5zV@OCjhOlMTUHvNmR-A%ss}Suj1T zukw}W`B4dd;dwMZ#6~y{W?a4 zH66qNuJYSj@RUjs-WVXORcuH%KShK1_(IGCYAfQGYC*Q7V?yg;NST4<=NiFLCB9xT zGq4EY;8&Z#CgKgv@ER)Mjn04gb*Fkf(;|?al;({;{wzP Jo{ONb`2#pNJ7@p^ delta 1449 zcmbO-kM-;vR+a|Vsa_jd-mo&}P5#L0ARAItnyRCqrIlTtSd?C@si148P>`6Hl31*l zoL@A#e!ke|EVf7{MvKYpY+B4f#hX8IB(aE!7NwSy7Ujh!7RN(W+XAH~U(E8Jyp?ye zAPdOQDH-gsQ!>PQK%C9he7_mFrBMvlMB(ojC`9IOb`ZR2$rv_yV}!_LJ#RT-kbzj- zHhrHKBhTam(<3)O@Q!C>)ST?#s|s@DbV%s-JhExz!*JQyIXRyZ5(-9vye zVRFM_If5a&`M~sRNKV{*VCEX!A?ZE&>|AWln>-J{^Y#lUOujK6BQy&Z6!|e`Pk#K) zmT+zYr`YL#lo+EoA9!ED$Y?p)@S`?3UnP7@!{=i4uUK5X`R&)A`16(i50ntyyx~U- z`}960MvkenjMq7*zV~GSgUJo$7Ta|g8NJyUTc=mBGlrAmlkNE&j6kmhPk+G4=nBsJ zlOO6yZ+GQloWh1X15URRWW?g7Z9IA7!^6&|zG{h{w%G^{}~ld$&F@j(%&1>gdTEe#mdnHDat3 TWM@k*Ff=kX+Wy9wQG*)*d2<)B diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.mlb.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.mlb.doctree index c75e1ad4cc3c6b078addf381042fc2ff21b5c75f..93bcf4948f6c431602fbf658b4c83e90382477b5 100755 GIT binary patch delta 575 zcmcaUi);5C)`lsJyv%BO3|j>lcg0LT$;dtZS`6bVp2Q6K+9?_tim_9ARHr+}Fp5k+ ze~D3uJ268!c1ngSSaf18qug|^%ZwIaT}3gBqSFJe0QKixW_09Wkz!!Tkj`S7zA>9o zXYv9uR;mAzKoOY?<_y^koeVi|htedN9n%?Q7=@*RNs{W=x#^TN-FI%rK7W zvtk%!r%wb*focMmil-!Q zcdlVNz$6m`)YZe6T2z!@6rY-xoS%}Jmp-M3wX`Hn*J8UuJ=1Jvb~B)oEMuwZhnkq2 z4gLUqk@XWqh=PQLK!hHM&;}BvJ)C)oxv3?U1*yeTfWen#CNCAU{8`arx>PHZ4oot!6(U*N z#$-47zp>@?zpYFTFv*BExa3-xm}@(dX?>e5rr&L6@`6b^bs!|C$0#sbOuyB^WGD;v Z;WLmo!9E1V9oUCipith<(!&(Y2moVMu}uH~ literal 167255 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 cNwGXqYUE+IQW-J3a}bjyL#!0k=}Fg8%>k diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.nba.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.nba.doctree index f6d6c77b80c4b9bec9d6d122493f67a212e7e975..b5139b2baef0d969c1796e19947c46bca2cd6e9a 100755 GIT binary patch delta 541 zcmY+>%_{_P9LI6L*>-C^?UDm|e%_XmoLuB#WtGeypeBBp>2t6NYn|3 zdi9I5EnzcXt{0TGMP-{;=Dy2B`TMPUkc9o-3 z98on{$M%1Mb~;K1UC+Lbk{t;bdCV!mB1Kq-i&dH$O{C<6v1l!6oZ3~xWY$AAhRHwx zkEq9k3(2@EfclG|P`;KIjHIb0%@n8E&?vc-q;g_E#Yj!SZ<)G0n9aq=u`i7;d?JPs zsF=YjOdMbjXE?PtaPfvR9t!GVG^1vd)b6$`EI`iXL3 m$-1w*F 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 diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.nfl.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.nfl.doctree index 5eb0161dd9f3874cf367af97bc321703119d7730..b18a175f4944e44ee8f9fa81c076c8a4e4efadee 100755 GIT binary patch delta 751 zcmY+CPiqrF9EP*AO)*Wip%S`is{Ip9v@QxF=1?S1@Q~_$0C8#7?8et<^FI_@iy~B{ z1~JU)TMQB~}QjdIwej_V=22L*8VT|fFeccn53|jAri-Ak-6)C~0O`&_*mvuqe;~<{hZgL3O(vq*r2rzJfZNF-!L>+hEH+ zd5nI&>8ABuM98xFv-EP_Bw+0xwnH}3k+F(NT17l;!6XBc!Je6fKSc7fKPGuhP;GZv zbKv0)&by59E(&|MLA;!Pzd>|CKiMP`?Aj(t5`1newj7EJU5b!UyxAu{uYqyh#wqR% zv0Q_Uupj&6;6;2rY_duCetx!vI}Tx_OW4|v!XEhK!@LvU&5Y>O@W~esNs`Nj^7KEKhYJ<} literal 206450 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 NIK+Y7aBB2`{sE!??yLX+ literal 102346 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 T%%VLrQo(JB-1KF_a_avBG@v`B diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.wbb.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.wbb.doctree index bbbcec872416b0292d1fe8aed2ff6028f025dc3d..2271fa6edc9b75fae4df9b70dd70c12d0e0dfc0b 100755 GIT binary patch delta 570 zcmY+>KS&!<0LSsXlLV`=hT;%vYE4DYP!FV&T~r9DAfCH;=$Tyn3Fa;&ca;i3=~lRk z;XU)9leicVOvNE<5CUmShb~?G(;<*0ICSdj<+OO6-}`>BPy-TrrayBxjg|1{!=8^7tE|>q_mN=HZ`WwbW3rCU#)@w{va@q zF!-M!wgp}rA)#&+Lkh#NGee}&w_!Sd+gTLJgsXiU3-e`>WCWZ&EIIX%qh6AKnj~W9 zKaBqA>gUxM@$;+6dy5ZFkv9SSKtH~_mV~;;8uwau@BJ<*Vd$o&n>nUd&ZkLLhy;nx zXFt`lStGlm=?Npbp|3OBZ<#4+qOu<+pF4KYL<4b5L%|ZZV4{FMoS=jnE^vcO%YLG( zY0Z3}(Q>T)gH0K68H$W883h?9GHNnzWDC*uX#+vWk{oNsu~r>x+p(Bqo#GO8%kD`T XhN&B-wvyFSImX{DkiYzTk<8x#Vn)^u literal 86056 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

U?B>wk-~QI9v~VjQhG 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 fWa2-qsWmF~b`{)Ve;QqKS/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 wQe6+;6r!7<=sNBAW3X6(+>@#pYr z_-WzKy+iQxQv7+_TKIVv{t)xwm8^6Ctq}ouLTxSt)cZvM-Z@d|j#JTbai=VJ+Kyvi zzK=h6EgXzF5c_q;*}&WURmPeiT89^zl$doyLgf@8-KW|nU+`>k%4P4av{U- z7a3v|M*)n8A_6U9e>R{6PjI3`zFgehKpR|KTzo@|hnTp*DHqMJj2*`bSgwxG_S-=XMME} zMlEVAP(myP4KtCCX*M;5!k23Y_A diff --git a/Sphinx-docs/_build/doctrees/tests.mbb.doctree b/Sphinx-docs/_build/doctrees/tests.mbb.doctree index 2924dc3b058d88e28ba60cfc24bf8b1df896d05b..dbc0874a42c5835b9752290f276256d553db20dd 100755 GIT binary patch delta 25 gcmdlZv0H+rfpx0EMiwS^HrCRTG+m3$yzKg{0A9TYZU6uP delta 26 hcmdlju}6ZXfpx0kMiwS^cDCdKLnBk8%{=V-tN>!k23Y_A diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.cfb.md b/Sphinx-docs/_build/markdown/sportsdataverse.cfb.md index e01d45c..e2a9286 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.cfb.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.cfb.md @@ -5,7 +5,7 @@ ## sportsdataverse.cfb.cfb_game_rosters module -### sportsdataverse.cfb.cfb_game_rosters.espn_cfb_game_rosters(game_id: int, raw=False, return_as_pandas=True, \*\*kwargs) +### sportsdataverse.cfb.cfb_game_rosters.espn_cfb_game_rosters(game_id: int, raw=False, return_as_pandas=False, \*\*kwargs) espn_cfb_game_rosters() - Pull the game by id. Args: @@ -49,7 +49,7 @@ Example: ## sportsdataverse.cfb.cfb_loaders module -### sportsdataverse.cfb.cfb_loaders.get_cfb_teams(return_as_pandas=True) +### sportsdataverse.cfb.cfb_loaders.get_cfb_teams(return_as_pandas=False) Load college football team ID information and logos Example: @@ -65,7 +65,7 @@ Returns: pd.DataFrame: Pandas dataframe containing teams available for the requested seasons. -### sportsdataverse.cfb.cfb_loaders.load_cfb_pbp(seasons: List[int], return_as_pandas=True) +### sportsdataverse.cfb.cfb_loaders.load_cfb_pbp(seasons: List[int], return_as_pandas=False) Load college football play by play data going back to 2003 Example: @@ -86,7 +86,7 @@ Raises: ValueError: If season is less than 2003. -### sportsdataverse.cfb.cfb_loaders.load_cfb_rosters(seasons: List[int], return_as_pandas=True) +### sportsdataverse.cfb.cfb_loaders.load_cfb_rosters(seasons: List[int], return_as_pandas=False) Load roster data Example: @@ -107,7 +107,7 @@ Raises: ValueError: If season is less than 2014. -### sportsdataverse.cfb.cfb_loaders.load_cfb_schedule(seasons: List[int], return_as_pandas=True) +### sportsdataverse.cfb.cfb_loaders.load_cfb_schedule(seasons: List[int], return_as_pandas=False) Load college football schedule data Example: @@ -128,7 +128,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.cfb.cfb_loaders.load_cfb_team_info(seasons: List[int], return_as_pandas=True) +### sportsdataverse.cfb.cfb_loaders.load_cfb_team_info(seasons: List[int], return_as_pandas=False) Load college football team info Example: @@ -204,7 +204,7 @@ Example: ## sportsdataverse.cfb.cfb_schedule module -### sportsdataverse.cfb.cfb_schedule.espn_cfb_calendar(season=None, groups=None, ondays=None, return_as_pandas=True, \*\*kwargs) +### sportsdataverse.cfb.cfb_schedule.espn_cfb_calendar(season=None, groups=None, ondays=None, return_as_pandas=False, \*\*kwargs) espn_cfb_calendar - look up the men’s college football calendar for a given season Args: @@ -223,7 +223,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.cfb.cfb_schedule.espn_cfb_schedule(dates=None, week=None, season_type=None, groups=None, limit=500, return_as_pandas=True, \*\*kwargs) +### sportsdataverse.cfb.cfb_schedule.espn_cfb_schedule(dates=None, week=None, season_type=None, groups=None, limit=500, return_as_pandas=False, \*\*kwargs) espn_cfb_schedule - look up the college football schedule for a given season Args: @@ -244,7 +244,7 @@ Returns: ## sportsdataverse.cfb.cfb_teams module -### sportsdataverse.cfb.cfb_teams.espn_cfb_teams(groups=None, return_as_pandas=True, \*\*kwargs) +### sportsdataverse.cfb.cfb_teams.espn_cfb_teams(groups=None, return_as_pandas=False, \*\*kwargs) espn_cfb_teams - look up the college football teams Args: diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.mbb.md b/Sphinx-docs/_build/markdown/sportsdataverse.mbb.md index d4ee416..0776063 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.mbb.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.mbb.md @@ -5,7 +5,7 @@ ## sportsdataverse.mbb.mbb_game_rosters module -### sportsdataverse.mbb.mbb_game_rosters.espn_mbb_game_rosters(game_id: int, raw=False, return_as_pandas=True, \*\*kwargs) +### sportsdataverse.mbb.mbb_game_rosters.espn_mbb_game_rosters(game_id: int, raw=False, return_as_pandas=False, \*\*kwargs) espn_mbb_game_rosters() - Pull the game by id. Args: @@ -49,7 +49,7 @@ Example: ## sportsdataverse.mbb.mbb_loaders module -### sportsdataverse.mbb.mbb_loaders.load_mbb_pbp(seasons: List[int], return_as_pandas=True) +### sportsdataverse.mbb.mbb_loaders.load_mbb_pbp(seasons: List[int], return_as_pandas=False) Load men’s college basketball play by play data going back to 2002 Example: @@ -71,7 +71,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.mbb.mbb_loaders.load_mbb_player_boxscore(seasons: List[int], return_as_pandas=True) +### sportsdataverse.mbb.mbb_loaders.load_mbb_player_boxscore(seasons: List[int], return_as_pandas=False) Load men’s college basketball player boxscore data Example: @@ -93,7 +93,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.mbb.mbb_loaders.load_mbb_schedule(seasons: List[int], return_as_pandas=True) +### sportsdataverse.mbb.mbb_loaders.load_mbb_schedule(seasons: List[int], return_as_pandas=False) Load men’s college basketball schedule data Example: @@ -115,7 +115,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.mbb.mbb_loaders.load_mbb_team_boxscore(seasons: List[int], return_as_pandas=True) +### sportsdataverse.mbb.mbb_loaders.load_mbb_team_boxscore(seasons: List[int], return_as_pandas=False) Load men’s college basketball team boxscore data Example: @@ -170,7 +170,7 @@ Example: ## sportsdataverse.mbb.mbb_schedule module -### sportsdataverse.mbb.mbb_schedule.espn_mbb_calendar(season=None, ondays=None, return_as_pandas=True, \*\*kwargs) +### sportsdataverse.mbb.mbb_schedule.espn_mbb_calendar(season=None, ondays=None, return_as_pandas=False, \*\*kwargs) espn_mbb_calendar - look up the men’s college basketball calendar for a given season Args: @@ -189,7 +189,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.mbb.mbb_schedule.espn_mbb_schedule(dates=None, groups=50, season_type=None, limit=500, return_as_pandas=True, \*\*kwargs) +### sportsdataverse.mbb.mbb_schedule.espn_mbb_schedule(dates=None, groups=50, season_type=None, limit=500, return_as_pandas=False, \*\*kwargs) espn_mbb_schedule - look up the men’s college basketball scheduler for a given season Args: @@ -209,7 +209,7 @@ Returns: ## sportsdataverse.mbb.mbb_teams module -### sportsdataverse.mbb.mbb_teams.espn_mbb_teams(groups=None, return_as_pandas=True, \*\*kwargs) +### sportsdataverse.mbb.mbb_teams.espn_mbb_teams(groups=None, return_as_pandas=False, \*\*kwargs) espn_mbb_teams - look up the men’s college basketball teams Args: diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.md b/Sphinx-docs/_build/markdown/sportsdataverse.md index acbfe75..e9bfecf 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.md @@ -222,22 +222,6 @@ ## sportsdataverse.decorators module - -### sportsdataverse.decorators.record_mem_usage(func) - -### sportsdataverse.decorators.record_time_usage(func) - -### sportsdataverse.decorators.timer(number=10) -Decorator that times the function it wraps over repeated executions - -Args: - - number: int, The number of repeated executions of the function being wrapped - -Returns: - - func - ## sportsdataverse.dl_utils module diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.mlb.md b/Sphinx-docs/_build/markdown/sportsdataverse.mlb.md index 1daccd6..57454c0 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.mlb.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.mlb.md @@ -336,288 +336,6 @@ Returns: ## sportsdataverse.mlb.retrosheet module -RETROSHEET NOTICE: - -> The information used here was obtained free of -> charge from and is copyrighted by Retrosheet. Interested -> parties may contact Retrosheet at “www.retrosheet.org”. - - -### sportsdataverse.mlb.retrosheet.retrosheet_ballparks() -Retrives the current TEAMABR.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. - -Args: - - None - -Returns: - - A pandas Dataframe with the biographical information of notable major league teams. - - -### sportsdataverse.mlb.retrosheet.retrosheet_ejections() -Retrives the current Ejecdata.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. - -Args: - - None - -Returns: - - A pandas Dataframe with the ejection data of known MLB ejections. - - -### sportsdataverse.mlb.retrosheet.retrosheet_franchises() -Retrives the current TEAMABR.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. - -Args: - - None - -Returns: - - A pandas Dataframe with the biographical information of notable major league teams. - - -### sportsdataverse.mlb.retrosheet.retrosheet_game_logs_team() -Retrives the team-level stats for MLB games in a season, or range of seasons. -THIS DOES NOT GET PLAYER STATS! -Use retrosplits_game_logs_player() for player-level game stats. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - - game_type (str): - - Optional parameter. By default, this is set to “regular”, or to put it in another way, this function call will return only regular season games. - - The full list of supported keywards for game_type are as follows. Case does not matter (you can set game_type to “rEgUlAr”, and the function call will still work): - - > - > * “regular”: Regular season games. - - - > * “asg”: All-Star games. - - - > * “playoffs”: Playoff games. - - filter_out_seasons (bool): - - If game_type is set to either “asg” or “playoffs”, and filter_out_seasons is set to true, this function will filter out seasons that do not match the inputted season and/or the range of seasons. By default, this is set to True. - -Returns: - - A pandas dataframe containing team-level stats for MLB games. - - -### sportsdataverse.mlb.retrosheet.retrosheet_people() -Retrives the current BioFile.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. - -Args: - - None - -Returns: - - A pandas Dataframe with the biographical information of various individuals who have played baseball. - - -### sportsdataverse.mlb.retrosheet.retrosheet_schedule() -Retrives the scheduled games of an MLB season, or MLB seasons. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - - original_2020_schedule (bool): - - Retrosheet keeps a record of the orignial 2020 MLB season, before the season was delayed due to the COVID-19 pandemic. - - - * If this is set to True, this function will return the original 2020 MLB season, before it was altered due to the COVID-19 pandemic, if the user wants this function to return the schedule for the 2020 MLB season. - - - * If this is set to False, this function will return the altered 2020 MLB season, after it was altered due to the COVID-19 pandemic, if the user wants this function to return the schedule for the 2020 MLB season. - -Returns: - - A pandas dataframe containg historical MLB schedules. - ## sportsdataverse.mlb.retrosplits module -RETROSHEET NOTICE: - -> The information used here was obtained free of -> charge from and is copyrighted by Retrosheet. Interested -> parties may contact Retrosheet at “www.retrosheet.org”. - - -### sportsdataverse.mlb.retrosplits.retrosplits_game_logs_player() -Retrives game-level player stats from the Retrosplits project. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - -Returns: - - A pandas dataframe containing game-level player stats from historical MLB games. - - -### sportsdataverse.mlb.retrosplits.retrosplits_game_logs_team() -Retrives game-level team stats from the Retrosplits project. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - -Returns: - - A pandas dataframe containing game-level team stats from historical MLB games. - - -### sportsdataverse.mlb.retrosplits.retrosplits_player_batting_by_platoon() -Retrives player-level, batting by platoon (left/right hitting vs. left/right pitching) split stats from the Retrosplits project. -The stats are batting stats, based off of the handedness of the batter vs the handedness of the pitcher. -The stats returned by this function are season-level stats, not game-level stats. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - -Returns: - - A pandas dataframe containing player-level, batting by platoon stats for batters. - - -### sportsdataverse.mlb.retrosplits.retrosplits_player_batting_by_position() -Retrives player-level, batting by position split stats from the Retrosplits project. -The stats returned by this function are season-level stats, not game-level stats. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - -Returns: - - A pandas dataframe containing batting by position split stats for MLB players. - - -### sportsdataverse.mlb.retrosplits.retrosplits_player_batting_by_runners() -Retrives player-level, batting by runners split stats from the Retrosplits project. -The stats are batting stats, based off of how many runners are on base at the time of the at bat. -The stats returned by this function are season-level stats, not game-level stats. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - -Returns: - - A pandas dataframe containing player-level, batting by runners split stats. - - -### sportsdataverse.mlb.retrosplits.retrosplits_player_head_to_head_stats() -Retrives batter vs. pitcher stats from the Retrosplits project. -The stats are batting stats, based off of the preformance of that specific batter agianst a specific pitcher for the durration of that specific season. -The stats returned by this function are season-level stats, not game-level stats. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - -Returns: - - A pandas dataframe containing batter vs. pitcher stats for a season, or for a range of seasons. - - -### sportsdataverse.mlb.retrosplits.retrosplits_player_pitching_by_platoon() -Retrives player-level, pitching by platoon (left/right pitching vs. left/right hitting) split stats from the Retrosplits project. -The stats are pitching stats, based off of the handedness of the pitcher vs the handedness of the batter. -The stats returned by this function are season-level stats, not game-level stats. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - -Returns: - - A pandas dataframe containing player-level, pitching by platoon stats for pitchers. - - -### sportsdataverse.mlb.retrosplits.retrosplits_player_pitching_by_runners() -Retrives player-level, pitching by runners split stats from the Retrosplits project. -The stats are pitching stats, based off of how many runners are on base at the time of the at bat. -The stats returned by this function are season-level stats, not game-level stats. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - -Returns: - - A pandas dataframe containing pitching by runners split stats for a season, or for a range of seasons. - ## Module contents diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.nba.md b/Sphinx-docs/_build/markdown/sportsdataverse.nba.md index f1af879..d5f749d 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.nba.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.nba.md @@ -4,228 +4,12 @@ ## sportsdataverse.nba.nba_game_rosters module - -### sportsdataverse.nba.nba_game_rosters.espn_nba_game_rosters(game_id: int, raw=False, return_as_pandas=True, \*\*kwargs) -espn_nba_game_rosters() - Pull the game by id. - -Args: - - game_id (int): Unique game_id, can be obtained from espn_nba_schedule(). - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Data frame of game roster data with columns: - ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, - ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, - ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, - ‘age’, ‘date_of_birth’, ‘slug’, ‘jersey’, ‘linked’, ‘active’, - ‘alternate_ids_sdr’, ‘birth_place_city’, ‘birth_place_state’, - ‘birth_place_country’, ‘headshot_href’, ‘headshot_alt’, - ‘experience_years’, ‘experience_display_value’, - ‘experience_abbreviation’, ‘status_id’, ‘status_name’, ‘status_type’, - ‘status_abbreviation’, ‘hand_type’, ‘hand_abbreviation’, - ‘hand_display_value’, ‘draft_display_text’, ‘draft_round’, ‘draft_year’, - ‘draft_selection’, ‘player_id’, ‘starter’, ‘valid’, ‘did_not_play’, - ‘display_name’, ‘ejected’, ‘athlete_href’, ‘position_href’, - ‘statistics_href’, ‘team_id’, ‘team_guid’, ‘team_uid’, ‘team_slug’, - ‘team_location’, ‘team_name’, ‘team_abbreviation’, - ‘team_display_name’, ‘team_short_display_name’, ‘team_color’, - ‘team_alternate_color’, ‘is_active’, ‘is_all_star’, - ‘logo_href’, ‘logo_dark_href’, ‘game_id’ - -Example: - - nba_df = sportsdataverse.nba.espn_nba_game_rosters(game_id=401307514) - - -### sportsdataverse.nba.nba_game_rosters.helper_nba_athlete_items(teams_rosters, \*\*kwargs) - -### sportsdataverse.nba.nba_game_rosters.helper_nba_game_items(summary) - -### sportsdataverse.nba.nba_game_rosters.helper_nba_roster_items(items, summary_url, \*\*kwargs) - -### sportsdataverse.nba.nba_game_rosters.helper_nba_team_items(items, \*\*kwargs) ## sportsdataverse.nba.nba_loaders module - -### sportsdataverse.nba.nba_loaders.load_nba_pbp(seasons: List[int], return_as_pandas=True) -Load NBA play by play data going back to 2002 - -Example: - - nba_df = sportsdataverse.nba.load_nba_pbp(seasons=range(2002,2022)) - -Args: - - seasons (list): Used to define different seasons. 2002 is the earliest available season. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing the - play-by-plays available for the requested seasons. - -Raises: - - ValueError: If season is less than 2002. - - -### sportsdataverse.nba.nba_loaders.load_nba_player_boxscore(seasons: List[int], return_as_pandas=True) -Load NBA player boxscore data - -Example: - - nba_df = sportsdataverse.nba.load_nba_player_boxscore(seasons=range(2002,2022)) - -Args: - - seasons (list): Used to define different seasons. 2002 is the earliest available season. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing the - player boxscores available for the requested seasons. - -Raises: - - ValueError: If season is less than 2002. - - -### sportsdataverse.nba.nba_loaders.load_nba_schedule(seasons: List[int], return_as_pandas=True) -Load NBA schedule data - -Example: - - nba_df = sportsdataverse.nba.load_nba_schedule(seasons=range(2002,2022)) - -Args: - - seasons (list): Used to define different seasons. 2002 is the earliest available season. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing the - schedule for the requested seasons. - -Raises: - - ValueError: If season is less than 2002. - - -### sportsdataverse.nba.nba_loaders.load_nba_team_boxscore(seasons: List[int], return_as_pandas=True) -Load NBA team boxscore data - -Example: - - nba_df = sportsdataverse.nba.load_nba_team_boxscore(seasons=range(2002,2022)) - -Args: - - seasons (list): Used to define different seasons. 2002 is the earliest available season. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing the - team boxscores available for the requested seasons. - -Raises: - - ValueError: If season is less than 2002. - ## sportsdataverse.nba.nba_pbp module - -### sportsdataverse.nba.nba_pbp.espn_nba_pbp(game_id: int, raw=False, \*\*kwargs) -espn_nba_pbp() - Pull the game by id - Data from API endpoints - nba/playbyplay, nba/summary - -Args: - - game_id (int): Unique game_id, can be obtained from nba_schedule(). - raw (bool): If True, returns the raw json from the API endpoint. If False, returns a cleaned dictionary of datasets. - -Returns: - - Dict: Dictionary of game data with keys - “gameId”, “plays”, “winprobability”, “boxscore”, “header”, “broadcasts”, - - “videos”, “playByPlaySource”, “standings”, “leaders”, “seasonseries”, “timeouts”, “pickcenter”, “againstTheSpread”, - “odds”, “predictor”, “espnWP”, “gameInfo”, “season” - -Example: - - nba_df = sportsdataverse.nba.espn_nba_pbp(game_id=401307514) - - -### sportsdataverse.nba.nba_pbp.helper_nba_game_data(pbp_txt, init) - -### sportsdataverse.nba.nba_pbp.helper_nba_pbp(game_id, pbp_txt) - -### sportsdataverse.nba.nba_pbp.helper_nba_pbp_features(game_id, pbp_txt, init) - -### sportsdataverse.nba.nba_pbp.helper_nba_pickcenter(pbp_txt) - -### sportsdataverse.nba.nba_pbp.nba_pbp_disk(game_id, path_to_json) ## sportsdataverse.nba.nba_schedule module - -### sportsdataverse.nba.nba_schedule.espn_nba_calendar(season=None, ondays=None, return_as_pandas=True, \*\*kwargs) -espn_nba_calendar - look up the NBA calendar for a given season from ESPN - -Args: - - season (int): Used to define different seasons. 2002 is the earliest available season. - ondays (boolean): Used to return dates for calendar ondays - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing - calendar dates for the requested season. - -Raises: - - ValueError: If season is less than 2002. - - -### sportsdataverse.nba.nba_schedule.espn_nba_schedule(dates=None, season_type=None, limit=500, return_as_pandas=True, \*\*kwargs) -espn_nba_schedule - look up the NBA schedule for a given date from ESPN - -Args: - - dates (int): Used to define different seasons. 2002 is the earliest available season. - season_type (int): season type, 1 for pre-season, 2 for regular season, 3 for post-season, - 4 for all-star, 5 for off-season - limit (int): number of records to return, default: 500. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing - schedule events for the requested season. - - -### sportsdataverse.nba.nba_schedule.most_recent_nba_season() - -### sportsdataverse.nba.nba_schedule.year_to_season(year) ## sportsdataverse.nba.nba_teams module - -### sportsdataverse.nba.nba_teams.espn_nba_teams(return_as_pandas=True, \*\*kwargs) -espn_nba_teams - look up NBA teams - -Args: - - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing teams for the requested league. - -Example: - - nba_df = sportsdataverse.nba.espn_nba_teams() - ## Module contents diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.nfl.md b/Sphinx-docs/_build/markdown/sportsdataverse.nfl.md index b2421a8..2812ba2 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.nfl.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.nfl.md @@ -6,603 +6,14 @@ ## sportsdataverse.nfl.nfl_game_rosters module - -### sportsdataverse.nfl.nfl_game_rosters.espn_nfl_game_rosters(game_id: int, raw=False, return_as_pandas=True, \*\*kwargs) -espn_nfl_game_rosters() - Pull the game by id. - -Args: - - game_id (int): Unique game_id, can be obtained from espn_nfl_schedule(). - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Data frame of game roster data with columns: - ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, - ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, - ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, - ‘age’, ‘date_of_birth’, ‘slug’, ‘jersey’, ‘linked’, ‘active’, - ‘alternate_ids_sdr’, ‘birth_place_city’, ‘birth_place_state’, - ‘birth_place_country’, ‘headshot_href’, ‘headshot_alt’, - ‘experience_years’, ‘experience_display_value’, - ‘experience_abbreviation’, ‘status_id’, ‘status_name’, ‘status_type’, - ‘status_abbreviation’, ‘hand_type’, ‘hand_abbreviation’, - ‘hand_display_value’, ‘draft_display_text’, ‘draft_round’, ‘draft_year’, - ‘draft_selection’, ‘player_id’, ‘starter’, ‘valid’, ‘did_not_play’, - ‘display_name’, ‘ejected’, ‘athlete_href’, ‘position_href’, - ‘statistics_href’, ‘team_id’, ‘team_guid’, ‘team_uid’, ‘team_slug’, - ‘team_location’, ‘team_name’, ‘team_nickname’, ‘team_abbreviation’, - ‘team_display_name’, ‘team_short_display_name’, ‘team_color’, - ‘team_alternate_color’, ‘is_active’, ‘is_all_star’, - ‘team_alternate_ids_sdr’, ‘logo_href’, ‘logo_dark_href’, ‘game_id’ - -Example: - - nfl_df = sportsdataverse.nfl.espn_nfl_game_rosters(game_id=401220403) - - -### sportsdataverse.nfl.nfl_game_rosters.helper_nfl_athlete_items(teams_rosters, \*\*kwargs) - -### sportsdataverse.nfl.nfl_game_rosters.helper_nfl_game_items(summary) - -### sportsdataverse.nfl.nfl_game_rosters.helper_nfl_roster_items(items, summary_url, \*\*kwargs) - -### sportsdataverse.nfl.nfl_game_rosters.helper_nfl_team_items(items, \*\*kwargs) ## sportsdataverse.nfl.nfl_games module - -### sportsdataverse.nfl.nfl_games.nfl_game_details() -Args: - - game_id (int): Game ID - -Returns: - - Dict: Dictionary of odds and props data with keys - -Example: - - nfl_df = nfl_game_details( - game_id = ‘7ae87c4c-d24c-11ec-b23d-d15a91047884’ - ) - - -### sportsdataverse.nfl.nfl_games.nfl_game_schedule() -Args: - - season (int): season - season_type (str): season type - REG, POST - week (int): week - -Returns: - - Dict: Dictionary of odds and props data with keys - -Example: - - - - ``` - ` - ``` - - nfl_df = nfl_game_schedule( - - season = 2021, seasonType=’REG’, week=1 - - )\` - - -### sportsdataverse.nfl.nfl_games.nfl_headers_gen() - -### sportsdataverse.nfl.nfl_games.nfl_token_gen() ## sportsdataverse.nfl.nfl_loaders module - -### sportsdataverse.nfl.nfl_loaders.load_nfl_combine(return_as_pandas=True) -Load NFL Combine information - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_combine() - -Args: - -Returns: - - pd.DataFrame: Pandas dataframe containing NFL combine data available. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_contracts(return_as_pandas=True) -Load NFL Historical contracts information - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_contracts() - -Args: - -Returns: - - pd.DataFrame: Pandas dataframe containing historical contracts available. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_depth_charts(seasons: List[int], return_as_pandas=True) -Load NFL Depth Chart data for selected seasons - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_depth_charts(seasons=range(2001,2021)) - -Args: - - seasons (list): Used to define different seasons. 2001 is the earliest available season. - -Returns: - - pd.DataFrame: Pandas dataframe containing depth chart data available for the requested seasons. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_draft_picks(return_as_pandas=True) -Load NFL Draft picks information - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_draft_picks() - -Args: - -Returns: - - pd.DataFrame: Pandas dataframe containing NFL Draft picks data available. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_injuries(seasons: List[int], return_as_pandas=True) -Load NFL injuries data for selected seasons - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_injuries(seasons=range(2009,2021)) - -Args: - - seasons (list): Used to define different seasons. 2009 is the earliest available season. - -Returns: - - pd.DataFrame: Pandas dataframe containing injuries data available for the requested seasons. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_ngs_passing(return_as_pandas=True) -Load NFL NextGen Stats Passing data going back to 2016 - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_ngs_passing() - -Returns: - - pd.DataFrame: Pandas dataframe containing the NextGen Stats Passing data available. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_ngs_receiving(return_as_pandas=True) -Load NFL NextGen Stats Receiving data going back to 2016 - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_ngs_receiving() - -Returns: - - pd.DataFrame: Pandas dataframe containing the NextGen Stats Receiving data available. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_ngs_rushing(return_as_pandas=True) -Load NFL NextGen Stats Rushing data going back to 2016 - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_ngs_rushing() - -Returns: - - pd.DataFrame: Pandas dataframe containing the NextGen Stats Rushing data available. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_officials(return_as_pandas=True) -Load NFL Officials information - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_officials() - -Args: - -Returns: - - pd.DataFrame: Pandas dataframe containing officials available. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_pbp(seasons: List[int], return_as_pandas=True) -Load NFL play by play data going back to 1999 - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_pbp(seasons=range(1999,2021)) - -Args: - - seasons (list): Used to define different seasons. 1999 is the earliest available season. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing the play-by-plays available for the requested seasons. - -Raises: - - ValueError: If season is less than 1999. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_pbp_participation(seasons: List[int], return_as_pandas=True) -Load NFL play-by-play participation data for selected seasons - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_pbp_participation(seasons=range(2016,2021)) - -Args: - - seasons (list): Used to define different seasons. 2016 is the earliest available season. - -Returns: - - pd.DataFrame: Pandas dataframe containing play-by-play participation data available for the requested seasons. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_def(return_as_pandas=True) -Load NFL Pro-Football Reference Advanced Defensive data going back to 2018 - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_pfr_def() - -Returns: - - pd.DataFrame: Pandas dataframe containing Pro-Football Reference - - advanced defensive stats data available. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_pass(return_as_pandas=True) -Load NFL Pro-Football Reference Advanced Passing data going back to 2018 - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_pfr_pass() - -Returns: - - pd.DataFrame: Pandas dataframe containing Pro-Football Reference - - advanced passing stats data available. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_rec(return_as_pandas=True) -Load NFL Pro-Football Reference Advanced Receiving data going back to 2018 - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_pfr_rec() - -Returns: - - pd.DataFrame: Pandas dataframe containing Pro-Football Reference - - advanced receiving stats data available. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_rush(return_as_pandas=True) -Load NFL Pro-Football Reference Advanced Rushing data going back to 2018 - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_pfr_rush() - -Returns: - - pd.DataFrame: Pandas dataframe containing Pro-Football Reference - - advanced rushing stats data available. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_weekly_def(seasons: List[int], return_as_pandas=True) -Load NFL Pro-Football Reference Weekly Advanced Defensive data going back to 2018 - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_pfr_weekly_def(seasons=range(2018,2021)) - -Args: - - seasons (list): Used to define different seasons. 2018 is the earliest available season. - -Returns: - - pd.DataFrame: Pandas dataframe containing Pro-Football Reference - - advanced defensive stats data available for the requested seasons. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_weekly_pass(seasons: List[int], return_as_pandas=True) -Load NFL Pro-Football Reference Weekly Advanced Passing data going back to 2018 - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_pfr_weekly_pass(seasons=range(2018,2021)) - -Args: - - seasons (list): Used to define different seasons. 2018 is the earliest available season. - -Returns: - - pd.DataFrame: Pandas dataframe containing Pro-Football Reference - - advanced passing stats data available for the requested seasons. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_weekly_rec(seasons: List[int], return_as_pandas=True) -Load NFL Pro-Football Reference Weekly Advanced Receiving data going back to 2018 - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_pfr_weekly_rec(seasons=range(2018,2021)) - -Args: - - seasons (list): Used to define different seasons. 2018 is the earliest available season. - -Returns: - - pd.DataFrame: Pandas dataframe containing Pro-Football Reference - - advanced receiving stats data available for the requested seasons. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_weekly_rush(seasons: List[int], return_as_pandas=True) -Load NFL Pro-Football Reference Weekly Advanced Rushing data going back to 2018 - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_pfr_weekly_rush(seasons=range(2018,2021)) - -Args: - - seasons (list): Used to define different seasons. 2018 is the earliest available season. - -Returns: - - pd.DataFrame: Pandas dataframe containing Pro-Football Reference - - advanced rushing stats data available for the requested seasons. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_player_stats(kicking=False, return_as_pandas=True) -Load NFL player stats data - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_player_stats() - -Args: - - kicking (bool): If True, load kicking stats. If False, load all other stats. - -Returns: - - pd.DataFrame: Pandas dataframe containing player stats. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_players(return_as_pandas=True) -Load NFL Player ID information - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_players() - -Args: - -Returns: - - pd.DataFrame: Pandas dataframe containing players available. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_rosters(seasons: List[int], return_as_pandas=True) -Load NFL roster data for all seasons - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_rosters(seasons=range(1999,2021)) - -Args: - - seasons (list): Used to define different seasons. 1920 is the earliest available season. - -Returns: - - pd.DataFrame: Pandas dataframe containing rosters available for the requested seasons. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_schedule(seasons: List[int], return_as_pandas=True) -Load NFL schedule data - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_schedule(seasons=range(1999,2021)) - -Args: - - seasons (list): Used to define different seasons. 1999 is the earliest available season. - -Returns: - - pd.DataFrame: Pandas dataframe containing the schedule for the requested seasons. - -Raises: - - ValueError: If season is less than 1999. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_snap_counts(seasons: List[int], return_as_pandas=True) -Load NFL snap counts data for selected seasons - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_snap_counts(seasons=range(2012,2021)) - -Args: - - seasons (list): Used to define different seasons. 2012 is the earliest available season. - -Returns: - - pd.DataFrame: Pandas dataframe containing snap counts available for the requested seasons. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_teams(return_as_pandas=True) -Load NFL team ID information and logos - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_teams() - -Args: - -Returns: - - pd.DataFrame: Pandas dataframe containing teams available. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_weekly_rosters(seasons: List[int], return_as_pandas=True) -Load NFL weekly roster data for selected seasons - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_weekly_rosters(seasons=range(2002,2021)) - -Args: - - seasons (list): Used to define different seasons. 2002 is the earliest available season. - -Returns: - - pd.DataFrame: Pandas dataframe containing weekly rosters available for the requested seasons. - ## sportsdataverse.nfl.nfl_pbp module - -### class sportsdataverse.nfl.nfl_pbp.NFLPlayProcess(gameId=0, raw=False, path_to_json='/') -Bases: `object` - - -#### \__init__(gameId=0, raw=False, path_to_json='/') -Initialize self. See help(type(self)) for accurate signature. - - -#### create_box_score() - -#### espn_nfl_pbp() -espn_nfl_pbp() - Pull the game by id. Data from API endpoints: nfl/playbyplay, nfl/summary - -Args: - - game_id (int): Unique game_id, can be obtained from nfl_schedule(). - -Returns: - - Dict: Dictionary of game data with keys - “gameId”, “plays”, “boxscore”, “header”, “broadcasts”, - - “videos”, “playByPlaySource”, “standings”, “leaders”, “timeouts”, “homeTeamSpread”, “overUnder”, - “pickcenter”, “againstTheSpread”, “odds”, “predictor”, “winprobability”, “espnWP”, - “gameInfo”, “season” - -Example: - - nfl_df = sportsdataverse.nfl.NFLPlayProcess(gameId=401220403).espn_nfl_pbp() - - -#### gameId( = 0) - -#### nfl_pbp_disk() - -#### path_to_json( = '/') - -#### ran_cleaning_pipeline( = False) - -#### ran_pipeline( = False) - -#### raw( = False) - -#### run_cleaning_pipeline() - -#### run_processing_pipeline() ## sportsdataverse.nfl.nfl_schedule module - -### sportsdataverse.nfl.nfl_schedule.espn_nfl_calendar(season=None, ondays=None, return_as_pandas=True, \*\*kwargs) -espn_nfl_calendar - look up the NFL calendar for a given season - -Args: - - season (int): Used to define different seasons. 2002 is the earliest available season. - ondays (boolean): Used to return dates for calendar ondays - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing calendar dates for the requested season. - -Raises: - - ValueError: If season is less than 2002. - - -### sportsdataverse.nfl.nfl_schedule.espn_nfl_schedule(dates=None, week=None, season_type=None, groups=None, limit=500, return_as_pandas=True, \*\*kwargs) -espn_nfl_schedule - look up the NFL schedule for a given season - -Args: - - dates (int): Used to define different seasons. 2002 is the earliest available season. - week (int): Week of the schedule. - season_type (int): 2 for regular season, 3 for post-season, 4 for off-season. - limit (int): number of records to return, default: 500. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. - - -### sportsdataverse.nfl.nfl_schedule.get_current_week() - -### sportsdataverse.nfl.nfl_schedule.most_recent_nfl_season() ## sportsdataverse.nfl.nfl_teams module - -### sportsdataverse.nfl.nfl_teams.espn_nfl_teams(return_as_pandas=True, \*\*kwargs) -espn_nfl_teams - look up NFL teams - -Args: - - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing teams for the requested league. - -Example: - - nfl_df = sportsdataverse.nfl.espn_nfl_teams() - ## Module contents diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.nhl.md b/Sphinx-docs/_build/markdown/sportsdataverse.nhl.md index 2b111d5..d9aeb28 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.nhl.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.nhl.md @@ -4,277 +4,14 @@ ## sportsdataverse.nhl.nhl_api module - -### sportsdataverse.nhl.nhl_api.nhl_api_pbp(game_id: int, \*\*kwargs) -nhl_api_pbp() - Pull the game by id. Data from API endpoints - nhl/playbyplay, nhl/summary - -Args: - - game_id (int): Unique game_id, can be obtained from nhl_schedule(). - -Returns: - - Dict: Dictionary of game data with keys - “gameId”, “plays”, “boxscore”, “header”, “broadcasts”, - - “videos”, “playByPlaySource”, “standings”, “leaders”, “seasonseries”, “pickcenter”, “againstTheSpread”, - “odds”, “onIce”, “gameInfo”, “season” - -Example: - - nhl_df = sportsdataverse.nhl.nhl_api_pbp(game_id=2021020079) - - -### sportsdataverse.nhl.nhl_api.nhl_api_schedule(start_date: str, end_date: str, return_as_pandas=True) -nhl_api_schedule() - Pull the game by id. Data from API endpoints - nhl/schedule - -Args: - - game_id (int): Unique game_id, can be obtained from nhl_schedule(). - -Returns: - - pd.DataFrame: Pandas dataframe containing the schedule for the requested seasons. - -Example: - - nhl_sched_df = sportsdataverse.nhl.nhl_api_schedule(start_date=2021-10-23, end_date=2021-10-28) - ## sportsdataverse.nhl.nhl_game_rosters module - -### sportsdataverse.nhl.nhl_game_rosters.espn_nhl_game_rosters(game_id: int, raw=False, return_as_pandas=True, \*\*kwargs) -espn_nhl_game_rosters() - Pull the game by id. - -Args: - - game_id (int): Unique game_id, can be obtained from espn_nhl_schedule(). - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Data frame of game roster data with columns: - ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, - ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, - ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, - ‘age’, ‘date_of_birth’, ‘slug’, ‘jersey’, ‘linked’, ‘active’, - ‘alternate_ids_sdr’, ‘birth_place_city’, ‘birth_place_state’, - ‘birth_place_country’, ‘headshot_href’, ‘headshot_alt’, - ‘experience_years’, ‘experience_display_value’, - ‘experience_abbreviation’, ‘status_id’, ‘status_name’, ‘status_type’, - ‘status_abbreviation’, ‘hand_type’, ‘hand_abbreviation’, - ‘hand_display_value’, ‘draft_display_text’, ‘draft_round’, ‘draft_year’, - ‘draft_selection’, ‘player_id’, ‘starter’, ‘valid’, ‘did_not_play’, - ‘display_name’, ‘ejected’, ‘athlete_href’, ‘position_href’, - ‘statistics_href’, ‘team_id’, ‘team_guid’, ‘team_uid’, ‘team_slug’, - ‘team_location’, ‘team_name’, ‘team_abbreviation’, - ‘team_display_name’, ‘team_short_display_name’, ‘team_color’, - ‘team_alternate_color’, ‘is_active’, ‘is_all_star’, - ‘logo_href’, ‘logo_dark_href’, ‘game_id’ - -Example: - - nhl_df = sportsdataverse.nhl.espn_nhl_game_rosters(game_id=401247153) - - -### sportsdataverse.nhl.nhl_game_rosters.helper_nhl_athlete_items(teams_rosters, \*\*kwargs) - -### sportsdataverse.nhl.nhl_game_rosters.helper_nhl_game_items(summary) - -### sportsdataverse.nhl.nhl_game_rosters.helper_nhl_roster_items(items, summary_url, \*\*kwargs) - -### sportsdataverse.nhl.nhl_game_rosters.helper_nhl_team_items(items, \*\*kwargs) ## sportsdataverse.nhl.nhl_loaders module - -### sportsdataverse.nhl.nhl_loaders.load_nhl_pbp(seasons: List[int], return_as_pandas=True) -Load NHL play by play data going back to 2011 - -Example: - - nhl_df = sportsdataverse.nhl.load_nhl_pbp(seasons=range(2011,2021)) - -Args: - - seasons (list): Used to define different seasons. 2011 is the earliest available season. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing the play-by-plays available for the requested seasons. - -Raises: - - ValueError: If season is less than 2011. - - -### sportsdataverse.nhl.nhl_loaders.load_nhl_player_boxscore(seasons: List[int], return_as_pandas=True) -Load NHL player boxscore data - -Example: - - nhl_df = sportsdataverse.nhl.load_nhl_player_boxscore(seasons=range(2011,2022)) - -Args: - - seasons (list): Used to define different seasons. 2011 is the earliest available season. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing the - player boxscores available for the requested seasons. - -Raises: - - ValueError: If season is less than 2011. - - -### sportsdataverse.nhl.nhl_loaders.load_nhl_schedule(seasons: List[int], return_as_pandas=True) -Load NHL schedule data - -Example: - - nhl_df = sportsdataverse.nhl.load_nhl_schedule(seasons=range(2002,2021)) - -Args: - - seasons (list): Used to define different seasons. 2002 is the earliest available season. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing the schedule for the requested seasons. - -Raises: - - ValueError: If season is less than 2002. - - -### sportsdataverse.nhl.nhl_loaders.load_nhl_team_boxscore(seasons: List[int], return_as_pandas=True) -Load NHL team boxscore data - -Example: - - nhl_df = sportsdataverse.nhl.load_nhl_team_boxscore(seasons=range(2011,2022)) - -Args: - - seasons (list): Used to define different seasons. 2011 is the earliest available season. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing the - team boxscores available for the requested seasons. - -Raises: - - ValueError: If season is less than 2011. - - -### sportsdataverse.nhl.nhl_loaders.nhl_teams(return_as_pandas=True) -Load NHL team ID information and logos - -Example: - - nhl_df = sportsdataverse.nhl.nhl_teams() - -Args: - - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing teams available for the requested seasons. - ## sportsdataverse.nhl.nhl_pbp module - -### sportsdataverse.nhl.nhl_pbp.espn_nhl_pbp(game_id: int, raw=False, \*\*kwargs) -espn_nhl_pbp() - Pull the game by id. Data from API endpoints - nhl/playbyplay, nhl/summary - -Args: - - game_id (int): Unique game_id, can be obtained from nhl_schedule(). - -Returns: - - Dict: Dictionary of game data with keys - “gameId”, “plays”, “boxscore”, “header”, “broadcasts”, - - “videos”, “playByPlaySource”, “standings”, “leaders”, “seasonseries”, “pickcenter”, “againstTheSpread”, - “odds”, “onIce”, “gameInfo”, “season” - -Example: - - nhl_df = sportsdataverse.nhl.espn_nhl_pbp(game_id=401247153) - - -### sportsdataverse.nhl.nhl_pbp.helper_nhl_game_data(pbp_txt, init) - -### sportsdataverse.nhl.nhl_pbp.helper_nhl_pbp(game_id, pbp_txt) - -### sportsdataverse.nhl.nhl_pbp.helper_nhl_pbp_features(game_id, pbp_txt, init) - -### sportsdataverse.nhl.nhl_pbp.helper_nhl_pickcenter(pbp_txt) - -### sportsdataverse.nhl.nhl_pbp.nhl_pbp_disk(game_id, path_to_json) ## sportsdataverse.nhl.nhl_schedule module - -### sportsdataverse.nhl.nhl_schedule.espn_nhl_calendar(season=None, ondays=None, return_as_pandas=True, \*\*kwargs) -espn_nhl_calendar - look up the NHL calendar for a given season - -Args: - - season (int): Used to define different seasons. 2002 is the earliest available season. - ondays (boolean): Used to return dates for calendar ondays - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing - calendar dates for the requested season. - -Raises: - - ValueError: If season is less than 2002. - - -### sportsdataverse.nhl.nhl_schedule.espn_nhl_schedule(dates=None, season_type=None, limit=500, return_as_pandas=True, \*\*kwargs) -espn_nhl_schedule - look up the NHL schedule for a given date - -Args: - - dates (int): Used to define different seasons. 2002 is the earliest available season. - season_type (int): season type, 1 for pre-season, 2 for regular season, 3 for post-season, 4 for all-star, 5 for off-season - limit (int): number of records to return, default: 500. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing - schedule events for the requested season. - - -### sportsdataverse.nhl.nhl_schedule.most_recent_nhl_season() - -### sportsdataverse.nhl.nhl_schedule.year_to_season(year) ## sportsdataverse.nhl.nhl_teams module - -### sportsdataverse.nhl.nhl_teams.espn_nhl_teams(return_as_pandas=True, \*\*kwargs) -espn_nhl_teams - look up NHL teams - -Args: - - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing teams for the requested league. - -Example: - - nhl_df = sportsdataverse.nhl.espn_nhl_teams() - ## Module contents diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.wbb.md b/Sphinx-docs/_build/markdown/sportsdataverse.wbb.md index fdbb500..1433676 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.wbb.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.wbb.md @@ -4,227 +4,12 @@ ## sportsdataverse.wbb.wbb_game_rosters module - -### sportsdataverse.wbb.wbb_game_rosters.espn_wbb_game_rosters(game_id: int, raw=False, return_as_pandas=True, \*\*kwargs) -espn_wbb_game_rosters() - Pull the game by id. - -Args: - - game_id (int): Unique game_id, can be obtained from wbb_schedule(). - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Data frame of game roster data with columns: - ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, - ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, - ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, - ‘age’, ‘date_of_birth’, ‘slug’, ‘jersey’, ‘linked’, ‘active’, - ‘alternate_ids_sdr’, ‘birth_place_city’, ‘birth_place_state’, - ‘birth_place_country’, ‘headshot_href’, ‘headshot_alt’, - ‘experience_years’, ‘experience_display_value’, - ‘experience_abbreviation’, ‘status_id’, ‘status_name’, ‘status_type’, - ‘status_abbreviation’, ‘hand_type’, ‘hand_abbreviation’, - ‘hand_display_value’, ‘draft_display_text’, ‘draft_round’, ‘draft_year’, - ‘draft_selection’, ‘player_id’, ‘starter’, ‘valid’, ‘did_not_play’, - ‘display_name’, ‘ejected’, ‘athlete_href’, ‘position_href’, - ‘statistics_href’, ‘team_id’, ‘team_guid’, ‘team_uid’, ‘team_slug’, - ‘team_location’, ‘team_name’, ‘team_nickname’, ‘team_abbreviation’, - ‘team_display_name’, ‘team_short_display_name’, ‘team_color’, - ‘team_alternate_color’, ‘is_active’, ‘is_all_star’, - ‘team_alternate_ids_sdr’, ‘logo_href’, ‘logo_dark_href’, ‘game_id’ - -Example: - - wbb_df = sportsdataverse.wbb.espn_wbb_game_rosters(game_id=401266534) - - -### sportsdataverse.wbb.wbb_game_rosters.helper_wbb_athlete_items(teams_rosters, \*\*kwargs) - -### sportsdataverse.wbb.wbb_game_rosters.helper_wbb_game_items(summary) - -### sportsdataverse.wbb.wbb_game_rosters.helper_wbb_roster_items(items, summary_url, \*\*kwargs) - -### sportsdataverse.wbb.wbb_game_rosters.helper_wbb_team_items(items, \*\*kwargs) ## sportsdataverse.wbb.wbb_loaders module - -### sportsdataverse.wbb.wbb_loaders.load_wbb_pbp(seasons: List[int], return_as_pandas=True) -Load women’s college basketball play by play data going back to 2002 - -Example: - - wbb_df = sportsdataverse.wbb.load_wbb_pbp(seasons=range(2002,2022)) - -Args: - - seasons (list): Used to define different seasons. 2002 is the earliest available season. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing the - play-by-plays available for the requested seasons. - -Raises: - - ValueError: If season is less than 2002. - - -### sportsdataverse.wbb.wbb_loaders.load_wbb_player_boxscore(seasons: List[int], return_as_pandas=True) -Load women’s college basketball player boxscore data - -Example: - - wbb_df = sportsdataverse.wbb.load_wbb_player_boxscore(seasons=range(2002,2022)) - -Args: - - seasons (list): Used to define different seasons. 2002 is the earliest available season. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing the - player boxscores available for the requested seasons. - -Raises: - - ValueError: If season is less than 2002. - - -### sportsdataverse.wbb.wbb_loaders.load_wbb_schedule(seasons: List[int], return_as_pandas=True) -Load women’s college basketball schedule data - -Example: - - wbb_df = sportsdataverse.wbb.load_wbb_schedule(seasons=range(2002,2022)) - -Args: - - seasons (list): Used to define different seasons. 2002 is the earliest available season. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing the - schedule for the requested seasons. - -Raises: - - ValueError: If season is less than 2002. - - -### sportsdataverse.wbb.wbb_loaders.load_wbb_team_boxscore(seasons: List[int], return_as_pandas=True) -Load women’s college basketball team boxscore data - -Example: - - wbb_df = sportsdataverse.wbb.load_wbb_team_boxscore(seasons=range(2002,2022)) - -Args: - - seasons (list): Used to define different seasons. 2002 is the earliest available season. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing the - team boxscores available for the requested seasons. - -Raises: - - ValueError: If season is less than 2002. - ## sportsdataverse.wbb.wbb_pbp module - -### sportsdataverse.wbb.wbb_pbp.espn_wbb_pbp(game_id: int, raw=False, \*\*kwargs) -espn_wbb_pbp() - Pull the game by id. Data from API endpoints - womens-college-basketball/playbyplay, -womens-college-basketball/summary - -Args: - - game_id (int): Unique game_id, can be obtained from wbb_schedule(). - -raw (bool): If True, returns the raw json from the API endpoint. If False, returns a cleaned dictionary of datasets. - -Returns: - - Dict: Dictionary of game data with keys - “gameId”, “plays”, “winprobability”, “boxscore”, “header”, - “broadcasts”, “videos”, “playByPlaySource”, “standings”, “leaders”, “timeouts”, “pickcenter”, - “againstTheSpread”, “odds”, “predictor”,”espnWP”, “gameInfo”, “season” - -Example: - - wbb_df = sportsdataverse.wb.espn_wbb_pbp(game_id=401266534) - - -### sportsdataverse.wbb.wbb_pbp.helper_wbb_game_data(pbp_txt, init) - -### sportsdataverse.wbb.wbb_pbp.helper_wbb_pbp(game_id, pbp_txt) - -### sportsdataverse.wbb.wbb_pbp.helper_wbb_pbp_features(game_id, pbp_txt, init) - -### sportsdataverse.wbb.wbb_pbp.helper_wbb_pickcenter(pbp_txt) - -### sportsdataverse.wbb.wbb_pbp.mbb_pbp_disk(game_id, path_to_json) ## sportsdataverse.wbb.wbb_schedule module - -### sportsdataverse.wbb.wbb_schedule.espn_wbb_calendar(season=None, ondays=None, return_as_pandas=True, \*\*kwargs) -espn_wbb_calendar - look up the women’s college basketball calendar for a given season - -Args: - - season (int): Used to define different seasons. 2002 is the earliest available season. - ondays (boolean): Used to return dates for calendar ondays - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing - calendar dates for the requested season. - -Raises: - - ValueError: If season is less than 2002. - - -### sportsdataverse.wbb.wbb_schedule.espn_wbb_schedule(dates=None, groups=50, season_type=None, limit=500, return_as_pandas=True, \*\*kwargs) -espn_wbb_schedule - look up the women’s college basketball schedule for a given season - -Args: - - dates (int): Used to define different seasons. 2002 is the earliest available season. - groups (int): Used to define different divisions. 50 is Division I, 51 is Division II/Division III. - season_type (int): 2 for regular season, 3 for post-season, 4 for off-season. - limit (int): number of records to return, default: 500. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. - - -### sportsdataverse.wbb.wbb_schedule.most_recent_wbb_season() ## sportsdataverse.wbb.wbb_teams module - -### sportsdataverse.wbb.wbb_teams.espn_wbb_teams(groups=None, return_as_pandas=True, \*\*kwargs) -espn_wbb_teams - look up the women’s college basketball teams - -Args: - - groups (int): Used to define different divisions. 50 is Division I, 51 is Division II/Division III. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing teams for the requested league. - -Example: - - wbb_df = sportsdataverse.wbb.espn_wbb_teams() - ## Module contents diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.wnba.md b/Sphinx-docs/_build/markdown/sportsdataverse.wnba.md index 0998bf3..e8855ff 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.wnba.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.wnba.md @@ -4,220 +4,12 @@ ## sportsdataverse.wnba.wnba_game_rosters module - -### sportsdataverse.wnba.wnba_game_rosters.espn_wnba_game_rosters(game_id: int, raw=False, return_as_pandas=True, \*\*kwargs) -espn_wnba_game_rosters() - Pull the game by id. - -Args: - - game_id (int): Unique game_id, can be obtained from espn_wnba_schedule(). - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Data frame of game roster data with columns: - ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, - ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, - ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, - ‘age’, ‘date_of_birth’, ‘slug’, ‘jersey’, ‘linked’, ‘active’, - ‘alternate_ids_sdr’, ‘birth_place_city’, ‘birth_place_state’, - ‘birth_place_country’, ‘headshot_href’, ‘headshot_alt’, - ‘experience_years’, ‘experience_display_value’, - ‘experience_abbreviation’, ‘status_id’, ‘status_name’, ‘status_type’, - ‘status_abbreviation’, ‘hand_type’, ‘hand_abbreviation’, - ‘hand_display_value’, ‘draft_display_text’, ‘draft_round’, ‘draft_year’, - ‘draft_selection’, ‘player_id’, ‘starter’, ‘valid’, ‘did_not_play’, - ‘display_name’, ‘ejected’, ‘athlete_href’, ‘position_href’, - ‘statistics_href’, ‘team_id’, ‘team_guid’, ‘team_uid’, ‘team_slug’, - ‘team_location’, ‘team_name’, ‘team_abbreviation’, - ‘team_display_name’, ‘team_short_display_name’, ‘team_color’, - ‘team_alternate_color’, ‘is_active’, ‘is_all_star’, - ‘logo_href’, ‘logo_dark_href’, ‘game_id’ - -Example: - - wnba_df = sportsdataverse.wnba.espn_wnba_game_rosters(game_id=401370395) - - -### sportsdataverse.wnba.wnba_game_rosters.helper_wnba_athlete_items(teams_rosters, \*\*kwargs) - -### sportsdataverse.wnba.wnba_game_rosters.helper_wnba_game_items(summary) - -### sportsdataverse.wnba.wnba_game_rosters.helper_wnba_roster_items(items, summary_url, \*\*kwargs) - -### sportsdataverse.wnba.wnba_game_rosters.helper_wnba_team_items(items, \*\*kwargs) ## sportsdataverse.wnba.wnba_loaders module - -### sportsdataverse.wnba.wnba_loaders.load_wnba_pbp(seasons: List[int], return_as_pandas=True) -Load WNBA play by play data going back to 2002 - -Example: - - wnba_df = sportsdataverse.wnba.load_wnba_pbp(seasons=range(2002,2022)) - -Args: - - seasons (list): Used to define different seasons. 2002 is the earliest available season. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing the - play-by-plays available for the requested seasons. - -Raises: - - ValueError: If season is less than 2002. - - -### sportsdataverse.wnba.wnba_loaders.load_wnba_player_boxscore(seasons: List[int], return_as_pandas=True) -Load WNBA player boxscore data - -Example: - - wnba_df = sportsdataverse.wnba.load_wnba_player_boxscore(seasons=range(2002,2022)) - -Args: - - seasons (list): Used to define different seasons. 2002 is the earliest available season. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing the - player boxscores available for the requested seasons. - -Raises: - - ValueError: If season is less than 2002. - - -### sportsdataverse.wnba.wnba_loaders.load_wnba_schedule(seasons: List[int], return_as_pandas=True) -Load WNBA schedule data - -Example: - - wnba_df = sportsdataverse.wnba.load_wnba_schedule(seasons=range(2002,2022)) - -Args: - - seasons (list): Used to define different seasons. 2002 is the earliest available season. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing the - schedule for the requested seasons. - -Raises: - - ValueError: If season is less than 2002. - - -### sportsdataverse.wnba.wnba_loaders.load_wnba_team_boxscore(seasons: List[int], return_as_pandas=True) -Load WNBA team boxscore data - -Example: - - wnba_df = sportsdataverse.wnba.load_wnba_team_boxscore(seasons=range(2002,2022)) - -Args: - - seasons (list): Used to define different seasons. 2002 is the earliest available season. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing the - team boxscores available for the requested seasons. - -Raises: - - ValueError: If season is less than 2002. - ## sportsdataverse.wnba.wnba_pbp module - -### sportsdataverse.wnba.wnba_pbp.espn_wnba_pbp(game_id: int, raw=False, \*\*kwargs) -espn_wnba_pbp() - Pull the game by id. Data from API endpoints - wnba/playbyplay, wnba/summary - -Args: - - game_id (int): Unique game_id, can be obtained from wnba_schedule(). - -Returns: - - Dict: Dictionary of game data with keys - “gameId”, “plays”, “winprobability”, “boxscore”, “header”, - - “broadcasts”, “videos”, “playByPlaySource”, “standings”, “leaders”, “seasonseries”, “timeouts”, - “pickcenter”, “againstTheSpread”, “odds”, “predictor”, “espnWP”, “gameInfo”, “season” - -Example: - - wnba_df = sportsdataverse.wnba.espn_wnba_pbp(game_id=401370395) - - -### sportsdataverse.wnba.wnba_pbp.helper_wnba_game_data(pbp_txt, init) - -### sportsdataverse.wnba.wnba_pbp.helper_wnba_pbp(game_id, pbp_txt) - -### sportsdataverse.wnba.wnba_pbp.helper_wnba_pbp_features(game_id, pbp_txt, init) - -### sportsdataverse.wnba.wnba_pbp.helper_wnba_pickcenter(pbp_txt) - -### sportsdataverse.wnba.wnba_pbp.wnba_pbp_disk(game_id, path_to_json) ## sportsdataverse.wnba.wnba_schedule module - -### sportsdataverse.wnba.wnba_schedule.espn_wnba_calendar(season=None, ondays=None, return_as_pandas=True, \*\*kwargs) -espn_wnba_calendar - look up the WNBA calendar for a given season - -Args: - - season (int): Used to define different seasons. 2002 is the earliest available season. - ondays (boolean): Used to return dates for calendar ondays - -Returns: - - pd.DataFrame: Pandas dataframe containing calendar dates for the requested season. - -Raises: - - ValueError: If season is less than 2002. - - -### sportsdataverse.wnba.wnba_schedule.espn_wnba_schedule(dates=None, season_type=None, limit=500, return_as_pandas=True, \*\*kwargs) -espn_wnba_schedule - look up the WNBA schedule for a given season - -Args: - - dates (int): Used to define different seasons. 2002 is the earliest available season. - season_type (int): 2 for regular season, 3 for post-season, 4 for off-season. - limit (int): number of records to return, default: 500. - -Returns: - - pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. - - -### sportsdataverse.wnba.wnba_schedule.most_recent_wnba_season() ## sportsdataverse.wnba.wnba_teams module - -### sportsdataverse.wnba.wnba_teams.espn_wnba_teams(return_as_pandas=True, \*\*kwargs) -espn_wnba_teams - look up WNBA teams - -Args: - - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing teams for the requested league. - -Example: - - wnba_df = sportsdataverse.wnba.espn_wnba_teams() - ## Module contents diff --git a/docs/docs/cfb/index.md b/docs/docs/cfb/index.md index e01d45c..e2a9286 100755 --- a/docs/docs/cfb/index.md +++ b/docs/docs/cfb/index.md @@ -5,7 +5,7 @@ ## sportsdataverse.cfb.cfb_game_rosters module -### sportsdataverse.cfb.cfb_game_rosters.espn_cfb_game_rosters(game_id: int, raw=False, return_as_pandas=True, \*\*kwargs) +### sportsdataverse.cfb.cfb_game_rosters.espn_cfb_game_rosters(game_id: int, raw=False, return_as_pandas=False, \*\*kwargs) espn_cfb_game_rosters() - Pull the game by id. Args: @@ -49,7 +49,7 @@ Example: ## sportsdataverse.cfb.cfb_loaders module -### sportsdataverse.cfb.cfb_loaders.get_cfb_teams(return_as_pandas=True) +### sportsdataverse.cfb.cfb_loaders.get_cfb_teams(return_as_pandas=False) Load college football team ID information and logos Example: @@ -65,7 +65,7 @@ Returns: pd.DataFrame: Pandas dataframe containing teams available for the requested seasons. -### sportsdataverse.cfb.cfb_loaders.load_cfb_pbp(seasons: List[int], return_as_pandas=True) +### sportsdataverse.cfb.cfb_loaders.load_cfb_pbp(seasons: List[int], return_as_pandas=False) Load college football play by play data going back to 2003 Example: @@ -86,7 +86,7 @@ Raises: ValueError: If season is less than 2003. -### sportsdataverse.cfb.cfb_loaders.load_cfb_rosters(seasons: List[int], return_as_pandas=True) +### sportsdataverse.cfb.cfb_loaders.load_cfb_rosters(seasons: List[int], return_as_pandas=False) Load roster data Example: @@ -107,7 +107,7 @@ Raises: ValueError: If season is less than 2014. -### sportsdataverse.cfb.cfb_loaders.load_cfb_schedule(seasons: List[int], return_as_pandas=True) +### sportsdataverse.cfb.cfb_loaders.load_cfb_schedule(seasons: List[int], return_as_pandas=False) Load college football schedule data Example: @@ -128,7 +128,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.cfb.cfb_loaders.load_cfb_team_info(seasons: List[int], return_as_pandas=True) +### sportsdataverse.cfb.cfb_loaders.load_cfb_team_info(seasons: List[int], return_as_pandas=False) Load college football team info Example: @@ -204,7 +204,7 @@ Example: ## sportsdataverse.cfb.cfb_schedule module -### sportsdataverse.cfb.cfb_schedule.espn_cfb_calendar(season=None, groups=None, ondays=None, return_as_pandas=True, \*\*kwargs) +### sportsdataverse.cfb.cfb_schedule.espn_cfb_calendar(season=None, groups=None, ondays=None, return_as_pandas=False, \*\*kwargs) espn_cfb_calendar - look up the men’s college football calendar for a given season Args: @@ -223,7 +223,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.cfb.cfb_schedule.espn_cfb_schedule(dates=None, week=None, season_type=None, groups=None, limit=500, return_as_pandas=True, \*\*kwargs) +### sportsdataverse.cfb.cfb_schedule.espn_cfb_schedule(dates=None, week=None, season_type=None, groups=None, limit=500, return_as_pandas=False, \*\*kwargs) espn_cfb_schedule - look up the college football schedule for a given season Args: @@ -244,7 +244,7 @@ Returns: ## sportsdataverse.cfb.cfb_teams module -### sportsdataverse.cfb.cfb_teams.espn_cfb_teams(groups=None, return_as_pandas=True, \*\*kwargs) +### sportsdataverse.cfb.cfb_teams.espn_cfb_teams(groups=None, return_as_pandas=False, \*\*kwargs) espn_cfb_teams - look up the college football teams Args: diff --git a/docs/docs/mbb/index.md b/docs/docs/mbb/index.md index d4ee416..0776063 100755 --- a/docs/docs/mbb/index.md +++ b/docs/docs/mbb/index.md @@ -5,7 +5,7 @@ ## sportsdataverse.mbb.mbb_game_rosters module -### sportsdataverse.mbb.mbb_game_rosters.espn_mbb_game_rosters(game_id: int, raw=False, return_as_pandas=True, \*\*kwargs) +### sportsdataverse.mbb.mbb_game_rosters.espn_mbb_game_rosters(game_id: int, raw=False, return_as_pandas=False, \*\*kwargs) espn_mbb_game_rosters() - Pull the game by id. Args: @@ -49,7 +49,7 @@ Example: ## sportsdataverse.mbb.mbb_loaders module -### sportsdataverse.mbb.mbb_loaders.load_mbb_pbp(seasons: List[int], return_as_pandas=True) +### sportsdataverse.mbb.mbb_loaders.load_mbb_pbp(seasons: List[int], return_as_pandas=False) Load men’s college basketball play by play data going back to 2002 Example: @@ -71,7 +71,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.mbb.mbb_loaders.load_mbb_player_boxscore(seasons: List[int], return_as_pandas=True) +### sportsdataverse.mbb.mbb_loaders.load_mbb_player_boxscore(seasons: List[int], return_as_pandas=False) Load men’s college basketball player boxscore data Example: @@ -93,7 +93,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.mbb.mbb_loaders.load_mbb_schedule(seasons: List[int], return_as_pandas=True) +### sportsdataverse.mbb.mbb_loaders.load_mbb_schedule(seasons: List[int], return_as_pandas=False) Load men’s college basketball schedule data Example: @@ -115,7 +115,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.mbb.mbb_loaders.load_mbb_team_boxscore(seasons: List[int], return_as_pandas=True) +### sportsdataverse.mbb.mbb_loaders.load_mbb_team_boxscore(seasons: List[int], return_as_pandas=False) Load men’s college basketball team boxscore data Example: @@ -170,7 +170,7 @@ Example: ## sportsdataverse.mbb.mbb_schedule module -### sportsdataverse.mbb.mbb_schedule.espn_mbb_calendar(season=None, ondays=None, return_as_pandas=True, \*\*kwargs) +### sportsdataverse.mbb.mbb_schedule.espn_mbb_calendar(season=None, ondays=None, return_as_pandas=False, \*\*kwargs) espn_mbb_calendar - look up the men’s college basketball calendar for a given season Args: @@ -189,7 +189,7 @@ Raises: ValueError: If season is less than 2002. -### sportsdataverse.mbb.mbb_schedule.espn_mbb_schedule(dates=None, groups=50, season_type=None, limit=500, return_as_pandas=True, \*\*kwargs) +### sportsdataverse.mbb.mbb_schedule.espn_mbb_schedule(dates=None, groups=50, season_type=None, limit=500, return_as_pandas=False, \*\*kwargs) espn_mbb_schedule - look up the men’s college basketball scheduler for a given season Args: @@ -209,7 +209,7 @@ Returns: ## sportsdataverse.mbb.mbb_teams module -### sportsdataverse.mbb.mbb_teams.espn_mbb_teams(groups=None, return_as_pandas=True, \*\*kwargs) +### sportsdataverse.mbb.mbb_teams.espn_mbb_teams(groups=None, return_as_pandas=False, \*\*kwargs) espn_mbb_teams - look up the men’s college basketball teams Args: diff --git a/docs/docs/mlb/index.md b/docs/docs/mlb/index.md index 1daccd6..57454c0 100755 --- a/docs/docs/mlb/index.md +++ b/docs/docs/mlb/index.md @@ -336,288 +336,6 @@ Returns: ## sportsdataverse.mlb.retrosheet module -RETROSHEET NOTICE: - -> The information used here was obtained free of -> charge from and is copyrighted by Retrosheet. Interested -> parties may contact Retrosheet at “www.retrosheet.org”. - - -### sportsdataverse.mlb.retrosheet.retrosheet_ballparks() -Retrives the current TEAMABR.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. - -Args: - - None - -Returns: - - A pandas Dataframe with the biographical information of notable major league teams. - - -### sportsdataverse.mlb.retrosheet.retrosheet_ejections() -Retrives the current Ejecdata.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. - -Args: - - None - -Returns: - - A pandas Dataframe with the ejection data of known MLB ejections. - - -### sportsdataverse.mlb.retrosheet.retrosheet_franchises() -Retrives the current TEAMABR.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. - -Args: - - None - -Returns: - - A pandas Dataframe with the biographical information of notable major league teams. - - -### sportsdataverse.mlb.retrosheet.retrosheet_game_logs_team() -Retrives the team-level stats for MLB games in a season, or range of seasons. -THIS DOES NOT GET PLAYER STATS! -Use retrosplits_game_logs_player() for player-level game stats. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - - game_type (str): - - Optional parameter. By default, this is set to “regular”, or to put it in another way, this function call will return only regular season games. - - The full list of supported keywards for game_type are as follows. Case does not matter (you can set game_type to “rEgUlAr”, and the function call will still work): - - > - > * “regular”: Regular season games. - - - > * “asg”: All-Star games. - - - > * “playoffs”: Playoff games. - - filter_out_seasons (bool): - - If game_type is set to either “asg” or “playoffs”, and filter_out_seasons is set to true, this function will filter out seasons that do not match the inputted season and/or the range of seasons. By default, this is set to True. - -Returns: - - A pandas dataframe containing team-level stats for MLB games. - - -### sportsdataverse.mlb.retrosheet.retrosheet_people() -Retrives the current BioFile.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. - -Args: - - None - -Returns: - - A pandas Dataframe with the biographical information of various individuals who have played baseball. - - -### sportsdataverse.mlb.retrosheet.retrosheet_schedule() -Retrives the scheduled games of an MLB season, or MLB seasons. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - - original_2020_schedule (bool): - - Retrosheet keeps a record of the orignial 2020 MLB season, before the season was delayed due to the COVID-19 pandemic. - - - * If this is set to True, this function will return the original 2020 MLB season, before it was altered due to the COVID-19 pandemic, if the user wants this function to return the schedule for the 2020 MLB season. - - - * If this is set to False, this function will return the altered 2020 MLB season, after it was altered due to the COVID-19 pandemic, if the user wants this function to return the schedule for the 2020 MLB season. - -Returns: - - A pandas dataframe containg historical MLB schedules. - ## sportsdataverse.mlb.retrosplits module -RETROSHEET NOTICE: - -> The information used here was obtained free of -> charge from and is copyrighted by Retrosheet. Interested -> parties may contact Retrosheet at “www.retrosheet.org”. - - -### sportsdataverse.mlb.retrosplits.retrosplits_game_logs_player() -Retrives game-level player stats from the Retrosplits project. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - -Returns: - - A pandas dataframe containing game-level player stats from historical MLB games. - - -### sportsdataverse.mlb.retrosplits.retrosplits_game_logs_team() -Retrives game-level team stats from the Retrosplits project. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - -Returns: - - A pandas dataframe containing game-level team stats from historical MLB games. - - -### sportsdataverse.mlb.retrosplits.retrosplits_player_batting_by_platoon() -Retrives player-level, batting by platoon (left/right hitting vs. left/right pitching) split stats from the Retrosplits project. -The stats are batting stats, based off of the handedness of the batter vs the handedness of the pitcher. -The stats returned by this function are season-level stats, not game-level stats. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - -Returns: - - A pandas dataframe containing player-level, batting by platoon stats for batters. - - -### sportsdataverse.mlb.retrosplits.retrosplits_player_batting_by_position() -Retrives player-level, batting by position split stats from the Retrosplits project. -The stats returned by this function are season-level stats, not game-level stats. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - -Returns: - - A pandas dataframe containing batting by position split stats for MLB players. - - -### sportsdataverse.mlb.retrosplits.retrosplits_player_batting_by_runners() -Retrives player-level, batting by runners split stats from the Retrosplits project. -The stats are batting stats, based off of how many runners are on base at the time of the at bat. -The stats returned by this function are season-level stats, not game-level stats. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - -Returns: - - A pandas dataframe containing player-level, batting by runners split stats. - - -### sportsdataverse.mlb.retrosplits.retrosplits_player_head_to_head_stats() -Retrives batter vs. pitcher stats from the Retrosplits project. -The stats are batting stats, based off of the preformance of that specific batter agianst a specific pitcher for the durration of that specific season. -The stats returned by this function are season-level stats, not game-level stats. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - -Returns: - - A pandas dataframe containing batter vs. pitcher stats for a season, or for a range of seasons. - - -### sportsdataverse.mlb.retrosplits.retrosplits_player_pitching_by_platoon() -Retrives player-level, pitching by platoon (left/right pitching vs. left/right hitting) split stats from the Retrosplits project. -The stats are pitching stats, based off of the handedness of the pitcher vs the handedness of the batter. -The stats returned by this function are season-level stats, not game-level stats. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - -Returns: - - A pandas dataframe containing player-level, pitching by platoon stats for pitchers. - - -### sportsdataverse.mlb.retrosplits.retrosplits_player_pitching_by_runners() -Retrives player-level, pitching by runners split stats from the Retrosplits project. -The stats are pitching stats, based off of how many runners are on base at the time of the at bat. -The stats returned by this function are season-level stats, not game-level stats. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - -Returns: - - A pandas dataframe containing pitching by runners split stats for a season, or for a range of seasons. - ## Module contents diff --git a/docs/docs/nba/index.md b/docs/docs/nba/index.md index f1af879..d5f749d 100755 --- a/docs/docs/nba/index.md +++ b/docs/docs/nba/index.md @@ -4,228 +4,12 @@ ## sportsdataverse.nba.nba_game_rosters module - -### sportsdataverse.nba.nba_game_rosters.espn_nba_game_rosters(game_id: int, raw=False, return_as_pandas=True, \*\*kwargs) -espn_nba_game_rosters() - Pull the game by id. - -Args: - - game_id (int): Unique game_id, can be obtained from espn_nba_schedule(). - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Data frame of game roster data with columns: - ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, - ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, - ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, - ‘age’, ‘date_of_birth’, ‘slug’, ‘jersey’, ‘linked’, ‘active’, - ‘alternate_ids_sdr’, ‘birth_place_city’, ‘birth_place_state’, - ‘birth_place_country’, ‘headshot_href’, ‘headshot_alt’, - ‘experience_years’, ‘experience_display_value’, - ‘experience_abbreviation’, ‘status_id’, ‘status_name’, ‘status_type’, - ‘status_abbreviation’, ‘hand_type’, ‘hand_abbreviation’, - ‘hand_display_value’, ‘draft_display_text’, ‘draft_round’, ‘draft_year’, - ‘draft_selection’, ‘player_id’, ‘starter’, ‘valid’, ‘did_not_play’, - ‘display_name’, ‘ejected’, ‘athlete_href’, ‘position_href’, - ‘statistics_href’, ‘team_id’, ‘team_guid’, ‘team_uid’, ‘team_slug’, - ‘team_location’, ‘team_name’, ‘team_abbreviation’, - ‘team_display_name’, ‘team_short_display_name’, ‘team_color’, - ‘team_alternate_color’, ‘is_active’, ‘is_all_star’, - ‘logo_href’, ‘logo_dark_href’, ‘game_id’ - -Example: - - nba_df = sportsdataverse.nba.espn_nba_game_rosters(game_id=401307514) - - -### sportsdataverse.nba.nba_game_rosters.helper_nba_athlete_items(teams_rosters, \*\*kwargs) - -### sportsdataverse.nba.nba_game_rosters.helper_nba_game_items(summary) - -### sportsdataverse.nba.nba_game_rosters.helper_nba_roster_items(items, summary_url, \*\*kwargs) - -### sportsdataverse.nba.nba_game_rosters.helper_nba_team_items(items, \*\*kwargs) ## sportsdataverse.nba.nba_loaders module - -### sportsdataverse.nba.nba_loaders.load_nba_pbp(seasons: List[int], return_as_pandas=True) -Load NBA play by play data going back to 2002 - -Example: - - nba_df = sportsdataverse.nba.load_nba_pbp(seasons=range(2002,2022)) - -Args: - - seasons (list): Used to define different seasons. 2002 is the earliest available season. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing the - play-by-plays available for the requested seasons. - -Raises: - - ValueError: If season is less than 2002. - - -### sportsdataverse.nba.nba_loaders.load_nba_player_boxscore(seasons: List[int], return_as_pandas=True) -Load NBA player boxscore data - -Example: - - nba_df = sportsdataverse.nba.load_nba_player_boxscore(seasons=range(2002,2022)) - -Args: - - seasons (list): Used to define different seasons. 2002 is the earliest available season. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing the - player boxscores available for the requested seasons. - -Raises: - - ValueError: If season is less than 2002. - - -### sportsdataverse.nba.nba_loaders.load_nba_schedule(seasons: List[int], return_as_pandas=True) -Load NBA schedule data - -Example: - - nba_df = sportsdataverse.nba.load_nba_schedule(seasons=range(2002,2022)) - -Args: - - seasons (list): Used to define different seasons. 2002 is the earliest available season. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing the - schedule for the requested seasons. - -Raises: - - ValueError: If season is less than 2002. - - -### sportsdataverse.nba.nba_loaders.load_nba_team_boxscore(seasons: List[int], return_as_pandas=True) -Load NBA team boxscore data - -Example: - - nba_df = sportsdataverse.nba.load_nba_team_boxscore(seasons=range(2002,2022)) - -Args: - - seasons (list): Used to define different seasons. 2002 is the earliest available season. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing the - team boxscores available for the requested seasons. - -Raises: - - ValueError: If season is less than 2002. - ## sportsdataverse.nba.nba_pbp module - -### sportsdataverse.nba.nba_pbp.espn_nba_pbp(game_id: int, raw=False, \*\*kwargs) -espn_nba_pbp() - Pull the game by id - Data from API endpoints - nba/playbyplay, nba/summary - -Args: - - game_id (int): Unique game_id, can be obtained from nba_schedule(). - raw (bool): If True, returns the raw json from the API endpoint. If False, returns a cleaned dictionary of datasets. - -Returns: - - Dict: Dictionary of game data with keys - “gameId”, “plays”, “winprobability”, “boxscore”, “header”, “broadcasts”, - - “videos”, “playByPlaySource”, “standings”, “leaders”, “seasonseries”, “timeouts”, “pickcenter”, “againstTheSpread”, - “odds”, “predictor”, “espnWP”, “gameInfo”, “season” - -Example: - - nba_df = sportsdataverse.nba.espn_nba_pbp(game_id=401307514) - - -### sportsdataverse.nba.nba_pbp.helper_nba_game_data(pbp_txt, init) - -### sportsdataverse.nba.nba_pbp.helper_nba_pbp(game_id, pbp_txt) - -### sportsdataverse.nba.nba_pbp.helper_nba_pbp_features(game_id, pbp_txt, init) - -### sportsdataverse.nba.nba_pbp.helper_nba_pickcenter(pbp_txt) - -### sportsdataverse.nba.nba_pbp.nba_pbp_disk(game_id, path_to_json) ## sportsdataverse.nba.nba_schedule module - -### sportsdataverse.nba.nba_schedule.espn_nba_calendar(season=None, ondays=None, return_as_pandas=True, \*\*kwargs) -espn_nba_calendar - look up the NBA calendar for a given season from ESPN - -Args: - - season (int): Used to define different seasons. 2002 is the earliest available season. - ondays (boolean): Used to return dates for calendar ondays - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing - calendar dates for the requested season. - -Raises: - - ValueError: If season is less than 2002. - - -### sportsdataverse.nba.nba_schedule.espn_nba_schedule(dates=None, season_type=None, limit=500, return_as_pandas=True, \*\*kwargs) -espn_nba_schedule - look up the NBA schedule for a given date from ESPN - -Args: - - dates (int): Used to define different seasons. 2002 is the earliest available season. - season_type (int): season type, 1 for pre-season, 2 for regular season, 3 for post-season, - 4 for all-star, 5 for off-season - limit (int): number of records to return, default: 500. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing - schedule events for the requested season. - - -### sportsdataverse.nba.nba_schedule.most_recent_nba_season() - -### sportsdataverse.nba.nba_schedule.year_to_season(year) ## sportsdataverse.nba.nba_teams module - -### sportsdataverse.nba.nba_teams.espn_nba_teams(return_as_pandas=True, \*\*kwargs) -espn_nba_teams - look up NBA teams - -Args: - - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing teams for the requested league. - -Example: - - nba_df = sportsdataverse.nba.espn_nba_teams() - ## Module contents diff --git a/docs/docs/nfl/index.md b/docs/docs/nfl/index.md index b2421a8..2812ba2 100755 --- a/docs/docs/nfl/index.md +++ b/docs/docs/nfl/index.md @@ -6,603 +6,14 @@ ## sportsdataverse.nfl.nfl_game_rosters module - -### sportsdataverse.nfl.nfl_game_rosters.espn_nfl_game_rosters(game_id: int, raw=False, return_as_pandas=True, \*\*kwargs) -espn_nfl_game_rosters() - Pull the game by id. - -Args: - - game_id (int): Unique game_id, can be obtained from espn_nfl_schedule(). - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Data frame of game roster data with columns: - ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, - ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, - ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, - ‘age’, ‘date_of_birth’, ‘slug’, ‘jersey’, ‘linked’, ‘active’, - ‘alternate_ids_sdr’, ‘birth_place_city’, ‘birth_place_state’, - ‘birth_place_country’, ‘headshot_href’, ‘headshot_alt’, - ‘experience_years’, ‘experience_display_value’, - ‘experience_abbreviation’, ‘status_id’, ‘status_name’, ‘status_type’, - ‘status_abbreviation’, ‘hand_type’, ‘hand_abbreviation’, - ‘hand_display_value’, ‘draft_display_text’, ‘draft_round’, ‘draft_year’, - ‘draft_selection’, ‘player_id’, ‘starter’, ‘valid’, ‘did_not_play’, - ‘display_name’, ‘ejected’, ‘athlete_href’, ‘position_href’, - ‘statistics_href’, ‘team_id’, ‘team_guid’, ‘team_uid’, ‘team_slug’, - ‘team_location’, ‘team_name’, ‘team_nickname’, ‘team_abbreviation’, - ‘team_display_name’, ‘team_short_display_name’, ‘team_color’, - ‘team_alternate_color’, ‘is_active’, ‘is_all_star’, - ‘team_alternate_ids_sdr’, ‘logo_href’, ‘logo_dark_href’, ‘game_id’ - -Example: - - nfl_df = sportsdataverse.nfl.espn_nfl_game_rosters(game_id=401220403) - - -### sportsdataverse.nfl.nfl_game_rosters.helper_nfl_athlete_items(teams_rosters, \*\*kwargs) - -### sportsdataverse.nfl.nfl_game_rosters.helper_nfl_game_items(summary) - -### sportsdataverse.nfl.nfl_game_rosters.helper_nfl_roster_items(items, summary_url, \*\*kwargs) - -### sportsdataverse.nfl.nfl_game_rosters.helper_nfl_team_items(items, \*\*kwargs) ## sportsdataverse.nfl.nfl_games module - -### sportsdataverse.nfl.nfl_games.nfl_game_details() -Args: - - game_id (int): Game ID - -Returns: - - Dict: Dictionary of odds and props data with keys - -Example: - - nfl_df = nfl_game_details( - game_id = ‘7ae87c4c-d24c-11ec-b23d-d15a91047884’ - ) - - -### sportsdataverse.nfl.nfl_games.nfl_game_schedule() -Args: - - season (int): season - season_type (str): season type - REG, POST - week (int): week - -Returns: - - Dict: Dictionary of odds and props data with keys - -Example: - - - - ``` - ` - ``` - - nfl_df = nfl_game_schedule( - - season = 2021, seasonType=’REG’, week=1 - - )\` - - -### sportsdataverse.nfl.nfl_games.nfl_headers_gen() - -### sportsdataverse.nfl.nfl_games.nfl_token_gen() ## sportsdataverse.nfl.nfl_loaders module - -### sportsdataverse.nfl.nfl_loaders.load_nfl_combine(return_as_pandas=True) -Load NFL Combine information - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_combine() - -Args: - -Returns: - - pd.DataFrame: Pandas dataframe containing NFL combine data available. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_contracts(return_as_pandas=True) -Load NFL Historical contracts information - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_contracts() - -Args: - -Returns: - - pd.DataFrame: Pandas dataframe containing historical contracts available. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_depth_charts(seasons: List[int], return_as_pandas=True) -Load NFL Depth Chart data for selected seasons - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_depth_charts(seasons=range(2001,2021)) - -Args: - - seasons (list): Used to define different seasons. 2001 is the earliest available season. - -Returns: - - pd.DataFrame: Pandas dataframe containing depth chart data available for the requested seasons. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_draft_picks(return_as_pandas=True) -Load NFL Draft picks information - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_draft_picks() - -Args: - -Returns: - - pd.DataFrame: Pandas dataframe containing NFL Draft picks data available. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_injuries(seasons: List[int], return_as_pandas=True) -Load NFL injuries data for selected seasons - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_injuries(seasons=range(2009,2021)) - -Args: - - seasons (list): Used to define different seasons. 2009 is the earliest available season. - -Returns: - - pd.DataFrame: Pandas dataframe containing injuries data available for the requested seasons. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_ngs_passing(return_as_pandas=True) -Load NFL NextGen Stats Passing data going back to 2016 - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_ngs_passing() - -Returns: - - pd.DataFrame: Pandas dataframe containing the NextGen Stats Passing data available. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_ngs_receiving(return_as_pandas=True) -Load NFL NextGen Stats Receiving data going back to 2016 - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_ngs_receiving() - -Returns: - - pd.DataFrame: Pandas dataframe containing the NextGen Stats Receiving data available. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_ngs_rushing(return_as_pandas=True) -Load NFL NextGen Stats Rushing data going back to 2016 - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_ngs_rushing() - -Returns: - - pd.DataFrame: Pandas dataframe containing the NextGen Stats Rushing data available. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_officials(return_as_pandas=True) -Load NFL Officials information - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_officials() - -Args: - -Returns: - - pd.DataFrame: Pandas dataframe containing officials available. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_pbp(seasons: List[int], return_as_pandas=True) -Load NFL play by play data going back to 1999 - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_pbp(seasons=range(1999,2021)) - -Args: - - seasons (list): Used to define different seasons. 1999 is the earliest available season. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing the play-by-plays available for the requested seasons. - -Raises: - - ValueError: If season is less than 1999. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_pbp_participation(seasons: List[int], return_as_pandas=True) -Load NFL play-by-play participation data for selected seasons - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_pbp_participation(seasons=range(2016,2021)) - -Args: - - seasons (list): Used to define different seasons. 2016 is the earliest available season. - -Returns: - - pd.DataFrame: Pandas dataframe containing play-by-play participation data available for the requested seasons. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_def(return_as_pandas=True) -Load NFL Pro-Football Reference Advanced Defensive data going back to 2018 - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_pfr_def() - -Returns: - - pd.DataFrame: Pandas dataframe containing Pro-Football Reference - - advanced defensive stats data available. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_pass(return_as_pandas=True) -Load NFL Pro-Football Reference Advanced Passing data going back to 2018 - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_pfr_pass() - -Returns: - - pd.DataFrame: Pandas dataframe containing Pro-Football Reference - - advanced passing stats data available. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_rec(return_as_pandas=True) -Load NFL Pro-Football Reference Advanced Receiving data going back to 2018 - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_pfr_rec() - -Returns: - - pd.DataFrame: Pandas dataframe containing Pro-Football Reference - - advanced receiving stats data available. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_rush(return_as_pandas=True) -Load NFL Pro-Football Reference Advanced Rushing data going back to 2018 - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_pfr_rush() - -Returns: - - pd.DataFrame: Pandas dataframe containing Pro-Football Reference - - advanced rushing stats data available. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_weekly_def(seasons: List[int], return_as_pandas=True) -Load NFL Pro-Football Reference Weekly Advanced Defensive data going back to 2018 - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_pfr_weekly_def(seasons=range(2018,2021)) - -Args: - - seasons (list): Used to define different seasons. 2018 is the earliest available season. - -Returns: - - pd.DataFrame: Pandas dataframe containing Pro-Football Reference - - advanced defensive stats data available for the requested seasons. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_weekly_pass(seasons: List[int], return_as_pandas=True) -Load NFL Pro-Football Reference Weekly Advanced Passing data going back to 2018 - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_pfr_weekly_pass(seasons=range(2018,2021)) - -Args: - - seasons (list): Used to define different seasons. 2018 is the earliest available season. - -Returns: - - pd.DataFrame: Pandas dataframe containing Pro-Football Reference - - advanced passing stats data available for the requested seasons. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_weekly_rec(seasons: List[int], return_as_pandas=True) -Load NFL Pro-Football Reference Weekly Advanced Receiving data going back to 2018 - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_pfr_weekly_rec(seasons=range(2018,2021)) - -Args: - - seasons (list): Used to define different seasons. 2018 is the earliest available season. - -Returns: - - pd.DataFrame: Pandas dataframe containing Pro-Football Reference - - advanced receiving stats data available for the requested seasons. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_weekly_rush(seasons: List[int], return_as_pandas=True) -Load NFL Pro-Football Reference Weekly Advanced Rushing data going back to 2018 - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_pfr_weekly_rush(seasons=range(2018,2021)) - -Args: - - seasons (list): Used to define different seasons. 2018 is the earliest available season. - -Returns: - - pd.DataFrame: Pandas dataframe containing Pro-Football Reference - - advanced rushing stats data available for the requested seasons. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_player_stats(kicking=False, return_as_pandas=True) -Load NFL player stats data - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_player_stats() - -Args: - - kicking (bool): If True, load kicking stats. If False, load all other stats. - -Returns: - - pd.DataFrame: Pandas dataframe containing player stats. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_players(return_as_pandas=True) -Load NFL Player ID information - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_players() - -Args: - -Returns: - - pd.DataFrame: Pandas dataframe containing players available. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_rosters(seasons: List[int], return_as_pandas=True) -Load NFL roster data for all seasons - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_rosters(seasons=range(1999,2021)) - -Args: - - seasons (list): Used to define different seasons. 1920 is the earliest available season. - -Returns: - - pd.DataFrame: Pandas dataframe containing rosters available for the requested seasons. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_schedule(seasons: List[int], return_as_pandas=True) -Load NFL schedule data - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_schedule(seasons=range(1999,2021)) - -Args: - - seasons (list): Used to define different seasons. 1999 is the earliest available season. - -Returns: - - pd.DataFrame: Pandas dataframe containing the schedule for the requested seasons. - -Raises: - - ValueError: If season is less than 1999. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_snap_counts(seasons: List[int], return_as_pandas=True) -Load NFL snap counts data for selected seasons - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_snap_counts(seasons=range(2012,2021)) - -Args: - - seasons (list): Used to define different seasons. 2012 is the earliest available season. - -Returns: - - pd.DataFrame: Pandas dataframe containing snap counts available for the requested seasons. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_teams(return_as_pandas=True) -Load NFL team ID information and logos - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_teams() - -Args: - -Returns: - - pd.DataFrame: Pandas dataframe containing teams available. - - -### sportsdataverse.nfl.nfl_loaders.load_nfl_weekly_rosters(seasons: List[int], return_as_pandas=True) -Load NFL weekly roster data for selected seasons - -Example: - - nfl_df = sportsdataverse.nfl.load_nfl_weekly_rosters(seasons=range(2002,2021)) - -Args: - - seasons (list): Used to define different seasons. 2002 is the earliest available season. - -Returns: - - pd.DataFrame: Pandas dataframe containing weekly rosters available for the requested seasons. - ## sportsdataverse.nfl.nfl_pbp module - -### class sportsdataverse.nfl.nfl_pbp.NFLPlayProcess(gameId=0, raw=False, path_to_json='/') -Bases: `object` - - -#### \__init__(gameId=0, raw=False, path_to_json='/') -Initialize self. See help(type(self)) for accurate signature. - - -#### create_box_score() - -#### espn_nfl_pbp() -espn_nfl_pbp() - Pull the game by id. Data from API endpoints: nfl/playbyplay, nfl/summary - -Args: - - game_id (int): Unique game_id, can be obtained from nfl_schedule(). - -Returns: - - Dict: Dictionary of game data with keys - “gameId”, “plays”, “boxscore”, “header”, “broadcasts”, - - “videos”, “playByPlaySource”, “standings”, “leaders”, “timeouts”, “homeTeamSpread”, “overUnder”, - “pickcenter”, “againstTheSpread”, “odds”, “predictor”, “winprobability”, “espnWP”, - “gameInfo”, “season” - -Example: - - nfl_df = sportsdataverse.nfl.NFLPlayProcess(gameId=401220403).espn_nfl_pbp() - - -#### gameId( = 0) - -#### nfl_pbp_disk() - -#### path_to_json( = '/') - -#### ran_cleaning_pipeline( = False) - -#### ran_pipeline( = False) - -#### raw( = False) - -#### run_cleaning_pipeline() - -#### run_processing_pipeline() ## sportsdataverse.nfl.nfl_schedule module - -### sportsdataverse.nfl.nfl_schedule.espn_nfl_calendar(season=None, ondays=None, return_as_pandas=True, \*\*kwargs) -espn_nfl_calendar - look up the NFL calendar for a given season - -Args: - - season (int): Used to define different seasons. 2002 is the earliest available season. - ondays (boolean): Used to return dates for calendar ondays - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing calendar dates for the requested season. - -Raises: - - ValueError: If season is less than 2002. - - -### sportsdataverse.nfl.nfl_schedule.espn_nfl_schedule(dates=None, week=None, season_type=None, groups=None, limit=500, return_as_pandas=True, \*\*kwargs) -espn_nfl_schedule - look up the NFL schedule for a given season - -Args: - - dates (int): Used to define different seasons. 2002 is the earliest available season. - week (int): Week of the schedule. - season_type (int): 2 for regular season, 3 for post-season, 4 for off-season. - limit (int): number of records to return, default: 500. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. - - -### sportsdataverse.nfl.nfl_schedule.get_current_week() - -### sportsdataverse.nfl.nfl_schedule.most_recent_nfl_season() ## sportsdataverse.nfl.nfl_teams module - -### sportsdataverse.nfl.nfl_teams.espn_nfl_teams(return_as_pandas=True, \*\*kwargs) -espn_nfl_teams - look up NFL teams - -Args: - - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing teams for the requested league. - -Example: - - nfl_df = sportsdataverse.nfl.espn_nfl_teams() - ## Module contents diff --git a/docs/docs/nhl/index.md b/docs/docs/nhl/index.md index 2b111d5..d9aeb28 100755 --- a/docs/docs/nhl/index.md +++ b/docs/docs/nhl/index.md @@ -4,277 +4,14 @@ ## sportsdataverse.nhl.nhl_api module - -### sportsdataverse.nhl.nhl_api.nhl_api_pbp(game_id: int, \*\*kwargs) -nhl_api_pbp() - Pull the game by id. Data from API endpoints - nhl/playbyplay, nhl/summary - -Args: - - game_id (int): Unique game_id, can be obtained from nhl_schedule(). - -Returns: - - Dict: Dictionary of game data with keys - “gameId”, “plays”, “boxscore”, “header”, “broadcasts”, - - “videos”, “playByPlaySource”, “standings”, “leaders”, “seasonseries”, “pickcenter”, “againstTheSpread”, - “odds”, “onIce”, “gameInfo”, “season” - -Example: - - nhl_df = sportsdataverse.nhl.nhl_api_pbp(game_id=2021020079) - - -### sportsdataverse.nhl.nhl_api.nhl_api_schedule(start_date: str, end_date: str, return_as_pandas=True) -nhl_api_schedule() - Pull the game by id. Data from API endpoints - nhl/schedule - -Args: - - game_id (int): Unique game_id, can be obtained from nhl_schedule(). - -Returns: - - pd.DataFrame: Pandas dataframe containing the schedule for the requested seasons. - -Example: - - nhl_sched_df = sportsdataverse.nhl.nhl_api_schedule(start_date=2021-10-23, end_date=2021-10-28) - ## sportsdataverse.nhl.nhl_game_rosters module - -### sportsdataverse.nhl.nhl_game_rosters.espn_nhl_game_rosters(game_id: int, raw=False, return_as_pandas=True, \*\*kwargs) -espn_nhl_game_rosters() - Pull the game by id. - -Args: - - game_id (int): Unique game_id, can be obtained from espn_nhl_schedule(). - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Data frame of game roster data with columns: - ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, - ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, - ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, - ‘age’, ‘date_of_birth’, ‘slug’, ‘jersey’, ‘linked’, ‘active’, - ‘alternate_ids_sdr’, ‘birth_place_city’, ‘birth_place_state’, - ‘birth_place_country’, ‘headshot_href’, ‘headshot_alt’, - ‘experience_years’, ‘experience_display_value’, - ‘experience_abbreviation’, ‘status_id’, ‘status_name’, ‘status_type’, - ‘status_abbreviation’, ‘hand_type’, ‘hand_abbreviation’, - ‘hand_display_value’, ‘draft_display_text’, ‘draft_round’, ‘draft_year’, - ‘draft_selection’, ‘player_id’, ‘starter’, ‘valid’, ‘did_not_play’, - ‘display_name’, ‘ejected’, ‘athlete_href’, ‘position_href’, - ‘statistics_href’, ‘team_id’, ‘team_guid’, ‘team_uid’, ‘team_slug’, - ‘team_location’, ‘team_name’, ‘team_abbreviation’, - ‘team_display_name’, ‘team_short_display_name’, ‘team_color’, - ‘team_alternate_color’, ‘is_active’, ‘is_all_star’, - ‘logo_href’, ‘logo_dark_href’, ‘game_id’ - -Example: - - nhl_df = sportsdataverse.nhl.espn_nhl_game_rosters(game_id=401247153) - - -### sportsdataverse.nhl.nhl_game_rosters.helper_nhl_athlete_items(teams_rosters, \*\*kwargs) - -### sportsdataverse.nhl.nhl_game_rosters.helper_nhl_game_items(summary) - -### sportsdataverse.nhl.nhl_game_rosters.helper_nhl_roster_items(items, summary_url, \*\*kwargs) - -### sportsdataverse.nhl.nhl_game_rosters.helper_nhl_team_items(items, \*\*kwargs) ## sportsdataverse.nhl.nhl_loaders module - -### sportsdataverse.nhl.nhl_loaders.load_nhl_pbp(seasons: List[int], return_as_pandas=True) -Load NHL play by play data going back to 2011 - -Example: - - nhl_df = sportsdataverse.nhl.load_nhl_pbp(seasons=range(2011,2021)) - -Args: - - seasons (list): Used to define different seasons. 2011 is the earliest available season. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing the play-by-plays available for the requested seasons. - -Raises: - - ValueError: If season is less than 2011. - - -### sportsdataverse.nhl.nhl_loaders.load_nhl_player_boxscore(seasons: List[int], return_as_pandas=True) -Load NHL player boxscore data - -Example: - - nhl_df = sportsdataverse.nhl.load_nhl_player_boxscore(seasons=range(2011,2022)) - -Args: - - seasons (list): Used to define different seasons. 2011 is the earliest available season. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing the - player boxscores available for the requested seasons. - -Raises: - - ValueError: If season is less than 2011. - - -### sportsdataverse.nhl.nhl_loaders.load_nhl_schedule(seasons: List[int], return_as_pandas=True) -Load NHL schedule data - -Example: - - nhl_df = sportsdataverse.nhl.load_nhl_schedule(seasons=range(2002,2021)) - -Args: - - seasons (list): Used to define different seasons. 2002 is the earliest available season. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing the schedule for the requested seasons. - -Raises: - - ValueError: If season is less than 2002. - - -### sportsdataverse.nhl.nhl_loaders.load_nhl_team_boxscore(seasons: List[int], return_as_pandas=True) -Load NHL team boxscore data - -Example: - - nhl_df = sportsdataverse.nhl.load_nhl_team_boxscore(seasons=range(2011,2022)) - -Args: - - seasons (list): Used to define different seasons. 2011 is the earliest available season. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing the - team boxscores available for the requested seasons. - -Raises: - - ValueError: If season is less than 2011. - - -### sportsdataverse.nhl.nhl_loaders.nhl_teams(return_as_pandas=True) -Load NHL team ID information and logos - -Example: - - nhl_df = sportsdataverse.nhl.nhl_teams() - -Args: - - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing teams available for the requested seasons. - ## sportsdataverse.nhl.nhl_pbp module - -### sportsdataverse.nhl.nhl_pbp.espn_nhl_pbp(game_id: int, raw=False, \*\*kwargs) -espn_nhl_pbp() - Pull the game by id. Data from API endpoints - nhl/playbyplay, nhl/summary - -Args: - - game_id (int): Unique game_id, can be obtained from nhl_schedule(). - -Returns: - - Dict: Dictionary of game data with keys - “gameId”, “plays”, “boxscore”, “header”, “broadcasts”, - - “videos”, “playByPlaySource”, “standings”, “leaders”, “seasonseries”, “pickcenter”, “againstTheSpread”, - “odds”, “onIce”, “gameInfo”, “season” - -Example: - - nhl_df = sportsdataverse.nhl.espn_nhl_pbp(game_id=401247153) - - -### sportsdataverse.nhl.nhl_pbp.helper_nhl_game_data(pbp_txt, init) - -### sportsdataverse.nhl.nhl_pbp.helper_nhl_pbp(game_id, pbp_txt) - -### sportsdataverse.nhl.nhl_pbp.helper_nhl_pbp_features(game_id, pbp_txt, init) - -### sportsdataverse.nhl.nhl_pbp.helper_nhl_pickcenter(pbp_txt) - -### sportsdataverse.nhl.nhl_pbp.nhl_pbp_disk(game_id, path_to_json) ## sportsdataverse.nhl.nhl_schedule module - -### sportsdataverse.nhl.nhl_schedule.espn_nhl_calendar(season=None, ondays=None, return_as_pandas=True, \*\*kwargs) -espn_nhl_calendar - look up the NHL calendar for a given season - -Args: - - season (int): Used to define different seasons. 2002 is the earliest available season. - ondays (boolean): Used to return dates for calendar ondays - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing - calendar dates for the requested season. - -Raises: - - ValueError: If season is less than 2002. - - -### sportsdataverse.nhl.nhl_schedule.espn_nhl_schedule(dates=None, season_type=None, limit=500, return_as_pandas=True, \*\*kwargs) -espn_nhl_schedule - look up the NHL schedule for a given date - -Args: - - dates (int): Used to define different seasons. 2002 is the earliest available season. - season_type (int): season type, 1 for pre-season, 2 for regular season, 3 for post-season, 4 for all-star, 5 for off-season - limit (int): number of records to return, default: 500. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing - schedule events for the requested season. - - -### sportsdataverse.nhl.nhl_schedule.most_recent_nhl_season() - -### sportsdataverse.nhl.nhl_schedule.year_to_season(year) ## sportsdataverse.nhl.nhl_teams module - -### sportsdataverse.nhl.nhl_teams.espn_nhl_teams(return_as_pandas=True, \*\*kwargs) -espn_nhl_teams - look up NHL teams - -Args: - - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing teams for the requested league. - -Example: - - nhl_df = sportsdataverse.nhl.espn_nhl_teams() - ## Module contents diff --git a/docs/docs/wbb/index.md b/docs/docs/wbb/index.md index fdbb500..1433676 100755 --- a/docs/docs/wbb/index.md +++ b/docs/docs/wbb/index.md @@ -4,227 +4,12 @@ ## sportsdataverse.wbb.wbb_game_rosters module - -### sportsdataverse.wbb.wbb_game_rosters.espn_wbb_game_rosters(game_id: int, raw=False, return_as_pandas=True, \*\*kwargs) -espn_wbb_game_rosters() - Pull the game by id. - -Args: - - game_id (int): Unique game_id, can be obtained from wbb_schedule(). - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Data frame of game roster data with columns: - ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, - ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, - ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, - ‘age’, ‘date_of_birth’, ‘slug’, ‘jersey’, ‘linked’, ‘active’, - ‘alternate_ids_sdr’, ‘birth_place_city’, ‘birth_place_state’, - ‘birth_place_country’, ‘headshot_href’, ‘headshot_alt’, - ‘experience_years’, ‘experience_display_value’, - ‘experience_abbreviation’, ‘status_id’, ‘status_name’, ‘status_type’, - ‘status_abbreviation’, ‘hand_type’, ‘hand_abbreviation’, - ‘hand_display_value’, ‘draft_display_text’, ‘draft_round’, ‘draft_year’, - ‘draft_selection’, ‘player_id’, ‘starter’, ‘valid’, ‘did_not_play’, - ‘display_name’, ‘ejected’, ‘athlete_href’, ‘position_href’, - ‘statistics_href’, ‘team_id’, ‘team_guid’, ‘team_uid’, ‘team_slug’, - ‘team_location’, ‘team_name’, ‘team_nickname’, ‘team_abbreviation’, - ‘team_display_name’, ‘team_short_display_name’, ‘team_color’, - ‘team_alternate_color’, ‘is_active’, ‘is_all_star’, - ‘team_alternate_ids_sdr’, ‘logo_href’, ‘logo_dark_href’, ‘game_id’ - -Example: - - wbb_df = sportsdataverse.wbb.espn_wbb_game_rosters(game_id=401266534) - - -### sportsdataverse.wbb.wbb_game_rosters.helper_wbb_athlete_items(teams_rosters, \*\*kwargs) - -### sportsdataverse.wbb.wbb_game_rosters.helper_wbb_game_items(summary) - -### sportsdataverse.wbb.wbb_game_rosters.helper_wbb_roster_items(items, summary_url, \*\*kwargs) - -### sportsdataverse.wbb.wbb_game_rosters.helper_wbb_team_items(items, \*\*kwargs) ## sportsdataverse.wbb.wbb_loaders module - -### sportsdataverse.wbb.wbb_loaders.load_wbb_pbp(seasons: List[int], return_as_pandas=True) -Load women’s college basketball play by play data going back to 2002 - -Example: - - wbb_df = sportsdataverse.wbb.load_wbb_pbp(seasons=range(2002,2022)) - -Args: - - seasons (list): Used to define different seasons. 2002 is the earliest available season. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing the - play-by-plays available for the requested seasons. - -Raises: - - ValueError: If season is less than 2002. - - -### sportsdataverse.wbb.wbb_loaders.load_wbb_player_boxscore(seasons: List[int], return_as_pandas=True) -Load women’s college basketball player boxscore data - -Example: - - wbb_df = sportsdataverse.wbb.load_wbb_player_boxscore(seasons=range(2002,2022)) - -Args: - - seasons (list): Used to define different seasons. 2002 is the earliest available season. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing the - player boxscores available for the requested seasons. - -Raises: - - ValueError: If season is less than 2002. - - -### sportsdataverse.wbb.wbb_loaders.load_wbb_schedule(seasons: List[int], return_as_pandas=True) -Load women’s college basketball schedule data - -Example: - - wbb_df = sportsdataverse.wbb.load_wbb_schedule(seasons=range(2002,2022)) - -Args: - - seasons (list): Used to define different seasons. 2002 is the earliest available season. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing the - schedule for the requested seasons. - -Raises: - - ValueError: If season is less than 2002. - - -### sportsdataverse.wbb.wbb_loaders.load_wbb_team_boxscore(seasons: List[int], return_as_pandas=True) -Load women’s college basketball team boxscore data - -Example: - - wbb_df = sportsdataverse.wbb.load_wbb_team_boxscore(seasons=range(2002,2022)) - -Args: - - seasons (list): Used to define different seasons. 2002 is the earliest available season. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing the - team boxscores available for the requested seasons. - -Raises: - - ValueError: If season is less than 2002. - ## sportsdataverse.wbb.wbb_pbp module - -### sportsdataverse.wbb.wbb_pbp.espn_wbb_pbp(game_id: int, raw=False, \*\*kwargs) -espn_wbb_pbp() - Pull the game by id. Data from API endpoints - womens-college-basketball/playbyplay, -womens-college-basketball/summary - -Args: - - game_id (int): Unique game_id, can be obtained from wbb_schedule(). - -raw (bool): If True, returns the raw json from the API endpoint. If False, returns a cleaned dictionary of datasets. - -Returns: - - Dict: Dictionary of game data with keys - “gameId”, “plays”, “winprobability”, “boxscore”, “header”, - “broadcasts”, “videos”, “playByPlaySource”, “standings”, “leaders”, “timeouts”, “pickcenter”, - “againstTheSpread”, “odds”, “predictor”,”espnWP”, “gameInfo”, “season” - -Example: - - wbb_df = sportsdataverse.wb.espn_wbb_pbp(game_id=401266534) - - -### sportsdataverse.wbb.wbb_pbp.helper_wbb_game_data(pbp_txt, init) - -### sportsdataverse.wbb.wbb_pbp.helper_wbb_pbp(game_id, pbp_txt) - -### sportsdataverse.wbb.wbb_pbp.helper_wbb_pbp_features(game_id, pbp_txt, init) - -### sportsdataverse.wbb.wbb_pbp.helper_wbb_pickcenter(pbp_txt) - -### sportsdataverse.wbb.wbb_pbp.mbb_pbp_disk(game_id, path_to_json) ## sportsdataverse.wbb.wbb_schedule module - -### sportsdataverse.wbb.wbb_schedule.espn_wbb_calendar(season=None, ondays=None, return_as_pandas=True, \*\*kwargs) -espn_wbb_calendar - look up the women’s college basketball calendar for a given season - -Args: - - season (int): Used to define different seasons. 2002 is the earliest available season. - ondays (boolean): Used to return dates for calendar ondays - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing - calendar dates for the requested season. - -Raises: - - ValueError: If season is less than 2002. - - -### sportsdataverse.wbb.wbb_schedule.espn_wbb_schedule(dates=None, groups=50, season_type=None, limit=500, return_as_pandas=True, \*\*kwargs) -espn_wbb_schedule - look up the women’s college basketball schedule for a given season - -Args: - - dates (int): Used to define different seasons. 2002 is the earliest available season. - groups (int): Used to define different divisions. 50 is Division I, 51 is Division II/Division III. - season_type (int): 2 for regular season, 3 for post-season, 4 for off-season. - limit (int): number of records to return, default: 500. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. - - -### sportsdataverse.wbb.wbb_schedule.most_recent_wbb_season() ## sportsdataverse.wbb.wbb_teams module - -### sportsdataverse.wbb.wbb_teams.espn_wbb_teams(groups=None, return_as_pandas=True, \*\*kwargs) -espn_wbb_teams - look up the women’s college basketball teams - -Args: - - groups (int): Used to define different divisions. 50 is Division I, 51 is Division II/Division III. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing teams for the requested league. - -Example: - - wbb_df = sportsdataverse.wbb.espn_wbb_teams() - ## Module contents diff --git a/docs/docs/wnba/index.md b/docs/docs/wnba/index.md index 0998bf3..e8855ff 100755 --- a/docs/docs/wnba/index.md +++ b/docs/docs/wnba/index.md @@ -4,220 +4,12 @@ ## sportsdataverse.wnba.wnba_game_rosters module - -### sportsdataverse.wnba.wnba_game_rosters.espn_wnba_game_rosters(game_id: int, raw=False, return_as_pandas=True, \*\*kwargs) -espn_wnba_game_rosters() - Pull the game by id. - -Args: - - game_id (int): Unique game_id, can be obtained from espn_wnba_schedule(). - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Data frame of game roster data with columns: - ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, - ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, - ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, - ‘age’, ‘date_of_birth’, ‘slug’, ‘jersey’, ‘linked’, ‘active’, - ‘alternate_ids_sdr’, ‘birth_place_city’, ‘birth_place_state’, - ‘birth_place_country’, ‘headshot_href’, ‘headshot_alt’, - ‘experience_years’, ‘experience_display_value’, - ‘experience_abbreviation’, ‘status_id’, ‘status_name’, ‘status_type’, - ‘status_abbreviation’, ‘hand_type’, ‘hand_abbreviation’, - ‘hand_display_value’, ‘draft_display_text’, ‘draft_round’, ‘draft_year’, - ‘draft_selection’, ‘player_id’, ‘starter’, ‘valid’, ‘did_not_play’, - ‘display_name’, ‘ejected’, ‘athlete_href’, ‘position_href’, - ‘statistics_href’, ‘team_id’, ‘team_guid’, ‘team_uid’, ‘team_slug’, - ‘team_location’, ‘team_name’, ‘team_abbreviation’, - ‘team_display_name’, ‘team_short_display_name’, ‘team_color’, - ‘team_alternate_color’, ‘is_active’, ‘is_all_star’, - ‘logo_href’, ‘logo_dark_href’, ‘game_id’ - -Example: - - wnba_df = sportsdataverse.wnba.espn_wnba_game_rosters(game_id=401370395) - - -### sportsdataverse.wnba.wnba_game_rosters.helper_wnba_athlete_items(teams_rosters, \*\*kwargs) - -### sportsdataverse.wnba.wnba_game_rosters.helper_wnba_game_items(summary) - -### sportsdataverse.wnba.wnba_game_rosters.helper_wnba_roster_items(items, summary_url, \*\*kwargs) - -### sportsdataverse.wnba.wnba_game_rosters.helper_wnba_team_items(items, \*\*kwargs) ## sportsdataverse.wnba.wnba_loaders module - -### sportsdataverse.wnba.wnba_loaders.load_wnba_pbp(seasons: List[int], return_as_pandas=True) -Load WNBA play by play data going back to 2002 - -Example: - - wnba_df = sportsdataverse.wnba.load_wnba_pbp(seasons=range(2002,2022)) - -Args: - - seasons (list): Used to define different seasons. 2002 is the earliest available season. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing the - play-by-plays available for the requested seasons. - -Raises: - - ValueError: If season is less than 2002. - - -### sportsdataverse.wnba.wnba_loaders.load_wnba_player_boxscore(seasons: List[int], return_as_pandas=True) -Load WNBA player boxscore data - -Example: - - wnba_df = sportsdataverse.wnba.load_wnba_player_boxscore(seasons=range(2002,2022)) - -Args: - - seasons (list): Used to define different seasons. 2002 is the earliest available season. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing the - player boxscores available for the requested seasons. - -Raises: - - ValueError: If season is less than 2002. - - -### sportsdataverse.wnba.wnba_loaders.load_wnba_schedule(seasons: List[int], return_as_pandas=True) -Load WNBA schedule data - -Example: - - wnba_df = sportsdataverse.wnba.load_wnba_schedule(seasons=range(2002,2022)) - -Args: - - seasons (list): Used to define different seasons. 2002 is the earliest available season. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing the - schedule for the requested seasons. - -Raises: - - ValueError: If season is less than 2002. - - -### sportsdataverse.wnba.wnba_loaders.load_wnba_team_boxscore(seasons: List[int], return_as_pandas=True) -Load WNBA team boxscore data - -Example: - - wnba_df = sportsdataverse.wnba.load_wnba_team_boxscore(seasons=range(2002,2022)) - -Args: - - seasons (list): Used to define different seasons. 2002 is the earliest available season. - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing the - team boxscores available for the requested seasons. - -Raises: - - ValueError: If season is less than 2002. - ## sportsdataverse.wnba.wnba_pbp module - -### sportsdataverse.wnba.wnba_pbp.espn_wnba_pbp(game_id: int, raw=False, \*\*kwargs) -espn_wnba_pbp() - Pull the game by id. Data from API endpoints - wnba/playbyplay, wnba/summary - -Args: - - game_id (int): Unique game_id, can be obtained from wnba_schedule(). - -Returns: - - Dict: Dictionary of game data with keys - “gameId”, “plays”, “winprobability”, “boxscore”, “header”, - - “broadcasts”, “videos”, “playByPlaySource”, “standings”, “leaders”, “seasonseries”, “timeouts”, - “pickcenter”, “againstTheSpread”, “odds”, “predictor”, “espnWP”, “gameInfo”, “season” - -Example: - - wnba_df = sportsdataverse.wnba.espn_wnba_pbp(game_id=401370395) - - -### sportsdataverse.wnba.wnba_pbp.helper_wnba_game_data(pbp_txt, init) - -### sportsdataverse.wnba.wnba_pbp.helper_wnba_pbp(game_id, pbp_txt) - -### sportsdataverse.wnba.wnba_pbp.helper_wnba_pbp_features(game_id, pbp_txt, init) - -### sportsdataverse.wnba.wnba_pbp.helper_wnba_pickcenter(pbp_txt) - -### sportsdataverse.wnba.wnba_pbp.wnba_pbp_disk(game_id, path_to_json) ## sportsdataverse.wnba.wnba_schedule module - -### sportsdataverse.wnba.wnba_schedule.espn_wnba_calendar(season=None, ondays=None, return_as_pandas=True, \*\*kwargs) -espn_wnba_calendar - look up the WNBA calendar for a given season - -Args: - - season (int): Used to define different seasons. 2002 is the earliest available season. - ondays (boolean): Used to return dates for calendar ondays - -Returns: - - pd.DataFrame: Pandas dataframe containing calendar dates for the requested season. - -Raises: - - ValueError: If season is less than 2002. - - -### sportsdataverse.wnba.wnba_schedule.espn_wnba_schedule(dates=None, season_type=None, limit=500, return_as_pandas=True, \*\*kwargs) -espn_wnba_schedule - look up the WNBA schedule for a given season - -Args: - - dates (int): Used to define different seasons. 2002 is the earliest available season. - season_type (int): 2 for regular season, 3 for post-season, 4 for off-season. - limit (int): number of records to return, default: 500. - -Returns: - - pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. - - -### sportsdataverse.wnba.wnba_schedule.most_recent_wnba_season() ## sportsdataverse.wnba.wnba_teams module - -### sportsdataverse.wnba.wnba_teams.espn_wnba_teams(return_as_pandas=True, \*\*kwargs) -espn_wnba_teams - look up WNBA teams - -Args: - - return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. - -Returns: - - pd.DataFrame: Pandas dataframe containing teams for the requested league. - -Example: - - wnba_df = sportsdataverse.wnba.espn_wnba_teams() - ## Module contents From 718ecab38672bc84087279e041937dc56d9ed19a Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Mon, 31 Jul 2023 01:20:17 -0400 Subject: [PATCH 47/79] mlb --- .../_build/doctrees/environment.pickle | Bin 410014 -> 924610 bytes .../_build/doctrees/sportsdataverse.doctree | Bin 50321 -> 57651 bytes .../doctrees/sportsdataverse.mlb.doctree | Bin 93755 -> 167254 bytes .../doctrees/sportsdataverse.nba.doctree | Bin 4756 -> 85989 bytes .../doctrees/sportsdataverse.nfl.doctree | Bin 5520 -> 207199 bytes .../doctrees/sportsdataverse.nhl.doctree | Bin 5118 -> 102595 bytes .../doctrees/sportsdataverse.wbb.doctree | Bin 4756 -> 86255 bytes .../doctrees/sportsdataverse.wnba.doctree | Bin 4811 -> 84126 bytes .../_build/markdown/sportsdataverse.md | 16 + .../_build/markdown/sportsdataverse.mlb.md | 282 +++++++++ .../_build/markdown/sportsdataverse.nba.md | 216 +++++++ .../_build/markdown/sportsdataverse.nfl.md | 589 ++++++++++++++++++ .../_build/markdown/sportsdataverse.nhl.md | 263 ++++++++ .../_build/markdown/sportsdataverse.wbb.md | 215 +++++++ .../_build/markdown/sportsdataverse.wnba.md | 208 +++++++ docs/docs/mlb/index.md | 282 +++++++++ docs/docs/nba/index.md | 216 +++++++ docs/docs/nfl/index.md | 589 ++++++++++++++++++ docs/docs/nhl/index.md | 263 ++++++++ docs/docs/wbb/index.md | 215 +++++++ docs/docs/wnba/index.md | 208 +++++++ sportsdataverse/mlb/mlbam_games.py | 4 +- sportsdataverse/mlb/mlbam_players.py | 6 +- sportsdataverse/mlb/mlbam_reports.py | 8 +- sportsdataverse/mlb/mlbam_stats.py | 8 +- sportsdataverse/mlb/mlbam_teams.py | 6 +- sportsdataverse/mlb/retrosheet.py | 16 +- sportsdataverse/mlb/retrosplits.py | 16 +- 28 files changed, 3594 insertions(+), 32 deletions(-) diff --git a/Sphinx-docs/_build/doctrees/environment.pickle b/Sphinx-docs/_build/doctrees/environment.pickle index eb8e4602e58a07eeb23efb71aeaa51c01ccffa21..17cfdcfe1806be8130c35d8ad5d1ae8599abaae1 100755 GIT binary patch literal 924610 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 gt^C*pdB*dr|O@m+79XRxURI(Y7DImB1% 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 delta 45846 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 DhR6jJ diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.doctree index 338e95e0454730aa85ac2cbe335545ece29ce513..234a04ba781d90d2bf003d3998dc5bdae65285bd 100755 GIT binary patch literal 57651 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 literal 50321 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 Ca}6K> diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.mlb.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.mlb.doctree index 93bcf4948f6c431602fbf658b4c83e90382477b5..5985b2acec740056063c9b2bc6ab6a2676226690 100755 GIT binary patch literal 167254 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 diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.nba.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.nba.doctree index b5139b2baef0d969c1796e19947c46bca2cd6e9a..5dee85b104ed3b5d30b2956eaa94c6485e96e569 100755 GIT binary patch literal 85989 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 C#h6V1 delta 566 zcmY+B%_{_99LGJg*^(8>VQamtmko^-PHtCA@pU?06eV)(LY98`#Rv+#*R^`1%0=p-* z>|A*F$!N>+9<)o`5ZQFe&!X|BS{a=ZN65L$i(PCdKI{O+_`ZMy+md=)QI7!T(F_k- z(1+H^27RI8VmG;7BD3$D>~J%e+XAu`F5}|-|G24A$hEzLJzWRbNvXU&_9L>IpQHfO zHU*so++_;7O-8rfCXh}*S=5)5@Um(jywc2?{|3!|k(A{VQNOt-k`)2@O3s|i-s85L z`3UJIviTArb9O^>Y=jJX@o2rtgN+Y5;fgMNz>ORA!AA_Ga#~p6*hF9IFNy*EKw!WgIx&4D*6fp{kVvXW};)3Fi x;*IKPx2GTQS+20<8n;~YmMdeqxaG>>6-C{sORB1tQZ;36RY|6K;f#_|@&}3~)#v~K diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.nfl.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.nfl.doctree index b18a175f4944e44ee8f9fa81c076c8a4e4efadee..4b49d360649d90f05800fc4872a3ab761aef55a4 100755 GIT binary patch literal 207199 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 diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.nhl.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.nhl.doctree index 457223516c605414d389c7d8a8e511b8c711dcdf..5e3c96ec73b35b6e10ff6498c1648ff2e9f1cb41 100755 GIT binary patch literal 102595 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 delta 615 zcmY+?Ur1A77{~EFuXwOjR0yZV80}g6NPZJ@P}_RCW$q;@AW) z5{9AgsrTADNg8+&g9urQV07vGE`%%;C2T6Jq|>Ko?pN#yD=H z4(*7d<8iC~stObaML8Z6R^}!dVkI->3745whOAq6c=dv>4*F|xdOD!GP{sH^TS)w_ zG>K1@7Ywr6JHEcFrr(K&WHw5T|8~$_MLvtsplpxP6vK9PgQCyq_Zx+XyMa8Mrq%`n zJ-UNleZ4*`%TK96w$0FpH`>R^piIor9K(;QdEhhe)tGlZvzVi1HQCo3z4RuF~kD(TyPuR i?&r8dlTu?!eWcWJr7kGdQEE}C2RO&EU7CMKPv|cUjo=Lc diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.wbb.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.wbb.doctree index 2271fa6edc9b75fae4df9b70dd70c12d0e0dfc0b..a26d89c576d683ad72937ecb47e0f71d6236f3dc 100755 GIT binary patch literal 86255 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 delta 576 zcmY+>Pe>a<6vuIA)nK}ce-A0O)u=JiR%tzX329JjAav+iSj?&$eh1A4vy0RcTY4!% zkcOFd&Vqsx&;$fM+Cm_u2;PNw5Q1pHqgOX8n&kX=AHKYIyVcA+j&IGsHo6*ej^7gd zM`X4uT|86Km*hUU70$M1#)-<#W}^0RCp5~ILW>d>Sm)kIKicsE5%i)FFYyMi!X4&( zbvHFE{sc)6>&SM?Dh3q}kHS;KEEn!$AG4D-n9hEekgQ^BHJGao8+gQ;Sn)=-nyzmY zn>a4NX6xms1W{#p4bxx8cx^_+y`;za6vcv}=i+Azaf$;kc?kRAa0%H*;u3OIcE5)8 z)v-fQbwUr2Hlm7TfXr3;#8rSyIB;rR>VHLgXCJLlBfYJY+L!f3b4eRp&|`)cGZI`q z`Vu53lGI9${L|#H@heKW#1!5`!7Nr`AdO9Au!}tYqKI=d<&3Gzn(;ZVC3xkJC5kBu zg<_Q=O_8C-}TU6JLm~LouD_S(cg`bi; JvHgjJ?*Y?{)R+JO diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.wnba.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.wnba.doctree index 4c478342799ef63416d923e2247f16d795621eb4..0ab24dde6066b8eafc2a680ab0fbd8e15e790dbe 100755 GIT binary patch literal 84126 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 The information used here was obtained free of +> charge from and is copyrighted by Retrosheet. Interested +> parties may contact Retrosheet at “www.retrosheet.org”. + + +### sportsdataverse.mlb.retrosheet.retrosheet_ballparks() +Retrives the current TEAMABR.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. + +Args: + + None + +Returns: + + A pandas Dataframe with the biographical information of notable major league teams. + + +### sportsdataverse.mlb.retrosheet.retrosheet_ejections() +Retrives the current Ejecdata.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. + +Args: + + None + +Returns: + + A pandas Dataframe with the ejection data of known MLB ejections. + + +### sportsdataverse.mlb.retrosheet.retrosheet_franchises() +Retrives the current TEAMABR.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. + +Args: + + None + +Returns: + + A pandas Dataframe with the biographical information of notable major league teams. + + +### sportsdataverse.mlb.retrosheet.retrosheet_game_logs_team() +Retrives the team-level stats for MLB games in a season, or range of seasons. +THIS DOES NOT GET PLAYER STATS! +Use retrosplits_game_logs_player() for player-level game stats. + +Args: + + first_season (int): + + Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. + + last_season (int): + + Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. + + game_type (str): + + Optional parameter. By default, this is set to “regular”, or to put it in another way, this function call will return only regular season games. + + The full list of supported keywards for game_type are as follows. Case does not matter (you can set game_type to “rEgUlAr”, and the function call will still work): + + > + > * “regular”: Regular season games. + + + > * “asg”: All-Star games. + + + > * “playoffs”: Playoff games. + + filter_out_seasons (bool): + + If game_type is set to either “asg” or “playoffs”, and filter_out_seasons is set to true, this function will filter out seasons that do not match the inputted season and/or the range of seasons. By default, this is set to True. + +Returns: + + A pandas dataframe containing team-level stats for MLB games. + + +### sportsdataverse.mlb.retrosheet.retrosheet_people() +Retrives the current BioFile.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. + +Args: + + None + +Returns: + + A pandas Dataframe with the biographical information of various individuals who have played baseball. + + +### sportsdataverse.mlb.retrosheet.retrosheet_schedule() +Retrives the scheduled games of an MLB season, or MLB seasons. + +Args: + + first_season (int): + + Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. + + last_season (int): + + Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. + + original_2020_schedule (bool): + + Retrosheet keeps a record of the orignial 2020 MLB season, before the season was delayed due to the COVID-19 pandemic. + + + * If this is set to True, this function will return the original 2020 MLB season, before it was altered due to the COVID-19 pandemic, if the user wants this function to return the schedule for the 2020 MLB season. + + + * If this is set to False, this function will return the altered 2020 MLB season, after it was altered due to the COVID-19 pandemic, if the user wants this function to return the schedule for the 2020 MLB season. + +Returns: + + A pandas dataframe containg historical MLB schedules. + ## sportsdataverse.mlb.retrosplits module +RETROSHEET NOTICE: + +> The information used here was obtained free of +> charge from and is copyrighted by Retrosheet. Interested +> parties may contact Retrosheet at “www.retrosheet.org”. + + +### sportsdataverse.mlb.retrosplits.retrosplits_game_logs_player() +Retrives game-level player stats from the Retrosplits project. + +Args: + + first_season (int): + + Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. + + last_season (int): + + Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. + +Returns: + + A pandas dataframe containing game-level player stats from historical MLB games. + + +### sportsdataverse.mlb.retrosplits.retrosplits_game_logs_team() +Retrives game-level team stats from the Retrosplits project. + +Args: + + first_season (int): + + Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. + + last_season (int): + + Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. + +Returns: + + A pandas dataframe containing game-level team stats from historical MLB games. + + +### sportsdataverse.mlb.retrosplits.retrosplits_player_batting_by_platoon() +Retrives player-level, batting by platoon (left/right hitting vs. left/right pitching) split stats from the Retrosplits project. +The stats are batting stats, based off of the handedness of the batter vs the handedness of the pitcher. +The stats returned by this function are season-level stats, not game-level stats. + +Args: + + first_season (int): + + Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. + + last_season (int): + + Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. + +Returns: + + A pandas dataframe containing player-level, batting by platoon stats for batters. + + +### sportsdataverse.mlb.retrosplits.retrosplits_player_batting_by_position() +Retrives player-level, batting by position split stats from the Retrosplits project. +The stats returned by this function are season-level stats, not game-level stats. + +Args: + + first_season (int): + + Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. + + last_season (int): + + Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. + +Returns: + + A pandas dataframe containing batting by position split stats for MLB players. + + +### sportsdataverse.mlb.retrosplits.retrosplits_player_batting_by_runners() +Retrives player-level, batting by runners split stats from the Retrosplits project. +The stats are batting stats, based off of how many runners are on base at the time of the at bat. +The stats returned by this function are season-level stats, not game-level stats. + +Args: + + first_season (int): + + Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. + + last_season (int): + + Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. + +Returns: + + A pandas dataframe containing player-level, batting by runners split stats. + + +### sportsdataverse.mlb.retrosplits.retrosplits_player_head_to_head_stats() +Retrives batter vs. pitcher stats from the Retrosplits project. +The stats are batting stats, based off of the preformance of that specific batter agianst a specific pitcher for the durration of that specific season. +The stats returned by this function are season-level stats, not game-level stats. + +Args: + + first_season (int): + + Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. + + last_season (int): + + Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. + +Returns: + + A pandas dataframe containing batter vs. pitcher stats for a season, or for a range of seasons. + + +### sportsdataverse.mlb.retrosplits.retrosplits_player_pitching_by_platoon() +Retrives player-level, pitching by platoon (left/right pitching vs. left/right hitting) split stats from the Retrosplits project. +The stats are pitching stats, based off of the handedness of the pitcher vs the handedness of the batter. +The stats returned by this function are season-level stats, not game-level stats. + +Args: + + first_season (int): + + Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. + + last_season (int): + + Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. + +Returns: + + A pandas dataframe containing player-level, pitching by platoon stats for pitchers. + + +### sportsdataverse.mlb.retrosplits.retrosplits_player_pitching_by_runners() +Retrives player-level, pitching by runners split stats from the Retrosplits project. +The stats are pitching stats, based off of how many runners are on base at the time of the at bat. +The stats returned by this function are season-level stats, not game-level stats. + +Args: + + first_season (int): + + Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. + + last_season (int): + + Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. + +Returns: + + A pandas dataframe containing pitching by runners split stats for a season, or for a range of seasons. + ## Module contents diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.nba.md b/Sphinx-docs/_build/markdown/sportsdataverse.nba.md index d5f749d..facfb9f 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.nba.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.nba.md @@ -4,12 +4,228 @@ ## sportsdataverse.nba.nba_game_rosters module + +### sportsdataverse.nba.nba_game_rosters.espn_nba_game_rosters(game_id: int, raw=False, return_as_pandas=False, \*\*kwargs) +espn_nba_game_rosters() - Pull the game by id. + +Args: + + game_id (int): Unique game_id, can be obtained from espn_nba_schedule(). + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Data frame of game roster data with columns: + ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, + ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, + ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, + ‘age’, ‘date_of_birth’, ‘slug’, ‘jersey’, ‘linked’, ‘active’, + ‘alternate_ids_sdr’, ‘birth_place_city’, ‘birth_place_state’, + ‘birth_place_country’, ‘headshot_href’, ‘headshot_alt’, + ‘experience_years’, ‘experience_display_value’, + ‘experience_abbreviation’, ‘status_id’, ‘status_name’, ‘status_type’, + ‘status_abbreviation’, ‘hand_type’, ‘hand_abbreviation’, + ‘hand_display_value’, ‘draft_display_text’, ‘draft_round’, ‘draft_year’, + ‘draft_selection’, ‘player_id’, ‘starter’, ‘valid’, ‘did_not_play’, + ‘display_name’, ‘ejected’, ‘athlete_href’, ‘position_href’, + ‘statistics_href’, ‘team_id’, ‘team_guid’, ‘team_uid’, ‘team_slug’, + ‘team_location’, ‘team_name’, ‘team_abbreviation’, + ‘team_display_name’, ‘team_short_display_name’, ‘team_color’, + ‘team_alternate_color’, ‘is_active’, ‘is_all_star’, + ‘logo_href’, ‘logo_dark_href’, ‘game_id’ + +Example: + + nba_df = sportsdataverse.nba.espn_nba_game_rosters(game_id=401307514) + + +### sportsdataverse.nba.nba_game_rosters.helper_nba_athlete_items(teams_rosters, \*\*kwargs) + +### sportsdataverse.nba.nba_game_rosters.helper_nba_game_items(summary) + +### sportsdataverse.nba.nba_game_rosters.helper_nba_roster_items(items, summary_url, \*\*kwargs) + +### sportsdataverse.nba.nba_game_rosters.helper_nba_team_items(items, \*\*kwargs) ## sportsdataverse.nba.nba_loaders module + +### sportsdataverse.nba.nba_loaders.load_nba_pbp(seasons: List[int], return_as_pandas=False) +Load NBA play by play data going back to 2002 + +Example: + + nba_df = sportsdataverse.nba.load_nba_pbp(seasons=range(2002,2022)) + +Args: + + seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing the + play-by-plays available for the requested seasons. + +Raises: + + ValueError: If season is less than 2002. + + +### sportsdataverse.nba.nba_loaders.load_nba_player_boxscore(seasons: List[int], return_as_pandas=False) +Load NBA player boxscore data + +Example: + + nba_df = sportsdataverse.nba.load_nba_player_boxscore(seasons=range(2002,2022)) + +Args: + + seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing the + player boxscores available for the requested seasons. + +Raises: + + ValueError: If season is less than 2002. + + +### sportsdataverse.nba.nba_loaders.load_nba_schedule(seasons: List[int], return_as_pandas=False) +Load NBA schedule data + +Example: + + nba_df = sportsdataverse.nba.load_nba_schedule(seasons=range(2002,2022)) + +Args: + + seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing the + schedule for the requested seasons. + +Raises: + + ValueError: If season is less than 2002. + + +### sportsdataverse.nba.nba_loaders.load_nba_team_boxscore(seasons: List[int], return_as_pandas=False) +Load NBA team boxscore data + +Example: + + nba_df = sportsdataverse.nba.load_nba_team_boxscore(seasons=range(2002,2022)) + +Args: + + seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing the + team boxscores available for the requested seasons. + +Raises: + + ValueError: If season is less than 2002. + ## sportsdataverse.nba.nba_pbp module + +### sportsdataverse.nba.nba_pbp.espn_nba_pbp(game_id: int, raw=False, \*\*kwargs) +espn_nba_pbp() - Pull the game by id - Data from API endpoints - nba/playbyplay, nba/summary + +Args: + + game_id (int): Unique game_id, can be obtained from nba_schedule(). + raw (bool): If True, returns the raw json from the API endpoint. If False, returns a cleaned dictionary of datasets. + +Returns: + + Dict: Dictionary of game data with keys - “gameId”, “plays”, “winprobability”, “boxscore”, “header”, “broadcasts”, + + “videos”, “playByPlaySource”, “standings”, “leaders”, “seasonseries”, “timeouts”, “pickcenter”, “againstTheSpread”, + “odds”, “predictor”, “espnWP”, “gameInfo”, “season” + +Example: + + nba_df = sportsdataverse.nba.espn_nba_pbp(game_id=401307514) + + +### sportsdataverse.nba.nba_pbp.helper_nba_game_data(pbp_txt, init) + +### sportsdataverse.nba.nba_pbp.helper_nba_pbp(game_id, pbp_txt) + +### sportsdataverse.nba.nba_pbp.helper_nba_pbp_features(game_id, pbp_txt, init) + +### sportsdataverse.nba.nba_pbp.helper_nba_pickcenter(pbp_txt) + +### sportsdataverse.nba.nba_pbp.nba_pbp_disk(game_id, path_to_json) ## sportsdataverse.nba.nba_schedule module + +### sportsdataverse.nba.nba_schedule.espn_nba_calendar(season=None, ondays=None, return_as_pandas=False, \*\*kwargs) +espn_nba_calendar - look up the NBA calendar for a given season from ESPN + +Args: + + season (int): Used to define different seasons. 2002 is the earliest available season. + ondays (boolean): Used to return dates for calendar ondays + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing + calendar dates for the requested season. + +Raises: + + ValueError: If season is less than 2002. + + +### sportsdataverse.nba.nba_schedule.espn_nba_schedule(dates=None, season_type=None, limit=500, return_as_pandas=False, \*\*kwargs) +espn_nba_schedule - look up the NBA schedule for a given date from ESPN + +Args: + + dates (int): Used to define different seasons. 2002 is the earliest available season. + season_type (int): season type, 1 for pre-season, 2 for regular season, 3 for post-season, + 4 for all-star, 5 for off-season + limit (int): number of records to return, default: 500. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing + schedule events for the requested season. + + +### sportsdataverse.nba.nba_schedule.most_recent_nba_season() + +### sportsdataverse.nba.nba_schedule.year_to_season(year) ## sportsdataverse.nba.nba_teams module + +### sportsdataverse.nba.nba_teams.espn_nba_teams(return_as_pandas=False, \*\*kwargs) +espn_nba_teams - look up NBA teams + +Args: + + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing teams for the requested league. + +Example: + + nba_df = sportsdataverse.nba.espn_nba_teams() + ## Module contents diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.nfl.md b/Sphinx-docs/_build/markdown/sportsdataverse.nfl.md index 2812ba2..d56a0ae 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.nfl.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.nfl.md @@ -6,14 +6,603 @@ ## sportsdataverse.nfl.nfl_game_rosters module + +### sportsdataverse.nfl.nfl_game_rosters.espn_nfl_game_rosters(game_id: int, raw=False, return_as_pandas=False, \*\*kwargs) +espn_nfl_game_rosters() - Pull the game by id. + +Args: + + game_id (int): Unique game_id, can be obtained from espn_nfl_schedule(). + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Data frame of game roster data with columns: + ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, + ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, + ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, + ‘age’, ‘date_of_birth’, ‘slug’, ‘jersey’, ‘linked’, ‘active’, + ‘alternate_ids_sdr’, ‘birth_place_city’, ‘birth_place_state’, + ‘birth_place_country’, ‘headshot_href’, ‘headshot_alt’, + ‘experience_years’, ‘experience_display_value’, + ‘experience_abbreviation’, ‘status_id’, ‘status_name’, ‘status_type’, + ‘status_abbreviation’, ‘hand_type’, ‘hand_abbreviation’, + ‘hand_display_value’, ‘draft_display_text’, ‘draft_round’, ‘draft_year’, + ‘draft_selection’, ‘player_id’, ‘starter’, ‘valid’, ‘did_not_play’, + ‘display_name’, ‘ejected’, ‘athlete_href’, ‘position_href’, + ‘statistics_href’, ‘team_id’, ‘team_guid’, ‘team_uid’, ‘team_slug’, + ‘team_location’, ‘team_name’, ‘team_nickname’, ‘team_abbreviation’, + ‘team_display_name’, ‘team_short_display_name’, ‘team_color’, + ‘team_alternate_color’, ‘is_active’, ‘is_all_star’, + ‘team_alternate_ids_sdr’, ‘logo_href’, ‘logo_dark_href’, ‘game_id’ + +Example: + + nfl_df = sportsdataverse.nfl.espn_nfl_game_rosters(game_id=401220403) + + +### sportsdataverse.nfl.nfl_game_rosters.helper_nfl_athlete_items(teams_rosters, \*\*kwargs) + +### sportsdataverse.nfl.nfl_game_rosters.helper_nfl_game_items(summary) + +### sportsdataverse.nfl.nfl_game_rosters.helper_nfl_roster_items(items, summary_url, \*\*kwargs) + +### sportsdataverse.nfl.nfl_game_rosters.helper_nfl_team_items(items, \*\*kwargs) ## sportsdataverse.nfl.nfl_games module + +### sportsdataverse.nfl.nfl_games.nfl_game_details() +Args: + + game_id (int): Game ID + +Returns: + + Dict: Dictionary of odds and props data with keys + +Example: + + nfl_df = nfl_game_details( + game_id = ‘7ae87c4c-d24c-11ec-b23d-d15a91047884’ + ) + + +### sportsdataverse.nfl.nfl_games.nfl_game_schedule() +Args: + + season (int): season + season_type (str): season type - REG, POST + week (int): week + +Returns: + + Dict: Dictionary of odds and props data with keys + +Example: + + + + ``` + ` + ``` + + nfl_df = nfl_game_schedule( + + season = 2021, seasonType=’REG’, week=1 + + )\` + + +### sportsdataverse.nfl.nfl_games.nfl_headers_gen() + +### sportsdataverse.nfl.nfl_games.nfl_token_gen() ## sportsdataverse.nfl.nfl_loaders module + +### sportsdataverse.nfl.nfl_loaders.load_nfl_combine(return_as_pandas=False) +Load NFL Combine information + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_combine() + +Args: + +Returns: + + pd.DataFrame: Pandas dataframe containing NFL combine data available. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_contracts(return_as_pandas=False) +Load NFL Historical contracts information + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_contracts() + +Args: + +Returns: + + pd.DataFrame: Pandas dataframe containing historical contracts available. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_depth_charts(seasons: List[int], return_as_pandas=False) +Load NFL Depth Chart data for selected seasons + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_depth_charts(seasons=range(2001,2021)) + +Args: + + seasons (list): Used to define different seasons. 2001 is the earliest available season. + +Returns: + + pd.DataFrame: Pandas dataframe containing depth chart data available for the requested seasons. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_draft_picks(return_as_pandas=False) +Load NFL Draft picks information + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_draft_picks() + +Args: + +Returns: + + pd.DataFrame: Pandas dataframe containing NFL Draft picks data available. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_injuries(seasons: List[int], return_as_pandas=False) +Load NFL injuries data for selected seasons + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_injuries(seasons=range(2009,2021)) + +Args: + + seasons (list): Used to define different seasons. 2009 is the earliest available season. + +Returns: + + pd.DataFrame: Pandas dataframe containing injuries data available for the requested seasons. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_ngs_passing(return_as_pandas=False) +Load NFL NextGen Stats Passing data going back to 2016 + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_ngs_passing() + +Returns: + + pd.DataFrame: Pandas dataframe containing the NextGen Stats Passing data available. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_ngs_receiving(return_as_pandas=False) +Load NFL NextGen Stats Receiving data going back to 2016 + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_ngs_receiving() + +Returns: + + pd.DataFrame: Pandas dataframe containing the NextGen Stats Receiving data available. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_ngs_rushing(return_as_pandas=False) +Load NFL NextGen Stats Rushing data going back to 2016 + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_ngs_rushing() + +Returns: + + pd.DataFrame: Pandas dataframe containing the NextGen Stats Rushing data available. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_officials(return_as_pandas=False) +Load NFL Officials information + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_officials() + +Args: + +Returns: + + pd.DataFrame: Pandas dataframe containing officials available. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_pbp(seasons: List[int], return_as_pandas=False) +Load NFL play by play data going back to 1999 + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_pbp(seasons=range(1999,2021)) + +Args: + + seasons (list): Used to define different seasons. 1999 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing the play-by-plays available for the requested seasons. + +Raises: + + ValueError: If season is less than 1999. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_pbp_participation(seasons: List[int], return_as_pandas=False) +Load NFL play-by-play participation data for selected seasons + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_pbp_participation(seasons=range(2016,2021)) + +Args: + + seasons (list): Used to define different seasons. 2016 is the earliest available season. + +Returns: + + pd.DataFrame: Pandas dataframe containing play-by-play participation data available for the requested seasons. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_def(return_as_pandas=False) +Load NFL Pro-Football Reference Advanced Defensive data going back to 2018 + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_pfr_def() + +Returns: + + pd.DataFrame: Pandas dataframe containing Pro-Football Reference + + advanced defensive stats data available. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_pass(return_as_pandas=False) +Load NFL Pro-Football Reference Advanced Passing data going back to 2018 + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_pfr_pass() + +Returns: + + pd.DataFrame: Pandas dataframe containing Pro-Football Reference + + advanced passing stats data available. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_rec(return_as_pandas=False) +Load NFL Pro-Football Reference Advanced Receiving data going back to 2018 + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_pfr_rec() + +Returns: + + pd.DataFrame: Pandas dataframe containing Pro-Football Reference + + advanced receiving stats data available. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_rush(return_as_pandas=False) +Load NFL Pro-Football Reference Advanced Rushing data going back to 2018 + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_pfr_rush() + +Returns: + + pd.DataFrame: Pandas dataframe containing Pro-Football Reference + + advanced rushing stats data available. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_weekly_def(seasons: List[int], return_as_pandas=False) +Load NFL Pro-Football Reference Weekly Advanced Defensive data going back to 2018 + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_pfr_weekly_def(seasons=range(2018,2021)) + +Args: + + seasons (list): Used to define different seasons. 2018 is the earliest available season. + +Returns: + + pd.DataFrame: Pandas dataframe containing Pro-Football Reference + + advanced defensive stats data available for the requested seasons. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_weekly_pass(seasons: List[int], return_as_pandas=False) +Load NFL Pro-Football Reference Weekly Advanced Passing data going back to 2018 + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_pfr_weekly_pass(seasons=range(2018,2021)) + +Args: + + seasons (list): Used to define different seasons. 2018 is the earliest available season. + +Returns: + + pd.DataFrame: Pandas dataframe containing Pro-Football Reference + + advanced passing stats data available for the requested seasons. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_weekly_rec(seasons: List[int], return_as_pandas=False) +Load NFL Pro-Football Reference Weekly Advanced Receiving data going back to 2018 + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_pfr_weekly_rec(seasons=range(2018,2021)) + +Args: + + seasons (list): Used to define different seasons. 2018 is the earliest available season. + +Returns: + + pd.DataFrame: Pandas dataframe containing Pro-Football Reference + + advanced receiving stats data available for the requested seasons. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_weekly_rush(seasons: List[int], return_as_pandas=False) +Load NFL Pro-Football Reference Weekly Advanced Rushing data going back to 2018 + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_pfr_weekly_rush(seasons=range(2018,2021)) + +Args: + + seasons (list): Used to define different seasons. 2018 is the earliest available season. + +Returns: + + pd.DataFrame: Pandas dataframe containing Pro-Football Reference + + advanced rushing stats data available for the requested seasons. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_player_stats(kicking=False, return_as_pandas=False) +Load NFL player stats data + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_player_stats() + +Args: + + kicking (bool): If True, load kicking stats. If False, load all other stats. + +Returns: + + pd.DataFrame: Pandas dataframe containing player stats. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_players(return_as_pandas=False) +Load NFL Player ID information + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_players() + +Args: + +Returns: + + pd.DataFrame: Pandas dataframe containing players available. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_rosters(seasons: List[int], return_as_pandas=False) +Load NFL roster data for all seasons + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_rosters(seasons=range(1999,2021)) + +Args: + + seasons (list): Used to define different seasons. 1920 is the earliest available season. + +Returns: + + pd.DataFrame: Pandas dataframe containing rosters available for the requested seasons. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_schedule(seasons: List[int], return_as_pandas=False) +Load NFL schedule data + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_schedule(seasons=range(1999,2021)) + +Args: + + seasons (list): Used to define different seasons. 1999 is the earliest available season. + +Returns: + + pd.DataFrame: Pandas dataframe containing the schedule for the requested seasons. + +Raises: + + ValueError: If season is less than 1999. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_snap_counts(seasons: List[int], return_as_pandas=False) +Load NFL snap counts data for selected seasons + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_snap_counts(seasons=range(2012,2021)) + +Args: + + seasons (list): Used to define different seasons. 2012 is the earliest available season. + +Returns: + + pd.DataFrame: Pandas dataframe containing snap counts available for the requested seasons. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_teams(return_as_pandas=False) +Load NFL team ID information and logos + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_teams() + +Args: + +Returns: + + pd.DataFrame: Pandas dataframe containing teams available. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_weekly_rosters(seasons: List[int], return_as_pandas=False) +Load NFL weekly roster data for selected seasons + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_weekly_rosters(seasons=range(2002,2021)) + +Args: + + seasons (list): Used to define different seasons. 2002 is the earliest available season. + +Returns: + + pd.DataFrame: Pandas dataframe containing weekly rosters available for the requested seasons. + ## sportsdataverse.nfl.nfl_pbp module + +### class sportsdataverse.nfl.nfl_pbp.NFLPlayProcess(gameId=0, raw=False, path_to_json='/') +Bases: `object` + + +#### \__init__(gameId=0, raw=False, path_to_json='/') +Initialize self. See help(type(self)) for accurate signature. + + +#### create_box_score() + +#### espn_nfl_pbp() +espn_nfl_pbp() - Pull the game by id. Data from API endpoints: nfl/playbyplay, nfl/summary + +Args: + + game_id (int): Unique game_id, can be obtained from nfl_schedule(). + +Returns: + + Dict: Dictionary of game data with keys - “gameId”, “plays”, “boxscore”, “header”, “broadcasts”, + + “videos”, “playByPlaySource”, “standings”, “leaders”, “timeouts”, “homeTeamSpread”, “overUnder”, + “pickcenter”, “againstTheSpread”, “odds”, “predictor”, “winprobability”, “espnWP”, + “gameInfo”, “season” + +Example: + + nfl_df = sportsdataverse.nfl.NFLPlayProcess(gameId=401220403).espn_nfl_pbp() + + +#### gameId( = 0) + +#### nfl_pbp_disk() + +#### path_to_json( = '/') + +#### ran_cleaning_pipeline( = False) + +#### ran_pipeline( = False) + +#### raw( = False) + +#### run_cleaning_pipeline() + +#### run_processing_pipeline() ## sportsdataverse.nfl.nfl_schedule module + +### sportsdataverse.nfl.nfl_schedule.espn_nfl_calendar(season=None, ondays=None, return_as_pandas=False, \*\*kwargs) +espn_nfl_calendar - look up the NFL calendar for a given season + +Args: + + season (int): Used to define different seasons. 2002 is the earliest available season. + ondays (boolean): Used to return dates for calendar ondays + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing calendar dates for the requested season. + +Raises: + + ValueError: If season is less than 2002. + + +### sportsdataverse.nfl.nfl_schedule.espn_nfl_schedule(dates=None, week=None, season_type=None, groups=None, limit=500, return_as_pandas=False, \*\*kwargs) +espn_nfl_schedule - look up the NFL schedule for a given season + +Args: + + dates (int): Used to define different seasons. 2002 is the earliest available season. + week (int): Week of the schedule. + season_type (int): 2 for regular season, 3 for post-season, 4 for off-season. + limit (int): number of records to return, default: 500. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. + + +### sportsdataverse.nfl.nfl_schedule.get_current_week() + +### sportsdataverse.nfl.nfl_schedule.most_recent_nfl_season() ## sportsdataverse.nfl.nfl_teams module + +### sportsdataverse.nfl.nfl_teams.espn_nfl_teams(return_as_pandas=False, \*\*kwargs) +espn_nfl_teams - look up NFL teams + +Args: + + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing teams for the requested league. + +Example: + + nfl_df = sportsdataverse.nfl.espn_nfl_teams() + ## Module contents diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.nhl.md b/Sphinx-docs/_build/markdown/sportsdataverse.nhl.md index d9aeb28..add82ee 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.nhl.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.nhl.md @@ -4,14 +4,277 @@ ## sportsdataverse.nhl.nhl_api module + +### sportsdataverse.nhl.nhl_api.nhl_api_pbp(game_id: int, \*\*kwargs) +nhl_api_pbp() - Pull the game by id. Data from API endpoints - nhl/playbyplay, nhl/summary + +Args: + + game_id (int): Unique game_id, can be obtained from nhl_schedule(). + +Returns: + + Dict: Dictionary of game data with keys - “gameId”, “plays”, “boxscore”, “header”, “broadcasts”, + + “videos”, “playByPlaySource”, “standings”, “leaders”, “seasonseries”, “pickcenter”, “againstTheSpread”, + “odds”, “onIce”, “gameInfo”, “season” + +Example: + + nhl_df = sportsdataverse.nhl.nhl_api_pbp(game_id=2021020079) + + +### sportsdataverse.nhl.nhl_api.nhl_api_schedule(start_date: str, end_date: str, return_as_pandas=False) +nhl_api_schedule() - Pull the game by id. Data from API endpoints - nhl/schedule + +Args: + + game_id (int): Unique game_id, can be obtained from nhl_schedule(). + +Returns: + + pd.DataFrame: Pandas dataframe containing the schedule for the requested seasons. + +Example: + + nhl_sched_df = sportsdataverse.nhl.nhl_api_schedule(start_date=2021-10-23, end_date=2021-10-28) + ## sportsdataverse.nhl.nhl_game_rosters module + +### sportsdataverse.nhl.nhl_game_rosters.espn_nhl_game_rosters(game_id: int, raw=False, return_as_pandas=False, \*\*kwargs) +espn_nhl_game_rosters() - Pull the game by id. + +Args: + + game_id (int): Unique game_id, can be obtained from espn_nhl_schedule(). + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Data frame of game roster data with columns: + ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, + ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, + ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, + ‘age’, ‘date_of_birth’, ‘slug’, ‘jersey’, ‘linked’, ‘active’, + ‘alternate_ids_sdr’, ‘birth_place_city’, ‘birth_place_state’, + ‘birth_place_country’, ‘headshot_href’, ‘headshot_alt’, + ‘experience_years’, ‘experience_display_value’, + ‘experience_abbreviation’, ‘status_id’, ‘status_name’, ‘status_type’, + ‘status_abbreviation’, ‘hand_type’, ‘hand_abbreviation’, + ‘hand_display_value’, ‘draft_display_text’, ‘draft_round’, ‘draft_year’, + ‘draft_selection’, ‘player_id’, ‘starter’, ‘valid’, ‘did_not_play’, + ‘display_name’, ‘ejected’, ‘athlete_href’, ‘position_href’, + ‘statistics_href’, ‘team_id’, ‘team_guid’, ‘team_uid’, ‘team_slug’, + ‘team_location’, ‘team_name’, ‘team_abbreviation’, + ‘team_display_name’, ‘team_short_display_name’, ‘team_color’, + ‘team_alternate_color’, ‘is_active’, ‘is_all_star’, + ‘logo_href’, ‘logo_dark_href’, ‘game_id’ + +Example: + + nhl_df = sportsdataverse.nhl.espn_nhl_game_rosters(game_id=401247153) + + +### sportsdataverse.nhl.nhl_game_rosters.helper_nhl_athlete_items(teams_rosters, \*\*kwargs) + +### sportsdataverse.nhl.nhl_game_rosters.helper_nhl_game_items(summary) + +### sportsdataverse.nhl.nhl_game_rosters.helper_nhl_roster_items(items, summary_url, \*\*kwargs) + +### sportsdataverse.nhl.nhl_game_rosters.helper_nhl_team_items(items, \*\*kwargs) ## sportsdataverse.nhl.nhl_loaders module + +### sportsdataverse.nhl.nhl_loaders.load_nhl_pbp(seasons: List[int], return_as_pandas=False) +Load NHL play by play data going back to 2011 + +Example: + + nhl_df = sportsdataverse.nhl.load_nhl_pbp(seasons=range(2011,2021)) + +Args: + + seasons (list): Used to define different seasons. 2011 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing the play-by-plays available for the requested seasons. + +Raises: + + ValueError: If season is less than 2011. + + +### sportsdataverse.nhl.nhl_loaders.load_nhl_player_boxscore(seasons: List[int], return_as_pandas=False) +Load NHL player boxscore data + +Example: + + nhl_df = sportsdataverse.nhl.load_nhl_player_boxscore(seasons=range(2011,2022)) + +Args: + + seasons (list): Used to define different seasons. 2011 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing the + player boxscores available for the requested seasons. + +Raises: + + ValueError: If season is less than 2011. + + +### sportsdataverse.nhl.nhl_loaders.load_nhl_schedule(seasons: List[int], return_as_pandas=False) +Load NHL schedule data + +Example: + + nhl_df = sportsdataverse.nhl.load_nhl_schedule(seasons=range(2002,2021)) + +Args: + + seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing the schedule for the requested seasons. + +Raises: + + ValueError: If season is less than 2002. + + +### sportsdataverse.nhl.nhl_loaders.load_nhl_team_boxscore(seasons: List[int], return_as_pandas=False) +Load NHL team boxscore data + +Example: + + nhl_df = sportsdataverse.nhl.load_nhl_team_boxscore(seasons=range(2011,2022)) + +Args: + + seasons (list): Used to define different seasons. 2011 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing the + team boxscores available for the requested seasons. + +Raises: + + ValueError: If season is less than 2011. + + +### sportsdataverse.nhl.nhl_loaders.nhl_teams(return_as_pandas=False) +Load NHL team ID information and logos + +Example: + + nhl_df = sportsdataverse.nhl.nhl_teams() + +Args: + + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing teams available for the requested seasons. + ## sportsdataverse.nhl.nhl_pbp module + +### sportsdataverse.nhl.nhl_pbp.espn_nhl_pbp(game_id: int, raw=False, \*\*kwargs) +espn_nhl_pbp() - Pull the game by id. Data from API endpoints - nhl/playbyplay, nhl/summary + +Args: + + game_id (int): Unique game_id, can be obtained from nhl_schedule(). + +Returns: + + Dict: Dictionary of game data with keys - “gameId”, “plays”, “boxscore”, “header”, “broadcasts”, + + “videos”, “playByPlaySource”, “standings”, “leaders”, “seasonseries”, “pickcenter”, “againstTheSpread”, + “odds”, “onIce”, “gameInfo”, “season” + +Example: + + nhl_df = sportsdataverse.nhl.espn_nhl_pbp(game_id=401247153) + + +### sportsdataverse.nhl.nhl_pbp.helper_nhl_game_data(pbp_txt, init) + +### sportsdataverse.nhl.nhl_pbp.helper_nhl_pbp(game_id, pbp_txt) + +### sportsdataverse.nhl.nhl_pbp.helper_nhl_pbp_features(game_id, pbp_txt, init) + +### sportsdataverse.nhl.nhl_pbp.helper_nhl_pickcenter(pbp_txt) + +### sportsdataverse.nhl.nhl_pbp.nhl_pbp_disk(game_id, path_to_json) ## sportsdataverse.nhl.nhl_schedule module + +### sportsdataverse.nhl.nhl_schedule.espn_nhl_calendar(season=None, ondays=None, return_as_pandas=False, \*\*kwargs) +espn_nhl_calendar - look up the NHL calendar for a given season + +Args: + + season (int): Used to define different seasons. 2002 is the earliest available season. + ondays (boolean): Used to return dates for calendar ondays + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing + calendar dates for the requested season. + +Raises: + + ValueError: If season is less than 2002. + + +### sportsdataverse.nhl.nhl_schedule.espn_nhl_schedule(dates=None, season_type=None, limit=500, return_as_pandas=False, \*\*kwargs) +espn_nhl_schedule - look up the NHL schedule for a given date + +Args: + + dates (int): Used to define different seasons. 2002 is the earliest available season. + season_type (int): season type, 1 for pre-season, 2 for regular season, 3 for post-season, 4 for all-star, 5 for off-season + limit (int): number of records to return, default: 500. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing + schedule events for the requested season. + + +### sportsdataverse.nhl.nhl_schedule.most_recent_nhl_season() + +### sportsdataverse.nhl.nhl_schedule.year_to_season(year) ## sportsdataverse.nhl.nhl_teams module + +### sportsdataverse.nhl.nhl_teams.espn_nhl_teams(return_as_pandas=False, \*\*kwargs) +espn_nhl_teams - look up NHL teams + +Args: + + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing teams for the requested league. + +Example: + + nhl_df = sportsdataverse.nhl.espn_nhl_teams() + ## Module contents diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.wbb.md b/Sphinx-docs/_build/markdown/sportsdataverse.wbb.md index 1433676..c8a7173 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.wbb.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.wbb.md @@ -4,12 +4,227 @@ ## sportsdataverse.wbb.wbb_game_rosters module + +### sportsdataverse.wbb.wbb_game_rosters.espn_wbb_game_rosters(game_id: int, raw=False, return_as_pandas=False, \*\*kwargs) +espn_wbb_game_rosters() - Pull the game by id. + +Args: + + game_id (int): Unique game_id, can be obtained from wbb_schedule(). + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Data frame of game roster data with columns: + ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, + ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, + ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, + ‘age’, ‘date_of_birth’, ‘slug’, ‘jersey’, ‘linked’, ‘active’, + ‘alternate_ids_sdr’, ‘birth_place_city’, ‘birth_place_state’, + ‘birth_place_country’, ‘headshot_href’, ‘headshot_alt’, + ‘experience_years’, ‘experience_display_value’, + ‘experience_abbreviation’, ‘status_id’, ‘status_name’, ‘status_type’, + ‘status_abbreviation’, ‘hand_type’, ‘hand_abbreviation’, + ‘hand_display_value’, ‘draft_display_text’, ‘draft_round’, ‘draft_year’, + ‘draft_selection’, ‘player_id’, ‘starter’, ‘valid’, ‘did_not_play’, + ‘display_name’, ‘ejected’, ‘athlete_href’, ‘position_href’, + ‘statistics_href’, ‘team_id’, ‘team_guid’, ‘team_uid’, ‘team_slug’, + ‘team_location’, ‘team_name’, ‘team_nickname’, ‘team_abbreviation’, + ‘team_display_name’, ‘team_short_display_name’, ‘team_color’, + ‘team_alternate_color’, ‘is_active’, ‘is_all_star’, + ‘team_alternate_ids_sdr’, ‘logo_href’, ‘logo_dark_href’, ‘game_id’ + +Example: + + wbb_df = sportsdataverse.wbb.espn_wbb_game_rosters(game_id=401266534) + + +### sportsdataverse.wbb.wbb_game_rosters.helper_wbb_athlete_items(teams_rosters, \*\*kwargs) + +### sportsdataverse.wbb.wbb_game_rosters.helper_wbb_game_items(summary) + +### sportsdataverse.wbb.wbb_game_rosters.helper_wbb_roster_items(items, summary_url, \*\*kwargs) + +### sportsdataverse.wbb.wbb_game_rosters.helper_wbb_team_items(items, \*\*kwargs) ## sportsdataverse.wbb.wbb_loaders module + +### sportsdataverse.wbb.wbb_loaders.load_wbb_pbp(seasons: List[int], return_as_pandas=False) +Load women’s college basketball play by play data going back to 2002 + +Example: + + wbb_df = sportsdataverse.wbb.load_wbb_pbp(seasons=range(2002,2022)) + +Args: + + seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing the + play-by-plays available for the requested seasons. + +Raises: + + ValueError: If season is less than 2002. + + +### sportsdataverse.wbb.wbb_loaders.load_wbb_player_boxscore(seasons: List[int], return_as_pandas=False) +Load women’s college basketball player boxscore data + +Example: + + wbb_df = sportsdataverse.wbb.load_wbb_player_boxscore(seasons=range(2002,2022)) + +Args: + + seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing the + player boxscores available for the requested seasons. + +Raises: + + ValueError: If season is less than 2002. + + +### sportsdataverse.wbb.wbb_loaders.load_wbb_schedule(seasons: List[int], return_as_pandas=False) +Load women’s college basketball schedule data + +Example: + + wbb_df = sportsdataverse.wbb.load_wbb_schedule(seasons=range(2002,2022)) + +Args: + + seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing the + schedule for the requested seasons. + +Raises: + + ValueError: If season is less than 2002. + + +### sportsdataverse.wbb.wbb_loaders.load_wbb_team_boxscore(seasons: List[int], return_as_pandas=False) +Load women’s college basketball team boxscore data + +Example: + + wbb_df = sportsdataverse.wbb.load_wbb_team_boxscore(seasons=range(2002,2022)) + +Args: + + seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing the + team boxscores available for the requested seasons. + +Raises: + + ValueError: If season is less than 2002. + ## sportsdataverse.wbb.wbb_pbp module + +### sportsdataverse.wbb.wbb_pbp.espn_wbb_pbp(game_id: int, raw=False, \*\*kwargs) +espn_wbb_pbp() - Pull the game by id. Data from API endpoints - womens-college-basketball/playbyplay, +womens-college-basketball/summary + +Args: + + game_id (int): Unique game_id, can be obtained from wbb_schedule(). + +raw (bool): If True, returns the raw json from the API endpoint. If False, returns a cleaned dictionary of datasets. + +Returns: + + Dict: Dictionary of game data with keys - “gameId”, “plays”, “winprobability”, “boxscore”, “header”, + “broadcasts”, “videos”, “playByPlaySource”, “standings”, “leaders”, “timeouts”, “pickcenter”, + “againstTheSpread”, “odds”, “predictor”,”espnWP”, “gameInfo”, “season” + +Example: + + wbb_df = sportsdataverse.wb.espn_wbb_pbp(game_id=401266534) + + +### sportsdataverse.wbb.wbb_pbp.helper_wbb_game_data(pbp_txt, init) + +### sportsdataverse.wbb.wbb_pbp.helper_wbb_pbp(game_id, pbp_txt) + +### sportsdataverse.wbb.wbb_pbp.helper_wbb_pbp_features(game_id, pbp_txt, init) + +### sportsdataverse.wbb.wbb_pbp.helper_wbb_pickcenter(pbp_txt) + +### sportsdataverse.wbb.wbb_pbp.mbb_pbp_disk(game_id, path_to_json) ## sportsdataverse.wbb.wbb_schedule module + +### sportsdataverse.wbb.wbb_schedule.espn_wbb_calendar(season=None, ondays=None, return_as_pandas=False, \*\*kwargs) +espn_wbb_calendar - look up the women’s college basketball calendar for a given season + +Args: + + season (int): Used to define different seasons. 2002 is the earliest available season. + ondays (boolean): Used to return dates for calendar ondays + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing + calendar dates for the requested season. + +Raises: + + ValueError: If season is less than 2002. + + +### sportsdataverse.wbb.wbb_schedule.espn_wbb_schedule(dates=None, groups=50, season_type=None, limit=500, return_as_pandas=False, \*\*kwargs) +espn_wbb_schedule - look up the women’s college basketball schedule for a given season + +Args: + + dates (int): Used to define different seasons. 2002 is the earliest available season. + groups (int): Used to define different divisions. 50 is Division I, 51 is Division II/Division III. + season_type (int): 2 for regular season, 3 for post-season, 4 for off-season. + limit (int): number of records to return, default: 500. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. + + +### sportsdataverse.wbb.wbb_schedule.most_recent_wbb_season() ## sportsdataverse.wbb.wbb_teams module + +### sportsdataverse.wbb.wbb_teams.espn_wbb_teams(groups=None, return_as_pandas=False, \*\*kwargs) +espn_wbb_teams - look up the women’s college basketball teams + +Args: + + groups (int): Used to define different divisions. 50 is Division I, 51 is Division II/Division III. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing teams for the requested league. + +Example: + + wbb_df = sportsdataverse.wbb.espn_wbb_teams() + ## Module contents diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.wnba.md b/Sphinx-docs/_build/markdown/sportsdataverse.wnba.md index e8855ff..50684d0 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.wnba.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.wnba.md @@ -4,12 +4,220 @@ ## sportsdataverse.wnba.wnba_game_rosters module + +### sportsdataverse.wnba.wnba_game_rosters.espn_wnba_game_rosters(game_id: int, raw=False, return_as_pandas=False, \*\*kwargs) +espn_wnba_game_rosters() - Pull the game by id. + +Args: + + game_id (int): Unique game_id, can be obtained from espn_wnba_schedule(). + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Data frame of game roster data with columns: + ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, + ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, + ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, + ‘age’, ‘date_of_birth’, ‘slug’, ‘jersey’, ‘linked’, ‘active’, + ‘alternate_ids_sdr’, ‘birth_place_city’, ‘birth_place_state’, + ‘birth_place_country’, ‘headshot_href’, ‘headshot_alt’, + ‘experience_years’, ‘experience_display_value’, + ‘experience_abbreviation’, ‘status_id’, ‘status_name’, ‘status_type’, + ‘status_abbreviation’, ‘hand_type’, ‘hand_abbreviation’, + ‘hand_display_value’, ‘draft_display_text’, ‘draft_round’, ‘draft_year’, + ‘draft_selection’, ‘player_id’, ‘starter’, ‘valid’, ‘did_not_play’, + ‘display_name’, ‘ejected’, ‘athlete_href’, ‘position_href’, + ‘statistics_href’, ‘team_id’, ‘team_guid’, ‘team_uid’, ‘team_slug’, + ‘team_location’, ‘team_name’, ‘team_abbreviation’, + ‘team_display_name’, ‘team_short_display_name’, ‘team_color’, + ‘team_alternate_color’, ‘is_active’, ‘is_all_star’, + ‘logo_href’, ‘logo_dark_href’, ‘game_id’ + +Example: + + wnba_df = sportsdataverse.wnba.espn_wnba_game_rosters(game_id=401370395) + + +### sportsdataverse.wnba.wnba_game_rosters.helper_wnba_athlete_items(teams_rosters, \*\*kwargs) + +### sportsdataverse.wnba.wnba_game_rosters.helper_wnba_game_items(summary) + +### sportsdataverse.wnba.wnba_game_rosters.helper_wnba_roster_items(items, summary_url, \*\*kwargs) + +### sportsdataverse.wnba.wnba_game_rosters.helper_wnba_team_items(items, \*\*kwargs) ## sportsdataverse.wnba.wnba_loaders module + +### sportsdataverse.wnba.wnba_loaders.load_wnba_pbp(seasons: List[int], return_as_pandas=False) +Load WNBA play by play data going back to 2002 + +Example: + + wnba_df = sportsdataverse.wnba.load_wnba_pbp(seasons=range(2002,2022)) + +Args: + + seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing the + play-by-plays available for the requested seasons. + +Raises: + + ValueError: If season is less than 2002. + + +### sportsdataverse.wnba.wnba_loaders.load_wnba_player_boxscore(seasons: List[int], return_as_pandas=False) +Load WNBA player boxscore data + +Example: + + wnba_df = sportsdataverse.wnba.load_wnba_player_boxscore(seasons=range(2002,2022)) + +Args: + + seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing the + player boxscores available for the requested seasons. + +Raises: + + ValueError: If season is less than 2002. + + +### sportsdataverse.wnba.wnba_loaders.load_wnba_schedule(seasons: List[int], return_as_pandas=False) +Load WNBA schedule data + +Example: + + wnba_df = sportsdataverse.wnba.load_wnba_schedule(seasons=range(2002,2022)) + +Args: + + seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing the + schedule for the requested seasons. + +Raises: + + ValueError: If season is less than 2002. + + +### sportsdataverse.wnba.wnba_loaders.load_wnba_team_boxscore(seasons: List[int], return_as_pandas=False) +Load WNBA team boxscore data + +Example: + + wnba_df = sportsdataverse.wnba.load_wnba_team_boxscore(seasons=range(2002,2022)) + +Args: + + seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing the + team boxscores available for the requested seasons. + +Raises: + + ValueError: If season is less than 2002. + ## sportsdataverse.wnba.wnba_pbp module + +### sportsdataverse.wnba.wnba_pbp.espn_wnba_pbp(game_id: int, raw=False, \*\*kwargs) +espn_wnba_pbp() - Pull the game by id. Data from API endpoints - wnba/playbyplay, wnba/summary + +Args: + + game_id (int): Unique game_id, can be obtained from wnba_schedule(). + +Returns: + + Dict: Dictionary of game data with keys - “gameId”, “plays”, “winprobability”, “boxscore”, “header”, + + “broadcasts”, “videos”, “playByPlaySource”, “standings”, “leaders”, “seasonseries”, “timeouts”, + “pickcenter”, “againstTheSpread”, “odds”, “predictor”, “espnWP”, “gameInfo”, “season” + +Example: + + wnba_df = sportsdataverse.wnba.espn_wnba_pbp(game_id=401370395) + + +### sportsdataverse.wnba.wnba_pbp.helper_wnba_game_data(pbp_txt, init) + +### sportsdataverse.wnba.wnba_pbp.helper_wnba_pbp(game_id, pbp_txt) + +### sportsdataverse.wnba.wnba_pbp.helper_wnba_pbp_features(game_id, pbp_txt, init) + +### sportsdataverse.wnba.wnba_pbp.helper_wnba_pickcenter(pbp_txt) + +### sportsdataverse.wnba.wnba_pbp.wnba_pbp_disk(game_id, path_to_json) ## sportsdataverse.wnba.wnba_schedule module + +### sportsdataverse.wnba.wnba_schedule.espn_wnba_calendar(season=None, ondays=None, return_as_pandas=False, \*\*kwargs) +espn_wnba_calendar - look up the WNBA calendar for a given season + +Args: + + season (int): Used to define different seasons. 2002 is the earliest available season. + ondays (boolean): Used to return dates for calendar ondays + +Returns: + + pd.DataFrame: Pandas dataframe containing calendar dates for the requested season. + +Raises: + + ValueError: If season is less than 2002. + + +### sportsdataverse.wnba.wnba_schedule.espn_wnba_schedule(dates=None, season_type=None, limit=500, return_as_pandas=False, \*\*kwargs) +espn_wnba_schedule - look up the WNBA schedule for a given season + +Args: + + dates (int): Used to define different seasons. 2002 is the earliest available season. + season_type (int): 2 for regular season, 3 for post-season, 4 for off-season. + limit (int): number of records to return, default: 500. + +Returns: + + pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. + + +### sportsdataverse.wnba.wnba_schedule.most_recent_wnba_season() ## sportsdataverse.wnba.wnba_teams module + +### sportsdataverse.wnba.wnba_teams.espn_wnba_teams(return_as_pandas=False, \*\*kwargs) +espn_wnba_teams - look up WNBA teams + +Args: + + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing teams for the requested league. + +Example: + + wnba_df = sportsdataverse.wnba.espn_wnba_teams() + ## Module contents diff --git a/docs/docs/mlb/index.md b/docs/docs/mlb/index.md index 57454c0..1daccd6 100755 --- a/docs/docs/mlb/index.md +++ b/docs/docs/mlb/index.md @@ -336,6 +336,288 @@ Returns: ## sportsdataverse.mlb.retrosheet module +RETROSHEET NOTICE: + +> The information used here was obtained free of +> charge from and is copyrighted by Retrosheet. Interested +> parties may contact Retrosheet at “www.retrosheet.org”. + + +### sportsdataverse.mlb.retrosheet.retrosheet_ballparks() +Retrives the current TEAMABR.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. + +Args: + + None + +Returns: + + A pandas Dataframe with the biographical information of notable major league teams. + + +### sportsdataverse.mlb.retrosheet.retrosheet_ejections() +Retrives the current Ejecdata.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. + +Args: + + None + +Returns: + + A pandas Dataframe with the ejection data of known MLB ejections. + + +### sportsdataverse.mlb.retrosheet.retrosheet_franchises() +Retrives the current TEAMABR.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. + +Args: + + None + +Returns: + + A pandas Dataframe with the biographical information of notable major league teams. + + +### sportsdataverse.mlb.retrosheet.retrosheet_game_logs_team() +Retrives the team-level stats for MLB games in a season, or range of seasons. +THIS DOES NOT GET PLAYER STATS! +Use retrosplits_game_logs_player() for player-level game stats. + +Args: + + first_season (int): + + Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. + + last_season (int): + + Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. + + game_type (str): + + Optional parameter. By default, this is set to “regular”, or to put it in another way, this function call will return only regular season games. + + The full list of supported keywards for game_type are as follows. Case does not matter (you can set game_type to “rEgUlAr”, and the function call will still work): + + > + > * “regular”: Regular season games. + + + > * “asg”: All-Star games. + + + > * “playoffs”: Playoff games. + + filter_out_seasons (bool): + + If game_type is set to either “asg” or “playoffs”, and filter_out_seasons is set to true, this function will filter out seasons that do not match the inputted season and/or the range of seasons. By default, this is set to True. + +Returns: + + A pandas dataframe containing team-level stats for MLB games. + + +### sportsdataverse.mlb.retrosheet.retrosheet_people() +Retrives the current BioFile.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. + +Args: + + None + +Returns: + + A pandas Dataframe with the biographical information of various individuals who have played baseball. + + +### sportsdataverse.mlb.retrosheet.retrosheet_schedule() +Retrives the scheduled games of an MLB season, or MLB seasons. + +Args: + + first_season (int): + + Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. + + last_season (int): + + Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. + + original_2020_schedule (bool): + + Retrosheet keeps a record of the orignial 2020 MLB season, before the season was delayed due to the COVID-19 pandemic. + + + * If this is set to True, this function will return the original 2020 MLB season, before it was altered due to the COVID-19 pandemic, if the user wants this function to return the schedule for the 2020 MLB season. + + + * If this is set to False, this function will return the altered 2020 MLB season, after it was altered due to the COVID-19 pandemic, if the user wants this function to return the schedule for the 2020 MLB season. + +Returns: + + A pandas dataframe containg historical MLB schedules. + ## sportsdataverse.mlb.retrosplits module +RETROSHEET NOTICE: + +> The information used here was obtained free of +> charge from and is copyrighted by Retrosheet. Interested +> parties may contact Retrosheet at “www.retrosheet.org”. + + +### sportsdataverse.mlb.retrosplits.retrosplits_game_logs_player() +Retrives game-level player stats from the Retrosplits project. + +Args: + + first_season (int): + + Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. + + last_season (int): + + Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. + +Returns: + + A pandas dataframe containing game-level player stats from historical MLB games. + + +### sportsdataverse.mlb.retrosplits.retrosplits_game_logs_team() +Retrives game-level team stats from the Retrosplits project. + +Args: + + first_season (int): + + Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. + + last_season (int): + + Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. + +Returns: + + A pandas dataframe containing game-level team stats from historical MLB games. + + +### sportsdataverse.mlb.retrosplits.retrosplits_player_batting_by_platoon() +Retrives player-level, batting by platoon (left/right hitting vs. left/right pitching) split stats from the Retrosplits project. +The stats are batting stats, based off of the handedness of the batter vs the handedness of the pitcher. +The stats returned by this function are season-level stats, not game-level stats. + +Args: + + first_season (int): + + Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. + + last_season (int): + + Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. + +Returns: + + A pandas dataframe containing player-level, batting by platoon stats for batters. + + +### sportsdataverse.mlb.retrosplits.retrosplits_player_batting_by_position() +Retrives player-level, batting by position split stats from the Retrosplits project. +The stats returned by this function are season-level stats, not game-level stats. + +Args: + + first_season (int): + + Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. + + last_season (int): + + Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. + +Returns: + + A pandas dataframe containing batting by position split stats for MLB players. + + +### sportsdataverse.mlb.retrosplits.retrosplits_player_batting_by_runners() +Retrives player-level, batting by runners split stats from the Retrosplits project. +The stats are batting stats, based off of how many runners are on base at the time of the at bat. +The stats returned by this function are season-level stats, not game-level stats. + +Args: + + first_season (int): + + Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. + + last_season (int): + + Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. + +Returns: + + A pandas dataframe containing player-level, batting by runners split stats. + + +### sportsdataverse.mlb.retrosplits.retrosplits_player_head_to_head_stats() +Retrives batter vs. pitcher stats from the Retrosplits project. +The stats are batting stats, based off of the preformance of that specific batter agianst a specific pitcher for the durration of that specific season. +The stats returned by this function are season-level stats, not game-level stats. + +Args: + + first_season (int): + + Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. + + last_season (int): + + Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. + +Returns: + + A pandas dataframe containing batter vs. pitcher stats for a season, or for a range of seasons. + + +### sportsdataverse.mlb.retrosplits.retrosplits_player_pitching_by_platoon() +Retrives player-level, pitching by platoon (left/right pitching vs. left/right hitting) split stats from the Retrosplits project. +The stats are pitching stats, based off of the handedness of the pitcher vs the handedness of the batter. +The stats returned by this function are season-level stats, not game-level stats. + +Args: + + first_season (int): + + Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. + + last_season (int): + + Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. + +Returns: + + A pandas dataframe containing player-level, pitching by platoon stats for pitchers. + + +### sportsdataverse.mlb.retrosplits.retrosplits_player_pitching_by_runners() +Retrives player-level, pitching by runners split stats from the Retrosplits project. +The stats are pitching stats, based off of how many runners are on base at the time of the at bat. +The stats returned by this function are season-level stats, not game-level stats. + +Args: + + first_season (int): + + Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. + + last_season (int): + + Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. + +Returns: + + A pandas dataframe containing pitching by runners split stats for a season, or for a range of seasons. + ## Module contents diff --git a/docs/docs/nba/index.md b/docs/docs/nba/index.md index d5f749d..facfb9f 100755 --- a/docs/docs/nba/index.md +++ b/docs/docs/nba/index.md @@ -4,12 +4,228 @@ ## sportsdataverse.nba.nba_game_rosters module + +### sportsdataverse.nba.nba_game_rosters.espn_nba_game_rosters(game_id: int, raw=False, return_as_pandas=False, \*\*kwargs) +espn_nba_game_rosters() - Pull the game by id. + +Args: + + game_id (int): Unique game_id, can be obtained from espn_nba_schedule(). + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Data frame of game roster data with columns: + ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, + ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, + ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, + ‘age’, ‘date_of_birth’, ‘slug’, ‘jersey’, ‘linked’, ‘active’, + ‘alternate_ids_sdr’, ‘birth_place_city’, ‘birth_place_state’, + ‘birth_place_country’, ‘headshot_href’, ‘headshot_alt’, + ‘experience_years’, ‘experience_display_value’, + ‘experience_abbreviation’, ‘status_id’, ‘status_name’, ‘status_type’, + ‘status_abbreviation’, ‘hand_type’, ‘hand_abbreviation’, + ‘hand_display_value’, ‘draft_display_text’, ‘draft_round’, ‘draft_year’, + ‘draft_selection’, ‘player_id’, ‘starter’, ‘valid’, ‘did_not_play’, + ‘display_name’, ‘ejected’, ‘athlete_href’, ‘position_href’, + ‘statistics_href’, ‘team_id’, ‘team_guid’, ‘team_uid’, ‘team_slug’, + ‘team_location’, ‘team_name’, ‘team_abbreviation’, + ‘team_display_name’, ‘team_short_display_name’, ‘team_color’, + ‘team_alternate_color’, ‘is_active’, ‘is_all_star’, + ‘logo_href’, ‘logo_dark_href’, ‘game_id’ + +Example: + + nba_df = sportsdataverse.nba.espn_nba_game_rosters(game_id=401307514) + + +### sportsdataverse.nba.nba_game_rosters.helper_nba_athlete_items(teams_rosters, \*\*kwargs) + +### sportsdataverse.nba.nba_game_rosters.helper_nba_game_items(summary) + +### sportsdataverse.nba.nba_game_rosters.helper_nba_roster_items(items, summary_url, \*\*kwargs) + +### sportsdataverse.nba.nba_game_rosters.helper_nba_team_items(items, \*\*kwargs) ## sportsdataverse.nba.nba_loaders module + +### sportsdataverse.nba.nba_loaders.load_nba_pbp(seasons: List[int], return_as_pandas=False) +Load NBA play by play data going back to 2002 + +Example: + + nba_df = sportsdataverse.nba.load_nba_pbp(seasons=range(2002,2022)) + +Args: + + seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing the + play-by-plays available for the requested seasons. + +Raises: + + ValueError: If season is less than 2002. + + +### sportsdataverse.nba.nba_loaders.load_nba_player_boxscore(seasons: List[int], return_as_pandas=False) +Load NBA player boxscore data + +Example: + + nba_df = sportsdataverse.nba.load_nba_player_boxscore(seasons=range(2002,2022)) + +Args: + + seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing the + player boxscores available for the requested seasons. + +Raises: + + ValueError: If season is less than 2002. + + +### sportsdataverse.nba.nba_loaders.load_nba_schedule(seasons: List[int], return_as_pandas=False) +Load NBA schedule data + +Example: + + nba_df = sportsdataverse.nba.load_nba_schedule(seasons=range(2002,2022)) + +Args: + + seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing the + schedule for the requested seasons. + +Raises: + + ValueError: If season is less than 2002. + + +### sportsdataverse.nba.nba_loaders.load_nba_team_boxscore(seasons: List[int], return_as_pandas=False) +Load NBA team boxscore data + +Example: + + nba_df = sportsdataverse.nba.load_nba_team_boxscore(seasons=range(2002,2022)) + +Args: + + seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing the + team boxscores available for the requested seasons. + +Raises: + + ValueError: If season is less than 2002. + ## sportsdataverse.nba.nba_pbp module + +### sportsdataverse.nba.nba_pbp.espn_nba_pbp(game_id: int, raw=False, \*\*kwargs) +espn_nba_pbp() - Pull the game by id - Data from API endpoints - nba/playbyplay, nba/summary + +Args: + + game_id (int): Unique game_id, can be obtained from nba_schedule(). + raw (bool): If True, returns the raw json from the API endpoint. If False, returns a cleaned dictionary of datasets. + +Returns: + + Dict: Dictionary of game data with keys - “gameId”, “plays”, “winprobability”, “boxscore”, “header”, “broadcasts”, + + “videos”, “playByPlaySource”, “standings”, “leaders”, “seasonseries”, “timeouts”, “pickcenter”, “againstTheSpread”, + “odds”, “predictor”, “espnWP”, “gameInfo”, “season” + +Example: + + nba_df = sportsdataverse.nba.espn_nba_pbp(game_id=401307514) + + +### sportsdataverse.nba.nba_pbp.helper_nba_game_data(pbp_txt, init) + +### sportsdataverse.nba.nba_pbp.helper_nba_pbp(game_id, pbp_txt) + +### sportsdataverse.nba.nba_pbp.helper_nba_pbp_features(game_id, pbp_txt, init) + +### sportsdataverse.nba.nba_pbp.helper_nba_pickcenter(pbp_txt) + +### sportsdataverse.nba.nba_pbp.nba_pbp_disk(game_id, path_to_json) ## sportsdataverse.nba.nba_schedule module + +### sportsdataverse.nba.nba_schedule.espn_nba_calendar(season=None, ondays=None, return_as_pandas=False, \*\*kwargs) +espn_nba_calendar - look up the NBA calendar for a given season from ESPN + +Args: + + season (int): Used to define different seasons. 2002 is the earliest available season. + ondays (boolean): Used to return dates for calendar ondays + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing + calendar dates for the requested season. + +Raises: + + ValueError: If season is less than 2002. + + +### sportsdataverse.nba.nba_schedule.espn_nba_schedule(dates=None, season_type=None, limit=500, return_as_pandas=False, \*\*kwargs) +espn_nba_schedule - look up the NBA schedule for a given date from ESPN + +Args: + + dates (int): Used to define different seasons. 2002 is the earliest available season. + season_type (int): season type, 1 for pre-season, 2 for regular season, 3 for post-season, + 4 for all-star, 5 for off-season + limit (int): number of records to return, default: 500. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing + schedule events for the requested season. + + +### sportsdataverse.nba.nba_schedule.most_recent_nba_season() + +### sportsdataverse.nba.nba_schedule.year_to_season(year) ## sportsdataverse.nba.nba_teams module + +### sportsdataverse.nba.nba_teams.espn_nba_teams(return_as_pandas=False, \*\*kwargs) +espn_nba_teams - look up NBA teams + +Args: + + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing teams for the requested league. + +Example: + + nba_df = sportsdataverse.nba.espn_nba_teams() + ## Module contents diff --git a/docs/docs/nfl/index.md b/docs/docs/nfl/index.md index 2812ba2..d56a0ae 100755 --- a/docs/docs/nfl/index.md +++ b/docs/docs/nfl/index.md @@ -6,14 +6,603 @@ ## sportsdataverse.nfl.nfl_game_rosters module + +### sportsdataverse.nfl.nfl_game_rosters.espn_nfl_game_rosters(game_id: int, raw=False, return_as_pandas=False, \*\*kwargs) +espn_nfl_game_rosters() - Pull the game by id. + +Args: + + game_id (int): Unique game_id, can be obtained from espn_nfl_schedule(). + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Data frame of game roster data with columns: + ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, + ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, + ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, + ‘age’, ‘date_of_birth’, ‘slug’, ‘jersey’, ‘linked’, ‘active’, + ‘alternate_ids_sdr’, ‘birth_place_city’, ‘birth_place_state’, + ‘birth_place_country’, ‘headshot_href’, ‘headshot_alt’, + ‘experience_years’, ‘experience_display_value’, + ‘experience_abbreviation’, ‘status_id’, ‘status_name’, ‘status_type’, + ‘status_abbreviation’, ‘hand_type’, ‘hand_abbreviation’, + ‘hand_display_value’, ‘draft_display_text’, ‘draft_round’, ‘draft_year’, + ‘draft_selection’, ‘player_id’, ‘starter’, ‘valid’, ‘did_not_play’, + ‘display_name’, ‘ejected’, ‘athlete_href’, ‘position_href’, + ‘statistics_href’, ‘team_id’, ‘team_guid’, ‘team_uid’, ‘team_slug’, + ‘team_location’, ‘team_name’, ‘team_nickname’, ‘team_abbreviation’, + ‘team_display_name’, ‘team_short_display_name’, ‘team_color’, + ‘team_alternate_color’, ‘is_active’, ‘is_all_star’, + ‘team_alternate_ids_sdr’, ‘logo_href’, ‘logo_dark_href’, ‘game_id’ + +Example: + + nfl_df = sportsdataverse.nfl.espn_nfl_game_rosters(game_id=401220403) + + +### sportsdataverse.nfl.nfl_game_rosters.helper_nfl_athlete_items(teams_rosters, \*\*kwargs) + +### sportsdataverse.nfl.nfl_game_rosters.helper_nfl_game_items(summary) + +### sportsdataverse.nfl.nfl_game_rosters.helper_nfl_roster_items(items, summary_url, \*\*kwargs) + +### sportsdataverse.nfl.nfl_game_rosters.helper_nfl_team_items(items, \*\*kwargs) ## sportsdataverse.nfl.nfl_games module + +### sportsdataverse.nfl.nfl_games.nfl_game_details() +Args: + + game_id (int): Game ID + +Returns: + + Dict: Dictionary of odds and props data with keys + +Example: + + nfl_df = nfl_game_details( + game_id = ‘7ae87c4c-d24c-11ec-b23d-d15a91047884’ + ) + + +### sportsdataverse.nfl.nfl_games.nfl_game_schedule() +Args: + + season (int): season + season_type (str): season type - REG, POST + week (int): week + +Returns: + + Dict: Dictionary of odds and props data with keys + +Example: + + + + ``` + ` + ``` + + nfl_df = nfl_game_schedule( + + season = 2021, seasonType=’REG’, week=1 + + )\` + + +### sportsdataverse.nfl.nfl_games.nfl_headers_gen() + +### sportsdataverse.nfl.nfl_games.nfl_token_gen() ## sportsdataverse.nfl.nfl_loaders module + +### sportsdataverse.nfl.nfl_loaders.load_nfl_combine(return_as_pandas=False) +Load NFL Combine information + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_combine() + +Args: + +Returns: + + pd.DataFrame: Pandas dataframe containing NFL combine data available. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_contracts(return_as_pandas=False) +Load NFL Historical contracts information + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_contracts() + +Args: + +Returns: + + pd.DataFrame: Pandas dataframe containing historical contracts available. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_depth_charts(seasons: List[int], return_as_pandas=False) +Load NFL Depth Chart data for selected seasons + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_depth_charts(seasons=range(2001,2021)) + +Args: + + seasons (list): Used to define different seasons. 2001 is the earliest available season. + +Returns: + + pd.DataFrame: Pandas dataframe containing depth chart data available for the requested seasons. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_draft_picks(return_as_pandas=False) +Load NFL Draft picks information + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_draft_picks() + +Args: + +Returns: + + pd.DataFrame: Pandas dataframe containing NFL Draft picks data available. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_injuries(seasons: List[int], return_as_pandas=False) +Load NFL injuries data for selected seasons + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_injuries(seasons=range(2009,2021)) + +Args: + + seasons (list): Used to define different seasons. 2009 is the earliest available season. + +Returns: + + pd.DataFrame: Pandas dataframe containing injuries data available for the requested seasons. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_ngs_passing(return_as_pandas=False) +Load NFL NextGen Stats Passing data going back to 2016 + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_ngs_passing() + +Returns: + + pd.DataFrame: Pandas dataframe containing the NextGen Stats Passing data available. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_ngs_receiving(return_as_pandas=False) +Load NFL NextGen Stats Receiving data going back to 2016 + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_ngs_receiving() + +Returns: + + pd.DataFrame: Pandas dataframe containing the NextGen Stats Receiving data available. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_ngs_rushing(return_as_pandas=False) +Load NFL NextGen Stats Rushing data going back to 2016 + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_ngs_rushing() + +Returns: + + pd.DataFrame: Pandas dataframe containing the NextGen Stats Rushing data available. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_officials(return_as_pandas=False) +Load NFL Officials information + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_officials() + +Args: + +Returns: + + pd.DataFrame: Pandas dataframe containing officials available. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_pbp(seasons: List[int], return_as_pandas=False) +Load NFL play by play data going back to 1999 + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_pbp(seasons=range(1999,2021)) + +Args: + + seasons (list): Used to define different seasons. 1999 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing the play-by-plays available for the requested seasons. + +Raises: + + ValueError: If season is less than 1999. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_pbp_participation(seasons: List[int], return_as_pandas=False) +Load NFL play-by-play participation data for selected seasons + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_pbp_participation(seasons=range(2016,2021)) + +Args: + + seasons (list): Used to define different seasons. 2016 is the earliest available season. + +Returns: + + pd.DataFrame: Pandas dataframe containing play-by-play participation data available for the requested seasons. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_def(return_as_pandas=False) +Load NFL Pro-Football Reference Advanced Defensive data going back to 2018 + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_pfr_def() + +Returns: + + pd.DataFrame: Pandas dataframe containing Pro-Football Reference + + advanced defensive stats data available. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_pass(return_as_pandas=False) +Load NFL Pro-Football Reference Advanced Passing data going back to 2018 + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_pfr_pass() + +Returns: + + pd.DataFrame: Pandas dataframe containing Pro-Football Reference + + advanced passing stats data available. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_rec(return_as_pandas=False) +Load NFL Pro-Football Reference Advanced Receiving data going back to 2018 + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_pfr_rec() + +Returns: + + pd.DataFrame: Pandas dataframe containing Pro-Football Reference + + advanced receiving stats data available. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_rush(return_as_pandas=False) +Load NFL Pro-Football Reference Advanced Rushing data going back to 2018 + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_pfr_rush() + +Returns: + + pd.DataFrame: Pandas dataframe containing Pro-Football Reference + + advanced rushing stats data available. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_weekly_def(seasons: List[int], return_as_pandas=False) +Load NFL Pro-Football Reference Weekly Advanced Defensive data going back to 2018 + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_pfr_weekly_def(seasons=range(2018,2021)) + +Args: + + seasons (list): Used to define different seasons. 2018 is the earliest available season. + +Returns: + + pd.DataFrame: Pandas dataframe containing Pro-Football Reference + + advanced defensive stats data available for the requested seasons. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_weekly_pass(seasons: List[int], return_as_pandas=False) +Load NFL Pro-Football Reference Weekly Advanced Passing data going back to 2018 + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_pfr_weekly_pass(seasons=range(2018,2021)) + +Args: + + seasons (list): Used to define different seasons. 2018 is the earliest available season. + +Returns: + + pd.DataFrame: Pandas dataframe containing Pro-Football Reference + + advanced passing stats data available for the requested seasons. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_weekly_rec(seasons: List[int], return_as_pandas=False) +Load NFL Pro-Football Reference Weekly Advanced Receiving data going back to 2018 + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_pfr_weekly_rec(seasons=range(2018,2021)) + +Args: + + seasons (list): Used to define different seasons. 2018 is the earliest available season. + +Returns: + + pd.DataFrame: Pandas dataframe containing Pro-Football Reference + + advanced receiving stats data available for the requested seasons. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_weekly_rush(seasons: List[int], return_as_pandas=False) +Load NFL Pro-Football Reference Weekly Advanced Rushing data going back to 2018 + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_pfr_weekly_rush(seasons=range(2018,2021)) + +Args: + + seasons (list): Used to define different seasons. 2018 is the earliest available season. + +Returns: + + pd.DataFrame: Pandas dataframe containing Pro-Football Reference + + advanced rushing stats data available for the requested seasons. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_player_stats(kicking=False, return_as_pandas=False) +Load NFL player stats data + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_player_stats() + +Args: + + kicking (bool): If True, load kicking stats. If False, load all other stats. + +Returns: + + pd.DataFrame: Pandas dataframe containing player stats. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_players(return_as_pandas=False) +Load NFL Player ID information + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_players() + +Args: + +Returns: + + pd.DataFrame: Pandas dataframe containing players available. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_rosters(seasons: List[int], return_as_pandas=False) +Load NFL roster data for all seasons + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_rosters(seasons=range(1999,2021)) + +Args: + + seasons (list): Used to define different seasons. 1920 is the earliest available season. + +Returns: + + pd.DataFrame: Pandas dataframe containing rosters available for the requested seasons. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_schedule(seasons: List[int], return_as_pandas=False) +Load NFL schedule data + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_schedule(seasons=range(1999,2021)) + +Args: + + seasons (list): Used to define different seasons. 1999 is the earliest available season. + +Returns: + + pd.DataFrame: Pandas dataframe containing the schedule for the requested seasons. + +Raises: + + ValueError: If season is less than 1999. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_snap_counts(seasons: List[int], return_as_pandas=False) +Load NFL snap counts data for selected seasons + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_snap_counts(seasons=range(2012,2021)) + +Args: + + seasons (list): Used to define different seasons. 2012 is the earliest available season. + +Returns: + + pd.DataFrame: Pandas dataframe containing snap counts available for the requested seasons. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_teams(return_as_pandas=False) +Load NFL team ID information and logos + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_teams() + +Args: + +Returns: + + pd.DataFrame: Pandas dataframe containing teams available. + + +### sportsdataverse.nfl.nfl_loaders.load_nfl_weekly_rosters(seasons: List[int], return_as_pandas=False) +Load NFL weekly roster data for selected seasons + +Example: + + nfl_df = sportsdataverse.nfl.load_nfl_weekly_rosters(seasons=range(2002,2021)) + +Args: + + seasons (list): Used to define different seasons. 2002 is the earliest available season. + +Returns: + + pd.DataFrame: Pandas dataframe containing weekly rosters available for the requested seasons. + ## sportsdataverse.nfl.nfl_pbp module + +### class sportsdataverse.nfl.nfl_pbp.NFLPlayProcess(gameId=0, raw=False, path_to_json='/') +Bases: `object` + + +#### \__init__(gameId=0, raw=False, path_to_json='/') +Initialize self. See help(type(self)) for accurate signature. + + +#### create_box_score() + +#### espn_nfl_pbp() +espn_nfl_pbp() - Pull the game by id. Data from API endpoints: nfl/playbyplay, nfl/summary + +Args: + + game_id (int): Unique game_id, can be obtained from nfl_schedule(). + +Returns: + + Dict: Dictionary of game data with keys - “gameId”, “plays”, “boxscore”, “header”, “broadcasts”, + + “videos”, “playByPlaySource”, “standings”, “leaders”, “timeouts”, “homeTeamSpread”, “overUnder”, + “pickcenter”, “againstTheSpread”, “odds”, “predictor”, “winprobability”, “espnWP”, + “gameInfo”, “season” + +Example: + + nfl_df = sportsdataverse.nfl.NFLPlayProcess(gameId=401220403).espn_nfl_pbp() + + +#### gameId( = 0) + +#### nfl_pbp_disk() + +#### path_to_json( = '/') + +#### ran_cleaning_pipeline( = False) + +#### ran_pipeline( = False) + +#### raw( = False) + +#### run_cleaning_pipeline() + +#### run_processing_pipeline() ## sportsdataverse.nfl.nfl_schedule module + +### sportsdataverse.nfl.nfl_schedule.espn_nfl_calendar(season=None, ondays=None, return_as_pandas=False, \*\*kwargs) +espn_nfl_calendar - look up the NFL calendar for a given season + +Args: + + season (int): Used to define different seasons. 2002 is the earliest available season. + ondays (boolean): Used to return dates for calendar ondays + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing calendar dates for the requested season. + +Raises: + + ValueError: If season is less than 2002. + + +### sportsdataverse.nfl.nfl_schedule.espn_nfl_schedule(dates=None, week=None, season_type=None, groups=None, limit=500, return_as_pandas=False, \*\*kwargs) +espn_nfl_schedule - look up the NFL schedule for a given season + +Args: + + dates (int): Used to define different seasons. 2002 is the earliest available season. + week (int): Week of the schedule. + season_type (int): 2 for regular season, 3 for post-season, 4 for off-season. + limit (int): number of records to return, default: 500. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. + + +### sportsdataverse.nfl.nfl_schedule.get_current_week() + +### sportsdataverse.nfl.nfl_schedule.most_recent_nfl_season() ## sportsdataverse.nfl.nfl_teams module + +### sportsdataverse.nfl.nfl_teams.espn_nfl_teams(return_as_pandas=False, \*\*kwargs) +espn_nfl_teams - look up NFL teams + +Args: + + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing teams for the requested league. + +Example: + + nfl_df = sportsdataverse.nfl.espn_nfl_teams() + ## Module contents diff --git a/docs/docs/nhl/index.md b/docs/docs/nhl/index.md index d9aeb28..add82ee 100755 --- a/docs/docs/nhl/index.md +++ b/docs/docs/nhl/index.md @@ -4,14 +4,277 @@ ## sportsdataverse.nhl.nhl_api module + +### sportsdataverse.nhl.nhl_api.nhl_api_pbp(game_id: int, \*\*kwargs) +nhl_api_pbp() - Pull the game by id. Data from API endpoints - nhl/playbyplay, nhl/summary + +Args: + + game_id (int): Unique game_id, can be obtained from nhl_schedule(). + +Returns: + + Dict: Dictionary of game data with keys - “gameId”, “plays”, “boxscore”, “header”, “broadcasts”, + + “videos”, “playByPlaySource”, “standings”, “leaders”, “seasonseries”, “pickcenter”, “againstTheSpread”, + “odds”, “onIce”, “gameInfo”, “season” + +Example: + + nhl_df = sportsdataverse.nhl.nhl_api_pbp(game_id=2021020079) + + +### sportsdataverse.nhl.nhl_api.nhl_api_schedule(start_date: str, end_date: str, return_as_pandas=False) +nhl_api_schedule() - Pull the game by id. Data from API endpoints - nhl/schedule + +Args: + + game_id (int): Unique game_id, can be obtained from nhl_schedule(). + +Returns: + + pd.DataFrame: Pandas dataframe containing the schedule for the requested seasons. + +Example: + + nhl_sched_df = sportsdataverse.nhl.nhl_api_schedule(start_date=2021-10-23, end_date=2021-10-28) + ## sportsdataverse.nhl.nhl_game_rosters module + +### sportsdataverse.nhl.nhl_game_rosters.espn_nhl_game_rosters(game_id: int, raw=False, return_as_pandas=False, \*\*kwargs) +espn_nhl_game_rosters() - Pull the game by id. + +Args: + + game_id (int): Unique game_id, can be obtained from espn_nhl_schedule(). + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Data frame of game roster data with columns: + ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, + ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, + ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, + ‘age’, ‘date_of_birth’, ‘slug’, ‘jersey’, ‘linked’, ‘active’, + ‘alternate_ids_sdr’, ‘birth_place_city’, ‘birth_place_state’, + ‘birth_place_country’, ‘headshot_href’, ‘headshot_alt’, + ‘experience_years’, ‘experience_display_value’, + ‘experience_abbreviation’, ‘status_id’, ‘status_name’, ‘status_type’, + ‘status_abbreviation’, ‘hand_type’, ‘hand_abbreviation’, + ‘hand_display_value’, ‘draft_display_text’, ‘draft_round’, ‘draft_year’, + ‘draft_selection’, ‘player_id’, ‘starter’, ‘valid’, ‘did_not_play’, + ‘display_name’, ‘ejected’, ‘athlete_href’, ‘position_href’, + ‘statistics_href’, ‘team_id’, ‘team_guid’, ‘team_uid’, ‘team_slug’, + ‘team_location’, ‘team_name’, ‘team_abbreviation’, + ‘team_display_name’, ‘team_short_display_name’, ‘team_color’, + ‘team_alternate_color’, ‘is_active’, ‘is_all_star’, + ‘logo_href’, ‘logo_dark_href’, ‘game_id’ + +Example: + + nhl_df = sportsdataverse.nhl.espn_nhl_game_rosters(game_id=401247153) + + +### sportsdataverse.nhl.nhl_game_rosters.helper_nhl_athlete_items(teams_rosters, \*\*kwargs) + +### sportsdataverse.nhl.nhl_game_rosters.helper_nhl_game_items(summary) + +### sportsdataverse.nhl.nhl_game_rosters.helper_nhl_roster_items(items, summary_url, \*\*kwargs) + +### sportsdataverse.nhl.nhl_game_rosters.helper_nhl_team_items(items, \*\*kwargs) ## sportsdataverse.nhl.nhl_loaders module + +### sportsdataverse.nhl.nhl_loaders.load_nhl_pbp(seasons: List[int], return_as_pandas=False) +Load NHL play by play data going back to 2011 + +Example: + + nhl_df = sportsdataverse.nhl.load_nhl_pbp(seasons=range(2011,2021)) + +Args: + + seasons (list): Used to define different seasons. 2011 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing the play-by-plays available for the requested seasons. + +Raises: + + ValueError: If season is less than 2011. + + +### sportsdataverse.nhl.nhl_loaders.load_nhl_player_boxscore(seasons: List[int], return_as_pandas=False) +Load NHL player boxscore data + +Example: + + nhl_df = sportsdataverse.nhl.load_nhl_player_boxscore(seasons=range(2011,2022)) + +Args: + + seasons (list): Used to define different seasons. 2011 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing the + player boxscores available for the requested seasons. + +Raises: + + ValueError: If season is less than 2011. + + +### sportsdataverse.nhl.nhl_loaders.load_nhl_schedule(seasons: List[int], return_as_pandas=False) +Load NHL schedule data + +Example: + + nhl_df = sportsdataverse.nhl.load_nhl_schedule(seasons=range(2002,2021)) + +Args: + + seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing the schedule for the requested seasons. + +Raises: + + ValueError: If season is less than 2002. + + +### sportsdataverse.nhl.nhl_loaders.load_nhl_team_boxscore(seasons: List[int], return_as_pandas=False) +Load NHL team boxscore data + +Example: + + nhl_df = sportsdataverse.nhl.load_nhl_team_boxscore(seasons=range(2011,2022)) + +Args: + + seasons (list): Used to define different seasons. 2011 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing the + team boxscores available for the requested seasons. + +Raises: + + ValueError: If season is less than 2011. + + +### sportsdataverse.nhl.nhl_loaders.nhl_teams(return_as_pandas=False) +Load NHL team ID information and logos + +Example: + + nhl_df = sportsdataverse.nhl.nhl_teams() + +Args: + + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing teams available for the requested seasons. + ## sportsdataverse.nhl.nhl_pbp module + +### sportsdataverse.nhl.nhl_pbp.espn_nhl_pbp(game_id: int, raw=False, \*\*kwargs) +espn_nhl_pbp() - Pull the game by id. Data from API endpoints - nhl/playbyplay, nhl/summary + +Args: + + game_id (int): Unique game_id, can be obtained from nhl_schedule(). + +Returns: + + Dict: Dictionary of game data with keys - “gameId”, “plays”, “boxscore”, “header”, “broadcasts”, + + “videos”, “playByPlaySource”, “standings”, “leaders”, “seasonseries”, “pickcenter”, “againstTheSpread”, + “odds”, “onIce”, “gameInfo”, “season” + +Example: + + nhl_df = sportsdataverse.nhl.espn_nhl_pbp(game_id=401247153) + + +### sportsdataverse.nhl.nhl_pbp.helper_nhl_game_data(pbp_txt, init) + +### sportsdataverse.nhl.nhl_pbp.helper_nhl_pbp(game_id, pbp_txt) + +### sportsdataverse.nhl.nhl_pbp.helper_nhl_pbp_features(game_id, pbp_txt, init) + +### sportsdataverse.nhl.nhl_pbp.helper_nhl_pickcenter(pbp_txt) + +### sportsdataverse.nhl.nhl_pbp.nhl_pbp_disk(game_id, path_to_json) ## sportsdataverse.nhl.nhl_schedule module + +### sportsdataverse.nhl.nhl_schedule.espn_nhl_calendar(season=None, ondays=None, return_as_pandas=False, \*\*kwargs) +espn_nhl_calendar - look up the NHL calendar for a given season + +Args: + + season (int): Used to define different seasons. 2002 is the earliest available season. + ondays (boolean): Used to return dates for calendar ondays + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing + calendar dates for the requested season. + +Raises: + + ValueError: If season is less than 2002. + + +### sportsdataverse.nhl.nhl_schedule.espn_nhl_schedule(dates=None, season_type=None, limit=500, return_as_pandas=False, \*\*kwargs) +espn_nhl_schedule - look up the NHL schedule for a given date + +Args: + + dates (int): Used to define different seasons. 2002 is the earliest available season. + season_type (int): season type, 1 for pre-season, 2 for regular season, 3 for post-season, 4 for all-star, 5 for off-season + limit (int): number of records to return, default: 500. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing + schedule events for the requested season. + + +### sportsdataverse.nhl.nhl_schedule.most_recent_nhl_season() + +### sportsdataverse.nhl.nhl_schedule.year_to_season(year) ## sportsdataverse.nhl.nhl_teams module + +### sportsdataverse.nhl.nhl_teams.espn_nhl_teams(return_as_pandas=False, \*\*kwargs) +espn_nhl_teams - look up NHL teams + +Args: + + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing teams for the requested league. + +Example: + + nhl_df = sportsdataverse.nhl.espn_nhl_teams() + ## Module contents diff --git a/docs/docs/wbb/index.md b/docs/docs/wbb/index.md index 1433676..c8a7173 100755 --- a/docs/docs/wbb/index.md +++ b/docs/docs/wbb/index.md @@ -4,12 +4,227 @@ ## sportsdataverse.wbb.wbb_game_rosters module + +### sportsdataverse.wbb.wbb_game_rosters.espn_wbb_game_rosters(game_id: int, raw=False, return_as_pandas=False, \*\*kwargs) +espn_wbb_game_rosters() - Pull the game by id. + +Args: + + game_id (int): Unique game_id, can be obtained from wbb_schedule(). + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Data frame of game roster data with columns: + ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, + ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, + ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, + ‘age’, ‘date_of_birth’, ‘slug’, ‘jersey’, ‘linked’, ‘active’, + ‘alternate_ids_sdr’, ‘birth_place_city’, ‘birth_place_state’, + ‘birth_place_country’, ‘headshot_href’, ‘headshot_alt’, + ‘experience_years’, ‘experience_display_value’, + ‘experience_abbreviation’, ‘status_id’, ‘status_name’, ‘status_type’, + ‘status_abbreviation’, ‘hand_type’, ‘hand_abbreviation’, + ‘hand_display_value’, ‘draft_display_text’, ‘draft_round’, ‘draft_year’, + ‘draft_selection’, ‘player_id’, ‘starter’, ‘valid’, ‘did_not_play’, + ‘display_name’, ‘ejected’, ‘athlete_href’, ‘position_href’, + ‘statistics_href’, ‘team_id’, ‘team_guid’, ‘team_uid’, ‘team_slug’, + ‘team_location’, ‘team_name’, ‘team_nickname’, ‘team_abbreviation’, + ‘team_display_name’, ‘team_short_display_name’, ‘team_color’, + ‘team_alternate_color’, ‘is_active’, ‘is_all_star’, + ‘team_alternate_ids_sdr’, ‘logo_href’, ‘logo_dark_href’, ‘game_id’ + +Example: + + wbb_df = sportsdataverse.wbb.espn_wbb_game_rosters(game_id=401266534) + + +### sportsdataverse.wbb.wbb_game_rosters.helper_wbb_athlete_items(teams_rosters, \*\*kwargs) + +### sportsdataverse.wbb.wbb_game_rosters.helper_wbb_game_items(summary) + +### sportsdataverse.wbb.wbb_game_rosters.helper_wbb_roster_items(items, summary_url, \*\*kwargs) + +### sportsdataverse.wbb.wbb_game_rosters.helper_wbb_team_items(items, \*\*kwargs) ## sportsdataverse.wbb.wbb_loaders module + +### sportsdataverse.wbb.wbb_loaders.load_wbb_pbp(seasons: List[int], return_as_pandas=False) +Load women’s college basketball play by play data going back to 2002 + +Example: + + wbb_df = sportsdataverse.wbb.load_wbb_pbp(seasons=range(2002,2022)) + +Args: + + seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing the + play-by-plays available for the requested seasons. + +Raises: + + ValueError: If season is less than 2002. + + +### sportsdataverse.wbb.wbb_loaders.load_wbb_player_boxscore(seasons: List[int], return_as_pandas=False) +Load women’s college basketball player boxscore data + +Example: + + wbb_df = sportsdataverse.wbb.load_wbb_player_boxscore(seasons=range(2002,2022)) + +Args: + + seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing the + player boxscores available for the requested seasons. + +Raises: + + ValueError: If season is less than 2002. + + +### sportsdataverse.wbb.wbb_loaders.load_wbb_schedule(seasons: List[int], return_as_pandas=False) +Load women’s college basketball schedule data + +Example: + + wbb_df = sportsdataverse.wbb.load_wbb_schedule(seasons=range(2002,2022)) + +Args: + + seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing the + schedule for the requested seasons. + +Raises: + + ValueError: If season is less than 2002. + + +### sportsdataverse.wbb.wbb_loaders.load_wbb_team_boxscore(seasons: List[int], return_as_pandas=False) +Load women’s college basketball team boxscore data + +Example: + + wbb_df = sportsdataverse.wbb.load_wbb_team_boxscore(seasons=range(2002,2022)) + +Args: + + seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing the + team boxscores available for the requested seasons. + +Raises: + + ValueError: If season is less than 2002. + ## sportsdataverse.wbb.wbb_pbp module + +### sportsdataverse.wbb.wbb_pbp.espn_wbb_pbp(game_id: int, raw=False, \*\*kwargs) +espn_wbb_pbp() - Pull the game by id. Data from API endpoints - womens-college-basketball/playbyplay, +womens-college-basketball/summary + +Args: + + game_id (int): Unique game_id, can be obtained from wbb_schedule(). + +raw (bool): If True, returns the raw json from the API endpoint. If False, returns a cleaned dictionary of datasets. + +Returns: + + Dict: Dictionary of game data with keys - “gameId”, “plays”, “winprobability”, “boxscore”, “header”, + “broadcasts”, “videos”, “playByPlaySource”, “standings”, “leaders”, “timeouts”, “pickcenter”, + “againstTheSpread”, “odds”, “predictor”,”espnWP”, “gameInfo”, “season” + +Example: + + wbb_df = sportsdataverse.wb.espn_wbb_pbp(game_id=401266534) + + +### sportsdataverse.wbb.wbb_pbp.helper_wbb_game_data(pbp_txt, init) + +### sportsdataverse.wbb.wbb_pbp.helper_wbb_pbp(game_id, pbp_txt) + +### sportsdataverse.wbb.wbb_pbp.helper_wbb_pbp_features(game_id, pbp_txt, init) + +### sportsdataverse.wbb.wbb_pbp.helper_wbb_pickcenter(pbp_txt) + +### sportsdataverse.wbb.wbb_pbp.mbb_pbp_disk(game_id, path_to_json) ## sportsdataverse.wbb.wbb_schedule module + +### sportsdataverse.wbb.wbb_schedule.espn_wbb_calendar(season=None, ondays=None, return_as_pandas=False, \*\*kwargs) +espn_wbb_calendar - look up the women’s college basketball calendar for a given season + +Args: + + season (int): Used to define different seasons. 2002 is the earliest available season. + ondays (boolean): Used to return dates for calendar ondays + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing + calendar dates for the requested season. + +Raises: + + ValueError: If season is less than 2002. + + +### sportsdataverse.wbb.wbb_schedule.espn_wbb_schedule(dates=None, groups=50, season_type=None, limit=500, return_as_pandas=False, \*\*kwargs) +espn_wbb_schedule - look up the women’s college basketball schedule for a given season + +Args: + + dates (int): Used to define different seasons. 2002 is the earliest available season. + groups (int): Used to define different divisions. 50 is Division I, 51 is Division II/Division III. + season_type (int): 2 for regular season, 3 for post-season, 4 for off-season. + limit (int): number of records to return, default: 500. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. + + +### sportsdataverse.wbb.wbb_schedule.most_recent_wbb_season() ## sportsdataverse.wbb.wbb_teams module + +### sportsdataverse.wbb.wbb_teams.espn_wbb_teams(groups=None, return_as_pandas=False, \*\*kwargs) +espn_wbb_teams - look up the women’s college basketball teams + +Args: + + groups (int): Used to define different divisions. 50 is Division I, 51 is Division II/Division III. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing teams for the requested league. + +Example: + + wbb_df = sportsdataverse.wbb.espn_wbb_teams() + ## Module contents diff --git a/docs/docs/wnba/index.md b/docs/docs/wnba/index.md index e8855ff..50684d0 100755 --- a/docs/docs/wnba/index.md +++ b/docs/docs/wnba/index.md @@ -4,12 +4,220 @@ ## sportsdataverse.wnba.wnba_game_rosters module + +### sportsdataverse.wnba.wnba_game_rosters.espn_wnba_game_rosters(game_id: int, raw=False, return_as_pandas=False, \*\*kwargs) +espn_wnba_game_rosters() - Pull the game by id. + +Args: + + game_id (int): Unique game_id, can be obtained from espn_wnba_schedule(). + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Data frame of game roster data with columns: + ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, + ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, + ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, + ‘age’, ‘date_of_birth’, ‘slug’, ‘jersey’, ‘linked’, ‘active’, + ‘alternate_ids_sdr’, ‘birth_place_city’, ‘birth_place_state’, + ‘birth_place_country’, ‘headshot_href’, ‘headshot_alt’, + ‘experience_years’, ‘experience_display_value’, + ‘experience_abbreviation’, ‘status_id’, ‘status_name’, ‘status_type’, + ‘status_abbreviation’, ‘hand_type’, ‘hand_abbreviation’, + ‘hand_display_value’, ‘draft_display_text’, ‘draft_round’, ‘draft_year’, + ‘draft_selection’, ‘player_id’, ‘starter’, ‘valid’, ‘did_not_play’, + ‘display_name’, ‘ejected’, ‘athlete_href’, ‘position_href’, + ‘statistics_href’, ‘team_id’, ‘team_guid’, ‘team_uid’, ‘team_slug’, + ‘team_location’, ‘team_name’, ‘team_abbreviation’, + ‘team_display_name’, ‘team_short_display_name’, ‘team_color’, + ‘team_alternate_color’, ‘is_active’, ‘is_all_star’, + ‘logo_href’, ‘logo_dark_href’, ‘game_id’ + +Example: + + wnba_df = sportsdataverse.wnba.espn_wnba_game_rosters(game_id=401370395) + + +### sportsdataverse.wnba.wnba_game_rosters.helper_wnba_athlete_items(teams_rosters, \*\*kwargs) + +### sportsdataverse.wnba.wnba_game_rosters.helper_wnba_game_items(summary) + +### sportsdataverse.wnba.wnba_game_rosters.helper_wnba_roster_items(items, summary_url, \*\*kwargs) + +### sportsdataverse.wnba.wnba_game_rosters.helper_wnba_team_items(items, \*\*kwargs) ## sportsdataverse.wnba.wnba_loaders module + +### sportsdataverse.wnba.wnba_loaders.load_wnba_pbp(seasons: List[int], return_as_pandas=False) +Load WNBA play by play data going back to 2002 + +Example: + + wnba_df = sportsdataverse.wnba.load_wnba_pbp(seasons=range(2002,2022)) + +Args: + + seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing the + play-by-plays available for the requested seasons. + +Raises: + + ValueError: If season is less than 2002. + + +### sportsdataverse.wnba.wnba_loaders.load_wnba_player_boxscore(seasons: List[int], return_as_pandas=False) +Load WNBA player boxscore data + +Example: + + wnba_df = sportsdataverse.wnba.load_wnba_player_boxscore(seasons=range(2002,2022)) + +Args: + + seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing the + player boxscores available for the requested seasons. + +Raises: + + ValueError: If season is less than 2002. + + +### sportsdataverse.wnba.wnba_loaders.load_wnba_schedule(seasons: List[int], return_as_pandas=False) +Load WNBA schedule data + +Example: + + wnba_df = sportsdataverse.wnba.load_wnba_schedule(seasons=range(2002,2022)) + +Args: + + seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing the + schedule for the requested seasons. + +Raises: + + ValueError: If season is less than 2002. + + +### sportsdataverse.wnba.wnba_loaders.load_wnba_team_boxscore(seasons: List[int], return_as_pandas=False) +Load WNBA team boxscore data + +Example: + + wnba_df = sportsdataverse.wnba.load_wnba_team_boxscore(seasons=range(2002,2022)) + +Args: + + seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing the + team boxscores available for the requested seasons. + +Raises: + + ValueError: If season is less than 2002. + ## sportsdataverse.wnba.wnba_pbp module + +### sportsdataverse.wnba.wnba_pbp.espn_wnba_pbp(game_id: int, raw=False, \*\*kwargs) +espn_wnba_pbp() - Pull the game by id. Data from API endpoints - wnba/playbyplay, wnba/summary + +Args: + + game_id (int): Unique game_id, can be obtained from wnba_schedule(). + +Returns: + + Dict: Dictionary of game data with keys - “gameId”, “plays”, “winprobability”, “boxscore”, “header”, + + “broadcasts”, “videos”, “playByPlaySource”, “standings”, “leaders”, “seasonseries”, “timeouts”, + “pickcenter”, “againstTheSpread”, “odds”, “predictor”, “espnWP”, “gameInfo”, “season” + +Example: + + wnba_df = sportsdataverse.wnba.espn_wnba_pbp(game_id=401370395) + + +### sportsdataverse.wnba.wnba_pbp.helper_wnba_game_data(pbp_txt, init) + +### sportsdataverse.wnba.wnba_pbp.helper_wnba_pbp(game_id, pbp_txt) + +### sportsdataverse.wnba.wnba_pbp.helper_wnba_pbp_features(game_id, pbp_txt, init) + +### sportsdataverse.wnba.wnba_pbp.helper_wnba_pickcenter(pbp_txt) + +### sportsdataverse.wnba.wnba_pbp.wnba_pbp_disk(game_id, path_to_json) ## sportsdataverse.wnba.wnba_schedule module + +### sportsdataverse.wnba.wnba_schedule.espn_wnba_calendar(season=None, ondays=None, return_as_pandas=False, \*\*kwargs) +espn_wnba_calendar - look up the WNBA calendar for a given season + +Args: + + season (int): Used to define different seasons. 2002 is the earliest available season. + ondays (boolean): Used to return dates for calendar ondays + +Returns: + + pd.DataFrame: Pandas dataframe containing calendar dates for the requested season. + +Raises: + + ValueError: If season is less than 2002. + + +### sportsdataverse.wnba.wnba_schedule.espn_wnba_schedule(dates=None, season_type=None, limit=500, return_as_pandas=False, \*\*kwargs) +espn_wnba_schedule - look up the WNBA schedule for a given season + +Args: + + dates (int): Used to define different seasons. 2002 is the earliest available season. + season_type (int): 2 for regular season, 3 for post-season, 4 for off-season. + limit (int): number of records to return, default: 500. + +Returns: + + pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. + + +### sportsdataverse.wnba.wnba_schedule.most_recent_wnba_season() ## sportsdataverse.wnba.wnba_teams module + +### sportsdataverse.wnba.wnba_teams.espn_wnba_teams(return_as_pandas=False, \*\*kwargs) +espn_wnba_teams - look up WNBA teams + +Args: + + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + +Returns: + + pd.DataFrame: Pandas dataframe containing teams for the requested league. + +Example: + + wnba_df = sportsdataverse.wnba.espn_wnba_teams() + ## Module contents diff --git a/sportsdataverse/mlb/mlbam_games.py b/sportsdataverse/mlb/mlbam_games.py index 875fd66..a431543 100755 --- a/sportsdataverse/mlb/mlbam_games.py +++ b/sportsdataverse/mlb/mlbam_games.py @@ -76,11 +76,11 @@ def mlbam_schedule(season: int, gameType="R"): resp_json = json.loads(resp_str) try: result_count = int(resp_json["org_game_type_date_info"]["queryResults"]["totalSize"]) - except: + except Exception: result_count = 0 if result_count > 0: main_df = json_normalize(resp_json["org_game_type_date_info"]["queryResults"]["row"]) else: - print(f"No results found for the provided playerID. \nTry a diffrient search for better results.") + print("No results found for the provided playerID. \nTry a different search for better results.") return main_df diff --git a/sportsdataverse/mlb/mlbam_players.py b/sportsdataverse/mlb/mlbam_players.py index 3eb6079..f13055b 100755 --- a/sportsdataverse/mlb/mlbam_players.py +++ b/sportsdataverse/mlb/mlbam_players.py @@ -101,14 +101,14 @@ def mlbam_player_info(playerID: int): resp_json = json.loads(resp_str) try: result_count = int(resp_json["player_info"]["queryResults"]["totalSize"]) - except: + except Exception: result_count = 0 if result_count > 0: main_df = json_normalize(resp_json["player_info"]["queryResults"]["row"]) print("Done") else: - print(f"No results found for the provided playerID. \nTry a different search for better results.") + print("No results found for the provided playerID. \nTry a different search for better results.") return main_df @@ -152,7 +152,7 @@ def mlbam_player_teams(playerID: int, season: int): resp_json = json.loads(resp_str) try: result_count = int(resp_json["player_teams"]["queryResults"]["totalSize"]) - except: + except Exception: result_count = 0 if result_count > 0: diff --git a/sportsdataverse/mlb/mlbam_reports.py b/sportsdataverse/mlb/mlbam_reports.py index eb554ac..61a16a9 100755 --- a/sportsdataverse/mlb/mlbam_reports.py +++ b/sportsdataverse/mlb/mlbam_reports.py @@ -50,7 +50,7 @@ def mlbam_transactions(startDate: str, endDate: str): searchURL = searchURL + f"start_date='{sd}'" searchURL = searchURL + f"start_date='{ed}'" - except: + except Exception: print("There's an issue with the way you've formatted you inputs.") try: @@ -61,7 +61,7 @@ def mlbam_transactions(startDate: str, endDate: str): resp_json = json.loads(resp_str) try: result_count = int(resp_json["transaction_all"]["queryResults"]["totalSize"]) - except: + except Exception: result_count = 0 if result_count > 0: @@ -71,7 +71,7 @@ def mlbam_transactions(startDate: str, endDate: str): else: print(f"No results found for the provided playerID. \nTry a different search for better results.") return main_df - except: + except Exception: print("Could not locate dates ") @@ -122,7 +122,7 @@ def mlbam_broadcast_info(season: int, home_away="e"): resp_json = json.loads(resp_str) try: result_count = int(resp_json["mlb_broadcast_info"]["queryResults"]["totalSize"]) - except: + except Exception: result_count = 0 if result_count > 0: diff --git a/sportsdataverse/mlb/mlbam_stats.py b/sportsdataverse/mlb/mlbam_stats.py index bcdfee6..691a6fa 100755 --- a/sportsdataverse/mlb/mlbam_stats.py +++ b/sportsdataverse/mlb/mlbam_stats.py @@ -89,7 +89,7 @@ def mlbam_player_season_hitting_stats(playerID: int, season: int, gameType="R"): resp_json = json.loads(resp_str) try: result_count = int(resp_json["sport_hitting_tm"]["queryResults"]["totalSize"]) - except: + except Exception: result_count = 0 if result_count > 0: @@ -178,7 +178,7 @@ def mlbam_player_season_pitching_stats(playerID: int, season: int, gameType="R") resp_json = json.loads(resp_str) try: result_count = int(resp_json["sport_pitching_tm"]["queryResults"]["totalSize"]) - except: + except Exception: result_count = 0 if result_count > 0: @@ -254,7 +254,7 @@ def mlbam_player_career_hitting_stats(playerID: int, gameType="R"): resp_json = json.loads(resp_str) try: result_count = int(resp_json["sport_career_hitting"]["queryResults"]["totalSize"]) - except: + except Exception: result_count = 0 if result_count > 0: @@ -331,7 +331,7 @@ def mlbam_player_career_pitching_stats(playerID: int, gameType="R"): resp_json = json.loads(resp_str) try: result_count = int(resp_json["sport_career_pitching"]["queryResults"]["totalSize"]) - except: + except Exception: result_count = 0 if result_count > 0: diff --git a/sportsdataverse/mlb/mlbam_teams.py b/sportsdataverse/mlb/mlbam_teams.py index 54079a7..cfb3374 100755 --- a/sportsdataverse/mlb/mlbam_teams.py +++ b/sportsdataverse/mlb/mlbam_teams.py @@ -55,7 +55,7 @@ def mlbam_teams(season: int, retriveAllStarRosters=False): resp_json = json.loads(resp_str) try: result_count = int(resp_json["team_all_season"]["queryResults"]["totalSize"]) - except: + except Exception: result_count = 0 if result_count > 0: @@ -94,7 +94,7 @@ def mlbam_40_man_roster(teamID: int): resp_json = json.loads(resp_str) try: result_count = int(resp_json["roster_40"]["queryResults"]["totalSize"]) - except: + except Exception: result_count = 0 if result_count > 0: @@ -149,7 +149,7 @@ def mlbam_team_roster(teamID: int, startSeason: int, endSeason: int): resp_json = json.loads(resp_str) try: result_count = int(resp_json["roster_team_alltime"]["queryResults"]["totalSize"]) - except: + except Exception: result_count = 0 if result_count > 0: diff --git a/sportsdataverse/mlb/retrosheet.py b/sportsdataverse/mlb/retrosheet.py index 9af94fa..d8a19ab 100755 --- a/sportsdataverse/mlb/retrosheet.py +++ b/sportsdataverse/mlb/retrosheet.py @@ -15,7 +15,7 @@ from tqdm import tqdm -def retrosheet_ballparks() -> pl.DataFrame(): +def retrosheet_ballparks() -> pd.DataFrame(): """ Retrives the current TEAMABR.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. @@ -45,7 +45,7 @@ def retrosheet_ballparks() -> pl.DataFrame(): return pd.DataFrame() -def retrosheet_ejections() -> pl.DataFrame(): +def retrosheet_ejections() -> pd.DataFrame(): """ Retrives the current Ejecdata.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. @@ -77,7 +77,7 @@ def retrosheet_ejections() -> pl.DataFrame(): return pd.DataFrame() -def retrosheet_franchises() -> pl.DataFrame(): +def retrosheet_franchises() -> pd.DataFrame(): """ Retrives the current TEAMABR.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. @@ -98,7 +98,7 @@ def retrosheet_franchises() -> pl.DataFrame(): return pd.DataFrame() -def retrosheet_people() -> pl.DataFrame(): +def retrosheet_people() -> pd.DataFrame(): """ Retrives the current BioFile.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. @@ -156,7 +156,7 @@ def retrosheet_people() -> pl.DataFrame(): return pd.DataFrame() -def retrosheet_schedule(first_season: int, last_season=None, original_2020_schedule=False) -> pl.DataFrame(): +def retrosheet_schedule(first_season: int, last_season=None, original_2020_schedule=False) -> pd.DataFrame(): """ Retrives the scheduled games of an MLB season, or MLB seasons. @@ -181,7 +181,7 @@ def retrosheet_schedule(first_season: int, last_season=None, original_2020_sched season_schedule_df = pd.DataFrame() try: last_season = int(last_season) - except: + except Exception: last_season = None if last_season == None: @@ -235,7 +235,7 @@ def retrosheet_schedule(first_season: int, last_season=None, original_2020_sched def retrosheet_game_logs_team( first_season: int, last_season=None, game_type="regular", filter_out_seasons=True -) -> pl.DataFrame(): +) -> pd.DataFrame(): """ Retrives the team-level stats for MLB games in a season, or range of seasons. THIS DOES NOT GET PLAYER STATS! @@ -270,7 +270,7 @@ def retrosheet_game_logs_team( season_game_log_df = pd.DataFrame() try: last_season = int(last_season) - except: + except Exception: last_season = None columns_list = [ diff --git a/sportsdataverse/mlb/retrosplits.py b/sportsdataverse/mlb/retrosplits.py index e10bf68..703bdc9 100755 --- a/sportsdataverse/mlb/retrosplits.py +++ b/sportsdataverse/mlb/retrosplits.py @@ -22,7 +22,7 @@ from sportsdataverse.errors import SeasonNotFoundError -def retrosplits_game_logs_player(first_season: int, last_season=None) -> pl.DataFrame(): +def retrosplits_game_logs_player(first_season: int, last_season=None) -> pd.DataFrame(): """ Retrives game-level player stats from the Retrosplits project. @@ -72,7 +72,7 @@ def retrosplits_game_logs_player(first_season: int, last_season=None) -> pl.Data return main_df -def retrosplits_game_logs_team(first_season: int, last_season=None) -> pl.DataFrame(): +def retrosplits_game_logs_team(first_season: int, last_season=None) -> pd.DataFrame(): """ Retrives game-level team stats from the Retrosplits project. @@ -124,7 +124,7 @@ def retrosplits_game_logs_team(first_season: int, last_season=None) -> pl.DataFr return main_df -def retrosplits_player_batting_by_position(first_season: int, last_season=None) -> pl.DataFrame(): +def retrosplits_player_batting_by_position(first_season: int, last_season=None) -> pd.DataFrame(): """ Retrives player-level, batting by position split stats from the Retrosplits project. The stats returned by this function are season-level stats, not game-level stats. @@ -187,7 +187,7 @@ def retrosplits_player_batting_by_position(first_season: int, last_season=None) return main_df -def retrosplits_player_batting_by_runners(first_season: int, last_season=None) -> pl.DataFrame(): +def retrosplits_player_batting_by_runners(first_season: int, last_season=None) -> pd.DataFrame(): """ Retrives player-level, batting by runners split stats from the Retrosplits project. The stats are batting stats, based off of how many runners are on base at the time of the at bat. @@ -251,7 +251,7 @@ def retrosplits_player_batting_by_runners(first_season: int, last_season=None) - return main_df -def retrosplits_player_batting_by_platoon(first_season: int, last_season=None) -> pl.DataFrame(): +def retrosplits_player_batting_by_platoon(first_season: int, last_season=None) -> pd.DataFrame(): """ Retrives player-level, batting by platoon (left/right hitting vs. left/right pitching) split stats from the Retrosplits project. The stats are batting stats, based off of the handedness of the batter vs the handedness of the pitcher. @@ -318,7 +318,7 @@ def retrosplits_player_batting_by_platoon(first_season: int, last_season=None) - return main_df -def retrosplits_player_head_to_head_stats(first_season: int, last_season=None) -> pl.DataFrame(): +def retrosplits_player_head_to_head_stats(first_season: int, last_season=None) -> pd.DataFrame(): """ Retrives batter vs. pitcher stats from the Retrosplits project. The stats are batting stats, based off of the preformance of that specific batter agianst a specific pitcher for the durration of that specific season. @@ -385,7 +385,7 @@ def retrosplits_player_head_to_head_stats(first_season: int, last_season=None) - return main_df -def retrosplits_player_pitching_by_runners(first_season: int, last_season=None) -> pl.DataFrame(): +def retrosplits_player_pitching_by_runners(first_season: int, last_season=None) -> pd.DataFrame(): """ Retrives player-level, pitching by runners split stats from the Retrosplits project. The stats are pitching stats, based off of how many runners are on base at the time of the at bat. @@ -450,7 +450,7 @@ def retrosplits_player_pitching_by_runners(first_season: int, last_season=None) return main_df -def retrosplits_player_pitching_by_platoon(first_season: int, last_season=None) -> pl.DataFrame(): +def retrosplits_player_pitching_by_platoon(first_season: int, last_season=None) -> pd.DataFrame(): """ Retrives player-level, pitching by platoon (left/right pitching vs. left/right hitting) split stats from the Retrosplits project. The stats are pitching stats, based off of the handedness of the pitcher vs the handedness of the batter. From 138e5465f5c47b28830db4d834047906c49a6079 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Mon, 31 Jul 2023 03:28:02 -0400 Subject: [PATCH 48/79] schedule function drops leaders key --- .../_build/doctrees/environment.pickle | Bin 924610 -> 924066 bytes sportsdataverse/cfb/cfb_schedule.py | 2 +- sportsdataverse/mbb/mbb_schedule.py | 2 +- sportsdataverse/nba/nba_schedule.py | 12 +----------- sportsdataverse/nfl/nfl_schedule.py | 2 +- sportsdataverse/nhl/nhl_schedule.py | 12 +----------- sportsdataverse/wbb/wbb_schedule.py | 2 +- sportsdataverse/wnba/wnba_schedule.py | 12 +----------- 8 files changed, 7 insertions(+), 37 deletions(-) diff --git a/Sphinx-docs/_build/doctrees/environment.pickle b/Sphinx-docs/_build/doctrees/environment.pickle index 17cfdcfe1806be8130c35d8ad5d1ae8599abaae1..78b0af5bb735ae10a7b7d2ca970b62cca25d3595 100755 GIT binary patch delta 52279 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
>pqb* ztDUpp7bNba1U|C7QM%%PPqH=AH~;%H{Eqmk7=9o4X_9#jAT$<8B|oP~IX`3O?LQB* zqDDaBjXWoWU1z=fRLn!Mi z3M7jHZFgI&d}J8Q#!?Hy*tMY>Mw{4W_-|Ix=vt63s=G4_mN=ip*7*Sa^Rrnz zecy@iOJ>~>ayFSQ#Ps_VR)^`FRJIt?FH>1PJ*TmyShB5x#pCCVcw{>3ihv>MtP;~> zlUOouN@uVwoC$HCAJSQW%eKC7dXSP~2bQE7&42?S9ncaeb?Zqrr#V^8~pe~{vE4#3j_#B+gHep&& z!t&I*0{HoC=Ah=3K!xIqIXR-PhjXBzP#~`>TPo@t%9FaWNd9J5HV=yo?8dG~gpJ)m zQt%v{%O&EwVXIF8a#}Y)%4&IPqiQ)SjSOONCg4K@6*E)T14(KO!IzZ)4C-My+z+va&?krl+ z8O~3|St5DGNT35%OhmRx1ppatrs=`1K+4V@qU%rfV0Ykibx+at=X$c$BrEKCxZ$2h zKriDT5zXo_g~8rOG-NtIpTg1rJP>IV_7-Y%pf{T*cq!!eGD|eizlkLPQV?I%M<~iu zeMD`sH?v#-s_mz8~Z3)XnF(bYo|64ccCiUswCs?pmE<}BuDCH;m@;myn zv_PB(mA)2I{h0+z0l(z=f&BslJ?NKZioo%%ResduW0t=AC0`hm{&2%v$z0xaE9{LWL)r zSpnpgnAxq6d}d}1kj%7z4HZKGdIxS@=`4}5p56Q_O zv~YG7%58Rq);0XtDWA@~6*6=ff&54iv+!v_O78ul=4&|H1bq+1^ZA-!R=|@tF{@p# zBZQH}>oZLjJ~f!tivd405S<^T@4;w;zV(Af!#IyqZ_wiB>zPHEJ~7te))O9F3mU%X zDj4mRPl9bh6v2vvck2($dAGJ7 zJ}QQNqJm@NmHk*3{>?2QhBK|KlfR4+cDg^=U!vNVn43O=^K-oSF zRP93oUC%@Yf>)smKVdyk40yc{zjr=-x*9J0H-Of4-vF+XmgKhUP zc7;4EaKEcrRv`Mvil)Gn5wF~75y<=H!Y;LZokb{CO^USVYDQDyPglc~4IHi=&bVmz zaDHICHIh%B1u6oUF8o>Kj^y1(g5YR2fU@D-I+6`n3gjPvNpsFfc7xKgmLd>Xkb~tR z>46U6Qa$5fH{tW;qr@D#XB4@Ocniyl*kVDufTz4@4d=0=*=&(9 zn13@GD%(0*xCKFD*m9(~VT@3g$g$#i%UESfAH>&<19l6>34d<)IJVSIGh^S1s*l`E1D7g`15zySIu$&wZIjs$%>Hz;@VLO2A`kA&6!8pBRH{IqIt@Di%`A{7 zl#b&CZ^7e7ofWI0xmck(^eYg(;Vsr(XAr>qT8%Y=&jp`sK}LWw@c>>II5R41|C+`| zAR;(nTT!84HlIq*)7esC84G9*uAEUJCc zyrRpzx=gqbkCfqjNS+ukEC)@+swzNt?ks2dNb2=+Hb-=7U(Tl3!uaSJz=$&9@C~t+ znIMakGgxndHk@ypZj-%l8B7*5f**YwbhdgpGxMgIEJ|QI_?{bVvb&~YuUQI+>QmAz zmZFeuT4IaflV-6SM8-&-R|j>StOF%b@nH~CU0wNHFxg7UQ;VS_4Cf?dBMePCAtmwA z@326ngT%m{jP0J{WObtKFkUzZCW^C8HctqCpis+MLh#yKa{S#fK(wHOMMu_@&MGgf zt0>o05xl)2d`t!O#V>Qj>a3~)QX%eZu*N=f#c2M^T%m;NmEb%>KCfiUFpaMg!*oj( z;9?UB4s6?7z&9gZCVzP!@A zu&jgA!u-Y@kU9O;hMqm({NrZP&lQX1{$8wk@5&ij(W^^zy;{$*I$>~0?p2I6@N)~W zKfi#p4EZPR{75~!TyO?m*Z|^yK21`f>WG12!zMs_r?EtnQ8RVLaZK5vUX z21g`xoSK4TXmQgmY%zJ^puo$ZiT#(co=9ijGBKN;S;i!MUUsXP`2v=+ zwW{9YDIWgPaxoA8Yq>J#6!N1hK+XCtl3C`tE5On$8U*C2#8E8v;R+Ulguht<;RKp7 zhwx)7K{ZDA0fqrDm_L@HDhH5vJ7?T3byK$JG>YTut%D)f8@AP0_~H6l`2g zvBuRDYFtf`#??-&V0E2yD!E^iESTWj>Cio&_uZ!qPJUAQ;$OoV1Z!} zcq^aZtS|bgK*_(0#p$bkLajD<9aKxAr+TD5Y9Tq=<%LMy5_m%wX>?qn)z8&CJlSm7S>5G zw1YCyJGZc|`uvwDf3QC5l`x*Nm5tD6y&lFFZe>^Mv)&5h`?j(|eb&)19=MGS(r3L* zS^fF6Xy)hA8Gi%sa~m6`$9+GH|Fq4O;)f9T+s=mQu}+5Z&D&XTeFvTvy>oUuOV?w5 z3hmgzhUziD5SW!;D3aZ_gW2>5UkQXacCbu+_8F1=sKXQ;q3?oossb#)4EP};mzUEGt zBS!Dn!22U1yZ+|2S?jADvUce^w*Ot7$ZGgJep zF*ba6UTFl+2V20mwFp>>OT{sw;IzL2upo`4x&Y$|*m9q$IK){JFT9WS(DRzA@=6^F z(bPCaZK*^CI3RC8^e4YF`2M%Q!2n#?kN9e1T>&kzC zz_5#4u0r52J$iva|KUM)r9OMGuA1N+m|6E6W?_1SVSL9!Y=S;#1owTI^_CT!q&JGB z%L^YD3Jzad&jHV~_F)#TFE>W%s~7=)%?&yi-qXPOcG?;cCi$_o=jEsWy- z5YY~Csbn4&A161K3f5x53<}(0VB4y+iGT}r@&c+9`nK@r_pnZS5{pFL#UFv#U9bXq z^j;RMM_nvXX~4Rm2Jn+TnZFCK4FWg$C3ms`e8NDB3kS<^P-j3CJW#ct#QzGxf7{a{ z{?4-@fbLSqYEcJRmrx$u2b|vRkFr2L!;JzV?_Cyx+7zAa1NmPVS;Kz}2hO!M{ zR_1AfwW>w8KM2EB-bnD{Th^-F5#BR`JMbhz5DC8QW_6*nPo=YN14OA0oVWAbCe?Zo zi35Vf+D`=u-oRifczY95rUXKI5r22wYsrThE*EZ0G5@{ zmBdbmg?}^0M;XRc^gU6jzohy&l>LFohH{DrM%gDY8+$3(MX;xo`Z)@gpRMo-1DxxA5DWnO!e}&jnI4cz7DpNx;-2eJPN@sKa1@otH=#S$wVMr8}?K zpjrW9;B2@F&Lff3!Jx>-cx-vWV*B$23UQB5Nu{8u1=DN+_z zl>HNBi!qEfCvepPA^3TOXK>v2agnZ1gyMm1-wm$C9vdJP*qFtIYd?PONys02CvFx3 zVZJ28WY$Z~%1`Zu$trjw8_1X3$^7-OKmlvp$a>*!l8v7|wMV@t1{3H&Z2~2P z{(=rnpvbepimNRznOi))>V}%qs`=%Lya;_DqGyP0C|Qn_KS*#1lc=)3r|hAEIy&aC zr4-1=!fHt03fM!(9)p<^rq)4jygLLC1{48f`1xnl5l7TVQ{%8;6n{9ys?5m*8z&0G zT$!V<@EVa#Q)E-BHAI=HsLTY>2y#OK7|g4NXx5i7CW_)mL0Y)66~liT;wH~20t4nu z#T%!B(?oW_J~i4w*`*>|%$>@%m>U6d|6;e(GVZ%(>vH_+u zdHX9+AxNJss1^i4<6##MZWmW45Q5eSC@6kh%YNt?2*yV$3PlJDa0p}qELRpQ<=%)& zoyn)|SEWtaH;d-sfPfN7{KNg~bVu-dDn_7w2UJr?pj$M_;I^7-i3zw|X(q>M8{nZl zc@*=DC@ZfmnO#w%Xe{BbQY9ZsSAX;1gX+Xg2)7G_R>sIDofdSS)S>#c{2iyyOdEBbZu7vQg zK!8>#0T{~O%|i~c!Fok`R45AVHf}q_LiMmG1Q9eA9GaBC&mUrbdW5I2MFaTgm(|&g zYI)YJT*&J>5vWB~YNN6)R7akBV$XB(dD~8XR#Ft64&L+Z7>(Pkl|D zfC=GmqV4K-lWepJ_P!{r>Cl-!yXnJ+0^{BXMVG|zs)@|(G5}5r43ZxVfM~w!b=75| zN=_p}2JTx1x(<#{)xiNF6*y~B!eU&-`UM~T2D@6{+OPPIH`Is;Q9r|fenX9zP|i6Z z>l!hk{n!&z;Jk>w@!v;?XzFuss+O9{{|6}!!2R1GIa){AKg#G3Pw7UX6g}`~jaFBF z-mTqf$kV(!5iY`OM%#R#q6ZDw=k=*ZLmMTqaRp%?>$rg$3n+TgU9fdB+ zDP{ir;UliYqZ2>(4p?#+n_whLVSS!ms?BOdCsbg7109ODsPj=r)esKBBLp1#nBd!w zsv#AEM+-R3Vz6x-7X`N!2}P>~!_o8?Iu>bS+L&U^&T6$EkS)9pha zJ+3YU39GxHK>-+bKNBDiBe5TeDe~UvSucP%LM{Vj00u;rtH4%cw zz#wx97{UDHJS_xB7}Wv;JjqZ_81KBV8eBrC5ePIIl^uLFAVU}nbb2!O?<_$t6!7HG zTELKulvg2Cb%6uTq@h zTmisX9-n8|;*o^1OQ1lT6yreIdqg&cAr~=QEQISW7&e6Xm_USKlP2pDWj`qzMPsF4 zF-vn{>uD-Op$ve<@{bm4BZNvnCy2rbf$f~d@B=|fHAqev`vnFJfMk8OFQ`4MxLAaJ zNMK{&F>;|6cqG`XqDb{=bu)jtH{xAFGo)Wgi#W zFc1plX=@YDKMgu_@?$k*M~LsKh`o5Xy{i5a>;nPAXe6DEP<4i&Cj`@9{{wv0Y0DrK zR(vb71#6pSgmg-fSav_E#wXxw7k{c+5kmMxAb@XpxvX@Q{kh2g>{HfXpZ%rC29bx- zp^g~csG)LSt0JR~csg&f=`+_V&k7=>k%XWe$WWtbMCv<%2J5waJq!O9*)YbGr3+#I zS7bvw;`m!Dp|uJ2ASF;h2tUc>zED@Rlzm=g!;Wx~Mum4BNteHgWL07+(F!!2C@3X$&l?!sBN{D{JT9*euC!WH}pQFL_qctRgDr z9D>+94o`H*2Q2i^;cn1+JZzyAp5O+?GZ7l}UNt?+3#C~oX((3oho#p=eS2F-GV;!dFktP#R3W!F=x@C9w+W-Y2Za}g>fH9gRXXi;%1EoZE}O+PK*YXG$@2+ z>){1N7w{$*@MTWiY|#quaD(C&iU!@|2E~mH4f?Db6n810{}sJG>;}e71g-E#Zcq&C zYtSFupco?8pm8y(eo!D9!^a8~?rlocz!Z+dFtP$}cJ_9IVq{l?4tIlML{)>9xA(oZ zc$fx_j#Y(86=Og^gF4)xxY*U8Q{AAj%BfG zbA#fVOF-+v(mHpzfpOWR6@JbQimL_J80KI8gL0>jnV!=H4%;#-y(k_3Ne zIdw3;?^_ntUzdT%UEybC^NQx_E1|NQ@{;+b5V`CDnX`(V7g7lRjzzjx*OT}8j*ZWc zM{4*pt7t{DGaH|BlqaMo69w3n_|%s_^&J~UFR9}z$uMpq!PyWJoSPs)+Xo4n8c1;P zLV`mH64Wn9usC>OskNkx^ zQ+(^pEYpi-{|&E?r}vgG`!|uoc@kQcG$B}W(PRh%241qlVIpsGN!y)`qSn31CHN#Dj(FArlrUKYn z3eks(*^3}r576g^n^Lgelfz9JR>Hc)v5e=9Fh%g)BTZ%mbr~U0MvX9~;q$x^resW| z5vHpIFWVg3fp82Tvz5gm#~FojX&>SK%`7Q^3&X0eHaSh$`*E}pKVQ>7buu)~y?LUrg`DxS-#7FOa5 z3|QY7hbbYQvi0(!i~xFJTiznv*xKYU9bmB}n*c}9%H}%sH@}0iA{dF}-IrJ+EkdE{ z_+E!8rgeLfX=*%3qQz-S5hCj27yu%Q;Aie(Q7EEeP9e-WPE$HQuXCC*%&3cv(Yzp$ zMW+ePY>b#yUQt~>pN=-K2nfAGHwPlWbdWv+}ljQ#OD2 z7SpK_)Z$=A7VsTHuYKV0MJ?P;;2hGpU==*Ss-_lhT9oziYDX~?4&^ELvG6p)pYB); zId-?hkOXRQtmEBpHAOi{3O1D_Ex0JpLp7A|i8}&_nn5Zj-+|V`*~(5Uzq-`~|9#=V zU+Y)5n(HU{Txa4HADT0;6QmE#oiTk*PCxz7+ykFuJ~H>hbj(NQu9z;CpC9|k+!LR_ z_{iK1)2I^y-yx^`{46+C(U`7t~_bZ#kBRLxk~^FqiAP9 z(P@Yzo;1hs{4ARtfuU`pva8$7*#gOGE81h{W7^CKmU+|R_%p)94E#@}RH z$bLlRSTfjp-Y>_J1j(cvOFSeuxmr8gP}<7wgHw@ zOdlCwNgqHGxte4qh7`uX+y;E9<7Qz28^s8fLu4?4^5B6~cm6<2G}Jv}pe2Eyct_Mo zM<=bVcMP=57!Xg@*c}r=sy5dM8!*yoi6s^p%2{HeNv3FtXm9lrOIm*_`Cicsrm#Sw z*H}RF87>=4%+fDMq?D-DG-$`CVkby5}Dbhb4trA!GvLhQtB;X{Nrp} z6rEYTuHKU6^K23-`GVGe*IOQRQ}VC<4XJl4sP{J}Z=z5Z@XD=Bcu1JKd@I>=YfNb(nh?BT6ve`e$T_&k$OtS@rPohF|YwwdR3 z@`3lpf3flx;4L>ia@YzN91IL(t9WoHpG3B~6%b~_-LiaRCOodKd$%lPnWg#W<5GzY+ zz3Z?~^L6}ngm|h*^~|0PiL5VK=MzKBxRa<`eW}G6|$nGd|jp~ zozAZT#_CK{Z%FoMn)0AWzR5J9a)fj?b%kU=XHz%vl*Y$wWd7psXgVz?z|nkrCl*FM z0m$@Z6O@Ix=tN-apB}Irj`QEpU>54YQw~|sE8_zXS=`(*xcMN$muArAhlF8$@Q^U9 zM-N#t%(xx^#|-XTh)Jczfoe72b_QW)WWNkC`5lK~08yC;yi*|p_*{#gS){n{A*kLK ziB~7IKL4_%JDK5g?+G&;@xG|E-}@F?4NQFB67NeS6X35{A}LHW)~lK3wR{hhU~5rT zqL4KA`zyRoo?OTRTEqTs`FxlI7jrhU5?GRaDzYDT$0W4Fa;CgNC|i~ifCS0rR9A|R zPO*}0u1>Mi9JC?DN|VUbDb_^M>~KEr85S8&{9ZtTa-%q0+z=z$wl&2X6GORfd|+N3 zUv~`Nqs6B1n^LV=KADMVf^TWzvCJ*%i?K*!^znYYm|7J>uBc5L#*Vb=_k&#rj9WDm| zY0fFHEQSyUaKMkjYpi(Su64~KYtS``8*YOe)9@dS3|ehq5qV_ry@#zd0*@Y6$nv-JbBz2EiBy2f6T!6@rWWSWzUxqv+-diwxj7N3Dt2e#cR321%x9C@Y!>h=_%uCk*eZY_M;+t%n94`7mD3ttCd zQ5Q)zN>R{g^@a~$B}<4tP!vR!mT~Z$n}QR;PfTEue#8$yb<`RT&6xsrIDt5j%{8c& zX08#y*)eN$6rlvE;*-Z=3qSCVHIcQf1al7m{o#K=>&bTlzaHbWb^t$|Zll%eztU~A zT20Il>D3uFTCLucVWZXR-V7V9R!_n+>1WYje2Q9qcyec31h<|vhht|C&$Q9fbz!C` z{E(cU%(T(cHM+BnmadM@wmw3&?l2eKV*#y9;Rle=^)H!iLcbpGY@=1|C!K9wkaVXk z@jNigMyur7EE}y&x6A39a{5D-V4-stkzUtD@ZZ=)uymk{?J5xIFI{YjLXEn2v89Q> zeonU~0cd8nEftd6vTZpce_9t?wjat?o;~0QUI6!l^U^-&Xq9?P>)ssO^KP@pZ(gO= zwnp{04K}t){dthB#ICGT{|w>`<=QHBGMXA5JkBPsQfnQX07LWQaX@IZ0GX04G>Jsw zl_$}el=F~d4-^WfP)ptX`LL=fURYjEF+o_>KIS;c1IF3n#5j{D7gr_a1HsuB<3!K} z5#!W?r}bLz8fW{^7khcpbvAhKf21`U&cNAPpS{jDHvqGlA@xoRW>q@M_6MF2*j&mL5aRo9?&? zn&*!vo8(agwS6hsUhkzWFvkPj=6do8C?0*)M%MYCuiD6o33^TN(fu`Bn&^*Li*mqp z*6|&$!j&&Iui4)8`5~bdT#f{{1z(4I8?4P1s(KsOlDCmVvbf$c2ihukA_&AyH!2+) z035;hd}fOXkR6LV9Xt7wuWa$@MBp!u2LQvf3-YkziPq=8vN>_F*P4CK)}JPkS#~>h z-|cexh}|xW?*_+8pcRc5p^%{eYqw{xEj%g6o&?FjAbUI{lY{JuklZAm$Th~pSNvp- zB^<8t2yXp6$UdK3<4GxY>iPNS-DZ~RywMUyVnh#MO zxcVd-7@?IL);l%T{>8A%pL|j08<3ZzQlUE9i(V|AZKpPGoNXuNdUm#u(uvvjG~s-` zS0tfwY3{g6cneQ)+T-GhFU4opfXHnWhf7p&wj!Jeyg>z*7EllZ6@Hjm$ET&*6ZwRC z2s7ceDZW|Pza}CU93mEjTJN1>uW-}iOBA+fg;?CfYQ$nPDXTC8#g1Aq10`$h5n{M4 zM0Xgpvt)%mB3IVVdmOtVFLEwKlhGd{+l4)?ca0x)Jjo}mvB$-dG=%b_ytHX{3vXIu zPheYG!D^kNSjD<#eC8NzQ!a}!x4zwMPmCA5P^=BKV!#%Aq)q09BKJJxQPH#g_S(rz zmF%_CB)n>`J(F$XyZ73YAvwAi=x^d@_JW>m;z5t%Q)|~p?JG!?AAiYCrtbZhsBFt2 zJNa?RhwKTyxU0lZy+p-}57}cx#&a*(QjIrP?^%QtcYYOn$6ukR01}I^Yat*C1IR);qR>9!BB4H8G;> z4#(ZRZ?~ZMI1)psunpJ;n%ZsR^ST8kLdQ3C3kvZ|A=1.13.0", "pandas>= 1.0.3", - "polars>=0.18.2", + "polars>=0.18.10", "tqdm>=4.50.0", "beautifulsoup4>=4.4.0", "inflection>=0.5.1", diff --git a/sportsdataverse.egg-info/PKG-INFO b/sportsdataverse.egg-info/PKG-INFO deleted file mode 100755 index bd86a9b..0000000 --- a/sportsdataverse.egg-info/PKG-INFO +++ /dev/null @@ -1,83 +0,0 @@ -Metadata-Version: 2.1 -Name: sportsdataverse -Version: 0.0.36 -Summary: Retrieve Sports data in Python -Home-page: https://github.com/sportsdataverse/sportsdataverse-py -Author: Saiem Gilani -Author-email: saiem.gilani@gmail.com -Maintainer: Saiem Gilani -License: MIT -Project-URL: Docs, https://py.sportsdataverse.org/ -Project-URL: Bug Tracker, https://github.com/sportsdataverse/sportsdataverse-py/issues -Keywords: nfl college football data epa statistics web scraping -Classifier: Development Status :: 3 - Alpha -Classifier: Intended Audience :: Developers -Classifier: Topic :: Software Development :: Libraries -Classifier: License :: OSI Approved :: MIT License -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.6 -Classifier: Programming Language :: Python :: 3.7 -Classifier: Programming Language :: Python :: 3.8 -Classifier: Programming Language :: Python :: 3.9 -Description-Content-Type: text/markdown -Provides-Extra: tests -Provides-Extra: docs -Provides-Extra: models -Provides-Extra: all -License-File: LICENSE - -# sportsdataverse-py - - -![Lifecycle:experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg?style=for-the-badge&logo=github) -[![PyPI](https://img.shields.io/pypi/v/sportsdataverse?label=sportsdataverse&logo=python&style=for-the-badge)](https://pypi.org/project/sportsdataverse/) -![Contributors](https://img.shields.io/github/contributors/sportsdataverse/sportsdataverse-py?style=for-the-badge) -[![Twitter -Follow](https://img.shields.io/twitter/follow/sportsdataverse?color=blue&label=%40sportsdataverse&logo=twitter&style=for-the-badge)](https://twitter.com/sportsdataverse) - - - - -See [CHANGELOG.md](https://py.sportsdataverse.org/CHANGELOG) for details. - -The goal of [sportsdataverse-py](https://py.sportsdataverse.org) is to provide the community with a python package for working with sports data as a companion to the [cfbfastR](https://cfbfastR.sportsdataverse.org/), [hoopR](https://hoopR.sportsdataverse.org/), and [wehoop](https://wehoop.sportsdataverse.org/) R packages. Beyond data aggregation and tidying ease, one of the multitude of services that [sportsdataverse-py](https://py.sportsdataverse.org) provides is for benchmarking open-source expected points and win probability metrics for American Football. - -## Installation - -sportsdataverse-py can be installed via pip: - -```bash -pip install sportsdataverse - -# with full dependencies -pip install sportsdataverse[all] -``` - -or from the repo (which may at times be more up to date): - -```bash -git clone https://github.com/sportsdataverse/sportsdataverse-py -cd sportsdataverse-py -pip install -e .[all] -``` - -# **Our Authors** - -- [Saiem Gilani](https://twitter.com/saiemgilani) -@saiemgilani -@saiemgilani - - -## **Citations** - -To cite the [**`sportsdataverse-py`**](https://py.sportsdataverse.org) Python package in publications, use: - -BibTex Citation -```bibtex -@misc{gilani_sdvpy_2021, - author = {Gilani, Saiem}, - title = {sportsdataverse-py: The SportsDataverse's Python Package for Sports Data.}, - url = {https://py.sportsdataverse.org}, - season = {2021} -} -``` diff --git a/sportsdataverse.egg-info/SOURCES.txt b/sportsdataverse.egg-info/SOURCES.txt deleted file mode 100755 index c318593..0000000 --- a/sportsdataverse.egg-info/SOURCES.txt +++ /dev/null @@ -1,256 +0,0 @@ -LICENSE -MANIFEST.in -README.md -requirements.txt -setup.py -docs/build/assets/css/styles.0bedf225.css -docs/node_modules/@docsearch/css/dist/_variables.css -docs/node_modules/@docsearch/css/dist/button.css -docs/node_modules/@docsearch/css/dist/modal.css -docs/node_modules/@docsearch/css/dist/style.css -docs/node_modules/@docusaurus/core/lib/client/baseUrlIssueBanner/styles.module.css -docs/node_modules/@docusaurus/plugin-debug/lib/theme/DebugLayout/styles.module.css -docs/node_modules/@docusaurus/plugin-debug/lib/theme/DebugRegistry/styles.module.css -docs/node_modules/@docusaurus/plugin-debug/lib/theme/DebugRoutes/styles.module.css -docs/node_modules/@docusaurus/plugin-debug/lib/theme/DebugSiteMetadata/styles.module.css -docs/node_modules/@docusaurus/plugin-debug/src/theme/DebugLayout/styles.module.css -docs/node_modules/@docusaurus/plugin-debug/src/theme/DebugRegistry/styles.module.css -docs/node_modules/@docusaurus/plugin-debug/src/theme/DebugRoutes/styles.module.css -docs/node_modules/@docusaurus/plugin-debug/src/theme/DebugSiteMetadata/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib/admonitions.css -docs/node_modules/@docusaurus/theme-classic/lib/nprogress.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/AnnouncementBar/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/BackToTopButton/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/BlogPostAuthors/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/BlogPostItem/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/BlogSidebar/Desktop/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/CodeBlock/Container/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/CodeBlock/Content/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/CodeBlock/CopyButton/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/CodeBlock/Line/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/CodeBlock/WordWrapButton/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/ColorModeToggle/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/Details/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/DocBreadcrumbs/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/DocCard/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/DocCategoryGeneratedIndexPage/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/DocItem/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/DocItemFooter/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/DocPage/Layout/index.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/DocPage/Layout/Main/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/DocPage/Layout/Sidebar/index.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/DocPage/Layout/Sidebar/ExpandButton/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/DocSidebarItem/Html.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/DocSidebarItem/Link.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/DocSidebar/Desktop/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/DocSidebar/Desktop/CollapseButton/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/DocSidebar/Desktop/Content/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/Footer/Logo/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/Heading/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/IconEdit/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/IconExternalLink/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/Layout/styles.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/MDXComponents/Img.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/MDXPage/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/NavbarItem/LocaleDropdownNavbarItem/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/Navbar/Content/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/Navbar/Layout/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/Navbar/Search/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/SkipToContent/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/TOC/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/TOCCollapsible/CollapseButton.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/TOCCollapsible/index.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/TOCInline/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/TabItem/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/Tabs/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/Tag/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/TagsListByLetter/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/TagsListInline/styles.module.css -docs/node_modules/@docusaurus/theme-classic/lib-next/theme/ThemedImage/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/admonitions.css -docs/node_modules/@docusaurus/theme-classic/src/nprogress.css -docs/node_modules/@docusaurus/theme-classic/src/theme/AnnouncementBar/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/BackToTopButton/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/BlogPostAuthors/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/BlogPostItem/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/BlogSidebar/Desktop/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/CodeBlock/Container/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/CodeBlock/Content/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/CodeBlock/CopyButton/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/CodeBlock/Line/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/CodeBlock/WordWrapButton/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/ColorModeToggle/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/Details/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/DocBreadcrumbs/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/DocCard/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/DocCategoryGeneratedIndexPage/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/DocItem/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/DocItemFooter/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/DocPage/Layout/index.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/DocPage/Layout/Main/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/DocPage/Layout/Sidebar/index.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/DocPage/Layout/Sidebar/ExpandButton/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/DocSidebarItem/Html.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/DocSidebarItem/Link.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/DocSidebar/Desktop/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/DocSidebar/Desktop/CollapseButton/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/DocSidebar/Desktop/Content/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/Footer/Logo/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/Heading/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/IconEdit/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/IconExternalLink/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/Layout/styles.css -docs/node_modules/@docusaurus/theme-classic/src/theme/MDXComponents/Img.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/MDXPage/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/NavbarItem/LocaleDropdownNavbarItem/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/Navbar/Content/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/Navbar/Layout/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/Navbar/Search/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/SkipToContent/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/TOC/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/TOCCollapsible/CollapseButton.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/TOCCollapsible/index.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/TOCInline/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/TabItem/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/Tabs/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/Tag/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/TagsListByLetter/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/TagsListInline/styles.module.css -docs/node_modules/@docusaurus/theme-classic/src/theme/ThemedImage/styles.module.css -docs/node_modules/@docusaurus/theme-common/lib/components/Details/styles.module.css -docs/node_modules/@docusaurus/theme-common/lib/hooks/styles.css -docs/node_modules/@docusaurus/theme-common/src/components/Details/styles.module.css -docs/node_modules/@docusaurus/theme-common/src/hooks/styles.css -docs/node_modules/@docusaurus/theme-search-algolia/lib/theme/SearchBar/styles.css -docs/node_modules/@docusaurus/theme-search-algolia/lib/theme/SearchPage/styles.module.css -docs/node_modules/@docusaurus/theme-search-algolia/src/theme/SearchBar/styles.css -docs/node_modules/@docusaurus/theme-search-algolia/src/theme/SearchPage/styles.module.css -docs/node_modules/@svgr/plugin-svgo/node_modules/svgo/Makefile -docs/node_modules/batch/Makefile -docs/node_modules/debug/Makefile -docs/node_modules/es-to-primitive/Makefile -docs/node_modules/hpack.js/node_modules/isarray/Makefile -docs/node_modules/infima/dist/css/default/default-rtl.css -docs/node_modules/infima/dist/css/default/default-rtl.min.css -docs/node_modules/infima/dist/css/default/default.css -docs/node_modules/infima/dist/css/default/default.min.css -docs/node_modules/infima/dist/css/default-dark/default-dark-rtl.css -docs/node_modules/infima/dist/css/default-dark/default-dark-rtl.min.css -docs/node_modules/infima/dist/css/default-dark/default-dark.css -docs/node_modules/infima/dist/css/default-dark/default-dark.min.css -docs/node_modules/nprogress/nprogress.css -docs/node_modules/prismjs/plugins/autolinker/prism-autolinker.css -docs/node_modules/prismjs/plugins/autolinker/prism-autolinker.min.css -docs/node_modules/prismjs/plugins/command-line/prism-command-line.css -docs/node_modules/prismjs/plugins/command-line/prism-command-line.min.css -docs/node_modules/prismjs/plugins/diff-highlight/prism-diff-highlight.css -docs/node_modules/prismjs/plugins/diff-highlight/prism-diff-highlight.min.css -docs/node_modules/prismjs/plugins/inline-color/prism-inline-color.css -docs/node_modules/prismjs/plugins/inline-color/prism-inline-color.min.css -docs/node_modules/prismjs/plugins/line-highlight/prism-line-highlight.css -docs/node_modules/prismjs/plugins/line-highlight/prism-line-highlight.min.css -docs/node_modules/prismjs/plugins/line-numbers/prism-line-numbers.css -docs/node_modules/prismjs/plugins/line-numbers/prism-line-numbers.min.css -docs/node_modules/prismjs/plugins/match-braces/prism-match-braces.css -docs/node_modules/prismjs/plugins/match-braces/prism-match-braces.min.css -docs/node_modules/prismjs/plugins/previewers/prism-previewers.css -docs/node_modules/prismjs/plugins/previewers/prism-previewers.min.css -docs/node_modules/prismjs/plugins/show-invisibles/prism-show-invisibles.css -docs/node_modules/prismjs/plugins/show-invisibles/prism-show-invisibles.min.css -docs/node_modules/prismjs/plugins/toolbar/prism-toolbar.css -docs/node_modules/prismjs/plugins/toolbar/prism-toolbar.min.css -docs/node_modules/prismjs/plugins/treeview/prism-treeview.css -docs/node_modules/prismjs/plugins/treeview/prism-treeview.min.css -docs/node_modules/prismjs/plugins/unescaped-markup/prism-unescaped-markup.css -docs/node_modules/prismjs/plugins/unescaped-markup/prism-unescaped-markup.min.css -docs/node_modules/prismjs/plugins/wpd/prism-wpd.css -docs/node_modules/prismjs/plugins/wpd/prism-wpd.min.css -docs/node_modules/prismjs/themes/prism-coy.css -docs/node_modules/prismjs/themes/prism-coy.min.css -docs/node_modules/prismjs/themes/prism-dark.css -docs/node_modules/prismjs/themes/prism-dark.min.css -docs/node_modules/prismjs/themes/prism-funky.css -docs/node_modules/prismjs/themes/prism-funky.min.css -docs/node_modules/prismjs/themes/prism-okaidia.css -docs/node_modules/prismjs/themes/prism-okaidia.min.css -docs/node_modules/prismjs/themes/prism-solarizedlight.css -docs/node_modules/prismjs/themes/prism-solarizedlight.min.css -docs/node_modules/prismjs/themes/prism-tomorrow.css -docs/node_modules/prismjs/themes/prism-tomorrow.min.css -docs/node_modules/prismjs/themes/prism-twilight.css -docs/node_modules/prismjs/themes/prism-twilight.min.css -docs/node_modules/prismjs/themes/prism.css -docs/node_modules/prismjs/themes/prism.min.css -docs/node_modules/remark-admonitions/styles/classic.css -docs/node_modules/remark-admonitions/styles/infima.css -docs/node_modules/require-like/Makefile -docs/node_modules/serve-index/public/style.css -docs/node_modules/trim/Makefile -docs/src/css/custom.css -docs/src/pages/styles.module.css -sportsdataverse/__init__.py -sportsdataverse/config.py -sportsdataverse/decorators.py -sportsdataverse/dl_utils.py -sportsdataverse/errors.py -sportsdataverse.egg-info/PKG-INFO -sportsdataverse.egg-info/SOURCES.txt -sportsdataverse.egg-info/dependency_links.txt -sportsdataverse.egg-info/requires.txt -sportsdataverse.egg-info/top_level.txt -sportsdataverse/cfb/__init__.py -sportsdataverse/cfb/cfb_loaders.py -sportsdataverse/cfb/cfb_pbp.py -sportsdataverse/cfb/cfb_pbp_pd.py -sportsdataverse/cfb/cfb_schedule.py -sportsdataverse/cfb/cfb_teams.py -sportsdataverse/cfb/model_vars.py -sportsdataverse/cfb/models/ep_model.model -sportsdataverse/cfb/models/qbr_model.model -sportsdataverse/cfb/models/wp_spread.model -sportsdataverse/mbb/__init__.py -sportsdataverse/mbb/mbb_game_rosters.py -sportsdataverse/mbb/mbb_loaders.py -sportsdataverse/mbb/mbb_pbp.py -sportsdataverse/mbb/mbb_schedule.py -sportsdataverse/mbb/mbb_teams.py -sportsdataverse/mlb/__init__.py -sportsdataverse/mlb/mlb_loaders.py -sportsdataverse/mlb/mlbam_games.py -sportsdataverse/mlb/mlbam_players.py -sportsdataverse/mlb/mlbam_reports.py -sportsdataverse/mlb/mlbam_stats.py -sportsdataverse/mlb/mlbam_teams.py -sportsdataverse/mlb/retrosheet.py -sportsdataverse/mlb/retrosplits.py -sportsdataverse/nba/__init__.py -sportsdataverse/nba/nba_loaders.py -sportsdataverse/nba/nba_pbp.py -sportsdataverse/nba/nba_schedule.py -sportsdataverse/nba/nba_teams.py -sportsdataverse/nfl/__init__.py -sportsdataverse/nfl/model_vars.py -sportsdataverse/nfl/nfl_games.py -sportsdataverse/nfl/nfl_loaders.py -sportsdataverse/nfl/nfl_pbp.py -sportsdataverse/nfl/nfl_schedule.py -sportsdataverse/nfl/nfl_teams.py -sportsdataverse/nfl/models/ep_model.model -sportsdataverse/nfl/models/qbr_model.model -sportsdataverse/nfl/models/wp_spread.model -sportsdataverse/nhl/__init__.py -sportsdataverse/nhl/nhl_api.py -sportsdataverse/nhl/nhl_loaders.py -sportsdataverse/nhl/nhl_pbp.py -sportsdataverse/nhl/nhl_schedule.py -sportsdataverse/nhl/nhl_teams.py -sportsdataverse/wbb/__init__.py -sportsdataverse/wbb/wbb_loaders.py -sportsdataverse/wbb/wbb_pbp.py -sportsdataverse/wbb/wbb_schedule.py -sportsdataverse/wbb/wbb_teams.py -sportsdataverse/wnba/__init__.py -sportsdataverse/wnba/wnba_loaders.py -sportsdataverse/wnba/wnba_pbp.py -sportsdataverse/wnba/wnba_schedule.py -sportsdataverse/wnba/wnba_teams.py diff --git a/sportsdataverse.egg-info/dependency_links.txt b/sportsdataverse.egg-info/dependency_links.txt deleted file mode 100755 index e69de29..0000000 diff --git a/sportsdataverse.egg-info/requires.txt b/sportsdataverse.egg-info/requires.txt deleted file mode 100755 index 7c4d934..0000000 --- a/sportsdataverse.egg-info/requires.txt +++ /dev/null @@ -1,57 +0,0 @@ -numpy>=1.13.0 -pandas>=1.0.3 -polars>=0.18.2 -tqdm>=4.50.0 -beautifulsoup4>=4.4.0 -inflection>=0.5.1 -requests>=2.18.1 -lxml>=4.2.1 -pyarrow>=8.0.0 -pyjanitor>=0.23.1 -pyreadr>=0.4.0 -scipy>=1.4.0 -matplotlib>=2.0.0 -attrs>=20.3.0 -xgboost>=1.2.0 - -[all] -pytest>=6.0.2 -mypy>=0.782 -pytest-cov>=2.10.1 -pytest-xdist>=2.1.0 -sphinx -beautifulsoup4>=4.4.0 -inflection>=0.5.1 -requests>=2.18.1 -lxml>=4.2.1 -pyarrow>=1.0.1 -pyjanitor>=0.23.1 -pyreadr>=0.4.0 -scipy>=1.4.0 -matplotlib>=2.0.0 -tqdm>=4.50.0 -attrs>=20.3.0 -xgboost>=1.2.0 - -[docs] -sphinx - -[models] -beautifulsoup4>=4.4.0 -inflection>=0.5.1 -requests>=2.18.1 -lxml>=4.2.1 -pyarrow>=1.0.1 -pyjanitor>=0.23.1 -pyreadr>=0.4.0 -scipy>=1.4.0 -matplotlib>=2.0.0 -tqdm>=4.50.0 -attrs>=20.3.0 -xgboost>=1.2.0 - -[tests] -pytest>=6.0.2 -mypy>=0.782 -pytest-cov>=2.10.1 -pytest-xdist>=2.1.0 diff --git a/sportsdataverse.egg-info/top_level.txt b/sportsdataverse.egg-info/top_level.txt deleted file mode 100755 index 70d5f97..0000000 --- a/sportsdataverse.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -sportsdataverse diff --git a/sportsdataverse/cfb/cfb_schedule.py b/sportsdataverse/cfb/cfb_schedule.py index fefb043..c6d7285 100755 --- a/sportsdataverse/cfb/cfb_schedule.py +++ b/sportsdataverse/cfb/cfb_schedule.py @@ -34,13 +34,13 @@ def espn_cfb_schedule( url = "http://site.api.espn.com/apis/site/v2/sports/football/college-football/scoreboard" resp = download(url=url, params=params, **kwargs) - ev = pl.DataFrame() + ev = pd.DataFrame() events_txt = resp.json() events = events_txt.get("events") if events is None: - return pd.DataFrame() if return_as_pandas else pl.DataFrame() + return None if len(events) == 0: - return pd.DataFrame() if return_as_pandas else pl.DataFrame() + return None for event in events: event.get("competitions")[0].get("competitors")[0].get("team").pop("links", None) @@ -83,16 +83,20 @@ def espn_cfb_schedule( season=(event.get("season").get("year")), season_type=(event.get("season").get("type")), week=(event.get("week", {}).get("number")), - home_linescores=pl.when(pl.col("status_type_description") == "Postponed") + home_linescores=pl.when(pl.col("status_type_description").is_in(["Postponed", "Canceled"])) .then(None) .otherwise(pl.col("home_linescores")), - away_linescores=pl.when(pl.col("status_type_description") == "Postponed") + away_linescores=pl.when(pl.col("status_type_description").is_in(["Postponed", "Canceled"])) .then(None) .otherwise(pl.col("away_linescores")), + ).with_columns( + season=pl.col("season").cast(pl.Int32), + season_type=pl.col("season_type").cast(pl.Int32), + week=pl.col("week").cast(pl.Int32), ) x = x[[s.name for s in x if s.null_count() != x.height]] - ev = pl.concat([ev, x], how="diagonal") - + ev = pd.concat([ev, x.to_pandas()], axis=0, ignore_index=True) + ev = pl.from_pandas(ev) ev.columns = [underscore(c) for c in ev.columns] return ev.to_pandas() if return_as_pandas else ev @@ -111,14 +115,28 @@ def _extract_home_away(event, arg1, arg2): event.get("competitions")[0].get("competitors")[arg1].get("curatedRank", {}).get("current", 99) ) event["competitions"][0][arg2]["linescores"] = ( - event.get("competitions")[0].get("competitors")[arg1].get("linescores", [{"value": "N/A"}]) + event.get("competitions")[0] + .get("competitors")[arg1] + .get("linescores", [{"value": 0}, {"value": 0}, {"value": 0}, {"value": 0}]) ) # add linescores back to main competitors if does not exist event["competitions"][0]["competitors"][arg1]["linescores"] = ( - event.get("competitions")[0].get("competitors")[arg1].get("linescores", []) + event.get("competitions")[0] + .get("competitors")[arg1] + .get("linescores", [{"value": 0}, {"value": 0}, {"value": 0}, {"value": 0}]) ) event["competitions"][0][arg2]["records"] = ( - event.get("competitions")[0].get("competitors")[arg1].get("records", []) + event.get("competitions")[0] + .get("competitors")[arg1] + .get( + "records", + [ + {"abbreviation": "Game", "name": "overall", "summary": "0-0", "type": "total"}, + {"abbreviation": "null", "name": "Home", "summary": "0-0", "type": "home"}, + {"abbreviation": "null", "name": "Road", "summary": "0-0", "type": "road"}, + {"abbreviation": "null", "name": "vs. Conf.", "summary": "0-0", "type": "vsconf"}, + ], + ) ) return event diff --git a/sportsdataverse/mbb/mbb_schedule.py b/sportsdataverse/mbb/mbb_schedule.py index d09205d..f4dac2b 100755 --- a/sportsdataverse/mbb/mbb_schedule.py +++ b/sportsdataverse/mbb/mbb_schedule.py @@ -30,7 +30,7 @@ def espn_mbb_schedule( } resp = download(url=url, params=params, **kwargs) - ev = pl.DataFrame() + ev = pd.DataFrame() events_txt = resp.json() events = events_txt.get("events") if events is None: @@ -84,10 +84,13 @@ def espn_mbb_schedule( away_linescores=pl.when(pl.col("status_type_description") == "Postponed") .then(None) .otherwise(pl.col("away_linescores")), + ).with_columns( + season=pl.col("season").cast(pl.Int32), + season_type=pl.col("season_type").cast(pl.Int32), ) x = x[[s.name for s in x if s.null_count() != x.height]] - ev = pl.concat([ev, x], how="diagonal") - + ev = pd.concat([ev, x.to_pandas()], axis=0, ignore_index=True) + ev = pl.from_pandas(ev) ev.columns = [underscore(c) for c in ev.columns] return ev.to_pandas() if return_as_pandas else ev @@ -105,14 +108,24 @@ def __extract_home_away(event, arg1, arg2): event.get("competitions")[0].get("competitors")[arg1].get("curatedRank", {}).get("current", 99) ) event["competitions"][0][arg2]["linescores"] = ( - event.get("competitions")[0].get("competitors")[arg1].get("linescores", [{"value": "N/A"}]) + event.get("competitions")[0].get("competitors")[arg1].get("linescores", [{"value": 0}, {"value": 0}]) ) # add linescores back to main competitors if does not exist event["competitions"][0]["competitors"][arg1]["linescores"] = ( - event.get("competitions")[0].get("competitors")[arg1].get("linescores", []) + event.get("competitions")[0].get("competitors")[arg1].get("linescores", [{"value": 0}, {"value": 0}]) ) event["competitions"][0][arg2]["records"] = ( - event.get("competitions")[0].get("competitors")[arg1].get("records", []) + event.get("competitions")[0] + .get("competitors")[arg1] + .get( + "records", + [ + {"abbreviation": "Game", "name": "overall", "summary": "0-0", "type": "total"}, + {"abbreviation": "null", "name": "Home", "summary": "0-0", "type": "home"}, + {"abbreviation": "null", "name": "Road", "summary": "0-0", "type": "road"}, + {"abbreviation": "null", "name": "vs. Conf.", "summary": "0-0", "type": "vsconf"}, + ], + ) ) return event diff --git a/sportsdataverse/nba/nba_schedule.py b/sportsdataverse/nba/nba_schedule.py index 0e1db4f..b55f080 100755 --- a/sportsdataverse/nba/nba_schedule.py +++ b/sportsdataverse/nba/nba_schedule.py @@ -24,7 +24,7 @@ def espn_nba_schedule(dates=None, season_type=None, limit=500, return_as_pandas= params = {"dates": dates, "seasonType": season_type, "limit": limit} resp = download(url=url, params=params, **kwargs) - ev = pl.DataFrame() + ev = pd.DataFrame() events_txt = resp.json() events = events_txt.get("events") if events is None: @@ -77,10 +77,13 @@ def espn_nba_schedule(dates=None, season_type=None, limit=500, return_as_pandas= away_linescores=pl.when(pl.col("status_type_description") == "Postponed") .then(None) .otherwise(pl.col("away_linescores")), + ).with_columns( + season=pl.col("season").cast(pl.Int32), + season_type=pl.col("season_type").cast(pl.Int32), ) x = x[[s.name for s in x if s.null_count() != x.height]] - ev = pl.concat([ev, x], how="diagonal") - + ev = pd.concat([ev, x.to_pandas()], axis=0, ignore_index=True) + ev = pl.from_pandas(ev) ev.columns = [underscore(c) for c in ev.columns] return ev.to_pandas() if return_as_pandas else ev @@ -94,16 +97,28 @@ def __extract_home_away(event, arg1, arg2): event["competitions"][0]["competitors"][arg1]["winner"] = ( event.get("competitions")[0].get("competitors")[arg1].get("winner", False) ) - event["competitions"][0][arg2]["linescores"] = ( - event.get("competitions")[0].get("competitors")[arg1].get("linescores", [{"value": "N/A"}]) + event.get("competitions")[0] + .get("competitors")[arg1] + .get("linescores", [{"value": 0}, {"value": 0}, {"value": 0}, {"value": 0}]) ) # add linescores back to main competitors if does not exist event["competitions"][0]["competitors"][arg1]["linescores"] = ( - event.get("competitions")[0].get("competitors")[arg1].get("linescores", []) + event.get("competitions")[0] + .get("competitors")[arg1] + .get("linescores", [{"value": 0}, {"value": 0}, {"value": 0}, {"value": 0}]) ) event["competitions"][0][arg2]["records"] = ( - event.get("competitions")[0].get("competitors")[arg1].get("records", []) + event.get("competitions")[0] + .get("competitors")[arg1] + .get( + "records", + [ + {"abbreviation": "Game", "name": "overall", "summary": "0-0", "type": "total"}, + {"abbreviation": "null", "name": "Home", "summary": "0-0", "type": "home"}, + {"abbreviation": "null", "name": "Road", "summary": "0-0", "type": "road"}, + ], + ) ) return event diff --git a/sportsdataverse/nfl/nfl_schedule.py b/sportsdataverse/nfl/nfl_schedule.py index 5f4ef3c..f99bd65 100755 --- a/sportsdataverse/nfl/nfl_schedule.py +++ b/sportsdataverse/nfl/nfl_schedule.py @@ -27,7 +27,7 @@ def espn_nfl_schedule( url = "http://site.api.espn.com/apis/site/v2/sports/football/nfl/scoreboard" resp = download(url=url, params=params, **kwargs) - ev = pl.DataFrame() + ev = pd.DataFrame() events_txt = resp.json() events = events_txt.get("events") if events is None: @@ -82,10 +82,14 @@ def espn_nfl_schedule( away_linescores=pl.when(pl.col("status_type_description") == "Postponed") .then(None) .otherwise(pl.col("away_linescores")), + ).with_columns( + season=pl.col("season").cast(pl.Int32), + season_type=pl.col("season_type").cast(pl.Int32), + week=pl.col("week").cast(pl.Int32), ) x = x[[s.name for s in x if s.null_count() != x.height]] - ev = pl.concat([ev, x], how="diagonal") - + ev = pd.concat([ev, x.to_pandas()], axis=0, ignore_index=True) + ev = pl.from_pandas(ev) ev.columns = [underscore(c) for c in ev.columns] return ev.to_pandas() if return_as_pandas else ev @@ -103,14 +107,27 @@ def _extract_home_away(event, arg1, arg2): event.get("competitions")[0].get("competitors")[arg1].get("curatedRank", {}).get("current", 99) ) event["competitions"][0][arg2]["linescores"] = ( - event.get("competitions")[0].get("competitors")[arg1].get("linescores", [{"value": "N/A"}]) + event.get("competitions")[0] + .get("competitors")[arg1] + .get("linescores", [{"value": 0}, {"value": 0}, {"value": 0}, {"value": 0}]) ) # add linescores back to main competitors if does not exist event["competitions"][0]["competitors"][arg1]["linescores"] = ( - event.get("competitions")[0].get("competitors")[arg1].get("linescores", []) + event.get("competitions")[0] + .get("competitors")[arg1] + .get("linescores", [{"value": 0}, {"value": 0}, {"value": 0}, {"value": 0}]) ) event["competitions"][0][arg2]["records"] = ( - event.get("competitions")[0].get("competitors")[arg1].get("records", []) + event.get("competitions")[0] + .get("competitors")[arg1] + .get( + "records", + [ + {"abbreviation": "Game", "name": "overall", "summary": "0-0", "type": "total"}, + {"abbreviation": "null", "name": "Home", "summary": "0-0", "type": "home"}, + {"abbreviation": "null", "name": "Road", "summary": "0-0", "type": "road"}, + ], + ) ) return event diff --git a/sportsdataverse/nhl/nhl_schedule.py b/sportsdataverse/nhl/nhl_schedule.py index bea0d66..aa7bd5a 100755 --- a/sportsdataverse/nhl/nhl_schedule.py +++ b/sportsdataverse/nhl/nhl_schedule.py @@ -25,7 +25,7 @@ def espn_nhl_schedule(dates=None, season_type=None, limit=500, return_as_pandas= params = {"dates": dates, "seasonType": season_type, "limit": limit} resp = download(url=url, params=params, **kwargs) - ev = pl.DataFrame() + ev = pd.DataFrame() events_txt = resp.json() events = events_txt.get("events") if events is None: @@ -78,10 +78,13 @@ def espn_nhl_schedule(dates=None, season_type=None, limit=500, return_as_pandas= away_linescores=pl.when(pl.col("status_type_description") == "Postponed") .then(None) .otherwise(pl.col("away_linescores")), + ).with_columns( + season=pl.col("season").cast(pl.Int32), + season_type=pl.col("season_type").cast(pl.Int32), ) x = x[[s.name for s in x if s.null_count() != x.height]] - ev = pl.concat([ev, x], how="diagonal") - + ev = pd.concat([ev, x.to_pandas()], axis=0, ignore_index=True) + ev = pl.from_pandas(ev) ev.columns = [underscore(c) for c in ev.columns] return ev.to_pandas() if return_as_pandas else ev @@ -95,18 +98,29 @@ def __extract_home_away(event, arg1, arg2): event["competitions"][0]["competitors"][arg1]["winner"] = ( event.get("competitions")[0].get("competitors")[arg1].get("winner", False) ) - event["competitions"][0][arg2]["linescores"] = ( - event.get("competitions")[0].get("competitors")[arg1].get("linescores", [{"value": "N/A"}]) + event.get("competitions")[0] + .get("competitors")[arg1] + .get("linescores", [{"value": 0}, {"value": 0}, {"value": 0}]) ) # add linescores back to main competitors if does not exist event["competitions"][0]["competitors"][arg1]["linescores"] = ( - event.get("competitions")[0].get("competitors")[arg1].get("linescores", []) + event.get("competitions")[0] + .get("competitors")[arg1] + .get("linescores", [{"value": 0}, {"value": 0}, {"value": 0}]) ) event["competitions"][0][arg2]["records"] = ( - event.get("competitions")[0].get("competitors")[arg1].get("records", []) + event.get("competitions")[0] + .get("competitors")[arg1] + .get( + "records", + [ + {"abbreviation": "Game", "name": "YTD", "summary": "0-0", "type": "ytd"}, + {"abbreviation": "HOME", "name": "Home", "summary": "0-0", "type": "home"}, + {"abbreviation": "AWAY", "name": "Road", "summary": "0-0", "type": "road"}, + ], + ) ) - return event def espn_nhl_calendar(season=None, ondays=None, return_as_pandas=False, **kwargs) -> pl.DataFrame: diff --git a/sportsdataverse/wbb/wbb_schedule.py b/sportsdataverse/wbb/wbb_schedule.py index 09654c0..5dc6aa9 100755 --- a/sportsdataverse/wbb/wbb_schedule.py +++ b/sportsdataverse/wbb/wbb_schedule.py @@ -31,7 +31,7 @@ def espn_wbb_schedule( } resp = download(url=url, params=params, **kwargs) - ev = pl.DataFrame() + ev = pd.DataFrame() events_txt = resp.json() events = events_txt.get("events") if events is None: @@ -85,10 +85,13 @@ def espn_wbb_schedule( away_linescores=pl.when(pl.col("status_type_description") == "Postponed") .then(None) .otherwise(pl.col("away_linescores")), + ).with_columns( + season=pl.col("season").cast(pl.Int32), + season_type=pl.col("season_type").cast(pl.Int32), ) x = x[[s.name for s in x if s.null_count() != x.height]] - ev = pl.concat([ev, x], how="diagonal") - + ev = pd.concat([ev, x.to_pandas()], axis=0, ignore_index=True) + ev = pl.from_pandas(ev) ev.columns = [underscore(c) for c in ev.columns] return ev.to_pandas() if return_as_pandas else ev @@ -106,14 +109,28 @@ def __extract_home_away(event, arg1, arg2): event.get("competitions")[0].get("competitors")[arg1].get("curatedRank", {}).get("current", 99) ) event["competitions"][0][arg2]["linescores"] = ( - event.get("competitions")[0].get("competitors")[arg1].get("linescores", [{"value": "N/A"}]) + event.get("competitions")[0] + .get("competitors")[arg1] + .get("linescores", [{"value": 0}, {"value": 0}, {"value": 0}, {"value": 0}]) ) # add linescores back to main competitors if does not exist event["competitions"][0]["competitors"][arg1]["linescores"] = ( - event.get("competitions")[0].get("competitors")[arg1].get("linescores", []) + event.get("competitions")[0] + .get("competitors")[arg1] + .get("linescores", [{"value": 0}, {"value": 0}, {"value": 0}, {"value": 0}]) ) event["competitions"][0][arg2]["records"] = ( - event.get("competitions")[0].get("competitors")[arg1].get("records", []) + event.get("competitions")[0] + .get("competitors")[arg1] + .get( + "records", + [ + {"abbreviation": "Game", "name": "overall", "summary": "0-0", "type": "total"}, + {"abbreviation": "null", "name": "Home", "summary": "0-0", "type": "home"}, + {"abbreviation": "null", "name": "Road", "summary": "0-0", "type": "road"}, + {"abbreviation": "null", "name": "vs. Conf.", "summary": "0-0", "type": "vsconf"}, + ], + ) ) return event diff --git a/sportsdataverse/wnba/wnba_schedule.py b/sportsdataverse/wnba/wnba_schedule.py index e14fd3d..717a980 100755 --- a/sportsdataverse/wnba/wnba_schedule.py +++ b/sportsdataverse/wnba/wnba_schedule.py @@ -21,7 +21,7 @@ def espn_wnba_schedule(dates=None, season_type=None, limit=500, return_as_pandas params = {"dates": dates, "seasonType": season_type, "limit": limit} resp = download(url=url, params=params, **kwargs) - ev = pl.DataFrame() + ev = pd.DataFrame() events_txt = resp.json() events = events_txt.get("events") if events is None: @@ -74,10 +74,13 @@ def espn_wnba_schedule(dates=None, season_type=None, limit=500, return_as_pandas away_linescores=pl.when(pl.col("status_type_description") == "Postponed") .then(None) .otherwise(pl.col("away_linescores")), + ).with_columns( + season=pl.col("season").cast(pl.Int32), + season_type=pl.col("season_type").cast(pl.Int32), ) x = x[[s.name for s in x if s.null_count() != x.height]] - ev = pl.concat([ev, x], how="diagonal") - + ev = pd.concat([ev, x.to_pandas()], axis=0, ignore_index=True) + ev = pl.from_pandas(ev) ev.columns = [underscore(c) for c in ev.columns] return ev.to_pandas() if return_as_pandas else ev @@ -87,20 +90,32 @@ def __extract_home_away(event, arg1, arg2): event["competitions"][0][arg2] = event.get("competitions")[0].get("competitors")[arg1].get("team") event["competitions"][0][arg2]["score"] = event.get("competitions")[0].get("competitors")[arg1].get("score") event["competitions"][0][arg2]["winner"] = event.get("competitions")[0].get("competitors")[arg1].get("winner") - ## add winner back to main competitors if does not exist + # add winner back to main competitors if does not exist event["competitions"][0]["competitors"][arg1]["winner"] = ( event.get("competitions")[0].get("competitors")[arg1].get("winner", False) ) - event["competitions"][0][arg2]["linescores"] = ( - event.get("competitions")[0].get("competitors")[arg1].get("linescores", [{"value": "N/A"}]) + event.get("competitions")[0] + .get("competitors")[arg1] + .get("linescores", [{"value": 0}, {"value": 0}, {"value": 0}, {"value": 0}]) ) - ## add linescores back to main competitors if does not exist + # add linescores back to main competitors if does not exist event["competitions"][0]["competitors"][arg1]["linescores"] = ( - event.get("competitions")[0].get("competitors")[arg1].get("linescores", []) + event.get("competitions")[0] + .get("competitors")[arg1] + .get("linescores", [{"value": 0}, {"value": 0}, {"value": 0}, {"value": 0}]) ) event["competitions"][0][arg2]["records"] = ( - event.get("competitions")[0].get("competitors")[arg1].get("records", []) + event.get("competitions")[0] + .get("competitors")[arg1] + .get( + "records", + [ + {"abbreviation": "Game", "name": "overall", "summary": "0-0", "type": "total"}, + {"abbreviation": "null", "name": "Home", "summary": "0-0", "type": "home"}, + {"abbreviation": "null", "name": "Road", "summary": "0-0", "type": "road"}, + ], + ) ) return event From 52cb72abf3c052c4ee515b8ae224f9e287ca6cb0 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Mon, 31 Jul 2023 15:35:53 -0400 Subject: [PATCH 54/79] updating test names --- tests/cfb/__init__.py | 1 - tests/cfb/{test_pbp.py => test_cfb_pbp.py} | 19 +++--- tests/cfb/test_cfb_schedule.py | 40 ++++++------- tests/mbb/__init__.py | 1 - tests/mbb/test_mbb_pbp.py | 13 +++++ tests/mbb/test_mbb_schedule.py | 58 +++++++++++++++++++ tests/mbb/test_pbp.py | 14 ----- tests/nfl/__init__.py | 1 - tests/nfl/test_espn_pbp.py | 21 ------- tests/nfl/test_nfl_pbp.py | 21 +++++++ .../{test_roster.py => test_nfl_roster.py} | 0 11 files changed, 121 insertions(+), 68 deletions(-) delete mode 100755 tests/cfb/__init__.py rename tests/cfb/{test_pbp.py => test_cfb_pbp.py} (94%) mode change 100755 => 100644 delete mode 100755 tests/mbb/__init__.py create mode 100644 tests/mbb/test_mbb_pbp.py create mode 100644 tests/mbb/test_mbb_schedule.py delete mode 100755 tests/mbb/test_pbp.py delete mode 100755 tests/nfl/__init__.py delete mode 100755 tests/nfl/test_espn_pbp.py create mode 100644 tests/nfl/test_nfl_pbp.py rename tests/nfl/{test_roster.py => test_nfl_roster.py} (100%) mode change 100755 => 100644 diff --git a/tests/cfb/__init__.py b/tests/cfb/__init__.py deleted file mode 100755 index 29e3d30..0000000 --- a/tests/cfb/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"tests " diff --git a/tests/cfb/test_pbp.py b/tests/cfb/test_cfb_pbp.py old mode 100755 new mode 100644 similarity index 94% rename from tests/cfb/test_pbp.py rename to tests/cfb/test_cfb_pbp.py index b62d931..55e6545 --- a/tests/cfb/test_pbp.py +++ b/tests/cfb/test_cfb_pbp.py @@ -1,4 +1,3 @@ -import pandas as pd import polars as pl import pytest @@ -24,7 +23,7 @@ def box_score(generated_data): def test_basic_pbp(generated_data): - assert generated_data.json != None + assert generated_data.json is not None generated_data.run_processing_pipeline() assert len(generated_data.plays_json) > 0 @@ -33,7 +32,7 @@ def test_basic_pbp(generated_data): def test_adv_box_score(box_score): - assert box_score != None + assert box_score is not None assert not set(box_score.keys()).difference( { "win_pct", @@ -52,15 +51,15 @@ def test_adv_box_score(box_score): def test_havoc_rate(box_score): defense_home = box_score["defensive"][0] # print(defense_home) - pd = defense_home.get("pass_breakups", 0) + passes_defended = defense_home.get("pass_breakups", 0) home_int = defense_home.get("Int", 0) tfl = defense_home.get("TFL", 0) fum = defense_home.get("fumbles", 0) plays = defense_home.get("scrimmage_plays", 0) assert plays > 0 - assert defense_home["havoc_total"] == (pd + home_int + tfl + fum) - assert round(defense_home["havoc_total_rate"], 4) == round(((pd + home_int + tfl + fum) / plays), 4) + assert defense_home["havoc_total"] == (passes_defended + home_int + tfl + fum) + assert round(defense_home["havoc_total_rate"], 4) == round(((passes_defended + home_int + tfl + fum) / plays), 4) @pytest.fixture() @@ -262,10 +261,10 @@ def schedule_data_check2(schedule_data2): @pytest.fixture() -def week_1_schedule(): +def week_1_cfb_schedule(): yield espn_cfb_schedule(dates=2022, week=1, return_as_pandas=False) -def week_1_schedule_check(week_1_schedule): - assert isinstance(week_1_schedule, pl.DataFrame) - assert len(week_1_schedule) > 0 +def week_1_schedule_check(week_1_cfb_schedule): + assert isinstance(week_1_cfb_schedule, pl.DataFrame) + assert len(week_1_cfb_schedule) > 0 diff --git a/tests/cfb/test_cfb_schedule.py b/tests/cfb/test_cfb_schedule.py index 5e09a3e..099464b 100644 --- a/tests/cfb/test_cfb_schedule.py +++ b/tests/cfb/test_cfb_schedule.py @@ -9,50 +9,50 @@ @pytest.fixture() -def calendar_data(): +def cfb_calendar_data(): yield espn_cfb_calendar(season=most_recent_cfb_season(), return_as_pandas=False) -def calendar_data_check(calendar_data): - assert isinstance(calendar_data, pl.DataFrame) - assert len(calendar_data) > 0 +def cfb_calendar_data_check(cfb_calendar_data): + assert isinstance(cfb_calendar_data, pl.DataFrame) + assert len(cfb_calendar_data) > 0 @pytest.fixture() -def calendar_ondays_data(): +def cfb_calendar_ondays_data(): yield espn_cfb_calendar(season=2021, ondays=True, return_as_pandas=False) -def calendar_ondays_data_check(calendar_ondays_data): - assert isinstance(calendar_ondays_data, pl.DataFrame) - assert len(calendar_ondays_data) > 0 +def cfb_calendar_ondays_data_check(cfb_calendar_ondays_data): + assert isinstance(cfb_calendar_ondays_data, pl.DataFrame) + assert len(cfb_calendar_ondays_data) > 0 @pytest.fixture() -def schedule_data(): +def cfb_schedule_data(): yield espn_cfb_schedule(return_as_pandas=False) -def schedule_data_check(schedule_data): - assert isinstance(schedule_data, pl.DataFrame) - assert len(schedule_data) > 0 +def cfb_schedule_data_check(cfb_schedule_data): + assert isinstance(cfb_schedule_data, pl.DataFrame) + assert len(cfb_schedule_data) > 0 @pytest.fixture() -def schedule_data2(): +def cfb_schedule_data2(): yield espn_cfb_schedule(dates=20220901, return_as_pandas=False) -def schedule_data_check2(schedule_data2): - assert isinstance(schedule_data, pl.DataFrame) - assert len(schedule_data) > 0 +def cfb_schedule_data_check2(cfb_schedule_data2): + assert isinstance(cfb_schedule_data, pl.DataFrame) + assert len(cfb_schedule_data) > 0 @pytest.fixture() -def week_1_schedule(): +def week_1_cfb_schedule(): yield espn_cfb_schedule(dates=2022, week=1, return_as_pandas=False) -def week_1_schedule_check(week_1_schedule): - assert isinstance(week_1_schedule, pl.DataFrame) - assert len(week_1_schedule) > 0 +def week_1_cfb_schedule_check(week_1_cfb_schedule): + assert isinstance(week_1_cfb_schedule, pl.DataFrame) + assert len(week_1_cfb_schedule) > 0 diff --git a/tests/mbb/__init__.py b/tests/mbb/__init__.py deleted file mode 100755 index 6de0e6e..0000000 --- a/tests/mbb/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .test_pbp import * diff --git a/tests/mbb/test_mbb_pbp.py b/tests/mbb/test_mbb_pbp.py new file mode 100644 index 0000000..8b7edc4 --- /dev/null +++ b/tests/mbb/test_mbb_pbp.py @@ -0,0 +1,13 @@ +import pytest + +from sportsdataverse.mbb import espn_mbb_pbp + + +@pytest.fixture() +def generated_mbb_data(): + yield espn_mbb_pbp(game_id=401265031) + + +def test_basic_mbb_pbp(generated_mbb_data): + assert generated_mbb_data is not None + assert len(generated_mbb_data.get("plays")) > 0 diff --git a/tests/mbb/test_mbb_schedule.py b/tests/mbb/test_mbb_schedule.py new file mode 100644 index 0000000..16cec8c --- /dev/null +++ b/tests/mbb/test_mbb_schedule.py @@ -0,0 +1,58 @@ +import polars as pl +import pytest + +from sportsdataverse.mbb.mbb_schedule import ( + espn_mbb_calendar, + espn_mbb_schedule, + most_recent_mbb_season, +) + + +@pytest.fixture() +def mbb_calendar_data(): + yield espn_mbb_calendar(season=most_recent_mbb_season(), return_as_pandas=False) + + +def mbb_calendar_data_check(mbb_calendar_data): + assert isinstance(mbb_calendar_data, pl.DataFrame) + assert len(mbb_calendar_data) > 0 + + +@pytest.fixture() +def mbb_calendar_ondays_data(): + yield espn_mbb_calendar(season=2021, ondays=True, return_as_pandas=False) + + +def mbb_calendar_ondays_data_check(mbb_calendar_ondays_data): + assert isinstance(mbb_calendar_ondays_data, pl.DataFrame) + assert len(mbb_calendar_ondays_data) > 0 + + +@pytest.fixture() +def mbb_schedule_data(): + yield espn_mbb_schedule(return_as_pandas=False) + + +def mbb_schedule_data_check(mbb_schedule_data): + assert isinstance(mbb_schedule_data, pl.DataFrame) + assert len(mbb_schedule_data) > 0 + + +@pytest.fixture() +def mbb_schedule_data2(): + yield espn_mbb_schedule(dates=20221201, return_as_pandas=False) + + +def mbb_schedule_data_check2(mbb_schedule_data2): + assert isinstance(mbb_schedule_data2, pl.DataFrame) + assert len(mbb_schedule_data2) > 0 + + +@pytest.fixture() +def january_schedule(): + yield espn_mbb_schedule(dates=20230123, return_as_pandas=False) + + +def january_schedule_check(january_schedule): + assert isinstance(january_schedule, pl.DataFrame) + assert len(january_schedule) > 0 diff --git a/tests/mbb/test_pbp.py b/tests/mbb/test_pbp.py deleted file mode 100755 index 8931ecc..0000000 --- a/tests/mbb/test_pbp.py +++ /dev/null @@ -1,14 +0,0 @@ -import pytest - -from sportsdataverse.mbb import espn_mbb_pbp - - -@pytest.fixture() -def generated_data(): - test = espn_mbb_pbp(game_id=401265031) - yield test - - -def test_basic_pbp(generated_data): - assert generated_data != None - assert len(generated_data.get("plays")) > 0 diff --git a/tests/nfl/__init__.py b/tests/nfl/__init__.py deleted file mode 100755 index 912d384..0000000 --- a/tests/nfl/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .test_espn_pbp import * diff --git a/tests/nfl/test_espn_pbp.py b/tests/nfl/test_espn_pbp.py deleted file mode 100755 index df84418..0000000 --- a/tests/nfl/test_espn_pbp.py +++ /dev/null @@ -1,21 +0,0 @@ -import polars as pl -import pytest - -from sportsdataverse.nfl.nfl_pbp import NFLPlayProcess - - -@pytest.fixture() -def generated_data(): - test = NFLPlayProcess(gameId=401437777) - test.espn_nfl_pbp() - test.run_processing_pipeline() - yield test - - -def test_basic_pbp(generated_data): - assert generated_data.json != None - - generated_data.run_processing_pipeline() - assert len(generated_data.plays_json) > 0 - assert generated_data.ran_pipeline == True - assert isinstance(generated_data.plays_json, pl.DataFrame) diff --git a/tests/nfl/test_nfl_pbp.py b/tests/nfl/test_nfl_pbp.py new file mode 100644 index 0000000..dcd32cf --- /dev/null +++ b/tests/nfl/test_nfl_pbp.py @@ -0,0 +1,21 @@ +import polars as pl +import pytest + +from sportsdataverse.nfl.nfl_pbp import NFLPlayProcess + + +@pytest.fixture() +def generated_nfl_data(): + test = NFLPlayProcess(gameId=401437777) + test.espn_nfl_pbp() + test.run_processing_pipeline() + yield test + + +def test_basic_nfl_pbp(generated_nfl_data): + assert generated_nfl_data.json is not None + + generated_nfl_data.run_processing_pipeline() + assert len(generated_nfl_data.plays_json) > 0 + assert generated_nfl_data.ran_pipeline == True + assert isinstance(generated_nfl_data.plays_json, pl.DataFrame) diff --git a/tests/nfl/test_roster.py b/tests/nfl/test_nfl_roster.py old mode 100755 new mode 100644 similarity index 100% rename from tests/nfl/test_roster.py rename to tests/nfl/test_nfl_roster.py From 635e0430df7fd3c416075fd27e399ac98e79db5e Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Mon, 31 Jul 2023 16:36:41 -0400 Subject: [PATCH 55/79] nfl_pbp blindly updated to match cfb_pbp --- .../_build/doctrees/environment.pickle | Bin 930542 -> 934634 bytes .../doctrees/sportsdataverse.nfl.doctree | Bin 207199 -> 207389 bytes .../_build/markdown/sportsdataverse.nfl.md | 2 +- docs/docs/nfl/index.md | 2 +- sportsdataverse/cfb/cfb_pbp.py | 234 +- sportsdataverse/nfl/nfl_pbp.py | 7671 +++++++++-------- 6 files changed, 4024 insertions(+), 3885 deletions(-) diff --git a/Sphinx-docs/_build/doctrees/environment.pickle b/Sphinx-docs/_build/doctrees/environment.pickle index 9cd05c7d856e9838c9c829d50a290c571c2bb9a5..43f1debe471defef86b7624baa52cd058f74178d 100755 GIT binary patch delta 67103 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 literal 930542 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

idLs?n8H5804(10e#GmBb7fdKo^pT&5?mQ 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 E0r>Or{Qv*} delta 5787 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 GjQ;_J+UT4B diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.nfl.md b/Sphinx-docs/_build/markdown/sportsdataverse.nfl.md index d56a0ae..af8e011 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.nfl.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.nfl.md @@ -510,7 +510,7 @@ Bases: `object` Initialize self. See help(type(self)) for accurate signature. -#### create_box_score() +#### create_box_score(play_df) #### espn_nfl_pbp() espn_nfl_pbp() - Pull the game by id. Data from API endpoints: nfl/playbyplay, nfl/summary diff --git a/docs/docs/nfl/index.md b/docs/docs/nfl/index.md index d56a0ae..af8e011 100755 --- a/docs/docs/nfl/index.md +++ b/docs/docs/nfl/index.md @@ -510,7 +510,7 @@ Bases: `object` Initialize self. See help(type(self)) for accurate signature. -#### create_box_score() +#### create_box_score(play_df) #### espn_nfl_pbp() espn_nfl_pbp() - Pull the game by id. Data from API endpoints: nfl/playbyplay, nfl/summary diff --git a/sportsdataverse/cfb/cfb_pbp.py b/sportsdataverse/cfb/cfb_pbp.py index 52124fa..804b8ee 100755 --- a/sportsdataverse/cfb/cfb_pbp.py +++ b/sportsdataverse/cfb/cfb_pbp.py @@ -6,8 +6,8 @@ import numpy as np import pandas as pd -import pkg_resources import polars as pl +from pkg_resources import resource_filename from xgboost import Booster, DMatrix from sportsdataverse.cfb.model_vars import ( @@ -35,9 +35,9 @@ ) from sportsdataverse.dl_utils import download, key_check -ep_model_file = pkg_resources.resource_filename("sportsdataverse", "cfb/models/ep_model.model") -wp_spread_file = pkg_resources.resource_filename("sportsdataverse", "cfb/models/wp_spread.model") -qbr_model_file = pkg_resources.resource_filename("sportsdataverse", "cfb/models/qbr_model.model") +ep_model_file = resource_filename("sportsdataverse", "cfb/models/ep_model.model") +wp_spread_file = resource_filename("sportsdataverse", "cfb/models/wp_spread.model") +qbr_model_file = resource_filename("sportsdataverse", "cfb/models/qbr_model.model") ep_model = Booster({"nthread": 4}) # init model ep_model.load_model(ep_model_file) @@ -622,13 +622,13 @@ def __helper_cfb_pbp_features(self, pbp_txt, init): pbp_txt["plays"] .with_columns( pl.when(pl.col("scoringType.displayName") == "Field Goal") - .then("Field Goal Good") + .then(pl.lit("Field Goal Good")) .otherwise(pl.col("type.text")) .alias("type.text") ) .with_columns( pl.when(pl.col("scoringType.displayName") == "Extra Point") - .then("Extra Point Good") + .then(pl.lit("Extra Point Good")) .otherwise(pl.col("type.text")) .alias("type.text") ) @@ -637,7 +637,7 @@ def __helper_cfb_pbp_features(self, pbp_txt, init): pbp_txt["plays"] .with_columns( pl.when(pl.col("type.text").is_null()) - .then("Unknown") + .then(pl.lit("Unknown")) .otherwise(pl.col("type.text")) .alias("type.text") ) @@ -648,7 +648,7 @@ def __helper_cfb_pbp_features(self, pbp_txt, init): .str.contains("(?i)extra point") .and_(pl.col("type.text").str.to_lowercase().str.contains("(?i)no good")) ) - .then("Extra Point Missed") + .then(pl.lit("Extra Point Missed")) .otherwise(pl.col("type.text")) .alias("type.text") ) @@ -659,7 +659,7 @@ def __helper_cfb_pbp_features(self, pbp_txt, init): .str.contains("(?i)extra point") .and_(pl.col("type.text").str.to_lowercase().str.contains("(?i)blocked")) ) - .then("Extra Point Missed") + .then(pl.lit("Extra Point Missed")) .otherwise(pl.col("type.text")) .alias("type.text") ) @@ -670,7 +670,7 @@ def __helper_cfb_pbp_features(self, pbp_txt, init): .str.contains("(?i)field goal") .and_(pl.col("type.text").str.to_lowercase().str.contains("(?i)blocked")) ) - .then("Extra Point Missed") + .then(pl.lit("Extra Point Missed")) .otherwise(pl.col("type.text")) .alias("type.text") ) @@ -681,7 +681,7 @@ def __helper_cfb_pbp_features(self, pbp_txt, init): .str.contains("(?i)field goal") .and_(pl.col("type.text").str.to_lowercase().str.contains("(?i)no good")) ) - .then("Extra Point Missed") + .then(pl.lit("Extra Point Missed")) .otherwise(pl.col("type.text")) .alias("type.text") ) @@ -1245,7 +1245,7 @@ def __add_new_play_types(self, play_df): .and_(pl.col("start.down") != 4) .and_(pl.col("type.text").is_in(defense_score_vec) == False) ) - .then("Fumble Recovery (Opponent)") + .then(pl.lit("Fumble Recovery (Opponent)")) .otherwise(pl.col("type.text")) .alias("type.text"), ) @@ -1256,7 +1256,7 @@ def __add_new_play_types(self, play_df): .and_(pl.col("change_of_poss") == 1) .and_(pl.col("td_play") == True) ) - .then("Fumble Recovery (Opponent) Touchdown") + .then(pl.lit("Fumble Recovery (Opponent) Touchdown")) .otherwise(pl.col("type.text")) .alias("type.text"), ) @@ -1270,7 +1270,7 @@ def __add_new_play_types(self, play_df): .and_(pl.col("start.down") != 4) .and_(pl.col("type.text").is_in(defense_score_vec) == False) ) - .then("Fumble Recovery (Opponent)") + .then(pl.lit("Fumble Recovery (Opponent)")) .otherwise(pl.col("type.text")) .alias("type.text"), ) @@ -1281,7 +1281,7 @@ def __add_new_play_types(self, play_df): .and_(pl.col("change_of_poss") == 1) .and_(pl.col("td_play") == True) ) - .then("Fumble Recovery (Opponent) Touchdown") + .then(pl.lit("Fumble Recovery (Opponent) Touchdown")) .otherwise(pl.col("type.text")) .alias("type.text"), ) @@ -1293,14 +1293,14 @@ def __add_new_play_types(self, play_df): .and_(pl.col("td_play") == True) .and_(pl.col("td_check") == True) ) - .then("Kickoff Return Touchdown") + .then(pl.lit("Kickoff Return Touchdown")) .otherwise(pl.col("type.text")) .alias("type.text"), ) .with_columns( # -- Fix punt return TDs ---- pl.when((pl.col("punt_play") == True).and_(pl.col("td_play") == True).and_(pl.col("td_check") == True)) - .then("Punt Return Touchdown") + .then(pl.lit("Punt Return Touchdown")) .otherwise(pl.col("type.text")) .alias("type.text"), ) @@ -1312,7 +1312,7 @@ def __add_new_play_types(self, play_df): .and_(pl.col("td_play") == True) .and_(pl.col("td_check") == True) ) - .then("Kickoff Return Touchdown") + .then(pl.lit("Kickoff Return Touchdown")) .otherwise(pl.col("type.text")) .alias("type.text"), ) @@ -1324,7 +1324,7 @@ def __add_new_play_types(self, play_df): .and_(pl.col("fumble_vec") == False) .and_(pl.col("td_check") == True) ) - .then("Rushing Touchdown") + .then(pl.lit("Rushing Touchdown")) .otherwise(pl.col("type.text")) .alias("type.text"), ) @@ -1336,7 +1336,7 @@ def __add_new_play_types(self, play_df): .and_(pl.col("td_check") == True) .and_(pl.col("type.text").is_in(int_vec) == False) ) - .then("Passing Touchdown") + .then(pl.lit("Passing Touchdown")) .otherwise(pl.col("type.text")) .alias("type.text"), ) @@ -1348,7 +1348,7 @@ def __add_new_play_types(self, play_df): .and_(pl.col("fumble_vec") == False) .and_(pl.col("type.text").is_in(int_vec) == False) ) - .then("Passing Touchdown") + .then(pl.lit("Passing Touchdown")) .otherwise(pl.col("type.text")) .alias("type.text"), ) @@ -1358,7 +1358,7 @@ def __add_new_play_types(self, play_df): pl.col("text").str.contains("(?i)for a TD") ) ) - .then("Blocked Field Goal Touchdown") + .then(pl.lit("Blocked Field Goal Touchdown")) .otherwise(pl.col("type.text")) .alias("type.text"), ) @@ -1366,7 +1366,7 @@ def __add_new_play_types(self, play_df): pl.when( (pl.col("type.text").is_in(["Blocked Punt"])).and_(pl.col("text").str.contains("(?i)for a TD")) ) - .then("Blocked Punt Touchdown") + .then(pl.lit("Blocked Punt Touchdown")) .otherwise(pl.col("type.text")) .alias("type.text"), ) @@ -1379,7 +1379,7 @@ def __add_new_play_types(self, play_df): .with_columns( # -- Fix Pass Interception Return TD play_type labels---- pl.when(pl.col("text").str.contains("(?i)pass intercepted for a TD")) - .then("Interception Return Touchdown") + .then(pl.lit("Interception Return Touchdown")) .otherwise(pl.col("type.text")) .alias("type.text"), ) @@ -1390,7 +1390,7 @@ def __add_new_play_types(self, play_df): .and_(pl.col("text").str.contains("(?i)fumbled")) .and_(pl.col("text").str.contains("(?i)TD")) ) - .then("Fumble Recovery (Opponent) Touchdown") + .then(pl.lit("Fumble Recovery (Opponent) Touchdown")) .otherwise(pl.col("type.text")) .alias("type.text"), ) @@ -1398,28 +1398,28 @@ def __add_new_play_types(self, play_df): # -- Fix generic pass plays ---- ##-- first one looks for complete pass pl.when((pl.col("type.text") == "Pass").and_(pl.col("text").str.contains("(?i)pass complete"))) - .then("Pass Completion") + .then(pl.lit("Pass Completion")) .otherwise(pl.col("type.text")) .alias("type.text"), ) .with_columns( ##-- second one looks for incomplete pass pl.when((pl.col("type.text") == "Pass").and_(pl.col("text").str.contains("(?i)pass incomplete"))) - .then("Pass Incompletion") + .then(pl.lit("Pass Incompletion")) .otherwise(pl.col("type.text")) .alias("type.text"), ) .with_columns( ##-- third one looks for interceptions pl.when((pl.col("type.text") == "Pass").and_(pl.col("text").str.contains("(?i)pass intercepted"))) - .then("Pass Interception") + .then(pl.lit("Pass Interception")) .otherwise(pl.col("type.text")) .alias("type.text"), ) .with_columns( ##-- fourth one looks for sacked pl.when((pl.col("type.text") == "Pass").and_(pl.col("text").str.contains("(?i)sacked"))) - .then("Sack") + .then(pl.lit("Sack")) .otherwise(pl.col("type.text")) .alias("type.text"), ) @@ -1430,21 +1430,21 @@ def __add_new_play_types(self, play_df): pl.col("text").str.contains("(?i)pass intercepted for a TD") ) ) - .then("Interception Return Touchdown") + .then(pl.lit("Interception Return Touchdown")) .otherwise(pl.col("type.text")) .alias("type.text"), ) .with_columns( # --- Moving non-Touchdown pass interceptions to one play_type: "Interception Return" ----- pl.when(pl.col("type.text").is_in(["Interception", "Pass Interception", "Pass Interception Return"])) - .then("Interception Return") + .then(pl.lit("Interception Return")) .otherwise(pl.col("type.text")) .alias("type.text"), ) .with_columns( # --- Moving Kickoff/Punt Touchdowns without fumbles to Kickoff/Punt Return Touchdown pl.when((pl.col("type.text") == "Kickoff Touchdown").and_(pl.col("fumble_vec") == False)) - .then("Kickoff Return Touchdown") + .then(pl.lit("Kickoff Return Touchdown")) .otherwise(pl.col("type.text")) .alias("type.text"), ) @@ -1454,13 +1454,13 @@ def __add_new_play_types(self, play_df): .and_(pl.col("td_play") == True) .and_(pl.col("fumble_vec") == False) ) - .then("Kickoff Return Touchdown") + .then(pl.lit("Kickoff Return Touchdown")) .when( (pl.col("type.text") == "Kickoff") .and_(pl.col("text").str.contains("(?i)for a TD")) .and_(pl.col("fumble_vec") == False) ) - .then("Kickoff Return Touchdown") + .then(pl.lit("Kickoff Return Touchdown")) .otherwise(pl.col("type.text")) .alias("type.text"), ) @@ -1470,7 +1470,7 @@ def __add_new_play_types(self, play_df): .and_(pl.col("fumble_vec") == True) .and_(pl.col("change_of_poss") == 1) ) - .then("Kickoff Team Fumble Recovery") + .then(pl.lit("Kickoff Team Fumble Recovery")) .otherwise(pl.col("type.text")) .alias("type.text"), ) @@ -1480,13 +1480,13 @@ def __add_new_play_types(self, play_df): .and_(pl.col("fumble_vec") == False) .and_(pl.col("change_of_poss") == 1) ) - .then("Punt Return Touchdown") + .then(pl.lit("Punt Return Touchdown")) .when( (pl.col("type.text") == "Punt") .and_(pl.col("text").str.contains("(?i)for a TD")) .and_(pl.col("change_of_poss") == 1) ) - .then("Punt Return Touchdown") + .then(pl.lit("Punt Return Touchdown")) .otherwise(pl.col("type.text")) .alias("type.text"), ) @@ -1496,25 +1496,25 @@ def __add_new_play_types(self, play_df): .and_(pl.col("fumble_vec") == True) .and_(pl.col("change_of_poss") == 0) ) - .then("Punt Team Fumble Recovery") + .then(pl.lit("Punt Team Fumble Recovery")) .otherwise(pl.col("type.text")) .alias("type.text"), ) .with_columns( pl.when(pl.col("type.text").is_in(["Punt Touchdown"])) - .then("Punt Team Fumble Recovery Touchdown") + .then(pl.lit("Punt Team Fumble Recovery Touchdown")) .when( (pl.col("scoringPlay") == True) .and_(pl.col("punt_play") == True) .and_(pl.col("change_of_poss") == 0) ) - .then("Punt Team Fumble Recovery Touchdown") + .then(pl.lit("Punt Team Fumble Recovery Touchdown")) .otherwise(pl.col("type.text")) .alias("type.text"), ) .with_columns( pl.when(pl.col("type.text").is_in(["Kickoff Touchdown"])) - .then("Kickoff Team Fumble Recovery Touchdown") + .then(pl.lit("Kickoff Team Fumble Recovery Touchdown")) .otherwise(pl.col("type.text")) .alias("type.text"), ) @@ -1524,7 +1524,7 @@ def __add_new_play_types(self, play_df): (pl.col("pass") == True).or_(pl.col("rush") == True) ) ) - .then("Fumble Recovery (Opponent) Touchdown") + .then(pl.lit("Fumble Recovery (Opponent) Touchdown")) .otherwise(pl.col("type.text")) .alias("type.text"), ) @@ -1535,31 +1535,31 @@ def __add_new_play_types(self, play_df): .and_((pl.col("pass") == True).or_(pl.col("rush") == True)) .and_(pl.col("safety") == True) ) - .then("Safety") + .then(pl.lit("Safety")) .otherwise(pl.col("type.text")) .alias("type.text"), ) .with_columns( pl.when(pl.col("kickoff_safety") == True) - .then("Kickoff (Safety)") + .then(pl.lit("Kickoff (Safety)")) .otherwise(pl.col("type.text")) .alias("type.text"), ) .with_columns( pl.when(pl.col("punt_safety") == True) - .then("Punt (Safety)") + .then(pl.lit("Punt (Safety)")) .otherwise(pl.col("type.text")) .alias("type.text"), ) .with_columns( pl.when(pl.col("penalty_safety") == True) - .then("Penalty (Safety)") + .then(pl.lit("Penalty (Safety)")) .otherwise(pl.col("type.text")) .alias("type.text"), ) .with_columns( pl.when((pl.col("type.text") == "Extra Point Good").and_(pl.col("text").str.contains("(?i)Two-Point"))) - .then("Two-Point Conversion Good") + .then(pl.lit("Two-Point Conversion Good")) .otherwise(pl.col("type.text")) .alias("type.text"), ) @@ -1567,7 +1567,7 @@ def __add_new_play_types(self, play_df): pl.when( (pl.col("type.text") == "Extra Point Missed").and_(pl.col("text").str.contains("(?i)Two-Point")) ) - .then("Two-Point Conversion Missed") + .then(pl.lit("Two-Point Conversion Missed")) .otherwise(pl.col("type.text")) .alias("type.text"), ) @@ -1639,121 +1639,121 @@ def __setup_penalty_data(self, play_df): ) .with_columns( penalty_detail=pl.when(pl.col("penalty_offset") == 1) - .then("Offsetting") + .then(pl.lit("Offsetting")) .when(pl.col("penalty_declined") == 1) - .then("Declined") + .then(pl.lit("Declined")) .when(pl.col("text").str.contains("(?i)roughing passer")) - .then("Roughing the Passer") + .then(pl.lit("Roughing the Passer")) .when(pl.col("text").str.contains("(?i)offensive holding")) - .then("Offensive Holding") + .then(pl.lit("Offensive Holding")) .when(pl.col("text").str.contains("(?i)pass interference")) - .then("Pass Interference") + .then(pl.lit("Pass Interference")) .when(pl.col("text").str.contains("(?i)encroachment")) - .then("Encroachment") + .then(pl.lit("Encroachment")) .when(pl.col("text").str.contains("(?i)defensive pass interference")) - .then("Defensive Pass Interference") + .then(pl.lit("Defensive Pass Interference")) .when(pl.col("text").str.contains("(?i)offensive pass interference")) - .then("Offensive Pass Interference") + .then(pl.lit("Offensive Pass Interference")) .when(pl.col("text").str.contains("(?i)illegal procedure")) - .then("Illegal Procedure") + .then(pl.lit("Illegal Procedure")) .when(pl.col("text").str.contains("(?i)defensive holding")) - .then("Defensive Holding") + .then(pl.lit("Defensive Holding")) .when(pl.col("text").str.contains("(?i)holding")) - .then("Holding") + .then(pl.lit("Holding")) .when(pl.col("text").str.contains("(?i)offensive offside|(?i)offside offense")) - .then("Offensive Offside") + .then(pl.lit("Offensive Offside")) .when(pl.col("text").str.contains("(?i)defensive offside|(?i)offside defense")) - .then("Defensive Offside") + .then(pl.lit("Defensive Offside")) .when(pl.col("text").str.contains("(?i)offside")) - .then("Offside") + .then(pl.lit("Offside")) .when(pl.col("text").str.contains("(?i)illegal fair catch signal")) - .then("Illegal Fair Catch Signal") + .then(pl.lit("Illegal Fair Catch Signal")) .when(pl.col("text").str.contains("(?i)illegal batting")) - .then("Illegal Batting") + .then(pl.lit("Illegal Batting")) .when(pl.col("text").str.contains("(?i)neutral zone infraction")) - .then("Neutral Zone Infraction") + .then(pl.lit("Neutral Zone Infraction")) .when(pl.col("text").str.contains("(?i)ineligible downfield")) - .then("Ineligible Downfield") + .then(pl.lit("Ineligible Downfield")) .when(pl.col("text").str.contains("(?i)illegal use of hands")) - .then("Illegal Use of Hands") + .then(pl.lit("Illegal Use of Hands")) .when(pl.col("text").str.contains("(?i)kickoff out of bounds|(?i)kickoff out-of-bounds")) - .then("Kickoff Out of Bounds") + .then(pl.lit("Kickoff Out of Bounds")) .when(pl.col("text").str.contains("(?i)12 men on the field")) - .then("12 Men on the Field") + .then(pl.lit("12 Men on the Field")) .when(pl.col("text").str.contains("(?i)illegal block")) - .then("Illegal Block") + .then(pl.lit("Illegal Block")) .when(pl.col("text").str.contains("(?i)personal foul")) - .then("Personal Foul") + .then(pl.lit("Personal Foul")) .when(pl.col("text").str.contains("(?i)false start")) - .then("False Start") + .then(pl.lit("False Start")) .when(pl.col("text").str.contains("(?i)substitution infraction")) - .then("Substitution Infraction") + .then(pl.lit("Substitution Infraction")) .when(pl.col("text").str.contains("(?i)illegal formation")) - .then("Illegal Formation") + .then(pl.lit("Illegal Formation")) .when(pl.col("text").str.contains("(?i)illegal touching")) - .then("Illegal Touching") + .then(pl.lit("Illegal Touching")) .when(pl.col("text").str.contains("(?i)sideline interference")) - .then("Sideline Interference") + .then(pl.lit("Sideline Interference")) .when(pl.col("text").str.contains("(?i)clipping")) - .then("Clipping") + .then(pl.lit("Clipping")) .when(pl.col("text").str.contains("(?i)sideline infraction")) - .then("Sideline Infraction") + .then(pl.lit("Sideline Infraction")) .when(pl.col("text").str.contains("(?i)crackback")) - .then("Crackback") + .then(pl.lit("Crackback")) .when(pl.col("text").str.contains("(?i)illegal snap")) - .then("Illegal Snap") + .then(pl.lit("Illegal Snap")) .when(pl.col("text").str.contains("(?i)illegal helmet contact")) - .then("Illegal Helmet Contact") + .then(pl.lit("Illegal Helmet Contact")) .when(pl.col("text").str.contains("(?i)roughing holder")) - .then("Roughing the Holder") + .then(pl.lit("Roughing the Holder")) .when(pl.col("text").str.contains("(?i)horse collar tackle")) - .then("Horse Collar Tackle") + .then(pl.lit("Horse Collar Tackle")) .when(pl.col("text").str.contains("(?i)illegal participation")) - .then("Illegal Participation") + .then(pl.lit("Illegal Participation")) .when(pl.col("text").str.contains("(?i)tripping")) - .then("Tripping") + .then(pl.lit("Tripping")) .when(pl.col("text").str.contains("(?i)illegal shift")) - .then("Illegal Shift") + .then(pl.lit("Illegal Shift")) .when(pl.col("text").str.contains("(?i)illegal motion")) - .then("Illegal Motion") + .then(pl.lit("Illegal Motion")) .when(pl.col("text").str.contains("(?i)roughing the kicker")) - .then("Roughing the Kicker") + .then(pl.lit("Roughing the Kicker")) .when(pl.col("text").str.contains("(?i)delay of game")) - .then("Delay of Game") + .then(pl.lit("Delay of Game")) .when(pl.col("text").str.contains("(?i)targeting")) - .then("Targeting") + .then(pl.lit("Targeting")) .when(pl.col("text").str.contains("(?i)face mask")) - .then("Face Mask") + .then(pl.lit("Face Mask")) .when(pl.col("text").str.contains("(?i)illegal forward pass")) - .then("Illegal Forward Pass") + .then(pl.lit("Illegal Forward Pass")) .when(pl.col("text").str.contains("(?i)intentional grounding")) - .then("Intentional Grounding") + .then(pl.lit("Intentional Grounding")) .when(pl.col("text").str.contains("(?i)illegal kicking")) - .then("Illegal Kicking") + .then(pl.lit("Illegal Kicking")) .when(pl.col("text").str.contains("(?i)illegal conduct")) - .then("Illegal Conduct") + .then(pl.lit("Illegal Conduct")) .when(pl.col("text").str.contains("(?i)kick catching interference")) - .then("Kick Catch Interference") + .then(pl.lit("Kick Catch Interference")) .when(pl.col("text").str.contains("(?i)kick catch interference")) - .then("Kick Catch Interference") + .then(pl.lit("Kick Catch Interference")) .when(pl.col("text").str.contains("(?i)unnecessary roughness")) - .then("Unnecessary Roughness") + .then(pl.lit("Unnecessary Roughness")) .when(pl.col("text").str.contains("(?i)Penalty, UR")) - .then("Unnecessary Roughness") + .then(pl.lit("Unnecessary Roughness")) .when(pl.col("text").str.contains("(?i)roughing the snapper")) - .then("Roughing the Snapper") + .then(pl.lit("Roughing the Snapper")) .when(pl.col("text").str.contains("(?i)illegal blindside block")) - .then("Illegal Blindside Block") + .then(pl.lit("Illegal Blindside Block")) .when(pl.col("text").str.contains("(?i)unsportsmanlike conduct")) - .then("Unsportsmanlike Conduct") + .then(pl.lit("Unsportsmanlike Conduct")) .when(pl.col("text").str.contains("(?i)running into kicker")) - .then("Running Into Kicker") + .then(pl.lit("Running Into Kicker")) .when(pl.col("text").str.contains("(?i)failure to wear required equipment")) - .then("Failure to Wear Required Equipment") + .then(pl.lit("Failure to Wear Required Equipment")) .when(pl.col("text").str.contains("(?i)player disqualification")) - .then("Player Disqualification") + .then(pl.lit("Player Disqualification")) .when(pl.col("penalty_flag") == True) - .then("Missing") + .then(pl.lit("Missing")) ) .with_columns( penalty_text=pl.when(pl.col("penalty_flag") == True) @@ -1951,23 +1951,23 @@ def __add_play_category_flags(self, play_df): ) .with_columns( pos_unit=pl.when(pl.col("punt") == True) - .then("Punt Offense") + .then(pl.lit("Punt Offense")) .when(pl.col("kickoff_play") == True) - .then("Kickoff Return") + .then(pl.lit("Kickoff Return")) .when(pl.col("fg_attempt") == True) - .then("Field Goal Offense") + .then(pl.lit("Field Goal Offense")) .when(pl.col("type.text") == "Defensive 2pt Conversion") - .then("Offense") - .otherwise("Offense"), + .then(pl.lit("Offense")) + .otherwise(pl.lit("Offense")), def_pos_unit=pl.when(pl.col("punt") == True) - .then("Punt Return") + .then(pl.lit("Punt Return")) .when(pl.col("kickoff_play") == True) - .then("Kickoff Defense") + .then(pl.lit("Kickoff Defense")) .when(pl.col("fg_attempt") == True) - .then("Field Goal Defense") + .then(pl.lit("Field Goal Defense")) .when(pl.col("type.text") == "Defensive 2pt Conversion") - .then("Defense") - .otherwise("Defense"), + .then(pl.lit("Defense")) + .otherwise(pl.lit("Defense")), # --- Lags/Leads play type ---- lead_play_type=pl.col("type.text").shift(-1), sp=pl.when( @@ -2324,7 +2324,7 @@ def __add_player_cols(self, play_df): ((pl.col("pass_player").str.strip().str.n_chars() == 0).or_(pl.col("pass_player").is_null())) ) ) - .then("TEAM") + .then(pl.lit("TEAM")) .otherwise(pl.col("pass_player")), # --- WR Names ----- receiver_player=pl.when( diff --git a/sportsdataverse/nfl/nfl_pbp.py b/sportsdataverse/nfl/nfl_pbp.py index 1d394ca..ea7bac6 100755 --- a/sportsdataverse/nfl/nfl_pbp.py +++ b/sportsdataverse/nfl/nfl_pbp.py @@ -2,15 +2,16 @@ import os import re import time -from functools import partial, reduce +from functools import reduce import numpy as np import pandas as pd -import pkg_resources -from numpy.core.fromnumeric import mean +import polars as pl +from pkg_resources import resource_filename from xgboost import Booster, DMatrix -from sportsdataverse.cfb.model_vars import ( +from sportsdataverse.dl_utils import download, key_check +from sportsdataverse.nfl.model_vars import ( defense_score_vec, end_change_vec, ep_class_to_score_mapping, @@ -33,7 +34,6 @@ wp_start_columns, wp_start_touchback_columns, ) -from sportsdataverse.dl_utils import download, key_check # "td" : float(p[0]), # "opp_td" : float(p[1]), @@ -42,9 +42,9 @@ # "safety" : float(p[4]), # "opp_safety" : float(p[5]), # "no_score" : float(p[6]) -ep_model_file = pkg_resources.resource_filename("sportsdataverse", "nfl/models/ep_model.model") -wp_spread_file = pkg_resources.resource_filename("sportsdataverse", "nfl/models/wp_spread.model") -qbr_model_file = pkg_resources.resource_filename("sportsdataverse", "nfl/models/qbr_model.model") +ep_model_file = resource_filename("sportsdataverse", "nfl/models/ep_model.model") +wp_spread_file = resource_filename("sportsdataverse", "nfl/models/wp_spread.model") +qbr_model_file = resource_filename("sportsdataverse", "nfl/models/qbr_model.model") ep_model = Booster({"nthread": 4}) # init model ep_model.load_model(ep_model_file) @@ -94,7 +94,7 @@ def espn_nfl_pbp(self): summary_url = ( f"http://site.api.espn.com/apis/site/v2/sports/football/nfl/summary?event={self.gameId}&{cache_buster}" ) - summary_resp = download(summary_url) + summary_resp = download(url=summary_url, **kwargs) summary = summary_resp.json() incoming_keys_expected = [ "boxscore", @@ -168,74 +168,25 @@ def nfl_pbp_disk(self): return self.json def __helper_nfl_pbp_drives(self, pbp_txt): - ( - pbp_txt, - gameSpread, - overUnder, - homeFavorite, - gameSpreadAvailable, - homeTeamId, - homeTeamMascot, - homeTeamName, - homeTeamAbbrev, - homeTeamNameAlt, - awayTeamId, - awayTeamMascot, - awayTeamName, - awayTeamAbbrev, - awayTeamNameAlt, - ) = self.__helper_nfl_pbp(pbp_txt) + pbp_txt, init = self.__helper_nfl_pbp(pbp_txt) - pbp_txt["plays"] = pd.DataFrame() + pbp_txt["plays"] = pl.DataFrame() # negotiating the drive meta keys into columns after unnesting drive plays # concatenating the previous and current drives categories when necessary if ( "drives" in pbp_txt.keys() and pbp_txt.get("header").get("competitions")[0].get("playByPlaySource") != "none" ): - pbp_txt = self.__helper_nfl_pbp_features( - pbp_txt, - gameSpread, - gameSpreadAvailable, - overUnder, - homeFavorite, - homeTeamId, - homeTeamMascot, - homeTeamName, - homeTeamAbbrev, - homeTeamNameAlt, - awayTeamId, - awayTeamMascot, - awayTeamName, - awayTeamAbbrev, - awayTeamNameAlt, - ) + pbp_txt = self.__helper_nfl_pbp_features(pbp_txt, init) else: pbp_txt["drives"] = {} return pbp_txt - def __helper_nfl_pbp_features( - self, - pbp_txt, - gameSpread, - gameSpreadAvailable, - overUnder, - homeFavorite, - homeTeamId, - homeTeamMascot, - homeTeamName, - homeTeamAbbrev, - homeTeamNameAlt, - awayTeamId, - awayTeamMascot, - awayTeamName, - awayTeamAbbrev, - awayTeamNameAlt, - ): - pbp_txt["plays"] = pd.DataFrame() + def __helper_nfl_pbp_features(self, pbp_txt, init): + pbp_txt["plays"] = pl.DataFrame() for key in pbp_txt.get("drives").keys(): prev_drives = pd.json_normalize( - data=pbp_txt.get("drives").get(f"{key}"), + data=pbp_txt.get("drives").get("{}".format(key)), record_path="plays", meta=[ "id", @@ -264,363 +215,527 @@ def __helper_nfl_pbp_features( meta_prefix="drive.", errors="ignore", ) - pbp_txt["plays"] = pd.concat([pbp_txt["plays"], prev_drives], ignore_index=True) - - pbp_txt["plays"] = pbp_txt["plays"].to_dict(orient="records") - pbp_txt["plays"] = pd.DataFrame(pbp_txt["plays"]) - pbp_txt["plays"]["id"] = int(self.gameId) - pbp_txt["plays"]["game_id"] = int(self.gameId) - pbp_txt["plays"]["season"] = pbp_txt.get("header").get("season").get("year") - pbp_txt["plays"]["seasonType"] = pbp_txt.get("header").get("season").get("type") - pbp_txt["plays"]["homeTeamId"] = homeTeamId - pbp_txt["plays"]["awayTeamId"] = awayTeamId - pbp_txt["plays"]["homeTeamName"] = str(homeTeamName) - pbp_txt["plays"]["awayTeamName"] = str(awayTeamName) - pbp_txt["plays"]["homeTeamMascot"] = str(homeTeamMascot) - pbp_txt["plays"]["awayTeamMascot"] = str(awayTeamMascot) - pbp_txt["plays"]["homeTeamAbbrev"] = str(homeTeamAbbrev) - pbp_txt["plays"]["awayTeamAbbrev"] = str(awayTeamAbbrev) - pbp_txt["plays"]["homeTeamNameAlt"] = str(homeTeamNameAlt) - pbp_txt["plays"]["awayTeamNameAlt"] = str(awayTeamNameAlt) - pbp_txt["plays"]["period.number"] = pbp_txt["plays"]["period.number"].apply(lambda x: int(x)) - pbp_txt["plays"]["homeTeamSpread"] = np.where(homeFavorite == True, abs(gameSpread), -1 * abs(gameSpread)) - pbp_txt["plays"]["gameSpread"] = gameSpread - pbp_txt["plays"]["gameSpreadAvailable"] = gameSpreadAvailable - pbp_txt["plays"]["overUnder"] = float(overUnder) - pbp_txt["plays"]["homeFavorite"] = homeFavorite - pbp_txt["homeTeamSpread"] = np.where(homeFavorite == True, abs(gameSpread), -1 * abs(gameSpread)) - pbp_txt["gameSpread"] = gameSpread - pbp_txt["gameSpreadAvailable"] = gameSpreadAvailable - pbp_txt["overUnder"] = float(overUnder) - pbp_txt["homeFavorite"] = homeFavorite + pbp_txt["plays"] = pl.concat([pbp_txt["plays"], pl.from_pandas(prev_drives)], how="vertical") - pbp_txt["timeouts"] = { - homeTeamId: {"1": [], "2": []}, - awayTeamId: {"1": [], "2": []}, - } - pbp_txt["plays"]["clock.mm"] = pbp_txt["plays"]["clock.displayValue"].str.split(pat=":") - pbp_txt["plays"][["clock.minutes", "clock.seconds"]] = pbp_txt["plays"]["clock.mm"].to_list() - pbp_txt["plays"]["half"] = np.where(pbp_txt["plays"]["period.number"] <= 2, "1", "2") - pbp_txt["plays"]["lag_half"] = pbp_txt["plays"]["half"].shift(1) - pbp_txt["plays"]["lead_half"] = pbp_txt["plays"]["half"].shift(-1) - pbp_txt["plays"]["start.TimeSecsRem"] = np.where( - pbp_txt["plays"]["period.number"].isin([1, 3]), - 900 + 60 * pbp_txt["plays"]["clock.minutes"].astype(int) + pbp_txt["plays"]["clock.seconds"].astype(int), - 60 * pbp_txt["plays"]["clock.minutes"].astype(int) + pbp_txt["plays"]["clock.seconds"].astype(int), - ) - pbp_txt["plays"]["start.adj_TimeSecsRem"] = np.select( - [ - pbp_txt["plays"]["period.number"] == 1, - pbp_txt["plays"]["period.number"] == 2, - pbp_txt["plays"]["period.number"] == 3, - pbp_txt["plays"]["period.number"] == 4, - ], - [ - 2700 - + 60 * pbp_txt["plays"]["clock.minutes"].astype(int) - + pbp_txt["plays"]["clock.seconds"].astype(int), - 1800 - + 60 * pbp_txt["plays"]["clock.minutes"].astype(int) - + pbp_txt["plays"]["clock.seconds"].astype(int), - 900 - + 60 * pbp_txt["plays"]["clock.minutes"].astype(int) - + pbp_txt["plays"]["clock.seconds"].astype(int), - 60 * pbp_txt["plays"]["clock.minutes"].astype(int) + pbp_txt["plays"]["clock.seconds"].astype(int), - ], - default=60 * pbp_txt["plays"]["clock.minutes"].astype(int) + pbp_txt["plays"]["clock.seconds"].astype(int), - ) - pbp_txt["plays"]["id"] = pbp_txt["plays"]["id"].apply(lambda x: int(x)) - pbp_txt["plays"] = pbp_txt["plays"].sort_values(by=["id", "start.adj_TimeSecsRem"]) - pbp_txt["plays"]["game_play_number"] = np.arange(len(pbp_txt["plays"])) + 1 - pbp_txt["plays"]["text"] = pbp_txt["plays"]["text"].astype(str) - pbp_txt["plays"]["start.team.id"] = ( - pbp_txt["plays"]["start.team.id"] - .fillna(method="ffill") # fill downward first to make sure all playIDs are accurate - .fillna(method="bfill") # fill upward so that any remaining NAs are covered - .apply(lambda x: int(x)) - ) - pbp_txt["plays"]["end.team.id"] = ( - pbp_txt["plays"]["end.team.id"].fillna(value=pbp_txt["plays"]["start.team.id"]).apply(lambda x: int(x)) - ) - pbp_txt["plays"]["start.pos_team.id"] = np.select( - [ - (pbp_txt["plays"]["type.text"].isin(kickoff_vec)) - & (pbp_txt["plays"]["start.team.id"].astype(int) == pbp_txt["plays"]["homeTeamId"].astype(int)), - (pbp_txt["plays"]["type.text"].isin(kickoff_vec)) - & (pbp_txt["plays"]["start.team.id"].astype(int) == pbp_txt["plays"]["awayTeamId"].astype(int)), - ], - [ - pbp_txt["plays"]["awayTeamId"].astype(int), - pbp_txt["plays"]["homeTeamId"].astype(int), - ], - default=pbp_txt["plays"]["start.team.id"].astype(int), - ) - pbp_txt["plays"]["start.def_pos_team.id"] = np.where( - pbp_txt["plays"]["start.pos_team.id"].astype(int) == pbp_txt["plays"]["homeTeamId"].astype(int), - pbp_txt["plays"]["awayTeamId"].astype(int), - pbp_txt["plays"]["homeTeamId"].astype(int), - ) - pbp_txt["plays"]["end.def_team.id"] = np.where( - pbp_txt["plays"]["end.team.id"].astype(int) == pbp_txt["plays"]["homeTeamId"].astype(int), - pbp_txt["plays"]["awayTeamId"].astype(int), - pbp_txt["plays"]["homeTeamId"].astype(int), - ) - pbp_txt["plays"]["end.pos_team.id"] = pbp_txt["plays"]["end.team.id"].apply(lambda x: int(x)) - pbp_txt["plays"]["end.def_pos_team.id"] = pbp_txt["plays"]["end.def_team.id"].apply(lambda x: int(x)) - pbp_txt["plays"]["start.pos_team.name"] = np.where( - pbp_txt["plays"]["start.pos_team.id"].astype(int) == pbp_txt["plays"]["homeTeamId"].astype(int), - pbp_txt["plays"]["homeTeamName"], - pbp_txt["plays"]["awayTeamName"], - ) - pbp_txt["plays"]["start.def_pos_team.name"] = np.where( - pbp_txt["plays"]["start.pos_team.id"].astype(int) == pbp_txt["plays"]["homeTeamId"].astype(int), - pbp_txt["plays"]["awayTeamName"], - pbp_txt["plays"]["homeTeamName"], - ) - pbp_txt["plays"]["end.pos_team.name"] = np.where( - pbp_txt["plays"]["end.pos_team.id"].astype(int) == pbp_txt["plays"]["homeTeamId"].astype(int), - pbp_txt["plays"]["homeTeamName"], - pbp_txt["plays"]["awayTeamName"], - ) - pbp_txt["plays"]["end.def_pos_team.name"] = np.where( - pbp_txt["plays"]["end.pos_team.id"].astype(int) == pbp_txt["plays"]["homeTeamId"].astype(int), - pbp_txt["plays"]["awayTeamName"], - pbp_txt["plays"]["homeTeamName"], - ) - pbp_txt["plays"]["start.is_home"] = np.where( - pbp_txt["plays"]["start.pos_team.id"].astype(int) == pbp_txt["plays"]["homeTeamId"].astype(int), - True, - False, - ) - pbp_txt["plays"]["end.is_home"] = np.where( - pbp_txt["plays"]["end.pos_team.id"].astype(int) == pbp_txt["plays"]["homeTeamId"].astype(int), - True, - False, - ) - pbp_txt["plays"]["homeTimeoutCalled"] = np.where( - (pbp_txt["plays"]["type.text"] == "Timeout") - & ( - (pbp_txt["plays"]["text"].str.lower().str.contains(str(homeTeamAbbrev), case=False)) - | (pbp_txt["plays"]["text"].str.lower().str.contains(str(homeTeamName), case=False)) - | (pbp_txt["plays"]["text"].str.lower().str.contains(str(homeTeamMascot), case=False)) - | (pbp_txt["plays"]["text"].str.lower().str.contains(str(homeTeamNameAlt), case=False)) - ), - True, - False, - ) - pbp_txt["plays"]["awayTimeoutCalled"] = np.where( - (pbp_txt["plays"]["type.text"] == "Timeout") - & ( - (pbp_txt["plays"]["text"].str.lower().str.contains(str(awayTeamAbbrev), case=False)) - | (pbp_txt["plays"]["text"].str.lower().str.contains(str(awayTeamName), case=False)) - | (pbp_txt["plays"]["text"].str.lower().str.contains(str(awayTeamMascot), case=False)) - | (pbp_txt["plays"]["text"].str.lower().str.contains(str(awayTeamNameAlt), case=False)) - ), - True, - False, - ) - pbp_txt["timeouts"][homeTeamId]["1"] = ( - pbp_txt["plays"] - .loc[(pbp_txt["plays"]["homeTimeoutCalled"] == True) & (pbp_txt["plays"]["period.number"] <= 2)] - .reset_index()["id"] - ) - pbp_txt["timeouts"][homeTeamId]["2"] = ( + # pbp_txt["plays"] = pbp_txt["plays"].to_dict(orient="records") + # pbp_txt["plays"] = pd.DataFrame(pbp_txt["plays"]) + pbp_txt["plays"] = ( pbp_txt["plays"] - .loc[(pbp_txt["plays"]["homeTimeoutCalled"] == True) & (pbp_txt["plays"]["period.number"] > 2)] - .reset_index()["id"] + .with_columns( + game_id=pl.lit(int(self.gameId)), + season=pbp_txt.get("header").get("season").get("year"), + seasonType=pbp_txt.get("header").get("season").get("type"), + week=pbp_txt.get("header").get("week"), + status_type_completed=pbp_txt.get("header") + .get("competitions")[0] + .get("status") + .get("type") + .get("completed"), + homeTeamId=pl.lit(init["homeTeamId"]), + awayTeamId=pl.lit(init["awayTeamId"]), + homeTeamName=pl.lit(str(init["homeTeamName"])), + awayTeamName=pl.lit(str(init["awayTeamName"])), + homeTeamMascot=pl.lit(str(init["homeTeamMascot"])), + awayTeamMascot=pl.lit(str(init["awayTeamMascot"])), + homeTeamAbbrev=pl.lit(str(init["homeTeamAbbrev"])), + awayTeamAbbrev=pl.lit(str(init["awayTeamAbbrev"])), + homeTeamNameAlt=pl.lit(str(init["homeTeamNameAlt"])), + awayTeamNameAlt=pl.lit(str(init["awayTeamNameAlt"])), + gameSpread=pl.lit(init["gameSpread"]).abs(), + homeFavorite=pl.lit(init["homeFavorite"]), + gameSpreadAvailable=pl.lit(init["gameSpreadAvailable"]), + overUnder=pl.lit(float(init["overUnder"])), + ) + .with_columns( + homeTeamSpread=pl.when(pl.col("homeFavorite") == True) + .then(pl.col("gameSpread")) + .otherwise(-1 * pl.col("gameSpread")), + ) + .with_columns( + pl.col("period.number").cast(pl.Int32), + pl.col("clock.displayValue") + .str.split(":") + .list.to_struct(n_field_strategy="max_width") + .alias("clock.mm"), + ) + .with_columns(pl.col("clock.mm").struct.rename_fields(["clock.minutes", "clock.seconds"])) + .unnest("clock.mm") + .with_columns( + pl.col("clock.minutes").cast(pl.Int32), + pl.col("clock.seconds").cast(pl.Int32), + half=pl.when(pl.col("period.number") <= 2).then(1).otherwise(2), + ) + .with_columns(lag_half=pl.col("half").shift(1), lead_half=pl.col("half").shift(-1)) + .with_columns( + pl.when(pl.col("period.number").is_in([1, 3])) + .then(900 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) + .otherwise(60 * pl.col("clock.minutes") + pl.col("clock.seconds")) + .alias("start.TimeSecsRem"), + pl.when(pl.col("period.number") == 1) + .then(2700 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) + .when(pl.col("period.number") == 2) + .then(1800 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) + .when(pl.col("period.number") == 3) + .then(900 + 60 * pl.col("clock.minutes") + pl.col("clock.seconds")) + .otherwise(60 * pl.col("clock.minutes") + pl.col("clock.seconds")) + .alias("start.adj_TimeSecsRem"), + pl.col("id").cast(pl.Int64), + ) ) - pbp_txt["timeouts"][awayTeamId]["1"] = ( + pbp_txt["plays"] = pbp_txt["plays"].sort(by=["id", "start.adj_TimeSecsRem"]) + + # drop play text dupes intelligently, even if they have different play_id values + pbp_txt["plays"] = ( pbp_txt["plays"] - .loc[(pbp_txt["plays"]["awayTimeoutCalled"] == True) & (pbp_txt["plays"]["period.number"] <= 2)] - .reset_index()["id"] + .with_columns( + pl.col("text").cast(str), + orig_play_type=pl.col("type.text"), + lead_text=pl.col("text").shift(-1), + lead_start_team=pl.col("start.team.id").shift(-1), + lead_start_yardsToEndzone=pl.col("start.yardsToEndzone").shift(-1), + lead_start_down=pl.col("start.down").shift(-1), + lead_start_distance=pl.col("start.distance").shift(-1), + lead_scoringPlay=pl.col("scoringPlay").shift(-1), + text_dupe=pl.lit(False), + ) + .with_columns( + text_dupe=pl.when( + (pl.col("start.team.id") == pl.col("lead_start_team")) + .and_(pl.col("start.down") == pl.col("lead_start_down")) + .and_(pl.col("start.yardsToEndzone") == pl.col("lead_start_yardsToEndzone")) + .and_(pl.col("start.distance") == pl.col("lead_start_distance")) + .and_(pl.col("text") == pl.col("lead_text")) + ) + .then(pl.lit(True)) + .when( + (pl.col("start.team.id") == pl.col("lead_start_team")) + .and_(pl.col("start.down") == pl.col("lead_start_down")) + .and_(pl.col("start.yardsToEndzone") == pl.col("lead_start_yardsToEndzone")) + .and_(pl.col("start.distance") == pl.col("lead_start_distance")) + .and_(pl.col("text").is_in(pl.col("lead_text"))) + ) + .then(pl.lit(True)) + .otherwise(pl.lit(False)) + ) ) - pbp_txt["timeouts"][awayTeamId]["2"] = ( + pbp_txt["plays"] = pbp_txt["plays"].filter(pl.col("text_dupe") == False) + pbp_txt["plays"] = pbp_txt["plays"].with_row_count("game_play_number", 1) + pbp_txt["plays"] = ( pbp_txt["plays"] - .loc[(pbp_txt["plays"]["awayTimeoutCalled"] == True) & (pbp_txt["plays"]["period.number"] > 2)] - .reset_index()["id"] + .with_columns( + pl.col("start.team.id").fill_null(strategy="forward").fill_null(strategy="backward").cast(pl.Int32) + ) + .with_columns(pl.col("end.team.id").fill_null(value=pl.col("start.team.id")).cast(pl.Int32)) + .with_columns( + pl.col("start.team.id").cast(pl.Int32), + pl.col("end.team.id").cast(pl.Int32), + pl.col("homeTeamId").cast(pl.Int32), + pl.col("awayTeamId").cast(pl.Int32), + pl.when(pl.col("type.text").is_in(kickoff_vec).and_(pl.col("start.team.id") == init["homeTeamId"])) + .then(pl.col("awayTeamId")) + .when(pl.col("type.text").is_in(kickoff_vec).and_(pl.col("start.team.id") == init["awayTeamId"])) + .then(pl.col("homeTeamId")) + .otherwise(pl.col("start.team.id")) + .alias("start.pos_team.id"), + ) + .with_columns( + pl.when(pl.col("start.pos_team.id") == init["homeTeamId"]) + .then(init["awayTeamId"]) + .otherwise(init["homeTeamId"]) + .alias("start.def_pos_team.id"), + pl.when(pl.col("end.team.id") == init["homeTeamId"]) + .then(init["awayTeamId"]) + .otherwise(init["homeTeamId"]) + .alias("end.def_pos_team.id"), + pl.col("end.team.id").alias("end.pos_team.id"), + ) + .with_columns( + pl.when(pl.col("start.pos_team.id") == init["homeTeamId"]) + .then(pl.col("homeTeamName")) + .otherwise(pl.col("awayTeamName")) + .alias("start.pos_team.name"), + pl.when(pl.col("start.pos_team.id") == init["homeTeamId"]) + .then(pl.col("awayTeamName")) + .otherwise(pl.col("homeTeamName")) + .alias("start.def_pos_team.name"), + pl.when(pl.col("end.pos_team.id") == init["homeTeamId"]) + .then(pl.col("homeTeamName")) + .otherwise(pl.col("awayTeamName")) + .alias("end.pos_team.name"), + pl.when(pl.col("end.pos_team.id") == init["homeTeamId"]) + .then(pl.col("awayTeamName")) + .otherwise(pl.col("homeTeamName")) + .alias("end.def_pos_team.name"), + pl.when(pl.col("start.pos_team.id") == init["homeTeamId"]) + .then(True) + .otherwise(False) + .alias("start.is_home"), + pl.when(pl.col("end.pos_team.id") == init["homeTeamId"]) + .then(True) + .otherwise(False) + .alias("end.is_home"), + pl.when( + (pl.col("type.text") == "Timeout").and_( + pl.col("text") + .str.to_lowercase() + .str.contains(str(init["homeTeamAbbrev"]).lower()) + .or_( + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamAbbrev"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamName"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamMascot"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["homeTeamNameAlt"]).lower()), + ) + ) + ) + .then(True) + .otherwise(False) + .alias("homeTimeoutCalled"), + pl.when( + (pl.col("type.text") == "Timeout").and_( + pl.col("text") + .str.to_lowercase() + .str.contains(str(init["awayTeamAbbrev"]).lower()) + .or_( + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamAbbrev"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamName"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamMascot"]).lower()), + pl.col("text").str.to_lowercase().str.contains(str(init["awayTeamNameAlt"]).lower()), + ) + ) + ) + .then(True) + .otherwise(False) + .alias("awayTimeoutCalled"), + ) ) - pbp_txt["timeouts"][homeTeamId]["1"] = pbp_txt["timeouts"][homeTeamId]["1"].apply(lambda x: int(x)) - pbp_txt["timeouts"][homeTeamId]["2"] = pbp_txt["timeouts"][homeTeamId]["2"].apply(lambda x: int(x)) - pbp_txt["timeouts"][awayTeamId]["1"] = pbp_txt["timeouts"][awayTeamId]["1"].apply(lambda x: int(x)) - pbp_txt["timeouts"][awayTeamId]["2"] = pbp_txt["timeouts"][awayTeamId]["2"].apply(lambda x: int(x)) - pbp_txt["plays"]["end.homeTeamTimeouts"] = 3 - pbp_txt["plays"].apply( - lambda x: ((pbp_txt["timeouts"][homeTeamId]["1"] <= x["id"]) & (x["period.number"] <= 2)) - | ((pbp_txt["timeouts"][homeTeamId]["2"] <= x["id"]) & (x["period.number"] > 2)), - axis=1, - ).apply(lambda x: int(x.sum()), axis=1) - pbp_txt["plays"]["end.awayTeamTimeouts"] = 3 - pbp_txt["plays"].apply( - lambda x: ((pbp_txt["timeouts"][awayTeamId]["1"] <= x["id"]) & (x["period.number"] <= 2)) - | ((pbp_txt["timeouts"][awayTeamId]["2"] <= x["id"]) & (x["period.number"] > 2)), - axis=1, - ).apply(lambda x: int(x.sum()), axis=1) - pbp_txt["plays"]["start.homeTeamTimeouts"] = pbp_txt["plays"]["end.homeTeamTimeouts"].shift(1) - pbp_txt["plays"]["start.awayTeamTimeouts"] = pbp_txt["plays"]["end.awayTeamTimeouts"].shift(1) - pbp_txt["plays"]["start.homeTeamTimeouts"] = np.where( - (pbp_txt["plays"]["game_play_number"] == 1) - | ((pbp_txt["plays"]["half"] == "2") & (pbp_txt["plays"]["lag_half"] == "1")), - 3, - pbp_txt["plays"]["start.homeTeamTimeouts"], - ) - pbp_txt["plays"]["start.awayTeamTimeouts"] = np.where( - (pbp_txt["plays"]["game_play_number"] == 1) - | ((pbp_txt["plays"]["half"] == "2") & (pbp_txt["plays"]["lag_half"] == "1")), - 3, - pbp_txt["plays"]["start.awayTeamTimeouts"], - ) - pbp_txt["plays"]["start.homeTeamTimeouts"] = pbp_txt["plays"]["start.homeTeamTimeouts"].apply(lambda x: int(x)) - pbp_txt["plays"]["start.awayTeamTimeouts"] = pbp_txt["plays"]["start.awayTeamTimeouts"].apply(lambda x: int(x)) - pbp_txt["plays"]["end.TimeSecsRem"] = pbp_txt["plays"]["start.TimeSecsRem"].shift(1) - pbp_txt["plays"]["end.adj_TimeSecsRem"] = pbp_txt["plays"]["start.adj_TimeSecsRem"].shift(1) - pbp_txt["plays"]["end.TimeSecsRem"] = np.where( - (pbp_txt["plays"]["game_play_number"] == 1) - | ((pbp_txt["plays"]["half"] == "2") & (pbp_txt["plays"]["lag_half"] == "1")), - 1800, - pbp_txt["plays"]["end.TimeSecsRem"], - ) - pbp_txt["plays"]["end.adj_TimeSecsRem"] = np.select( - [ - (pbp_txt["plays"]["game_play_number"] == 1), - ((pbp_txt["plays"]["half"] == "2") & (pbp_txt["plays"]["lag_half"] == "1")), - ], - [3600, 1800], - default=pbp_txt["plays"]["end.adj_TimeSecsRem"], + pbp_txt["timeouts"] = { + init["homeTeamId"]: {"1": [], "2": []}, + init["awayTeamId"]: {"1": [], "2": []}, + } + pbp_txt["timeouts"][init["homeTeamId"]]["1"] = ( + pbp_txt["plays"] + .filter((pl.col("homeTimeoutCalled") == True).and_(pl.col("period.number") <= 2)) + .get_column("id") + .to_list() ) - pbp_txt["plays"]["start.posTeamTimeouts"] = np.where( - pbp_txt["plays"]["start.pos_team.id"] == pbp_txt["plays"]["homeTeamId"], - pbp_txt["plays"]["start.homeTeamTimeouts"], - pbp_txt["plays"]["start.awayTeamTimeouts"], + pbp_txt["timeouts"][init["homeTeamId"]]["2"] = ( + pbp_txt["plays"] + .filter((pl.col("homeTimeoutCalled") == True).and_(pl.col("period.number") > 2)) + .get_column("id") + .to_list() ) - pbp_txt["plays"]["start.defPosTeamTimeouts"] = np.where( - pbp_txt["plays"]["start.def_pos_team.id"] == pbp_txt["plays"]["homeTeamId"], - pbp_txt["plays"]["start.homeTeamTimeouts"], - pbp_txt["plays"]["start.awayTeamTimeouts"], + pbp_txt["timeouts"][init["awayTeamId"]]["1"] = ( + pbp_txt["plays"] + .filter((pl.col("awayTimeoutCalled") == True).and_(pl.col("period.number") <= 2)) + .get_column("id") + .to_list() ) - pbp_txt["plays"]["end.posTeamTimeouts"] = np.where( - pbp_txt["plays"]["end.pos_team.id"] == pbp_txt["plays"]["homeTeamId"], - pbp_txt["plays"]["end.homeTeamTimeouts"], - pbp_txt["plays"]["end.awayTeamTimeouts"], + pbp_txt["timeouts"][init["awayTeamId"]]["2"] = ( + pbp_txt["plays"] + .filter((pl.col("awayTimeoutCalled") == True).and_(pl.col("period.number") > 2)) + .get_column("id") + .to_list() ) - pbp_txt["plays"]["end.defPosTeamTimeouts"] = np.where( - pbp_txt["plays"]["end.def_pos_team.id"] == pbp_txt["plays"]["homeTeamId"], - pbp_txt["plays"]["end.homeTeamTimeouts"], - pbp_txt["plays"]["end.awayTeamTimeouts"], + pbp_txt["plays"] = ( + pbp_txt["plays"] + .with_columns( + pl.when( + ( + (pbp_txt["timeouts"][init["homeTeamId"]]["1"] <= pl.col("id")).and_( + pl.col("period.number") <= 2, len(pbp_txt["timeouts"][init["homeTeamId"]]["1"]) == 1 + ) + ).or_( + (pbp_txt["timeouts"][init["homeTeamId"]]["2"] <= pl.col("id")).and_( + pl.col("period.number") > 2, len(pbp_txt["timeouts"][init["homeTeamId"]]["2"]) == 1 + ) + ) + ) + .then(2) + .when( + ( + (pbp_txt["timeouts"][init["homeTeamId"]]["1"] <= pl.col("id")).and_( + pl.col("period.number") <= 2, len(pbp_txt["timeouts"][init["homeTeamId"]]["1"]) == 2 + ) + ).or_( + (pbp_txt["timeouts"][init["homeTeamId"]]["2"] <= pl.col("id")).and_( + pl.col("period.number") > 2, len(pbp_txt["timeouts"][init["homeTeamId"]]["2"]) == 2 + ) + ) + ) + .then(1) + .when( + ( + (pbp_txt["timeouts"][init["homeTeamId"]]["1"] <= pl.col("id")).and_( + pl.col("period.number") <= 2, len(pbp_txt["timeouts"][init["homeTeamId"]]["1"]) == 3 + ) + ).or_( + (pbp_txt["timeouts"][init["homeTeamId"]]["2"] <= pl.col("id")).and_( + pl.col("period.number") > 2, len(pbp_txt["timeouts"][init["homeTeamId"]]["2"]) == 3 + ) + ) + ) + .then(0) + .otherwise(3) + .alias("end.homeTeamTimeouts"), + pl.when( + ( + (pbp_txt["timeouts"][init["awayTeamId"]]["1"] <= pl.col("id")).and_( + pl.col("period.number") <= 2, len(pbp_txt["timeouts"][init["awayTeamId"]]["1"]) == 1 + ) + ).or_( + (pbp_txt["timeouts"][init["awayTeamId"]]["2"] <= pl.col("id")).and_( + pl.col("period.number") > 2, len(pbp_txt["timeouts"][init["awayTeamId"]]["2"]) == 1 + ) + ) + ) + .then(2) + .when( + ( + (pbp_txt["timeouts"][init["awayTeamId"]]["1"] <= pl.col("id")).and_( + pl.col("period.number") <= 2, len(pbp_txt["timeouts"][init["awayTeamId"]]["1"]) == 2 + ) + ).or_( + (pbp_txt["timeouts"][init["awayTeamId"]]["2"] <= pl.col("id")).and_( + pl.col("period.number") > 2, len(pbp_txt["timeouts"][init["awayTeamId"]]["2"]) == 2 + ) + ) + ) + .then(1) + .when( + ( + (pbp_txt["timeouts"][init["awayTeamId"]]["1"] <= pl.col("id")).and_( + pl.col("period.number") <= 2, len(pbp_txt["timeouts"][init["awayTeamId"]]["1"]) == 3 + ) + ).or_( + (pbp_txt["timeouts"][init["awayTeamId"]]["2"] <= pl.col("id")).and_( + pl.col("period.number") > 2, len(pbp_txt["timeouts"][init["awayTeamId"]]["2"]) == 3 + ) + ) + ) + .then(0) + .otherwise(3) + .alias("end.awayTeamTimeouts"), + ) + .with_columns( + pl.col("end.homeTeamTimeouts").shift_and_fill(periods=1, fill_value=3).alias("start.homeTeamTimeouts"), + pl.col("end.awayTeamTimeouts").shift_and_fill(periods=1, fill_value=3).alias("start.awayTeamTimeouts"), + pl.col("start.TimeSecsRem").shift(periods=1).alias("end.TimeSecsRem"), + pl.col("start.adj_TimeSecsRem").shift(periods=1).alias("end.adj_TimeSecsRem"), + ) + .with_columns( + pl.when(pl.col("game_play_number") == 1) + .then(pl.lit(1800)) + .when((pl.col("half") == 2) & (pl.col("lag_half") == 1)) + .then(pl.lit(1800)) + .otherwise(pl.col("end.TimeSecsRem")) + .alias("end.TimeSecsRem"), + pl.when(pl.col("game_play_number") == 1) + .then(pl.lit(3600)) + .when((pl.col("half") == 2) & (pl.col("lag_half") == 1)) + .then(pl.lit(1800)) + .otherwise(pl.col("end.adj_TimeSecsRem")) + .alias("end.adj_TimeSecsRem"), + pl.when(pl.col("start.pos_team.id") == pl.col("homeTeamId")) + .then(pl.col("start.homeTeamTimeouts")) + .otherwise(pl.col("start.awayTeamTimeouts")) + .alias("start.posTeamTimeouts"), + pl.when(pl.col("start.pos_team.id") == pl.col("homeTeamId")) + .then(pl.col("start.awayTeamTimeouts")) + .otherwise(pl.col("start.homeTeamTimeouts")) + .alias("start.defPosTeamTimeouts"), + pl.when(pl.col("end.pos_team.id") == pl.col("homeTeamId")) + .then(pl.col("end.homeTeamTimeouts")) + .otherwise(pl.col("end.awayTeamTimeouts")) + .alias("end.posTeamTimeouts"), + pl.when(pl.col("end.pos_team.id") == pl.col("homeTeamId")) + .then(pl.col("end.awayTeamTimeouts")) + .otherwise(pl.col("end.homeTeamTimeouts")) + .alias("end.defPosTeamTimeouts"), + pl.when( + (pl.col("game_play_number") == 1).and_( + pl.col("type.text").is_in(kickoff_vec), pl.col("start.pos_team.id") == pl.col("homeTeamId") + ) + ) + .then(pl.col("homeTeamId")) + .otherwise(pl.col("awayTeamId")) + .alias("firstHalfKickoffTeamId"), + pl.col("period.number").alias("period"), + pl.when(pl.col("start.team.id") == pl.col("homeTeamId")) + .then(pl.lit(100) - pl.col("start.yardLine")) + .otherwise(pl.col("start.yardLine")) + .alias("start.yard"), + ) + .with_columns( + pl.when(pl.col("start.yardLine").is_null() == False) + .then(pl.col("start.yardLine")) + .otherwise(pl.col("start.yard")) + .alias("start.yardLine"), + ) + .with_columns( + pl.when(pl.col("start.yardLine").is_null() == False) + .then(pl.col("start.yardsToEndzone")) + .otherwise(pl.col("start.yardLine")) + .alias("start.yardsToEndzone") + ) + .with_columns( + pl.when(pl.col("start.yardsToEndzone") == 0) + .then(pl.col("start.yard")) + .otherwise(pl.col("start.yardsToEndzone")) + .alias("start.yardsToEndzone"), + pl.when(pl.col("end.team.id") == pl.col("homeTeamId")) + .then(pl.lit(100) - pl.col("end.yardLine")) + .otherwise(pl.col("end.yardLine")) + .alias("end.yard"), + ) + .with_columns( + pl.when((pl.col("type.text") == "Penalty").and_(pl.col("text").str.contains(r"(?i)declined"))) + .then(pl.col("start.yard")) + .otherwise(pl.col("end.yard")) + .alias("end.yard"), + ) + .with_columns( + pl.when(pl.col("end.yardLine").is_null() == False) + .then(pl.col("end.yardsToEndzone")) + .otherwise(pl.col("end.yard")) + .alias("end.yardsToEndzone"), + pl.when( + (pl.col("start.distance") == 0).and_(pl.col("start.downDistanceText").str.contains(r"(?i)goal")) + ) + .then(pl.col("start.yardsToEndzone")) + .otherwise(pl.col("start.distance")) + .alias("start.distance"), + ) + .with_columns( + pl.when((pl.col("type.text") == "Penalty").and_(pl.col("text").str.contains(r"(?i)declined"))) + .then(pl.col("start.yardsToEndzone")) + .otherwise(pl.col("end.yardsToEndzone")) + .alias("end.yardsToEndzone"), + ) ) pbp_txt["firstHalfKickoffTeamId"] = np.where( (pbp_txt["plays"]["game_play_number"] == 1) - & (pbp_txt["plays"]["type.text"].isin(kickoff_vec)) - & (pbp_txt["plays"]["start.team.id"] == pbp_txt["plays"]["homeTeamId"]), - pbp_txt["plays"]["homeTeamId"], - pbp_txt["plays"]["awayTeamId"], - ) - pbp_txt["plays"]["firstHalfKickoffTeamId"] = pbp_txt["firstHalfKickoffTeamId"] - pbp_txt["plays"]["period"] = pbp_txt["plays"]["period.number"] - pbp_txt["plays"]["start.yard"] = np.where( - (pbp_txt["plays"]["start.team.id"] == homeTeamId), - 100 - pbp_txt["plays"]["start.yardLine"], - pbp_txt["plays"]["start.yardLine"], - ) - pbp_txt["plays"]["start.yardsToEndzone"] = np.where( - pbp_txt["plays"]["start.yardLine"].isna() == False, - pbp_txt["plays"]["start.yardsToEndzone"], - pbp_txt["plays"]["start.yard"], - ) - pbp_txt["plays"]["start.yardsToEndzone"] = np.where( - pbp_txt["plays"]["start.yardsToEndzone"] == 0, - pbp_txt["plays"]["start.yard"], - pbp_txt["plays"]["start.yardsToEndzone"], - ) - pbp_txt["plays"]["end.yard"] = np.where( - (pbp_txt["plays"]["end.team.id"] == homeTeamId), - 100 - pbp_txt["plays"]["end.yardLine"], - pbp_txt["plays"]["end.yardLine"], - ) - pbp_txt["plays"]["end.yard"] = np.where( - (pbp_txt["plays"]["type.text"] == "Penalty") - & (pbp_txt["plays"]["text"].str.contains("declined", case=False, flags=0, na=False, regex=True)), - pbp_txt["plays"]["start.yard"], - pbp_txt["plays"]["end.yard"], - ) - pbp_txt["plays"]["end.yardsToEndzone"] = np.where( - pbp_txt["plays"]["end.yardLine"].isna() == False, - pbp_txt["plays"]["end.yardsToEndzone"], - pbp_txt["plays"]["end.yard"], - ) - pbp_txt["plays"]["end.yardsToEndzone"] = np.where( - (pbp_txt["plays"]["type.text"] == "Penalty") - & (pbp_txt["plays"]["text"].str.contains("declined", case=False, flags=0, na=False, regex=True)), - pbp_txt["plays"]["start.yardsToEndzone"], - pbp_txt["plays"]["end.yardsToEndzone"], - ) - - pbp_txt["plays"]["start.distance"] = np.where( - (pbp_txt["plays"]["start.distance"] == 0) - & (pbp_txt["plays"]["start.downDistanceText"].str.lower().str.contains("goal")), - pbp_txt["plays"]["start.yardsToEndzone"], - pbp_txt["plays"]["start.distance"], - ) - - pbp_txt["timeouts"][homeTeamId]["1"] = np.array(pbp_txt["timeouts"][homeTeamId]["1"]).tolist() - pbp_txt["timeouts"][homeTeamId]["2"] = np.array(pbp_txt["timeouts"][homeTeamId]["2"]).tolist() - pbp_txt["timeouts"][awayTeamId]["1"] = np.array(pbp_txt["timeouts"][awayTeamId]["1"]).tolist() - pbp_txt["timeouts"][awayTeamId]["2"] = np.array(pbp_txt["timeouts"][awayTeamId]["2"]).tolist() - if "scoringType.displayName" in pbp_txt["plays"].keys(): - pbp_txt["plays"]["type.text"] = np.where( - pbp_txt["plays"]["scoringType.displayName"] == "Field Goal", - "Field Goal Good", - pbp_txt["plays"]["type.text"], - ) - pbp_txt["plays"]["type.text"] = np.where( - pbp_txt["plays"]["scoringType.displayName"] == "Extra Point", - "Extra Point Good", - pbp_txt["plays"]["type.text"], + & (pbp_txt["plays"]["type.text"].is_in(kickoff_vec)) + & (pbp_txt["plays"]["start.team.id"] == init["homeTeamId"]), + init["homeTeamId"], + init["awayTeamId"], + ) + pbp_txt["firstHalfKickoffTeamId"] = pbp_txt["firstHalfKickoffTeamId"][0] + + if "scoringType.displayName" in pbp_txt["plays"].columns: + pbp_txt["plays"] = ( + pbp_txt["plays"] + .with_columns( + pl.when(pl.col("scoringType.displayName") == "Field Goal") + .then(pl.lit("Field Goal Good")) + .otherwise(pl.col("type.text")) + .alias("type.text") + ) + .with_columns( + pl.when(pl.col("scoringType.displayName") == "Extra Point") + .then(pl.lit("Extra Point Good")) + .otherwise(pl.col("type.text")) + .alias("type.text") + ) + ) + pbp_txt["plays"] = ( + pbp_txt["plays"] + .with_columns( + pl.when(pl.col("type.text").is_null()) + .then(pl.lit("Unknown")) + .otherwise(pl.col("type.text")) + .alias("type.text") + ) + .with_columns( + pl.when( + pl.col("type.text") + .str.to_lowercase() + .str.contains("(?i)extra point") + .and_(pl.col("type.text").str.to_lowercase().str.contains("(?i)no good")) + ) + .then(pl.lit("Extra Point Missed")) + .otherwise(pl.col("type.text")) + .alias("type.text") + ) + .with_columns( + pl.when( + pl.col("type.text") + .str.to_lowercase() + .str.contains("(?i)extra point") + .and_(pl.col("type.text").str.to_lowercase().str.contains("(?i)blocked")) + ) + .then(pl.lit("Extra Point Missed")) + .otherwise(pl.col("type.text")) + .alias("type.text") + ) + .with_columns( + pl.when( + pl.col("type.text") + .str.to_lowercase() + .str.contains("(?i)field goal") + .and_(pl.col("type.text").str.to_lowercase().str.contains("(?i)blocked")) + ) + .then(pl.lit("Extra Point Missed")) + .otherwise(pl.col("type.text")) + .alias("type.text") + ) + .with_columns( + pl.when( + pl.col("type.text") + .str.to_lowercase() + .str.contains("(?i)field goal") + .and_(pl.col("type.text").str.to_lowercase().str.contains("(?i)no good")) + ) + .then(pl.lit("Extra Point Missed")) + .otherwise(pl.col("type.text")) + .alias("type.text") ) - - pbp_txt["plays"]["playType"] = np.where( - pbp_txt["plays"]["type.text"].isna() == False, - pbp_txt["plays"]["type.text"], - "Unknown", - ) - pbp_txt["plays"]["type.text"] = np.where( - pbp_txt["plays"]["text"].str.lower().str.contains("extra point", case=False) - & pbp_txt["plays"]["text"].str.lower().str.contains("no good", case=False), - "Extra Point Missed", - pbp_txt["plays"]["type.text"], - ) - pbp_txt["plays"]["type.text"] = np.where( - pbp_txt["plays"]["text"].str.lower().str.contains("extra point", case=False) - & pbp_txt["plays"]["text"].str.lower().str.contains("blocked", case=False), - "Extra Point Missed", - pbp_txt["plays"]["type.text"], - ) - pbp_txt["plays"]["type.text"] = np.where( - pbp_txt["plays"]["text"].str.lower().str.contains("field goal", case=False) - & pbp_txt["plays"]["text"].str.lower().str.contains("blocked", case=False), - "Blocked Field Goal", - pbp_txt["plays"]["type.text"], - ) - pbp_txt["plays"]["type.text"] = np.where( - pbp_txt["plays"]["text"].str.lower().str.contains("field goal", case=False) - & pbp_txt["plays"]["text"].str.lower().str.contains("no good", case=False), - "Field Goal Missed", - pbp_txt["plays"]["type.text"], ) - del pbp_txt["plays"]["clock.mm"] - pbp_txt["plays"] = pbp_txt["plays"].replace({np.nan: None}) + return pbp_txt def __helper_nfl_pbp(self, pbp_txt): - gameSpread, overUnder, homeFavorite, gameSpreadAvailable = self.__helper_nfl_pickcenter(pbp_txt) + init = self.__helper_nfl_pickcenter(pbp_txt) + return self.__helper_nfl_game_data(pbp_txt, init) + + def __helper_nfl_pickcenter(self, pbp_txt): + # Spread definition + if len(pbp_txt.get("pickcenter", [])) > 1: + homeFavorite = pbp_txt.get("pickcenter", {})[0].get("homeTeamOdds", {}).get("favorite", "") + if "spread" in pbp_txt.get("pickcenter", {})[1].keys(): + gameSpread = pbp_txt.get("pickcenter", {})[1].get("spread", "") + overUnder = pbp_txt.get("pickcenter", {})[1].get("overUnder", "") + else: + gameSpread = pbp_txt.get("pickcenter", {})[0].get("spread", "") + overUnder = pbp_txt.get("pickcenter", {})[0].get("overUnder", "") + gameSpreadAvailable = True + # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") + else: + gameSpread = 2.5 + overUnder = 140.5 + homeFavorite = True + gameSpreadAvailable = False + + return { + "gameSpread": gameSpread, + "overUnder": overUnder, + "homeFavorite": homeFavorite, + "gameSpreadAvailable": gameSpreadAvailable, + } + + def __helper_nfl_game_data(self, pbp_txt, init): pbp_txt["timeouts"] = {} pbp_txt["teamInfo"] = pbp_txt["header"]["competitions"][0] pbp_txt["season"] = pbp_txt["header"]["season"] pbp_txt["playByPlaySource"] = pbp_txt["header"]["competitions"][0]["playByPlaySource"] pbp_txt["boxscoreSource"] = pbp_txt["header"]["competitions"][0]["boxscoreSource"] - pbp_txt["gameSpreadAvailable"] = gameSpreadAvailable - pbp_txt["gameSpread"] = gameSpread - pbp_txt["homeFavorite"] = homeFavorite - pbp_txt["homeTeamSpread"] = np.where(homeFavorite == True, abs(gameSpread), -1 * abs(gameSpread)) - pbp_txt["overUnder"] = overUnder + pbp_txt["gameSpreadAvailable"] = init["gameSpreadAvailable"] + pbp_txt["gameSpread"] = init["gameSpread"] + pbp_txt["homeFavorite"] = init["homeFavorite"] + pbp_txt["homeTeamSpread"] = np.where( + init["homeFavorite"] == True, abs(init["gameSpread"]), -1 * abs(init["gameSpread"]) + ) + pbp_txt["overUnder"] = init["overUnder"] # Home and Away identification variables if pbp_txt["header"]["competitions"][0]["competitors"][0]["homeAway"] == "home": pbp_txt["header"]["competitions"][0]["home"] = pbp_txt["header"]["competitions"][0]["competitors"][0][ @@ -630,7 +745,7 @@ def __helper_nfl_pbp(self, pbp_txt): homeTeamMascot = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["name"]) homeTeamName = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["location"]) homeTeamAbbrev = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["abbreviation"]) - homeTeamNameAlt = re.sub("Stat(.+)", "St", str(homeTeamName)) + homeTeamNameAlt = re.sub("Stat(.+)", "St", homeTeamName) pbp_txt["header"]["competitions"][0]["away"] = pbp_txt["header"]["competitions"][0]["competitors"][1][ "team" ] @@ -638,7 +753,7 @@ def __helper_nfl_pbp(self, pbp_txt): awayTeamMascot = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["name"]) awayTeamName = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["location"]) awayTeamAbbrev = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["abbreviation"]) - awayTeamNameAlt = re.sub("Stat(.+)", "St", str(awayTeamName)) + awayTeamNameAlt = re.sub("Stat(.+)", "St", awayTeamName) else: pbp_txt["header"]["competitions"][0]["away"] = pbp_txt["header"]["competitions"][0]["competitors"][0][ "team" @@ -647,7 +762,7 @@ def __helper_nfl_pbp(self, pbp_txt): awayTeamMascot = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["name"]) awayTeamName = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["location"]) awayTeamAbbrev = str(pbp_txt["header"]["competitions"][0]["competitors"][0]["team"]["abbreviation"]) - awayTeamNameAlt = re.sub("Stat(.+)", "St", str(awayTeamName)) + awayTeamNameAlt = re.sub("Stat(.+)", "St", awayTeamName) pbp_txt["header"]["competitions"][0]["home"] = pbp_txt["header"]["competitions"][0]["competitors"][1][ "team" ] @@ -655,275 +770,18 @@ def __helper_nfl_pbp(self, pbp_txt): homeTeamMascot = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["name"]) homeTeamName = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["location"]) homeTeamAbbrev = str(pbp_txt["header"]["competitions"][0]["competitors"][1]["team"]["abbreviation"]) - homeTeamNameAlt = re.sub("Stat(.+)", "St", str(homeTeamName)) - return ( - pbp_txt, - gameSpread, - overUnder, - homeFavorite, - gameSpreadAvailable, - homeTeamId, - homeTeamMascot, - homeTeamName, - homeTeamAbbrev, - homeTeamNameAlt, - awayTeamId, - awayTeamMascot, - awayTeamName, - awayTeamAbbrev, - awayTeamNameAlt, - ) - - def __helper_nfl_pickcenter(self, pbp_txt): - # Spread definition - if len(pbp_txt.get("pickcenter", [])) > 1: - homeFavorite = pbp_txt.get("pickcenter", {})[0].get("homeTeamOdds", {}).get("favorite", "") - if "spread" in pbp_txt.get("pickcenter", {})[1].keys(): - gameSpread = pbp_txt.get("pickcenter", {})[1].get("spread", "") - overUnder = pbp_txt.get("pickcenter", {})[1].get("overUnder", "") - gameSpreadAvailable = True - else: - gameSpread = pbp_txt.get("pickcenter", {})[0].get("spread", "") - overUnder = pbp_txt.get("pickcenter", {})[0].get("overUnder", "") - gameSpreadAvailable = True - # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") - else: - gameSpread = 2.5 - overUnder = 55.5 - homeFavorite = True - gameSpreadAvailable = False - return gameSpread, overUnder, homeFavorite, gameSpreadAvailable - - def __setup_penalty_data(self, play_df): - """ - Creates the following columns in play_df: - * Penalty flag - * Penalty declined - * Penalty no play - * Penalty off-set - * Penalty 1st down conversion - * Penalty in text - * Yds Penalty - """ - ##-- 'Penalty' in play text ---- - # -- T/F flag conditions penalty_flag - play_df["penalty_flag"] = False - play_df.loc[(play_df["type.text"] == "Penalty"), "penalty_flag"] = True - play_df.loc[ - play_df["text"].str.contains("penalty", case=False, flags=0, na=False, regex=True), - "penalty_flag", - ] = True - - # -- T/F flag conditions penalty_declined - play_df["penalty_declined"] = False - play_df.loc[ - (play_df["type.text"] == "Penalty") - & (play_df["text"].str.contains("declined", case=False, flags=0, na=False, regex=True)), - "penalty_declined", - ] = True - play_df.loc[ - (play_df["text"].str.contains("penalty", case=False, flags=0, na=False, regex=True)) - & (play_df["text"].str.contains("declined", case=False, flags=0, na=False, regex=True)), - "penalty_declined", - ] = True - - # -- T/F flag conditions penalty_no_play - play_df["penalty_no_play"] = False - play_df.loc[ - (play_df["type.text"] == "Penalty") - & (play_df["text"].str.contains("no play", case=False, flags=0, na=False, regex=True)), - "penalty_no_play", - ] = True - play_df.loc[ - (play_df["text"].str.contains("penalty", case=False, flags=0, na=False, regex=True)) - & (play_df["text"].str.contains("no play", case=False, flags=0, na=False, regex=True)), - "penalty_no_play", - ] = True - - # -- T/F flag conditions penalty_offset - play_df["penalty_offset"] = False - play_df.loc[ - (play_df["type.text"] == "Penalty") - & (play_df["text"].str.contains(r"off-setting", case=False, flags=0, na=False, regex=True)), - "penalty_offset", - ] = True - play_df.loc[ - (play_df["text"].str.contains("penalty", case=False, flags=0, na=False, regex=True)) - & (play_df["text"].str.contains(r"off-setting", case=False, flags=0, na=False, regex=True)), - "penalty_offset", - ] = True - - # -- T/F flag conditions penalty_1st_conv - play_df["penalty_1st_conv"] = False - play_df.loc[ - (play_df["type.text"] == "Penalty") - & (play_df["text"].str.contains("1st down", case=False, flags=0, na=False, regex=True)), - "penalty_1st_conv", - ] = True - play_df.loc[ - (play_df["text"].str.contains("penalty", case=False, flags=0, na=False, regex=True)) - & (play_df["text"].str.contains("1st down", case=False, flags=0, na=False, regex=True)), - "penalty_1st_conv", - ] = True - - # -- T/F flag for penalty text but not penalty play type -- - play_df["penalty_in_text"] = False - play_df.loc[ - (play_df["text"].str.contains("penalty", case=False, flags=0, na=False, regex=True)) - & (~(play_df["type.text"] == "Penalty")) - & (~play_df["text"].str.contains("declined", case=False, flags=0, na=False, regex=True)) - & (~play_df["text"].str.contains(r"off-setting", case=False, flags=0, na=False, regex=True)) - & (~play_df["text"].str.contains("no play", case=False, flags=0, na=False, regex=True)), - "penalty_in_text", - ] = True - - play_df["penalty_detail"] = np.select( - [ - (play_df.penalty_offset == 1), - (play_df.penalty_declined == 1), - play_df.text.str.contains(" roughing passer ", case=False, regex=True), - play_df.text.str.contains(" offensive holding ", case=False, regex=True), - play_df.text.str.contains(" pass interference", case=False, regex=True), - play_df.text.str.contains(" encroachment", case=False, regex=True), - play_df.text.str.contains(" defensive pass interference ", case=False, regex=True), - play_df.text.str.contains(" offensive pass interference ", case=False, regex=True), - play_df.text.str.contains(" illegal procedure ", case=False, regex=True), - play_df.text.str.contains(" defensive holding ", case=False, regex=True), - play_df.text.str.contains(" holding ", case=False, regex=True), - play_df.text.str.contains(" offensive offside | offside offense", case=False, regex=True), - play_df.text.str.contains(" defensive offside | offside defense", case=False, regex=True), - play_df.text.str.contains(" offside ", case=False, regex=True), - play_df.text.str.contains(" illegal fair catch signal ", case=False, regex=True), - play_df.text.str.contains(" illegal batting ", case=False, regex=True), - play_df.text.str.contains(" neutral zone infraction ", case=False, regex=True), - play_df.text.str.contains(" ineligible downfield ", case=False, regex=True), - play_df.text.str.contains(" illegal use of hands ", case=False, regex=True), - play_df.text.str.contains( - " kickoff out of bounds | kickoff out-of-bounds ", - case=False, - regex=True, - ), - play_df.text.str.contains(" 12 men on the field ", case=False, regex=True), - play_df.text.str.contains(" illegal block ", case=False, regex=True), - play_df.text.str.contains(" personal foul ", case=False, regex=True), - play_df.text.str.contains(" false start ", case=False, regex=True), - play_df.text.str.contains(" substitution infraction ", case=False, regex=True), - play_df.text.str.contains(" illegal formation ", case=False, regex=True), - play_df.text.str.contains(" illegal touching ", case=False, regex=True), - play_df.text.str.contains(" sideline interference ", case=False, regex=True), - play_df.text.str.contains(" clipping ", case=False, regex=True), - play_df.text.str.contains(" sideline infraction ", case=False, regex=True), - play_df.text.str.contains(" crackback ", case=False, regex=True), - play_df.text.str.contains(" illegal snap ", case=False, regex=True), - play_df.text.str.contains(" illegal helmet contact ", case=False, regex=True), - play_df.text.str.contains(" roughing holder ", case=False, regex=True), - play_df.text.str.contains(" horse collar tackle ", case=False, regex=True), - play_df.text.str.contains(" illegal participation ", case=False, regex=True), - play_df.text.str.contains(" tripping ", case=False, regex=True), - play_df.text.str.contains(" illegal shift ", case=False, regex=True), - play_df.text.str.contains(" illegal motion ", case=False, regex=True), - play_df.text.str.contains(" roughing the kicker ", case=False, regex=True), - play_df.text.str.contains(" delay of game ", case=False, regex=True), - play_df.text.str.contains(" targeting ", case=False, regex=True), - play_df.text.str.contains(" face mask ", case=False, regex=True), - play_df.text.str.contains(" illegal forward pass ", case=False, regex=True), - play_df.text.str.contains(" intentional grounding ", case=False, regex=True), - play_df.text.str.contains(" illegal kicking ", case=False, regex=True), - play_df.text.str.contains(" illegal conduct ", case=False, regex=True), - play_df.text.str.contains(" kick catching interference ", case=False, regex=True), - play_df.text.str.contains(" unnecessary roughness ", case=False, regex=True), - play_df.text.str.contains("Penalty, UR"), - play_df.text.str.contains(" unsportsmanlike conduct ", case=False, regex=True), - play_df.text.str.contains(" running into kicker ", case=False, regex=True), - play_df.text.str.contains(" failure to wear required equipment ", case=False, regex=True), - play_df.text.str.contains(" player disqualification ", case=False, regex=True), - (play_df.penalty_flag == True), - ], - [ - "Off-Setting", - "Penalty Declined", - "Roughing the Passer", - "Offensive Holding", - "Pass Interference", - "Encroachment", - "Defensive Pass Interference", - "Offensive Pass Interference", - "Illegal Procedure", - "Defensive Holding", - "Holding", - "Offensive Offside", - "Defensive Offside", - "Offside", - "Illegal Fair Catch Signal", - "Illegal Batting", - "Neutral Zone Infraction", - "Ineligible Man Down-Field", - "Illegal Use of Hands", - "Kickoff Out-of-Bounds", - "12 Men on the Field", - "Illegal Block", - "Personal Foul", - "False Start", - "Substitution Infraction", - "Illegal Formation", - "Illegal Touching", - "Sideline Interference", - "Clipping", - "Sideline Infraction", - "Crackback", - "Illegal Snap", - "Illegal Helmet contact", - "Roughing the Holder", - "Horse-Collar Tackle", - "Illegal Participation", - "Tripping", - "Illegal Shift", - "Illegal Motion", - "Roughing the Kicker", - "Delay of Game", - "Targeting", - "Face Mask", - "Illegal Forward Pass", - "Intentional Grounding", - "Illegal Kicking", - "Illegal Conduct", - "Kick Catch Interference", - "Unnecessary Roughness", - "Unnecessary Roughness", - "Unsportsmanlike Conduct", - "Running Into Kicker", - "Failure to Wear Required Equipment", - "Player Disqualification", - "Missing", - ], - default=None, - ) - - play_df["penalty_text"] = np.select( - [(play_df.penalty_flag == True)], - [play_df.text.str.extract(r"Penalty(.+)", flags=re.IGNORECASE)[0]], - default=None, - ) - - play_df["yds_penalty"] = np.select( - [(play_df.penalty_flag == True)], - [play_df.penalty_text.str.extract("(.{0,3})yards|yds|yd to the ", flags=re.IGNORECASE)[0]], - default=None, - ) - play_df["yds_penalty"] = play_df["yds_penalty"].str.replace(" yards to the | yds to the | yd to the ", "") - play_df["yds_penalty"] = np.select( - [ - (play_df.penalty_flag == True) - & (play_df.text.str.contains(r"ards\)", case=False, regex=True)) - & (play_df.yds_penalty.isna()), - ], - [play_df.text.str.extract(r"(.{0,4})yards\)|Yards\)|yds\)|Yds\)", flags=re.IGNORECASE)[0]], - default=play_df.yds_penalty, - ) - play_df["yds_penalty"] = play_df.yds_penalty.str.replace("yards\\)|Yards\\)|yds\\)|Yds\\)", "").str.replace( - "\\(", "" - ) - return play_df + homeTeamNameAlt = re.sub("Stat(.+)", "St", homeTeamName) + init["homeTeamId"] = homeTeamId + init["homeTeamMascot"] = homeTeamMascot + init["homeTeamName"] = homeTeamName + init["homeTeamAbbrev"] = homeTeamAbbrev + init["homeTeamNameAlt"] = homeTeamNameAlt + init["awayTeamId"] = awayTeamId + init["awayTeamMascot"] = awayTeamMascot + init["awayTeamName"] = awayTeamName + init["awayTeamAbbrev"] = awayTeamAbbrev + init["awayTeamNameAlt"] = awayTeamNameAlt + return pbp_txt, init def __add_downs_data(self, play_df): """ @@ -931,34 +789,37 @@ def __add_downs_data(self, play_df): * id, drive_id, game_id * down, ydstogo (distance), game_half, period """ - play_df = play_df.copy(deep=True) - play_df.loc[:, "id"] = play_df["id"].astype(float) - play_df.sort_values(by=["id", "start.adj_TimeSecsRem"], inplace=True) - play_df.drop_duplicates(subset=["text", "id", "type.text", "start.down"], keep="last", inplace=True) - play_df = play_df[ - ( - play_df["type.text"].str.contains("end of|coin toss|end period|wins toss", case=False, regex=True) - == False - ) - ] - - play_df.loc[:, "period"] = play_df["period.number"].astype(int) - play_df.loc[(play_df.period <= 2), "half"] = 1 - play_df.loc[(play_df.period > 2), "half"] = 2 - play_df["lead_half"] = play_df.half.shift(-1) - play_df["lag_scoringPlay"] = play_df.scoringPlay.shift(1) - play_df.loc[play_df.lead_half.isna() == True, "lead_half"] = 2 - play_df["end_of_half"] = play_df.half != play_df.lead_half + play_df = play_df.sort(by=["id", "start.adj_TimeSecsRem"]) - play_df["down_1"] = play_df["start.down"] == 1 - play_df["down_2"] = play_df["start.down"] == 2 - play_df["down_3"] = play_df["start.down"] == 3 - play_df["down_4"] = play_df["start.down"] == 4 + play_df = play_df.unique( + subset=["text", "id", "type.text", "start.down", "sequenceNumber"], keep="last", maintain_order=True + ) + play_df = play_df.filter( + pl.col("type.text").str.contains("(?i)end of|(?i)coin toss|(?i)end period|(?i)wins toss") == False + ) + play_df = ( + play_df.with_columns( + period=pl.col("period.number"), + half=pl.when(pl.col("period.number") <= 2).then(1).otherwise(2), + ) + .with_columns( + lead_half=pl.col("half").shift(-1), + lag_scoringPlay=pl.col("scoringPlay").shift(1), + ) + .with_columns( + pl.when(pl.col("lead_half").is_null()).then(2).otherwise(pl.col("lead_half")).alias("lead_half"), + end_of_half=pl.col("half") != pl.col("lead_half"), + down_1=pl.col("start.down") == 1, + down_2=pl.col("start.down") == 2, + down_3=pl.col("start.down") == 3, + down_4=pl.col("start.down") == 4, + down_1_end=pl.col("end.down") == 1, + down_2_end=pl.col("end.down") == 2, + down_3_end=pl.col("end.down") == 3, + down_4_end=pl.col("end.down") == 4, + ) + ) - play_df["down_1_end"] = play_df["end.down"] == 1 - play_df["down_2_end"] = play_df["end.down"] == 2 - play_df["down_3_end"] = play_df["end.down"] == 3 - play_df["down_4_end"] = play_df["end.down"] == 4 return play_df def __add_play_type_flags(self, play_df): @@ -967,121 +828,106 @@ def __add_play_type_flags(self, play_df): * Flags for fumbles, scores, kickoffs, punts, field goals """ # --- Touchdown, Fumble, Special Teams flags ----------------- - play_df.loc[:, "scoring_play"] = False - play_df.loc[play_df["type.text"].isin(scores_vec), "scoring_play"] = True - play_df["td_play"] = play_df.text.str.contains( - r"touchdown|for a TD", case=False, flags=0, na=False, regex=True - ) - play_df["touchdown"] = play_df["type.text"].str.contains( - "touchdown", case=False, flags=0, na=False, regex=True - ) - ## Portion of touchdown check for plays where touchdown is not listed in the play_type-- - play_df["td_check"] = play_df["text"].str.contains("Touchdown", case=False, flags=0, na=False, regex=True) - play_df["safety"] = play_df["text"].str.contains("safety", case=False, flags=0, na=False, regex=True) - # --- Fumbles---- - play_df["fumble_vec"] = np.select( - [ - play_df["text"].str.contains("fumble", case=False, flags=0, na=False, regex=True), - (~play_df["text"].str.contains("fumble", case=False, flags=0, na=False, regex=True)) - & (play_df["type.text"] == "Rush") - & (play_df["start.pos_team.id"] != play_df["end.pos_team.id"]), - (~play_df["text"].str.contains("fumble", case=False, flags=0, na=False, regex=True)) - & (play_df["type.text"] == "Sack") - & (play_df["start.pos_team.id"] != play_df["end.pos_team.id"]), - ], - [True, True, True], - default=False, - ) - play_df["forced_fumble"] = play_df["text"].str.contains("forced by", case=False, flags=0, na=False, regex=True) - # --- Kicks---- - play_df["kickoff_play"] = play_df["type.text"].isin(kickoff_vec) - play_df["kickoff_tb"] = np.select( - [ - (play_df["text"].str.contains("touchback", case=False, flags=0, na=False, regex=True)) - & (play_df.kickoff_play == True), - (play_df["text"].str.contains("kickoff$", case=False, flags=0, na=False, regex=True)) - & (play_df.kickoff_play == True), - ], - [True, True], - default=False, - ) - play_df["kickoff_onside"] = ( - play_df["text"].str.contains(r"on-side|onside|on side", case=False, flags=0, na=False, regex=True) - ) & (play_df.kickoff_play == True) - play_df["kickoff_oob"] = ( - play_df["text"].str.contains( - r"out-of-bounds|out of bounds", - case=False, - flags=0, - na=False, - regex=True, - ) - ) & (play_df.kickoff_play == True) - play_df["kickoff_fair_catch"] = ( - play_df["text"].str.contains(r"fair catch|fair caught", case=False, flags=0, na=False, regex=True) - ) & (play_df.kickoff_play == True) - play_df["kickoff_downed"] = ( - play_df["text"].str.contains("downed", case=False, flags=0, na=False, regex=True) - ) & (play_df.kickoff_play == True) - play_df["kick_play"] = play_df["text"].str.contains(r"kick|kickoff", case=False, flags=0, na=False, regex=True) - play_df["kickoff_safety"] = ( - (~play_df["type.text"].isin(["Blocked Punt", "Penalty"])) - & (play_df["text"].str.contains("kickoff", case=False, flags=0, na=False, regex=True)) - & (play_df.safety == True) - ) - # --- Punts---- - play_df["punt"] = np.where(play_df["type.text"].isin(punt_vec), True, False) - play_df["punt_play"] = play_df["text"].str.contains("punt", case=False, flags=0, na=False, regex=True) - play_df["punt_tb"] = np.where( - (play_df["text"].str.contains("touchback", case=False, flags=0, na=False, regex=True)) - & (play_df.punt == True), - True, - False, - ) - play_df["punt_oob"] = np.where( - ( - play_df["text"].str.contains( - r"out-of-bounds|out of bounds", - case=False, - flags=0, - na=False, - regex=True, - ) - ) - & (play_df.punt == True), - True, - False, - ) - play_df["punt_fair_catch"] = np.where( - (play_df["text"].str.contains(r"fair catch|fair caught", case=False, flags=0, na=False, regex=True)) - & (play_df.punt == True), - True, - False, - ) - play_df["punt_downed"] = np.where( - (play_df["text"].str.contains("downed", case=False, flags=0, na=False, regex=True)) - & (play_df.punt == True), - True, - False, - ) - play_df["punt_safety"] = np.where( - (play_df["type.text"].isin(["Blocked Punt", "Punt"])) - & (play_df["text"].str.contains("punt", case=False, flags=0, na=False, regex=True)) - & (play_df.safety == True), - True, - False, - ) - play_df["penalty_safety"] = np.where( - (play_df["type.text"].isin(["Penalty"])) & (play_df.safety == True), - True, - False, - ) - play_df["punt_blocked"] = np.where( - (play_df["text"].str.contains("blocked", case=False, flags=0, na=False, regex=True)) - & (play_df.punt == True), - True, - False, + play_df = ( + play_df.with_columns( + scoring_play=pl.when(pl.col("type.text").is_in(scores_vec)).then(True).otherwise(False), + td_play=pl.col("text").str.contains("(?i)touchdown|(?i)for a TD"), + touchdown=pl.col("type.text").str.contains("(?i)touchdown"), + ## Portion of touchdown check for plays where touchdown is not listed in the play_type-- + td_check=pl.col("text").str.contains("(?i)touchdown"), + safety=pl.col("text").str.contains("(?i)safety"), + fumble_vec=pl.when(pl.col("text").str.contains("(?i)fumble")) + .then(True) + .when( + (pl.col("text").str.contains("(?i)fumble")).and_( + pl.col("type.text") == "Rush", pl.col("start.pos_team.id") != pl.col("end.pos_team.id") + ) + ) + .then(True) + .when( + (pl.col("text").str.contains("(?i)fumble")).and_( + pl.col("type.text") == "Sack", pl.col("start.pos_team.id") != pl.col("end.pos_team.id") + ) + ) + .then(True) + .otherwise(False), + forced_fumble=pl.when(pl.col("text").str.contains("(?i)forced by")).then(True).otherwise(False), + # --- Kicks---- + kickoff_play=pl.col("type.text").is_in(kickoff_vec), + ) + .with_columns( + kickoff_tb=pl.when((pl.col("text").str.contains("(?i)touchback")).and_(pl.col("kickoff_play") == True)) + .then(True) + .when((pl.col("text").str.contains("(?i)kickoff$")).and_(pl.col("kickoff_play") == True)) + .then(True) + .otherwise(False), + kickoff_onside=pl.when( + (pl.col("text").str.contains("(?i)on-side|(?i)onside|(?i)on side")).and_( + pl.col("kickoff_play") == True + ) + ) + .then(True) + .otherwise(False), + kickoff_oob=pl.when( + (pl.col("text").str.contains("(?i)out-of-bounds|(?i)out of bounds")).and_( + pl.col("kickoff_play") == True + ) + ) + .then(True) + .otherwise(False), + kickoff_fair_catch=pl.when( + (pl.col("text").str.contains("(?i)fair catch|(?i)fair caught")).and_( + pl.col("kickoff_play") == True + ) + ) + .then(True) + .otherwise(False), + kickoff_downed=pl.when( + (pl.col("text").str.contains("(?i)downed")).and_(pl.col("kickoff_play") == True) + ) + .then(True) + .otherwise(False), + kick_play=pl.col("text").str.contains("(?i)kick|(?i)kickoff"), + kickoff_safety=pl.when( + (pl.col("text").str.contains("(?i)kickoff")).and_( + pl.col("safety") == True, pl.col("type.text").is_in(["Blocked Punt", "Penalty"]) == False + ) + ) + .then(True) + .otherwise(False), + # --- Punts---- + punt=pl.col("type.text").is_in(punt_vec), + punt_play=pl.col("text").str.contains("(?i)punt"), + ) + .with_columns( + punt_tb=pl.when((pl.col("text").str.contains("(?i)touchback")).and_(pl.col("punt") == True)) + .then(True) + .otherwise(False), + punt_oob=pl.when( + (pl.col("text").str.contains("(?i)out-of-bounds|(?i)out of bounds")).and_(pl.col("punt") == True) + ) + .then(True) + .otherwise(False), + punt_fair_catch=pl.when( + (pl.col("text").str.contains("(?i)fair catch|(?i)fair caught")).and_(pl.col("punt") == True) + ) + .then(True) + .otherwise(False), + punt_downed=pl.when((pl.col("text").str.contains("(?i)downed")).and_(pl.col("punt") == True)) + .then(True) + .otherwise(False), + punt_safety=pl.when((pl.col("text").str.contains("(?i)punt")).and_(pl.col("safety") == True)) + .then(True) + .otherwise(False), + punt_blocked=pl.when((pl.col("text").str.contains("(?i)blocked")).and_(pl.col("punt") == True)) + .then(True) + .otherwise(False), + penalty_safety=pl.when((pl.col("type.text").is_in(["Penalty"])).and_(pl.col("safety") == True)) + .then(True) + .otherwise(False), + ) ) + return play_df def __add_rush_pass_flags(self, play_df): @@ -1089,159 +935,120 @@ def __add_rush_pass_flags(self, play_df): Creates the following columns in play_df: * Rush, Pass, Sacks """ - # --- Pass/Rush---- - play_df["rush"] = np.where( - ( - (play_df["type.text"] == "Rush") - | (play_df["type.text"] == "Rushing Touchdown") - | ( - play_df["type.text"].isin( - [ - "Safety", - "Fumble Recovery (Opponent)", - "Fumble Recovery (Opponent) Touchdown", - "Fumble Recovery (Own)", - "Fumble Recovery (Own) Touchdown", - "Fumble Return Touchdown", - ] + + play_df = ( + play_df.with_columns( + # --- Pass/Rush---- + pl.when( + (pl.col("type.text") == "Rush") + .or_(pl.col("type.text") == "Rushing Touchdown") + .or_( + ( + pl.col("type.text").is_in( + [ + "Safety", + "Fumble Recovery (Opponent)", + "Fumble Recovery (Opponent) Touchdown", + "Fumble Recovery (Own)", + "Fumble Recovery (Own) Touchdown", + "Fumble Return Touchdown", + ] + ) + ).and_(pl.col("text").str.contains("run for")) ) - & play_df["text"].str.contains("run for") ) - ), - True, - False, - ) - play_df["pass"] = np.where( - ( - ( - play_df["type.text"].isin( - [ - "Pass Reception", - "Pass Completion", - "Passing Touchdown", - "Sack", - "Pass", - "Interception", - "Pass Interception Return", - "Interception Return Touchdown", - "Pass Incompletion", - "Sack Touchdown", - "Interception Return", - ] + .then(True) + .otherwise(False) + .alias("rush"), + pl.when( + ( + pl.col("type.text").is_in( + [ + "Pass Reception", + "Pass Completion", + "Passing Touchdown", + "Sack", + "Pass", + "Interception", + "Pass Interception Return", + "Interception Return Touchdown", + "Pass Incompletion", + "Sack Touchdown", + "Interception Return", + ] + ) ) - ) - | ( - (play_df["type.text"] == "Safety") - & (play_df["text"].str.contains("sacked", case=False, flags=0, na=False, regex=True)) - ) - | ( - (play_df["type.text"] == "Safety") - & (play_df["text"].str.contains("pass complete", case=False, flags=0, na=False, regex=True)) - ) - | ( - (play_df["type.text"] == "Fumble Recovery (Own)") - & ( - play_df["text"].str.contains( - r"pass complete|pass incomplete|pass intercepted", - case=False, - flags=0, - na=False, - regex=True, + .or_((pl.col("type.text") == "Safety").and_(pl.col("text").str.contains("sacked"))) + .or_((pl.col("type.text") == "Safety").and_(pl.col("text").str.contains("pass complete"))) + .or_( + (pl.col("type.text") == "Fumble Recovery (Own)").and_( + pl.col("text").str.contains(r"pass complete|pass incomplete|pass intercepted") ) ) - ) - | ( - (play_df["type.text"] == "Fumble Recovery (Own)") - & (play_df["text"].str.contains("sacked", case=False, flags=0, na=False, regex=True)) - ) - | ( - (play_df["type.text"] == "Fumble Recovery (Own) Touchdown") - & ( - play_df["text"].str.contains( - r"pass complete|pass incomplete|pass intercepted", - case=False, - flags=0, - na=False, - regex=True, + .or_((pl.col("type.text") == "Fumble Recovery (Own)").and_(pl.col("text").str.contains("sacked"))) + .or_( + (pl.col("type.text") == "Fumble Recovery (Own) Touchdown").and_( + pl.col("text").str.contains(r"pass complete|pass incomplete|pass intercepted") ) ) - ) - | ( - (play_df["type.text"] == "Fumble Recovery (Own) Touchdown") - & (play_df["text"].str.contains("sacked", case=False, flags=0, na=False, regex=True)) - ) - | ( - (play_df["type.text"] == "Fumble Recovery (Opponent)") - & ( - play_df["text"].str.contains( - r"pass complete|pass incomplete|pass intercepted", - case=False, - flags=0, - na=False, - regex=True, + .or_( + (pl.col("type.text") == "Fumble Recovery (Own) Touchdown").and_( + pl.col("text").str.contains("sacked") ) ) - ) - | ( - (play_df["type.text"] == "Fumble Recovery (Opponent)") - & (play_df["text"].str.contains("sacked", case=False, flags=0, na=False, regex=True)) - ) - | ( - (play_df["type.text"] == "Fumble Recovery (Opponent) Touchdown") - & ( - play_df["text"].str.contains( - r"pass complete|pass incomplete", - case=False, - flags=0, - na=False, - regex=True, + .or_( + (pl.col("type.text") == "Fumble Recovery (Opponent)").and_( + pl.col("text").str.contains(r"pass complete|pass incomplete|pass intercepted") ) ) - ) - | ( - (play_df["type.text"] == "Fumble Return Touchdown") - & ( - play_df["text"].str.contains( - r"pass complete|pass incomplete", - case=False, - flags=0, - na=False, - regex=True, + .or_( + (pl.col("type.text") == "Fumble Recovery (Opponent)").and_( + pl.col("text").str.contains("sacked") + ) + ) + .or_( + (pl.col("type.text") == "Fumble Recovery (Opponent) Touchdown").and_( + pl.col("text").str.contains(r"pass complete|pass incomplete") + ) + ) + .or_( + (pl.col("type.text") == "Fumble Return Touchdown").and_( + pl.col("text").str.contains(r"pass complete|pass incomplete") ) ) + .or_( + (pl.col("type.text") == "Fumble Return Touchdown").and_(pl.col("text").str.contains("sacked")) + ) ) - | ( - (play_df["type.text"] == "Fumble Return Touchdown") - & (play_df["text"].str.contains("sacked", case=False, flags=0, na=False, regex=True)) + .then(True) + .otherwise(False) + .alias("pass"), + ) + .with_columns( + # --- Sacks---- + sack_vec=pl.when( + (pl.col("type.text").is_in(["Sack", "Sack Touchdown"])).or_( + ( + pl.col("type.text").is_in( + [ + "Fumble Recovery (Own)", + "Fumble Recovery (Own) Touchdown", + "Fumble Recovery (Opponent)", + "Fumble Recovery (Opponent) Touchdown", + "Fumble Return Touchdown", + ] + ) + ).and_(pl.col("text").str.contains("(?i)sacked"), pl.col("pass") == True) + ) ) - ), - True, - False, - ) - # --- Sacks---- - play_df["sack_vec"] = np.where( - ( - (play_df["type.text"].isin(["Sack", "Sack Touchdown"])) - | ( - ( - play_df["type.text"].isin( - [ - "Fumble Recovery (Own)", - "Fumble Recovery (Own) Touchdown", - "Fumble Recovery (Opponent)", - "Fumble Recovery (Opponent) Touchdown", - "Fumble Return Touchdown", - ] - ) - & (play_df["pass"] == True) - & (play_df["text"].str.contains("sacked", case=False, flags=0, na=False, regex=True)) - ) - ) - ), - True, - False, + .then(True) + .otherwise(False), + ) + .with_columns( + pl.when(pl.col("sack_vec") == True).then(True).otherwise(pl.col("pass")).alias("pass"), + ) ) - play_df["pass"] = np.where(play_df["sack_vec"] == True, True, play_df["pass"]) + return play_df def __add_team_score_variables(self, play_df): @@ -1250,115 +1057,180 @@ def __add_team_score_variables(self, play_df): * Team Score variables * Fix change of poss variables """ - # ------------------------- - play_df["pos_team"] = play_df["start.pos_team.id"] - play_df["def_pos_team"] = play_df["start.def_pos_team.id"] - play_df["is_home"] = play_df.pos_team == play_df["homeTeamId"] - # --- Team Score variables ------ - play_df["lag_homeScore"] = play_df["homeScore"].shift(1) - play_df["lag_awayScore"] = play_df["awayScore"].shift(1) - play_df["lag_HA_score_diff"] = play_df["lag_homeScore"] - play_df["lag_awayScore"] - play_df["HA_score_diff"] = play_df["homeScore"] - play_df["awayScore"] - play_df["net_HA_score_pts"] = play_df["HA_score_diff"] - play_df["lag_HA_score_diff"] - play_df["H_score_diff"] = play_df["homeScore"] - play_df["lag_homeScore"] - play_df["A_score_diff"] = play_df["awayScore"] - play_df["lag_awayScore"] - play_df["homeScore"] = np.select( - [ - (play_df.scoringPlay == False) & (play_df["game_play_number"] != 1) & (play_df["H_score_diff"] >= 9), - (play_df.scoringPlay == False) - & (play_df["game_play_number"] != 1) - & (play_df["H_score_diff"] < 9) - & (play_df["H_score_diff"] > 1), - (play_df.scoringPlay == False) - & (play_df["game_play_number"] != 1) - & (play_df["H_score_diff"] >= -9) - & (play_df["H_score_diff"] < -1), - ], - [play_df["lag_homeScore"], play_df["lag_homeScore"], play_df["homeScore"]], - default=play_df["homeScore"], - ) - play_df["awayScore"] = np.select( - [ - (play_df.scoringPlay == False) & (play_df["game_play_number"] != 1) & (play_df["A_score_diff"] >= 9), - (play_df.scoringPlay == False) - & (play_df["game_play_number"] != 1) - & (play_df["A_score_diff"] < 9) - & (play_df["A_score_diff"] > 1), - (play_df.scoringPlay == False) - & (play_df["game_play_number"] != 1) - & (play_df["A_score_diff"] >= -9) - & (play_df["A_score_diff"] < -1), - ], - [play_df["lag_awayScore"], play_df["lag_awayScore"], play_df["awayScore"]], - default=play_df["awayScore"], - ) - play_df.drop(["lag_homeScore", "lag_awayScore"], axis=1, inplace=True) - play_df["lag_homeScore"] = play_df["homeScore"].shift(1) - play_df["lag_homeScore"] = np.where((play_df.lag_homeScore.isna()), 0, play_df["lag_homeScore"]) - play_df["lag_awayScore"] = play_df["awayScore"].shift(1) - play_df["lag_awayScore"] = np.where((play_df.lag_awayScore.isna()), 0, play_df["lag_awayScore"]) - play_df["start.homeScore"] = np.where((play_df["game_play_number"] == 1), 0, play_df["lag_homeScore"]) - play_df["start.awayScore"] = np.where((play_df["game_play_number"] == 1), 0, play_df["lag_awayScore"]) - play_df["end.homeScore"] = play_df["homeScore"] - play_df["end.awayScore"] = play_df["awayScore"] - play_df["pos_team_score"] = np.where( - play_df.pos_team == play_df["homeTeamId"], - play_df.homeScore, - play_df.awayScore, - ) - play_df["def_pos_team_score"] = np.where( - play_df.pos_team == play_df["homeTeamId"], - play_df.awayScore, - play_df.homeScore, - ) - play_df["start.pos_team_score"] = np.where( - play_df["start.pos_team.id"] == play_df["homeTeamId"], - play_df["start.homeScore"], - play_df["start.awayScore"], - ) - play_df["start.def_pos_team_score"] = np.where( - play_df["start.pos_team.id"] == play_df["homeTeamId"], - play_df["start.awayScore"], - play_df["start.homeScore"], - ) - play_df["start.pos_score_diff"] = play_df["start.pos_team_score"] - play_df["start.def_pos_team_score"] - play_df["end.pos_team_score"] = np.where( - play_df["end.pos_team.id"] == play_df["homeTeamId"], - play_df["end.homeScore"], - play_df["end.awayScore"], - ) - play_df["end.def_pos_team_score"] = np.where( - play_df["end.pos_team.id"] == play_df["homeTeamId"], - play_df["end.awayScore"], - play_df["end.homeScore"], - ) - play_df["end.pos_score_diff"] = play_df["end.pos_team_score"] - play_df["end.def_pos_team_score"] - play_df["lag_pos_team"] = play_df["pos_team"].shift(1) - play_df.loc[play_df.lag_pos_team.isna() == True, "lag_pos_team"] = play_df.pos_team - play_df["lead_pos_team"] = play_df["pos_team"].shift(-1) - play_df["lead_pos_team2"] = play_df["pos_team"].shift(-2) - play_df["pos_score_diff"] = play_df.pos_team_score - play_df.def_pos_team_score - play_df["lag_pos_score_diff"] = play_df["pos_score_diff"].shift(1) - play_df.loc[play_df.lag_pos_score_diff.isna(), "lag_pos_score_diff"] = 0 - play_df["pos_score_pts"] = np.where( - play_df.lag_pos_team == play_df.pos_team, - play_df.pos_score_diff - play_df.lag_pos_score_diff, - play_df.pos_score_diff + play_df.lag_pos_score_diff, - ) - play_df["pos_score_diff_start"] = np.select( - [ - (play_df.kickoff_play == True) & (play_df.lag_pos_team == play_df.pos_team), - (play_df.kickoff_play == True) | (play_df.lag_pos_team != play_df.pos_team), - ], - [play_df.lag_pos_score_diff, -1 * play_df.lag_pos_score_diff], - default=play_df.lag_pos_score_diff, + play_df = ( + play_df.with_columns( + pos_team=pl.col("start.pos_team.id"), + def_pos_team=pl.col("start.def_pos_team.id"), + ) + .with_columns( + is_home=pl.col("pos_team") == pl.col("homeTeamId"), + # --- Team Score variables ------ + lag_homeScore=pl.col("homeScore").shift(1), + lag_awayScore=pl.col("awayScore").shift(1), + ) + .with_columns( + lag_HA_score_diff=pl.col("lag_homeScore") - pl.col("lag_awayScore"), + HA_score_diff=pl.col("homeScore") - pl.col("awayScore"), + ) + .with_columns( + net_HA_score_pts=pl.col("HA_score_diff") - pl.col("lag_HA_score_diff"), + H_score_diff=pl.col("homeScore") - pl.col("lag_homeScore"), + A_score_diff=pl.col("awayScore") - pl.col("lag_awayScore"), + ) + .with_columns( + homeScore=pl.when( + (pl.col("scoringPlay") == False) + & (pl.col("game_play_number") != 1) + & (pl.col("H_score_diff") >= 9) + ) + .then(pl.col("lag_homeScore")) + .when( + (pl.col("scoringPlay") == False) + & (pl.col("game_play_number") != 1) + & (pl.col("H_score_diff") < 9) + & (pl.col("H_score_diff") > 1) + ) + .then(pl.col("lag_homeScore")) + .when( + (pl.col("scoringPlay") == False) + & (pl.col("game_play_number") != 1) + & (pl.col("H_score_diff") >= -9) + & (pl.col("H_score_diff") < -1) + ) + .then(pl.col("homeScore")) + .otherwise(pl.col("homeScore")), + awayScore=pl.when( + (pl.col("scoringPlay") == False) + & (pl.col("game_play_number") != 1) + & (pl.col("A_score_diff") >= 9) + ) + .then(pl.col("lag_awayScore")) + .when( + (pl.col("scoringPlay") == False) + & (pl.col("game_play_number") != 1) + & (pl.col("A_score_diff") < 9) + & (pl.col("A_score_diff") > 1) + ) + .then(pl.col("lag_awayScore")) + .when( + (pl.col("scoringPlay") == False) + & (pl.col("game_play_number") != 1) + & (pl.col("A_score_diff") >= -9) + & (pl.col("A_score_diff") < -1) + ) + .then(pl.col("awayScore")) + .otherwise(pl.col("awayScore")), + ) + .drop(["lag_homeScore", "lag_awayScore"]) + .with_columns( + lag_homeScore=pl.col("homeScore").shift(1), + lag_awayScore=pl.col("awayScore").shift(1), + ) + .with_columns( + lag_homeScore=pl.when(pl.col("lag_homeScore").is_null()).then(0).otherwise(pl.col("lag_homeScore")), + lag_awayScore=pl.when(pl.col("lag_awayScore").is_null()).then(0).otherwise(pl.col("lag_awayScore")), + ) + .with_columns( + pl.when(pl.col("game_play_number") == 1) + .then(0) + .otherwise(pl.col("lag_homeScore")) + .alias("start.homeScore"), + pl.when(pl.col("game_play_number") == 1) + .then(0) + .otherwise(pl.col("lag_awayScore")) + .alias("start.awayScore"), + pl.col("homeScore").alias("end.homeScore"), + pl.col("awayScore").alias("end.awayScore"), + pl.when(pl.col("pos_team") == pl.col("homeTeamId")) + .then(pl.col("homeScore")) + .otherwise(pl.col("awayScore")) + .alias("pos_team_score"), + pl.when(pl.col("pos_team") == pl.col("homeTeamId")) + .then(pl.col("awayScore")) + .otherwise(pl.col("homeScore")) + .alias("def_pos_team_score"), + ) + .with_columns( + pl.when(pl.col("start.pos_team.id") == pl.col("homeTeamId")) + .then(pl.col("start.homeScore")) + .otherwise(pl.col("start.awayScore")) + .alias("start.pos_team_score"), + pl.when(pl.col("start.pos_team.id") == pl.col("homeTeamId")) + .then(pl.col("start.awayScore")) + .otherwise(pl.col("start.homeScore")) + .alias("start.def_pos_team_score"), + ) + .with_columns( + (pl.col("start.pos_team_score") - pl.col("start.def_pos_team_score")).alias("start.pos_score_diff"), + pl.when(pl.col("pos_team") == pl.col("homeTeamId")) + .then(pl.col("end.homeScore")) + .otherwise(pl.col("end.awayScore")) + .alias("end.pos_team_score"), + pl.when(pl.col("pos_team") == pl.col("homeTeamId")) + .then(pl.col("end.awayScore")) + .otherwise(pl.col("end.homeScore")) + .alias("end.def_pos_team_score"), + ) + .with_columns( + (pl.col("end.pos_team_score") - pl.col("end.def_pos_team_score")).alias("end.pos_score_diff"), + pl.col("pos_team").shift(1).alias("lag_pos_team"), + ) + .with_columns( + pl.when(pl.col("lag_pos_team").is_null()) + .then(pl.col("pos_team")) + .otherwise(pl.col("lag_pos_team")) + .alias("lag_pos_team"), + pl.col("pos_team").shift(-1).alias("lead_pos_team"), + pl.col("pos_team").shift(-2).alias("lead_pos_team2"), + (pl.col("pos_team_score") - pl.col("def_pos_team_score")).alias("pos_score_diff"), + ) + .with_columns( + pl.col("pos_score_diff").shift(1).alias("lag_pos_score_diff"), + ) + .with_columns( + pl.when(pl.col("lag_pos_score_diff").is_null()) + .then(0) + .otherwise(pl.col("lag_pos_score_diff")) + .alias("lag_pos_score_diff"), + ) + .with_columns( + pl.when(pl.col("lag_pos_team") == pl.col("pos_team")) + .then(pl.col("pos_score_diff") - pl.col("lag_pos_score_diff")) + .otherwise(pl.col("pos_score_diff") + pl.col("lag_pos_score_diff")) + .alias("pos_score_pts"), + pl.when((pl.col("kickoff_play") == True).and_(pl.col("lag_pos_team") == pl.col("pos_team"))) + .then(pl.col("lag_pos_score_diff")) + .when((pl.col("kickoff_play") == True).or_(pl.col("lag_pos_team") != pl.col("pos_team"))) + .then(-1 * pl.col("lag_pos_score_diff")) + .otherwise(pl.col("lag_pos_score_diff")) + .alias("pos_score_diff_start"), + ) + .with_columns( + pl.when(pl.col("pos_score_diff_start").is_null() == True) + .then(pl.col("pos_score_diff")) + .otherwise(pl.col("pos_score_diff_start")) + .alias("pos_score_diff_start"), + pl.when(pl.col("start.pos_team.id") == pl.col("firstHalfKickoffTeamId")) + .then(True) + .otherwise(False) + .alias("start.pos_team_receives_2H_kickoff"), + pl.when(pl.col("end.pos_team.id") == pl.col("firstHalfKickoffTeamId")) + .then(True) + .otherwise(False) + .alias("end.pos_team_receives_2H_kickoff"), + pl.when(pl.col("start.pos_team.id") == pl.col("end.pos_team.id")) + .then(False) + .otherwise(True) + .alias("change_of_poss"), + ) + .with_columns( + pl.when(pl.col("change_of_poss").is_null() == True) + .then(False) + .otherwise(pl.col("change_of_poss")) + .alias("change_of_poss"), + ) ) - # --- Timeouts ------ - play_df.loc[play_df.pos_score_diff_start.isna() == True, "pos_score_diff_start"] = play_df.pos_score_diff - play_df["start.pos_team_receives_2H_kickoff"] = play_df["start.pos_team.id"] == play_df.firstHalfKickoffTeamId - play_df["end.pos_team_receives_2H_kickoff"] = play_df["end.pos_team.id"] == play_df.firstHalfKickoffTeamId - play_df["change_of_poss"] = np.where(play_df["start.pos_team.id"] == play_df["end.pos_team.id"], False, True) - play_df["change_of_poss"] = np.where(play_df["change_of_poss"].isna(), 0, play_df["change_of_poss"]) + return play_df def __add_new_play_types(self, play_df): @@ -1367,1781 +1239,1836 @@ def __add_new_play_types(self, play_df): * Fix play types """ # -------------------------------------------------- - ## Fix Strip-Sacks to Fumbles---- - play_df["type.text"] = np.where( - (play_df.fumble_vec == True) - & (play_df["pass"] == True) - & (play_df.change_of_poss == 1) - & (play_df.td_play == False) - & (play_df["start.down"] != 4) - & ~(play_df["type.text"].isin(defense_score_vec)), - "Fumble Recovery (Opponent)", - play_df["type.text"], - ) - play_df["type.text"] = np.where( - (play_df.fumble_vec == True) - & (play_df["pass"] == True) - & (play_df.change_of_poss == 1) - & (play_df.td_play == True), - "Fumble Recovery (Opponent) Touchdown", - play_df["type.text"], - ) - ## Fix rushes with fumbles and a change of possession to fumbles---- - play_df["type.text"] = np.where( - (play_df.fumble_vec == True) - & (play_df["rush"] == True) - & (play_df.change_of_poss == 1) - & (play_df.td_play == False) - & (play_df["start.down"] != 4) - & ~(play_df["type.text"].isin(defense_score_vec)), - "Fumble Recovery (Opponent)", - play_df["type.text"], - ) - play_df["type.text"] = np.where( - (play_df.fumble_vec == True) - & (play_df["rush"] == True) - & (play_df.change_of_poss == 1) - & (play_df.td_play == True), - "Fumble Recovery (Opponent) Touchdown", - play_df["type.text"], - ) - - # -- Fix kickoff fumble return TDs ---- - play_df["type.text"] = np.where( - (play_df.kickoff_play == True) - & (play_df.change_of_poss == 1) - & (play_df.td_play == True) - & (play_df.td_check == True), - "Kickoff Return Touchdown", - play_df["type.text"], - ) - # -- Fix punt return TDs ---- - play_df["type.text"] = np.where( - (play_df.punt_play == True) & (play_df.td_play == True) & (play_df.td_check == True), - "Punt Return Touchdown", - play_df["type.text"], - ) - # -- Fix kick return TDs---- - play_df["type.text"] = np.where( - (play_df.kickoff_play == True) - & (play_df.fumble_vec == False) - & (play_df.td_play == True) - & (play_df.td_check == True), - "Kickoff Return Touchdown", - play_df["type.text"], - ) - # -- Fix rush/pass tds that aren't explicit---- - play_df["type.text"] = np.where( - (play_df.td_play == True) - & (play_df.rush == True) - & (play_df.fumble_vec == False) - & (play_df.td_check == True), - "Rushing Touchdown", - play_df["type.text"], - ) - play_df["type.text"] = np.where( - (play_df.td_play == True) - & (play_df["pass"] == True) - & (play_df.fumble_vec == False) - & (play_df.td_check == True) - & ~(play_df["type.text"].isin(int_vec)), - "Passing Touchdown", - play_df["type.text"], - ) - play_df["type.text"] = np.where( - (play_df["pass"] == True) - & (play_df["type.text"].isin(["Pass Reception", "Pass Completion", "Pass"])) - & (play_df.statYardage == play_df["start.yardsToEndzone"]) - & (play_df.fumble_vec == False) - & ~(play_df["type.text"].isin(int_vec)), - "Passing Touchdown", - play_df["type.text"], - ) - play_df["type.text"] = np.where( - (play_df["type.text"].isin(["Blocked Field Goal"])) - & (play_df["text"].str.contains("for a TD", case=False, flags=0, na=False, regex=True)), - "Blocked Field Goal Touchdown", - play_df["type.text"], - ) - - play_df["type.text"] = np.where( - (play_df["type.text"].isin(["Blocked Punt"])) - & (play_df["text"].str.contains("for a TD", case=False, flags=0, na=False, regex=True)), - "Blocked Punt Touchdown", - play_df["type.text"], - ) - # -- Fix duplicated TD play_type labels---- - play_df["type.text"] = np.where( - play_df["type.text"] == "Punt Touchdown Touchdown", - "Punt Touchdown", - play_df["type.text"], - ) - play_df["type.text"] = np.where( - play_df["type.text"] == "Fumble Return Touchdown Touchdown", - "Fumble Return Touchdown", - play_df["type.text"], - ) - play_df["type.text"] = np.where( - play_df["type.text"] == "Rushing Touchdown Touchdown", - "Rushing Touchdown", - play_df["type.text"], - ) - play_df["type.text"] = np.where( - play_df["type.text"] == "Uncategorized Touchdown Touchdown", - "Uncategorized Touchdown", - play_df["type.text"], - ) - # -- Fix Pass Interception Return TD play_type labels---- - play_df["type.text"] = np.where( - play_df["text"].str.contains("pass intercepted for a TD", case=False, flags=0, na=False, regex=True), - "Interception Return Touchdown", - play_df["type.text"], - ) - # -- Fix Sack/Fumbles Touchdown play_type labels---- - play_df["type.text"] = np.where( - (play_df["text"].str.contains("sacked", case=False, flags=0, na=False, regex=True)) - & (play_df["text"].str.contains("fumbled", case=False, flags=0, na=False, regex=True)) - & (play_df["text"].str.contains("TD", case=False, flags=0, na=False, regex=True)), - "Fumble Recovery (Opponent) Touchdown", - play_df["type.text"], - ) - # -- Fix generic pass plays ---- - ##-- first one looks for complete pass - play_df["type.text"] = np.where( - (play_df["type.text"] == "Pass") - & (play_df.text.str.contains("pass complete", case=False, flags=0, na=False, regex=True)), - "Pass Completion", - play_df["type.text"], - ) - ##-- second one looks for incomplete pass - play_df["type.text"] = np.where( - (play_df["type.text"] == "Pass") - & (play_df.text.str.contains("pass incomplete", case=False, flags=0, na=False, regex=True)), - "Pass Incompletion", - play_df["type.text"], - ) - ##-- third one looks for interceptions - play_df["type.text"] = np.where( - (play_df["type.text"] == "Pass") - & (play_df.text.str.contains("pass intercepted", case=False, flags=0, na=False, regex=True)), - "Pass Interception", - play_df["type.text"], - ) - ##-- fourth one looks for sacked - play_df["type.text"] = np.where( - (play_df["type.text"] == "Pass") - & (play_df.text.str.contains("sacked", case=False, flags=0, na=False, regex=True)), - "Sack", - play_df["type.text"], - ) - ##-- fifth one play type is Passing Touchdown, but its intercepted - play_df["type.text"] = np.where( - (play_df["type.text"] == "Passing Touchdown") - & ( - play_df.text.str.contains( - "pass intercepted for a TD", - case=False, - flags=0, - na=False, - regex=True, - ) - ), - "Interception Return Touchdown", - play_df["type.text"], - ) - play_df["type.text"] = np.where( - (play_df["type.text"] == "Passing Touchdown") - & ( - play_df.text.str.contains( - "pass intercepted for a TD", - case=False, - flags=0, - na=False, - regex=True, - ) - ), - "Interception Return Touchdown", - play_df["type.text"], - ) - # --- Moving non-Touchdown pass interceptions to one play_type: "Interception Return" ----- - play_df["type.text"] = np.where( - play_df["type.text"].isin(["Interception"]), - "Interception Return", - play_df["type.text"], - ) - play_df["type.text"] = np.where( - play_df["type.text"].isin(["Pass Interception"]), - "Interception Return", - play_df["type.text"], - ) - play_df["type.text"] = np.where( - play_df["type.text"].isin(["Pass Interception Return"]), - "Interception Return", - play_df["type.text"], - ) - - # --- Moving Kickoff/Punt Touchdowns without fumbles to Kickoff/Punt Return Touchdown - play_df["type.text"] = np.where( - (play_df["type.text"] == "Kickoff Touchdown") & (play_df.fumble_vec == False), - "Kickoff Return Touchdown", - play_df["type.text"], - ) - - play_df["type.text"] = np.select( - [ - (play_df["type.text"] == "Kickoff Touchdown") & (play_df.fumble_vec == False), - (play_df["type.text"] == "Kickoff") & (play_df["td_play"] == True) & (play_df.fumble_vec == False), - (play_df["type.text"] == "Kickoff") - & (play_df.text.str.contains("for a TD", case=False, flags=0, na=False, regex=True)) - & (play_df.fumble_vec == False), - ], - [ - "Kickoff Return Touchdown", - "Kickoff Return Touchdown", - "Kickoff Return Touchdown", - ], - default=play_df["type.text"], - ) - - play_df["type.text"] = np.where( - (play_df["type.text"].isin(["Kickoff", "Kickoff Return (Offense)"])) - & (play_df.fumble_vec == True) - & (play_df.change_of_poss == 1), - "Kickoff Team Fumble Recovery", - play_df["type.text"], + play_df = ( + play_df.with_columns( + # --- Fix Strip Sacks to Fumbles ---- + pl.when( + (pl.col("fumble_vec") == True) + .and_(pl.col("pass") == True) + .and_(pl.col("change_of_poss") == 1) + .and_(pl.col("td_play") == False) + .and_(pl.col("start.down") != 4) + .and_(pl.col("type.text").is_in(defense_score_vec) == False) + ) + .then(pl.lit("Fumble Recovery (Opponent)")) + .otherwise(pl.col("type.text")) + .alias("type.text"), + ) + .with_columns( + pl.when( + (pl.col("fumble_vec") == True) + .and_(pl.col("pass") == True) + .and_(pl.col("change_of_poss") == 1) + .and_(pl.col("td_play") == True) + ) + .then(pl.lit("Fumble Recovery (Opponent) Touchdown")) + .otherwise(pl.col("type.text")) + .alias("type.text"), + ) + .with_columns( + # --- Fix rushes with fumbles and a change of possession to fumbles---- + pl.when( + (pl.col("fumble_vec") == True) + .and_(pl.col("rush") == True) + .and_(pl.col("change_of_poss") == 1) + .and_(pl.col("td_play") == False) + .and_(pl.col("start.down") != 4) + .and_(pl.col("type.text").is_in(defense_score_vec) == False) + ) + .then(pl.lit("Fumble Recovery (Opponent)")) + .otherwise(pl.col("type.text")) + .alias("type.text"), + ) + .with_columns( + pl.when( + (pl.col("fumble_vec") == True) + .and_(pl.col("rush") == True) + .and_(pl.col("change_of_poss") == 1) + .and_(pl.col("td_play") == True) + ) + .then(pl.lit("Fumble Recovery (Opponent) Touchdown")) + .otherwise(pl.col("type.text")) + .alias("type.text"), + ) + .with_columns( + # -- Fix kickoff fumble return TDs ---- + pl.when( + (pl.col("kickoff_play") == True) + .and_(pl.col("change_of_poss") == 1) + .and_(pl.col("td_play") == True) + .and_(pl.col("td_check") == True) + ) + .then(pl.lit("Kickoff Return Touchdown")) + .otherwise(pl.col("type.text")) + .alias("type.text"), + ) + .with_columns( + # -- Fix punt return TDs ---- + pl.when((pl.col("punt_play") == True).and_(pl.col("td_play") == True).and_(pl.col("td_check") == True)) + .then(pl.lit("Punt Return Touchdown")) + .otherwise(pl.col("type.text")) + .alias("type.text"), + ) + .with_columns( + # -- Fix kick return TDs ---- + pl.when( + (pl.col("kickoff_play") == True) + .and_(pl.col("fumble_vec") == False) + .and_(pl.col("td_play") == True) + .and_(pl.col("td_check") == True) + ) + .then(pl.lit("Kickoff Return Touchdown")) + .otherwise(pl.col("type.text")) + .alias("type.text"), + ) + .with_columns( + # -- Fix rush/pass tds that aren't explicit---- + pl.when( + (pl.col("td_play") == True) + .and_(pl.col("rush") == True) + .and_(pl.col("fumble_vec") == False) + .and_(pl.col("td_check") == True) + ) + .then(pl.lit("Rushing Touchdown")) + .otherwise(pl.col("type.text")) + .alias("type.text"), + ) + .with_columns( + pl.when( + (pl.col("td_play") == True) + .and_(pl.col("pass") == True) + .and_(pl.col("fumble_vec") == False) + .and_(pl.col("td_check") == True) + .and_(pl.col("type.text").is_in(int_vec) == False) + ) + .then(pl.lit("Passing Touchdown")) + .otherwise(pl.col("type.text")) + .alias("type.text"), + ) + .with_columns( + pl.when( + (pl.col("pass") == True) + .and_(pl.col("type.text").is_in(["Pass Reception", "Pass Completion", "Pass"])) + .and_(pl.col("statYardage") == pl.col("start.yardsToEndzone")) + .and_(pl.col("fumble_vec") == False) + .and_(pl.col("type.text").is_in(int_vec) == False) + ) + .then(pl.lit("Passing Touchdown")) + .otherwise(pl.col("type.text")) + .alias("type.text"), + ) + .with_columns( + pl.when( + (pl.col("type.text").is_in(["Blocked Field Goal"])).and_( + pl.col("text").str.contains("(?i)for a TD") + ) + ) + .then(pl.lit("Blocked Field Goal Touchdown")) + .otherwise(pl.col("type.text")) + .alias("type.text"), + ) + .with_columns( + pl.when( + (pl.col("type.text").is_in(["Blocked Punt"])).and_(pl.col("text").str.contains("(?i)for a TD")) + ) + .then(pl.lit("Blocked Punt Touchdown")) + .otherwise(pl.col("type.text")) + .alias("type.text"), + ) + .with_columns( + # -- Fix duplicated TD play_type labels---- + pl.col("type.text") + .str.replace(r"(?i)Touchdown Touchdown", "Touchdown") + .alias("type.text") + ) + .with_columns( + # -- Fix Pass Interception Return TD play_type labels---- + pl.when(pl.col("text").str.contains("(?i)pass intercepted for a TD")) + .then(pl.lit("Interception Return Touchdown")) + .otherwise(pl.col("type.text")) + .alias("type.text"), + ) + .with_columns( + # -- Fix Sack/Fumbles Touchdown play_type labels---- + pl.when( + (pl.col("text").str.contains("(?i)sacked")) + .and_(pl.col("text").str.contains("(?i)fumbled")) + .and_(pl.col("text").str.contains("(?i)TD")) + ) + .then(pl.lit("Fumble Recovery (Opponent) Touchdown")) + .otherwise(pl.col("type.text")) + .alias("type.text"), + ) + .with_columns( + # -- Fix generic pass plays ---- + ##-- first one looks for complete pass + pl.when((pl.col("type.text") == "Pass").and_(pl.col("text").str.contains("(?i)pass complete"))) + .then(pl.lit("Pass Completion")) + .otherwise(pl.col("type.text")) + .alias("type.text"), + ) + .with_columns( + ##-- second one looks for incomplete pass + pl.when((pl.col("type.text") == "Pass").and_(pl.col("text").str.contains("(?i)pass incomplete"))) + .then(pl.lit("Pass Incompletion")) + .otherwise(pl.col("type.text")) + .alias("type.text"), + ) + .with_columns( + ##-- third one looks for interceptions + pl.when((pl.col("type.text") == "Pass").and_(pl.col("text").str.contains("(?i)pass intercepted"))) + .then(pl.lit("Pass Interception")) + .otherwise(pl.col("type.text")) + .alias("type.text"), + ) + .with_columns( + ##-- fourth one looks for sacked + pl.when((pl.col("type.text") == "Pass").and_(pl.col("text").str.contains("(?i)sacked"))) + .then(pl.lit("Sack")) + .otherwise(pl.col("type.text")) + .alias("type.text"), + ) + .with_columns( + ##-- fifth one play type is Passing Touchdown, but its intercepted + pl.when( + (pl.col("type.text") == "Passing Touchdown").and_( + pl.col("text").str.contains("(?i)pass intercepted for a TD") + ) + ) + .then(pl.lit("Interception Return Touchdown")) + .otherwise(pl.col("type.text")) + .alias("type.text"), + ) + .with_columns( + # --- Moving non-Touchdown pass interceptions to one play_type: "Interception Return" ----- + pl.when(pl.col("type.text").is_in(["Interception", "Pass Interception", "Pass Interception Return"])) + .then(pl.lit("Interception Return")) + .otherwise(pl.col("type.text")) + .alias("type.text"), + ) + .with_columns( + # --- Moving Kickoff/Punt Touchdowns without fumbles to Kickoff/Punt Return Touchdown + pl.when((pl.col("type.text") == "Kickoff Touchdown").and_(pl.col("fumble_vec") == False)) + .then(pl.lit("Kickoff Return Touchdown")) + .otherwise(pl.col("type.text")) + .alias("type.text"), + ) + .with_columns( + pl.when( + (pl.col("type.text") == "Kickoff") + .and_(pl.col("td_play") == True) + .and_(pl.col("fumble_vec") == False) + ) + .then(pl.lit("Kickoff Return Touchdown")) + .when( + (pl.col("type.text") == "Kickoff") + .and_(pl.col("text").str.contains("(?i)for a TD")) + .and_(pl.col("fumble_vec") == False) + ) + .then(pl.lit("Kickoff Return Touchdown")) + .otherwise(pl.col("type.text")) + .alias("type.text"), + ) + .with_columns( + pl.when( + (pl.col("type.text").is_in(["Kickoff", "Kickoff Return (Offense)"])) + .and_(pl.col("fumble_vec") == True) + .and_(pl.col("change_of_poss") == 1) + ) + .then(pl.lit("Kickoff Team Fumble Recovery")) + .otherwise(pl.col("type.text")) + .alias("type.text"), + ) + .with_columns( + pl.when( + (pl.col("type.text") == "Punt Touchdown") + .and_(pl.col("fumble_vec") == False) + .and_(pl.col("change_of_poss") == 1) + ) + .then(pl.lit("Punt Return Touchdown")) + .when( + (pl.col("type.text") == "Punt") + .and_(pl.col("text").str.contains("(?i)for a TD")) + .and_(pl.col("change_of_poss") == 1) + ) + .then(pl.lit("Punt Return Touchdown")) + .otherwise(pl.col("type.text")) + .alias("type.text"), + ) + .with_columns( + pl.when( + (pl.col("type.text") == "Punt") + .and_(pl.col("fumble_vec") == True) + .and_(pl.col("change_of_poss") == 0) + ) + .then(pl.lit("Punt Team Fumble Recovery")) + .otherwise(pl.col("type.text")) + .alias("type.text"), + ) + .with_columns( + pl.when(pl.col("type.text").is_in(["Punt Touchdown"])) + .then(pl.lit("Punt Team Fumble Recovery Touchdown")) + .when( + (pl.col("scoringPlay") == True) + .and_(pl.col("punt_play") == True) + .and_(pl.col("change_of_poss") == 0) + ) + .then(pl.lit("Punt Team Fumble Recovery Touchdown")) + .otherwise(pl.col("type.text")) + .alias("type.text"), + ) + .with_columns( + pl.when(pl.col("type.text").is_in(["Kickoff Touchdown"])) + .then(pl.lit("Kickoff Team Fumble Recovery Touchdown")) + .otherwise(pl.col("type.text")) + .alias("type.text"), + ) + .with_columns( + pl.when( + (pl.col("type.text").is_in(["Fumble Return Touchdown"])).and_( + (pl.col("pass") == True).or_(pl.col("rush") == True) + ) + ) + .then(pl.lit("Fumble Recovery (Opponent) Touchdown")) + .otherwise(pl.col("type.text")) + .alias("type.text"), + ) + .with_columns( + # --- Safeties (kickoff, punt, penalty) ---- + pl.when( + (pl.col("type.text").is_in(["Pass Reception", "Rush", "Rushing Touchdown"])) + .and_((pl.col("pass") == True).or_(pl.col("rush") == True)) + .and_(pl.col("safety") == True) + ) + .then(pl.lit("Safety")) + .otherwise(pl.col("type.text")) + .alias("type.text"), + ) + .with_columns( + pl.when(pl.col("kickoff_safety") == True) + .then(pl.lit("Kickoff (Safety)")) + .otherwise(pl.col("type.text")) + .alias("type.text"), + ) + .with_columns( + pl.when(pl.col("punt_safety") == True) + .then(pl.lit("Punt (Safety)")) + .otherwise(pl.col("type.text")) + .alias("type.text"), + ) + .with_columns( + pl.when(pl.col("penalty_safety") == True) + .then(pl.lit("Penalty (Safety)")) + .otherwise(pl.col("type.text")) + .alias("type.text"), + ) + .with_columns( + pl.when((pl.col("type.text") == "Extra Point Good").and_(pl.col("text").str.contains("(?i)Two-Point"))) + .then(pl.lit("Two-Point Conversion Good")) + .otherwise(pl.col("type.text")) + .alias("type.text"), + ) + .with_columns( + pl.when( + (pl.col("type.text") == "Extra Point Missed").and_(pl.col("text").str.contains("(?i)Two-Point")) + ) + .then(pl.lit("Two-Point Conversion Missed")) + .otherwise(pl.col("type.text")) + .alias("type.text"), + ) ) - play_df["type.text"] = np.select( - [ - (play_df["type.text"] == "Punt Touchdown") - & (play_df.fumble_vec == False) - & (play_df.change_of_poss == 1), - (play_df["type.text"] == "Punt") - & (play_df.text.str.contains("for a TD", case=False, flags=0, na=False, regex=True)) - & (play_df.change_of_poss == 1), - ], - ["Punt Return Touchdown", "Punt Return Touchdown"], - default=play_df["type.text"], - ) + return play_df - play_df["type.text"] = np.where( - (play_df["type.text"] == "Punt") & (play_df.fumble_vec == True) & (play_df.change_of_poss == 0), - "Punt Team Fumble Recovery", - play_df["type.text"], - ) - play_df["type.text"] = np.where( - (play_df["type.text"].isin(["Punt Touchdown"])) - | ((play_df["scoringPlay"] == True) & (play_df["punt_play"] == True) & (play_df.change_of_poss == 0)), - "Punt Team Fumble Recovery Touchdown", - play_df["type.text"], - ) - play_df["type.text"] = np.where( - play_df["type.text"].isin(["Kickoff Touchdown"]), - "Kickoff Team Fumble Recovery Touchdown", - play_df["type.text"], - ) - play_df["type.text"] = np.where( - (play_df["type.text"].isin(["Fumble Return Touchdown"])) - & ((play_df["pass"] == True) | (play_df["rush"] == True)), - "Fumble Recovery (Opponent) Touchdown", - play_df["type.text"], + def __setup_penalty_data(self, play_df): + """ + Creates the following columns in play_df: + * Penalty flag + * Penalty declined + * Penalty no play + * Penalty off-set + * Penalty 1st down conversion + * Penalty in text + * Yds Penalty + """ + ##-- 'Penalty' in play text ---- + play_df = ( + play_df.with_columns( + # -- T/F flag conditions penalty_flag + penalty_flag=pl.when( + (pl.col("type.text") == "Penalty").or_(pl.col("text").str.contains("(?i)penalty")) + ) + .then(True) + .otherwise(False), + # -- T/F flag conditions penalty_declined + penalty_declined=pl.when( + (pl.col("type.text") == "Penalty").and_(pl.col("text").str.contains("(?i)declined")) + ) + .then(True) + .otherwise(False), + # -- T/F flag conditions penalty_no_play + penalty_no_play=pl.when( + (pl.col("type.text") == "Penalty").and_(pl.col("text").str.contains("(?i)no play")) + ) + .then(True) + .otherwise(False), + # -- T/F flag conditions penalty_offset + penalty_offset=pl.when( + (pl.col("type.text") == "Penalty").and_(pl.col("text").str.contains("(?i)off-setting")) + ) + .then(True) + .when( + (pl.col("text").str.contains("(?i)penalty")).and_(pl.col("text").str.contains("(?i)off-setting")) + ) + .then(True) + .otherwise(False), + # -- T/F flag conditions penalty_1st_conv + penalty_1st_conv=pl.when( + (pl.col("type.text") == "Penalty").and_(pl.col("text").str.contains("(?i)1st down")) + ) + .then(True) + .when((pl.col("text").str.contains("(?i)penalty")).and_(pl.col("text").str.contains("(?i)1st down"))) + .then(True) + .otherwise(False), + # -- T/F flag for penalty text but not penalty play type -- + penalty_in_text=pl.when( + (pl.col("text").str.contains("(?i)penalty")).and_( + pl.col("type.text") != "Penalty", + pl.col("text").str.contains("(?i)declined") == False, + pl.col("text").str.contains("(?i)off-setting") == False, + pl.col("text").str.contains("(?i)no play") == False, + ) + ) + .then(True) + .otherwise(False), + ) + .with_columns( + penalty_detail=pl.when(pl.col("penalty_offset") == 1) + .then(pl.lit("Offsetting")) + .when(pl.col("penalty_declined") == 1) + .then(pl.lit("Declined")) + .when(pl.col("text").str.contains("(?i)roughing passer")) + .then(pl.lit("Roughing the Passer")) + .when(pl.col("text").str.contains("(?i)offensive holding")) + .then(pl.lit("Offensive Holding")) + .when(pl.col("text").str.contains("(?i)pass interference")) + .then(pl.lit("Pass Interference")) + .when(pl.col("text").str.contains("(?i)encroachment")) + .then(pl.lit("Encroachment")) + .when(pl.col("text").str.contains("(?i)defensive pass interference")) + .then(pl.lit("Defensive Pass Interference")) + .when(pl.col("text").str.contains("(?i)offensive pass interference")) + .then(pl.lit("Offensive Pass Interference")) + .when(pl.col("text").str.contains("(?i)illegal procedure")) + .then(pl.lit("Illegal Procedure")) + .when(pl.col("text").str.contains("(?i)defensive holding")) + .then(pl.lit("Defensive Holding")) + .when(pl.col("text").str.contains("(?i)holding")) + .then(pl.lit("Holding")) + .when(pl.col("text").str.contains("(?i)offensive offside|(?i)offside offense")) + .then(pl.lit("Offensive Offside")) + .when(pl.col("text").str.contains("(?i)defensive offside|(?i)offside defense")) + .then(pl.lit("Defensive Offside")) + .when(pl.col("text").str.contains("(?i)offside")) + .then(pl.lit("Offside")) + .when(pl.col("text").str.contains("(?i)illegal fair catch signal")) + .then(pl.lit("Illegal Fair Catch Signal")) + .when(pl.col("text").str.contains("(?i)illegal batting")) + .then(pl.lit("Illegal Batting")) + .when(pl.col("text").str.contains("(?i)neutral zone infraction")) + .then(pl.lit("Neutral Zone Infraction")) + .when(pl.col("text").str.contains("(?i)ineligible downfield")) + .then(pl.lit("Ineligible Downfield")) + .when(pl.col("text").str.contains("(?i)illegal use of hands")) + .then(pl.lit("Illegal Use of Hands")) + .when(pl.col("text").str.contains("(?i)kickoff out of bounds|(?i)kickoff out-of-bounds")) + .then(pl.lit("Kickoff Out of Bounds")) + .when(pl.col("text").str.contains("(?i)12 men on the field")) + .then(pl.lit("12 Men on the Field")) + .when(pl.col("text").str.contains("(?i)illegal block")) + .then(pl.lit("Illegal Block")) + .when(pl.col("text").str.contains("(?i)personal foul")) + .then(pl.lit("Personal Foul")) + .when(pl.col("text").str.contains("(?i)false start")) + .then(pl.lit("False Start")) + .when(pl.col("text").str.contains("(?i)substitution infraction")) + .then(pl.lit("Substitution Infraction")) + .when(pl.col("text").str.contains("(?i)illegal formation")) + .then(pl.lit("Illegal Formation")) + .when(pl.col("text").str.contains("(?i)illegal touching")) + .then(pl.lit("Illegal Touching")) + .when(pl.col("text").str.contains("(?i)sideline interference")) + .then(pl.lit("Sideline Interference")) + .when(pl.col("text").str.contains("(?i)clipping")) + .then(pl.lit("Clipping")) + .when(pl.col("text").str.contains("(?i)sideline infraction")) + .then(pl.lit("Sideline Infraction")) + .when(pl.col("text").str.contains("(?i)crackback")) + .then(pl.lit("Crackback")) + .when(pl.col("text").str.contains("(?i)illegal snap")) + .then(pl.lit("Illegal Snap")) + .when(pl.col("text").str.contains("(?i)illegal helmet contact")) + .then(pl.lit("Illegal Helmet Contact")) + .when(pl.col("text").str.contains("(?i)roughing holder")) + .then(pl.lit("Roughing the Holder")) + .when(pl.col("text").str.contains("(?i)horse collar tackle")) + .then(pl.lit("Horse Collar Tackle")) + .when(pl.col("text").str.contains("(?i)illegal participation")) + .then(pl.lit("Illegal Participation")) + .when(pl.col("text").str.contains("(?i)tripping")) + .then(pl.lit("Tripping")) + .when(pl.col("text").str.contains("(?i)illegal shift")) + .then(pl.lit("Illegal Shift")) + .when(pl.col("text").str.contains("(?i)illegal motion")) + .then(pl.lit("Illegal Motion")) + .when(pl.col("text").str.contains("(?i)roughing the kicker")) + .then(pl.lit("Roughing the Kicker")) + .when(pl.col("text").str.contains("(?i)delay of game")) + .then(pl.lit("Delay of Game")) + .when(pl.col("text").str.contains("(?i)targeting")) + .then(pl.lit("Targeting")) + .when(pl.col("text").str.contains("(?i)face mask")) + .then(pl.lit("Face Mask")) + .when(pl.col("text").str.contains("(?i)illegal forward pass")) + .then(pl.lit("Illegal Forward Pass")) + .when(pl.col("text").str.contains("(?i)intentional grounding")) + .then(pl.lit("Intentional Grounding")) + .when(pl.col("text").str.contains("(?i)illegal kicking")) + .then(pl.lit("Illegal Kicking")) + .when(pl.col("text").str.contains("(?i)illegal conduct")) + .then(pl.lit("Illegal Conduct")) + .when(pl.col("text").str.contains("(?i)kick catching interference")) + .then(pl.lit("Kick Catch Interference")) + .when(pl.col("text").str.contains("(?i)kick catch interference")) + .then(pl.lit("Kick Catch Interference")) + .when(pl.col("text").str.contains("(?i)unnecessary roughness")) + .then(pl.lit("Unnecessary Roughness")) + .when(pl.col("text").str.contains("(?i)Penalty, UR")) + .then(pl.lit("Unnecessary Roughness")) + .when(pl.col("text").str.contains("(?i)roughing the snapper")) + .then(pl.lit("Roughing the Snapper")) + .when(pl.col("text").str.contains("(?i)illegal blindside block")) + .then(pl.lit("Illegal Blindside Block")) + .when(pl.col("text").str.contains("(?i)unsportsmanlike conduct")) + .then(pl.lit("Unsportsmanlike Conduct")) + .when(pl.col("text").str.contains("(?i)running into kicker")) + .then(pl.lit("Running Into Kicker")) + .when(pl.col("text").str.contains("(?i)failure to wear required equipment")) + .then(pl.lit("Failure to Wear Required Equipment")) + .when(pl.col("text").str.contains("(?i)player disqualification")) + .then(pl.lit("Player Disqualification")) + .when(pl.col("penalty_flag") == True) + .then(pl.lit("Missing")) + ) + .with_columns( + penalty_text=pl.when(pl.col("penalty_flag") == True) + .then(pl.col("text").str.extract(r"(?i)Penalty(.+)", 1)) + .otherwise(None), + ) + .with_columns( + yds_penalty=pl.when(pl.col("penalty_flag") == True) + .then( + pl.col("penalty_text") + .str.extract(r"(?i)(.{0,3}) yards|(?i)yds|(?i)yd to the", 1) + .str.replace(" yards to the | yds to the | yd to the ", "") + ) + .otherwise(None), + ) + .with_columns( + yds_penalty=pl.when( + (pl.col("penalty_flag") == True).and_( + pl.col("yds_penalty").is_null(), pl.col("text").str.contains(r"(?i)ards\)") + ) + ) + .then( + pl.col("text") + .str.extract(r"(.{0,4})yards\)|Yards\)|yds\)|Yds\)", 1) + .str.replace("yards\\)|Yards\\)|yds\\)|Yds\\)", "") + .str.replace("\\(", "") + ) + .otherwise(pl.col("yds_penalty")), + ) ) - # --- Safeties (kickoff, punt, penalty) ---- - play_df["type.text"] = np.where( - ( - play_df["type.text"].isin(["Pass Reception", "Rush", "Rushing Touchdown"]) - & ((play_df["pass"] == True) | (play_df["rush"] == True)) - & (play_df["safety"] == True) - ), - "Safety", - play_df["type.text"], - ) - play_df["type.text"] = np.where((play_df.kickoff_safety == True), "Kickoff (Safety)", play_df["type.text"]) - play_df["type.text"] = np.where((play_df.punt_safety == True), "Punt (Safety)", play_df["type.text"]) - play_df["type.text"] = np.where((play_df.penalty_safety == True), "Penalty (Safety)", play_df["type.text"]) - play_df["type.text"] = np.where( - (play_df["type.text"] == "Extra Point Good") - & (play_df["text"].str.contains("Two-Point", case=False, flags=0, na=False, regex=True)), - "Two-Point Conversion Good", - play_df["type.text"], - ) - play_df["type.text"] = np.where( - (play_df["type.text"] == "Extra Point Missed") - & (play_df["text"].str.contains("Two-Point", case=False, flags=0, na=False, regex=True)), - "Two-Point Conversion Missed", - play_df["type.text"], - ) return play_df def __add_play_category_flags(self, play_df): - # -------------------------------------------------- - # --- Sacks ---- - play_df["sack"] = np.select( - [ - play_df["type.text"].isin(["Sack"]), - ( - play_df["type.text"].isin( - [ - "Fumble Recovery (Own)", - "Fumble Recovery (Own) Touchdown", - "Fumble Recovery (Opponent)", - "Fumble Recovery (Opponent) Touchdown", - ] + play_df = ( + play_df.with_columns( + # --- Sacks ----- + sack=pl.when(pl.col("type.text").is_in(["Sack"])) + .then(True) + .when( + ( + pl.col("type.text").is_in( + [ + "Fumble Recovery (Opponent)", + "Fumble Recovery (Opponent) Touchdown", + "Fumble Recovery (Own)", + "Fumble Recovery (Own) Touchdown", + ] + ) ) + .and_(pl.col("pass") == True) + .and_(pl.col("text").str.contains("(?i)sacked")) ) - & (play_df["pass"] == True) - & (play_df["text"].str.contains("sacked", case=False, flags=0, na=False, regex=True)), - ( - (play_df["type.text"].isin(["Safety"])) - & (play_df["text"].str.contains("sacked", case=False, flags=0, na=False, regex=True)) - ), - ], - [True, True, True], - default=False, - ) - # --- Interceptions ------ - play_df["int"] = play_df["type.text"].isin(["Interception Return", "Interception Return Touchdown"]) - play_df["int_td"] = play_df["type.text"].isin(["Interception Return Touchdown"]) - - # --- Pass Completions, Attempts and Targets ------- - play_df["completion"] = np.select( - [ - play_df["type.text"].isin(["Pass Reception", "Pass Completion", "Passing Touchdown"]), - ( - play_df["type.text"].isin( - [ - "Fumble Recovery (Own)", - "Fumble Recovery (Own) Touchdown", - "Fumble Recovery (Opponent)", - "Fumble Recovery (Opponent) Touchdown", - ] + .then(True) + .when((pl.col("type.text").is_in(["Safety"])).and_(pl.col("text").str.contains("(?i)sacked"))) + .then(True) + .otherwise(False), + # --- Interceptions ------ + int=pl.col("type.text").is_in(["Interception Return", "Interception Return Touchdown"]), + int_td=pl.col("type.text").is_in(["Interception Return Touchdown"]), + # --- Pass Completions, Attempts and Targets ------- + completion=pl.when( + pl.col("type.text").is_in(["Pass Reception", "Pass Completion", "Passing Touchdown"]) + ) + .then(True) + .when( + ( + pl.col("type.text").is_in( + [ + "Fumble Recovery (Opponent)", + "Fumble Recovery (Opponent) Touchdown", + "Fumble Recovery (Own)", + "Fumble Recovery (Own) Touchdown", + ] + ) ) - & (play_df["pass"] == True) - & ~(play_df["text"].str.contains("sacked", case=False, flags=0, na=False, regex=True)) - ), - ], - [True, True], - default=False, - ) - - play_df["pass_attempt"] = np.select( - [ - ( - play_df["type.text"].isin( - [ - "Pass Reception", - "Pass Completion", - "Passing Touchdown", - "Pass Incompletion", - ] + .and_(pl.col("pass") == True) + .and_(pl.col("text").str.contains("(?i)sacked") == False) + ) + .then(True) + .otherwise(False), + pass_attempt=pl.when( + pl.col("type.text").is_in( + ["Pass Reception", "Pass Completion", "Passing Touchdown", "Pass Incompletion"] ) - ), - ( - play_df["type.text"].isin( - [ - "Fumble Recovery (Own)", - "Fumble Recovery (Own) Touchdown", - "Fumble Recovery (Opponent)", - "Fumble Recovery (Opponent) Touchdown", - ] + ) + .then(True) + .when( + ( + pl.col("type.text").is_in( + [ + "Fumble Recovery (Opponent)", + "Fumble Recovery (Opponent) Touchdown", + "Fumble Recovery (Own)", + "Fumble Recovery (Own) Touchdown", + ] + ) ) - & (play_df["pass"] == True) - & ~(play_df["text"].str.contains("sacked", case=False, flags=0, na=False, regex=True)) - ), - ( - (play_df["pass"] == True) - & ~(play_df["text"].str.contains("sacked", case=False, flags=0, na=False, regex=True)) - ), - ], - [True, True, True], - default=False, - ) - - play_df["target"] = np.select( - [ - ( - play_df["type.text"].isin( - [ - "Pass Reception", - "Pass Completion", - "Passing Touchdown", - "Pass Incompletion", - ] + .and_(pl.col("pass") == True) + .and_(pl.col("text").str.contains("(?i)sacked") == False) + ) + .then(True) + .when((pl.col("pass") == True).and_(pl.col("text").str.contains("(?i)sacked") == False)) + .then(True) + .otherwise(False), + target=pl.when( + pl.col("type.text").is_in( + ["Pass Reception", "Pass Completion", "Passing Touchdown", "Pass Incompletion"] ) - ), - ( - play_df["type.text"].isin( - [ - "Fumble Recovery (Own)", - "Fumble Recovery (Own) Touchdown", - "Fumble Recovery (Opponent)", - "Fumble Recovery (Opponent) Touchdown", - ] + ) + .then(True) + .when( + ( + pl.col("type.text").is_in( + [ + "Fumble Recovery (Opponent)", + "Fumble Recovery (Opponent) Touchdown", + "Fumble Recovery (Own)", + "Fumble Recovery (Own) Touchdown", + ] + ) ) - & (play_df["pass"] == True) - & ~(play_df["text"].str.contains("sacked", case=False, flags=0, na=False, regex=True)) - ), - ( - (play_df["pass"] == True) - & ~(play_df["text"].str.contains("sacked", case=False, flags=0, na=False, regex=True)) - ), - ], - [True, True, True], - default=False, - ) - - play_df["pass_breakup"] = play_df["text"].str.contains( - "broken up by", case=False, flags=0, na=False, regex=True - ) - # --- Pass/Rush TDs ------ - play_df["pass_td"] = (play_df["type.text"] == "Passing Touchdown") | ( - (play_df["pass"] == True) & (play_df["td_play"] == True) - ) - play_df["rush_td"] = (play_df["type.text"] == "Rushing Touchdown") | ( - (play_df["rush"] == True) & (play_df["td_play"] == True) - ) - # --- Change of possession via turnover - play_df["turnover_vec"] = play_df["type.text"].isin(turnover_vec) - play_df["offense_score_play"] = play_df["type.text"].isin(offense_score_vec) - play_df["defense_score_play"] = play_df["type.text"].isin(defense_score_vec) - play_df["downs_turnover"] = np.where( - (play_df["type.text"].isin(normalplay)) - & (play_df["statYardage"] < play_df["start.distance"]) - & (play_df["start.down"] == 4) - & (play_df["penalty_1st_conv"] == False), - True, - False, - ) - # --- Touchdowns---- - play_df["scoring_play"] = play_df["type.text"].isin(scores_vec) - play_df["yds_punted"] = ( - play_df["text"].str.extract(r"(?<= punt for)[^,]+(\d+)", flags=re.IGNORECASE).astype(float) - ) - play_df["yds_punt_gained"] = np.where(play_df.punt == True, play_df["statYardage"], None) - play_df["fg_attempt"] = np.where( - (play_df["type.text"].str.contains("Field Goal", case=False, flags=0, na=False, regex=True)) - | (play_df["text"].str.contains("Field Goal", case=False, flags=0, na=False, regex=True)), - True, - False, - ) - play_df["fg_made"] = play_df["type.text"] == "Field Goal Good" - play_df["yds_fg"] = ( - play_df["text"] - .str.extract( - r"(\\d{0,2}\s?)Yd|(\\d{0,2}\s?)Yard FG|(\\d{0,2}\s?)Field|(\\d{0,2}\s?)Yard Field", - flags=re.IGNORECASE, - ) - .bfill(axis=1)[0] - .astype(float) - ) - # -------------------------------------------------- - play_df["start.yardsToEndzone"] = np.where( - play_df["fg_attempt"] == True, - play_df["yds_fg"] - 17, - play_df["start.yardsToEndzone"], - ) - play_df["start.yardsToEndzone"] = np.select( - [ - (play_df["start.yardsToEndzone"].isna()) - & (~play_df["type.text"].isin(kickoff_vec)) - & (play_df["start.pos_team.id"] == play_df["homeTeamId"]), - (play_df["start.yardsToEndzone"].isna()) - & (~play_df["type.text"].isin(kickoff_vec)) - & (play_df["start.pos_team.id"] == play_df["awayTeamId"]), - ], - [ - 100 - play_df["start.yardLine"].astype(float), - play_df["start.yardLine"].astype(float), - ], - default=play_df["start.yardsToEndzone"], - ) - play_df["pos_unit"] = np.select( - [ - play_df.punt == True, - play_df.kickoff_play == True, - play_df.fg_attempt == True, - play_df["type.text"] == "Defensive 2pt Conversion", - ], - ["Punt Offense", "Kickoff Return", "Field Goal Offense", "Offense"], - default="Offense", - ) - play_df["def_pos_unit"] = np.select( - [ - play_df.punt == True, - play_df.kickoff_play == True, - play_df.fg_attempt == True, - play_df["type.text"] == "Defensive 2pt Conversion", - ], - ["Punt Return", "Kickoff Defense", "Field Goal Defense", "Defense"], - default="Defense", - ) - # --- Lags/Leads play type ---- - play_df["lead_play_type"] = play_df["type.text"].shift(-1) - - play_df["sp"] = np.where( - (play_df.fg_attempt == True) | (play_df.punt == True) | (play_df.kickoff_play == True), - True, - False, - ) - play_df["play"] = np.where( - (~play_df["type.text"].isin(["Timeout", "End Period", "End of Half", "Penalty"])), - True, - False, - ) - play_df["scrimmage_play"] = np.where( - (play_df.sp == False) - & ( - ~play_df["type.text"].isin( - [ - "Timeout", - "Extra Point Good", - "Extra Point Missed", - "Two-Point Pass", - "Two-Point Rush", - "Penalty", - ] - ) - ), - True, - False, - ) - # -------------------------------------------------- - # --- Change of pos_team by lead('pos_team', 1)---- - play_df["change_of_pos_team"] = np.where( - (play_df.pos_team == play_df.lead_pos_team) - & (~(play_df.lead_play_type.isin(["End Period", "End of Half"])) | play_df.lead_play_type.isna() == True), - False, - np.where( - (play_df.pos_team == play_df.lead_pos_team2) - & ( - (play_df.lead_play_type.isin(["End Period", "End of Half"])) | play_df.lead_play_type.isna() - == True - ), - False, - True, - ), - ) - play_df["change_of_pos_team"] = np.where( - play_df["change_of_poss"].isna(), False, play_df["change_of_pos_team"] - ) - play_df["pos_score_diff_end"] = np.where( - ( - (play_df["type.text"].isin(end_change_vec)) - & (play_df["start.pos_team.id"] != play_df["end.pos_team.id"]) - ) - | (play_df.downs_turnover == True), - -1 * play_df.pos_score_diff, - play_df.pos_score_diff, - ) - play_df["pos_score_diff_end"] = np.select( - [ - (abs(play_df.pos_score_pts) >= 8) - & (play_df.scoring_play == False) - & (play_df.change_of_pos_team == False), - (abs(play_df.pos_score_pts) >= 8) - & (play_df.scoring_play == False) - & (play_df.change_of_pos_team == True), - ], - [play_df["pos_score_diff_start"], -1 * play_df["pos_score_diff_start"]], - default=play_df["pos_score_diff_end"], + .and_(pl.col("pass") == True) + .and_(pl.col("text").str.contains("(?i)sacked") == False) + ) + .then(True) + .when((pl.col("pass") == True).and_(pl.col("text").str.contains("(?i)sacked") == False)) + .then(True) + .otherwise(False), + pass_breakup=pl.when(pl.col("text").str.contains("(?i)broken up by")).then(True).otherwise(False), + # --- Pass/Rush TDs ------ + pass_td=pl.when(pl.col("type.text").is_in(["Passing Touchdown"])) + .then(True) + .when((pl.col("pass") == True).and_(pl.col("td_play") == True)) + .then(True) + .otherwise(False), + rush_td=pl.when(pl.col("type.text").is_in(["Rushing Touchdown"])) + .then(True) + .when((pl.col("rush") == True).and_(pl.col("td_play") == True)) + .then(True) + .otherwise(False), + # --- Change of possession via turnover + turnover_vec=pl.col("type.text").is_in(turnover_vec), + offense_score_play=pl.col("type.text").is_in(offense_score_vec), + defense_score_play=pl.col("type.text").is_in(defense_score_vec), + downs_turnover=pl.when( + (pl.col("type.text").is_in(normalplay)) + .and_(pl.col("statYardage") < pl.col("start.distance")) + .and_(pl.col("start.down") == 4) + .and_(pl.col("penalty_1st_conv") == False) + ) + .then(True) + .otherwise(False), + # --- Touchdowns ---- + scoring_play=pl.col("type.text").is_in(scores_vec), + yds_punted=pl.col("text").str.extract(r"(?i)(punt for \d+)").str.extract(r"(\d+)").cast(pl.Int32), + yds_punt_gained=pl.when(pl.col("punt") == True).then(pl.col("statYardage")).otherwise(None), + fg_attempt=pl.when( + (pl.col("type.text").str.contains(r"(?i)Field Goal")).or_( + pl.col("text").str.contains(r"(?i)Field Goal") + ) + ) + .then(True) + .otherwise(False), + fg_made=pl.col("type.text") == "Field Goal Good", + yds_fg=pl.col("text") + .str.extract( + r"(?i)(\d+)\s?Yd Field|(?i)(\d+)\s?YD FG|(?i)(\d+)\s?Yard FG|(?i)(\d+)\s?Field|(?i)(\d+)\s?Yard Field", + 0, + ) + .str.extract(r"(\d+)") + .cast(pl.Int32), + ) + .with_columns( + pl.when(pl.col("fg_attempt") == True) + .then(pl.col("yds_fg") - 17) + .otherwise(pl.col("start.yardsToEndzone")) + .alias("start.yardsToEndzone"), + ) + .with_columns( + pl.when( + (pl.col("start.yardsToEndzone").is_null()) + .and_(pl.col("type.text").is_in(kickoff_vec) == False) + .and_(pl.col("start.pos_team.id") == pl.col("homeTeamId")) + ) + .then(100 - pl.col("start.yardLine").cast(pl.Int32)) + .when( + (pl.col("start.yardsToEndzone").is_null()) + .and_(pl.col("type.text").is_in(kickoff_vec) == False) + .and_(pl.col("start.pos_team.id") == pl.col("awayTeamId")) + ) + .then(pl.col("start.yardLine").cast(pl.Int32)) + .otherwise(pl.col("start.yardsToEndzone")) + .alias("start.yardsToEndzone"), + ) + .with_columns( + pos_unit=pl.when(pl.col("punt") == True) + .then(pl.lit("Punt Offense")) + .when(pl.col("kickoff_play") == True) + .then(pl.lit("Kickoff Return")) + .when(pl.col("fg_attempt") == True) + .then(pl.lit("Field Goal Offense")) + .when(pl.col("type.text") == "Defensive 2pt Conversion") + .then(pl.lit("Offense")) + .otherwise(pl.lit("Offense")), + def_pos_unit=pl.when(pl.col("punt") == True) + .then(pl.lit("Punt Return")) + .when(pl.col("kickoff_play") == True) + .then(pl.lit("Kickoff Defense")) + .when(pl.col("fg_attempt") == True) + .then(pl.lit("Field Goal Defense")) + .when(pl.col("type.text") == "Defensive 2pt Conversion") + .then(pl.lit("Defense")) + .otherwise(pl.lit("Defense")), + # --- Lags/Leads play type ---- + lead_play_type=pl.col("type.text").shift(-1), + sp=pl.when( + (pl.col("fg_attempt") == True).or_(pl.col("punt") == True).or_(pl.col("kickoff_play") == True) + ) + .then(True) + .otherwise(False), + play=pl.when(pl.col("type.text").is_in(["Timeout", "End Period", "End of Half", "Penalty"]) == False) + .then(True) + .otherwise(False), + ) + .with_columns( + scrimmage_play=pl.when( + (pl.col("sp") == False).and_( + pl.col("type.text").is_in( + [ + "Timeout", + "Extra Point Good", + "Extra Point Missed", + "Two-Point Pass", + "Two-Point Rush", + "Penalty", + ] + ) + == False + ) + ) + .then(True) + .otherwise(False), + # --- Change of pos_team by lead('pos_team', 1)---- + change_of_pos_team=pl.when( + (pl.col("pos_team") == pl.col("lead_pos_team")).and_( + ((pl.col("lead_play_type").is_in(["End Period", "End of Half"])) == False).or_( + pl.col("lead_play_type").is_null() + ) + ) + ) + .then(False) + .when( + (pl.col("pos_team") == pl.col("lead_pos_team2")).and_( + (pl.col("lead_play_type").is_in(["End Period", "End of Half"])).or_( + pl.col("lead_play_type").is_null() + ) + ) + ) + .then(False) + .otherwise(True), + ) + .with_columns( + change_of_pos_team=pl.when(pl.col("change_of_poss").is_null()) + .then(False) + .otherwise(pl.col("change_of_pos_team")), + pos_score_diff_end=pl.when( + ( + (pl.col("type.text").is_in(end_change_vec)).and_( + pl.col("start.pos_team.id") != pl.col("end.pos_team.id") + ) + ).or_(pl.col("downs_turnover") == True) + ) + .then(-1 * pl.col("pos_score_diff")) + .otherwise(pl.col("pos_score_diff")), + ) + .with_columns( + pos_score_diff_end=pl.when( + (pl.col("pos_score_pts").abs() >= 8) + .and_(pl.col("scoring_play") == False) + .and_(pl.col("change_of_pos_team") == False) + ) + .then(pl.col("pos_score_diff_start")) + .when( + (pl.col("pos_score_pts").abs() >= 8) + .and_(pl.col("scoring_play") == False) + .and_(pl.col("change_of_pos_team") == True) + ) + .then(-1 * pl.col("pos_score_diff_start")) + .otherwise(pl.col("pos_score_diff_end")), + fumble_lost=pl.when((pl.col("fumble_vec") == True).and_(pl.col("change_of_pos_team") == True)) + .then(True) + .otherwise(False), + fumble_recovered=pl.when((pl.col("fumble_vec") == True).and_(pl.col("change_of_pos_team") == False)) + .then(True) + .otherwise(False), + ) ) - play_df["fumble_lost"] = np.where((play_df.fumble_vec == True) & (play_df.change_of_poss == True), True, False) - play_df["fumble_recovered"] = np.where( - (play_df.fumble_vec == True) & (play_df.change_of_poss == False), - True, - False, - ) return play_df def __add_yardage_cols(self, play_df): - play_df.insert(0, "yds_rushed", None) - play_df["yds_rushed"] = np.select( - [ - (play_df.rush == True) - & (play_df.text.str.contains("run for no gain", case=False, flags=0, na=False, regex=True)), - (play_df.rush == True) - & (play_df.text.str.contains("for no gain", case=False, flags=0, na=False, regex=True)), - (play_df.rush == True) - & (play_df.text.str.contains("run for a loss of", case=False, flags=0, na=False, regex=True)), - (play_df.rush == True) - & (play_df.text.str.contains("rush for a loss of", case=False, flags=0, na=False, regex=True)), - (play_df.rush == True) - & (play_df.text.str.contains("run for", case=False, flags=0, na=False, regex=True)), - (play_df.rush == True) - & (play_df.text.str.contains("rush for", case=False, flags=0, na=False, regex=True)), - (play_df.rush == True) - & (play_df.text.str.contains("Yd Run", case=False, flags=0, na=False, regex=True)), - (play_df.rush == True) - & (play_df.text.str.contains("Yd Rush", case=False, flags=0, na=False, regex=True)), - (play_df.rush == True) - & (play_df.text.str.contains("Yard Rush", case=False, flags=0, na=False, regex=True)), - (play_df.rush == True) - & (play_df.text.str.contains("rushed", case=False, flags=0, na=False, regex=True)) - & (~play_df.text.str.contains("touchdown", case=False, flags=0, na=False, regex=True)), - (play_df.rush == True) - & (play_df.text.str.contains("rushed", case=False, flags=0, na=False, regex=True)) - & (play_df.text.str.contains("touchdown", case=False, flags=0, na=False, regex=True)), - ], - [ - 0.0, - 0.0, - -1 - * play_df.text.str.extract(r"((?<=run for a loss of)[^,]+)", flags=re.IGNORECASE)[0] - .str.extract(r"(\d+)")[0] - .astype(float), - -1 - * play_df.text.str.extract(r"((?<=rush for a loss of)[^,]+)", flags=re.IGNORECASE)[0] - .str.extract(r"(\d+)")[0] - .astype(float), - play_df.text.str.extract(r"((?<=run for)[^,]+)", flags=re.IGNORECASE)[0] - .str.extract(r"(\d+)")[0] - .astype(float), - play_df.text.str.extract(r"((?<=rush for)[^,]+)", flags=re.IGNORECASE)[0] - .str.extract(r"(\d+)")[0] - .astype(float), - play_df.text.str.extract(r"(\d+) Yd Run", flags=re.IGNORECASE)[0].astype(float), - play_df.text.str.extract(r"(\d+) Yd Rush", flags=re.IGNORECASE)[0].astype(float), - play_df.text.str.extract(r"(\d+) Yard Rush", flags=re.IGNORECASE)[0].astype(float), - play_df.text.str.extract(r"for (\d+) yards", flags=re.IGNORECASE)[0].astype(float), - play_df.text.str.extract(r"for a (\d+) yard", flags=re.IGNORECASE)[0].astype(float), - ], - default=None, - ) - - play_df.insert(0, "yds_receiving", None) - play_df["yds_receiving"] = np.select( - [ - (play_df["pass"] == True) - & (play_df.text.str.contains("complete to", case=False)) - & (play_df.text.str.contains(r"for no gain", case=False)), - (play_df["pass"] == True) - & (play_df.text.str.contains("complete to", case=False)) - & (play_df.text.str.contains("for a loss", case=False)), - (play_df["pass"] == True) & (play_df.text.str.contains("complete to", case=False)), - (play_df["pass"] == True) & (play_df.text.str.contains("complete to", case=False)), - (play_df["pass"] == True) & (play_df.text.str.contains("incomplete", case=False)), - (play_df["pass"] == True) & (play_df["type.text"].str.contains("incompletion", case=False)), - (play_df["pass"] == True) & (play_df.text.str.contains("Yd pass", case=False)), - ], - [ - 0.0, - -1 - * play_df.text.str.extract(r"((?<=for a loss of)[^,]+)", flags=re.IGNORECASE)[0] - .str.extract(r"(\d+)")[0] - .astype(float), - play_df.text.str.extract(r"((?<=for)[^,]+)", flags=re.IGNORECASE)[0] - .str.extract(r"(\d+)")[0] - .astype(float), - play_df.text.str.extract(r"((?<=for)[^,]+)", flags=re.IGNORECASE)[0] - .str.extract(r"(\d+)")[0] - .astype(float), - 0.0, - 0.0, - play_df.text.str.extract(r"(\d+)\s+Yd\s+pass", flags=re.IGNORECASE)[0] - .str.extract(r"(\d+)")[0] - .astype(float), - ], - default=None, - ) - - play_df.insert(0, "yds_int_return", None) - play_df["yds_int_return"] = np.select( - [ - (play_df["pass"] == True) - & (play_df["int_td"] == True) - & (play_df.text.str.contains("Yd Interception Return", case=False)), - (play_df["pass"] == True) - & (play_df["int"] == True) - & (play_df.text.str.contains(r"for no gain", case=False)), - (play_df["pass"] == True) - & (play_df["int"] == True) - & (play_df.text.str.contains(r"for a loss of", case=False)), - (play_df["pass"] == True) - & (play_df["int"] == True) - & (play_df.text.str.contains(r"for a TD", case=False)), - (play_df["pass"] == True) - & (play_df["int"] == True) - & (play_df.text.str.contains(r"return for", case=False)), - (play_df["pass"] == True) & (play_df["int"] == True), - ], - [ - play_df.text.str.extract(r"(.+) Interception Return", flags=re.IGNORECASE)[0] - .str.extract(r"(\d+)")[0] - .astype(float), - 0.0, - -1 - * play_df.text.str.extract(r"((?<= for a loss of)[^,]+)", flags=re.IGNORECASE)[0] - .str.extract(r"(\d+)")[0] - .astype(float), - play_df.text.str.extract(r"((?<= return for)[^,]+)", flags=re.IGNORECASE)[0] - .str.extract(r"(\d+)")[0] - .astype(float), - play_df.text.str.extract(r"((?<= return for)[^,]+)", flags=re.IGNORECASE)[0] - .str.extract(r"(\d+)")[0] - .astype(float), - play_df.text.str.replace("for a 1st", "") - .str.extract(r"((?<=for)[^,]+)", flags=re.IGNORECASE)[0] - .str.extract(r"(\d+)")[0] - .astype(float), - ], - default=None, - ) - - # play_df['yds_fumble_return'] = None - # play_df['yds_penalty'] = None - - play_df.insert(0, "yds_kickoff", None) - play_df["yds_kickoff"] = np.where( - (play_df["kickoff_play"] == True), - play_df.text.str.extract(r"((?<= kickoff for)[^,]+)", flags=re.IGNORECASE)[0] - .str.extract(r"(\d+)")[0] - .astype(float), - play_df["yds_kickoff"], - ) - - play_df.insert(0, "yds_kickoff_return", None) - play_df["yds_kickoff_return"] = np.select( - [ - (play_df.kickoff_play == True) & (play_df.kickoff_tb == True) & (play_df.season > 2013), - (play_df.kickoff_play == True) & (play_df.kickoff_tb == True) & (play_df.season <= 2013), - (play_df.kickoff_play == True) - & (play_df.fumble_vec == False) - & (play_df.text.str.contains(r"for no gain|fair catch|fair caught", regex=True, case=False)), - (play_df.kickoff_play == True) - & (play_df.fumble_vec == False) - & (play_df.text.str.contains(r"out-of-bounds|out of bounds", regex=True, case=False)), - ((play_df.kickoff_downed == True) | (play_df.kickoff_fair_catch == True)), - (play_df.kickoff_play == True) & (play_df.text.str.contains(r"returned by", regex=True, case=False)), - (play_df.kickoff_play == True) & (play_df.text.str.contains(r"return for", regex=True, case=False)), - (play_df.kickoff_play == True), - ], - [ - 25, - 20, - 0, - 40, - 0, - play_df.text.str.extract(r"((?<= for)[^,]+)", flags=re.IGNORECASE)[0] - .str.extract(r"(\d+)")[0] - .astype(float), - play_df.text.str.extract(r"((?<= return for)[^,]+)", flags=re.IGNORECASE)[0] - .str.extract(r"(\d+)")[0] - .astype(float), - play_df.text.str.extract(r"((?<= returned for)[^,]+)", flags=re.IGNORECASE)[0] - .str.extract(r"(\d+)")[0] - .astype(float), - ], - default=play_df["yds_kickoff_return"], - ) - - play_df["yds_punted"] = None - play_df["yds_punted"] = np.select( - [ - (play_df.punt == True) & (play_df.punt_blocked == True), - (play_df.punt == True), - ], - [ - 0, - play_df.text.str.extract(r"((?<= punt for)[^,]+)", flags=re.IGNORECASE)[0] - .str.extract(r"(\d+)")[0] - .astype(float), - ], - default=play_df.yds_punted, - ) - - play_df.insert(0, "yds_punt_return", None) - play_df["yds_punt_return"] = np.select( - [ - (play_df.punt == True) & (play_df.punt_tb == 1), - (play_df.punt == True) - & ( - play_df["text"].str.contains( - r"fair catch|fair caught", - case=False, - flags=0, - na=False, - regex=True, - ) - ), - (play_df.punt == True) - & ((play_df.punt_downed == True) | (play_df.punt_oob == True) | (play_df.punt_fair_catch == True)), - (play_df.punt == True) - & (play_df["text"].str.contains(r"no return", case=False, flags=0, na=False, regex=True)), - (play_df.punt == True) - & (play_df["text"].str.contains(r"returned \d+ yards", case=False, flags=0, na=False, regex=True)), - (play_df.punt == True) & (play_df.punt_blocked == False), - (play_df.punt == True) & (play_df.punt_blocked == True), - ], - [ - 20, - 0, - 0, - 0, - play_df.text.str.extract(r"((?<= returned)[^,]+)", flags=re.IGNORECASE)[0] - .str.extract(r"(\d+)")[0] - .astype(float), - play_df.text.str.extract(r"((?<= returns for)[^,]+)", flags=re.IGNORECASE)[0] - .str.extract(r"(\d+)")[0] - .astype(float), - play_df.text.str.extract(r"((?<= return for)[^,]+)", flags=re.IGNORECASE)[0] - .str.extract(r"(\d+)")[0] - .astype(float), - ], - default=None, - ) - play_df.insert(0, "yds_fumble_return", None) - - play_df["yds_fumble_return"] = np.select( - [(play_df.fumble_vec == True) & (play_df.kickoff_play == False)], - [ - play_df.text.str.extract(r"((?<= return for)[^,]+)", flags=re.IGNORECASE)[0] - .str.extract(r"(\d+)")[0] - .astype(float) - ], - default=None, - ) - play_df.insert(0, "yds_sacked", None) - - play_df["yds_sacked"] = np.select( - [(play_df.sack == True)], - [ - -1 - * play_df.text.str.extract(r"((?<= sacked)[^,]+)", flags=re.IGNORECASE)[0] - .str.extract(r"(\d+)")[0] - .astype(float) - ], - default=None, - ) - - play_df["yds_penalty"] = np.select( - [(play_df.penalty_detail == 1)], - [ - -1 - * play_df.text.str.extract(r"((?<= sacked)[^,]+)", flags=re.IGNORECASE)[0] - .str.extract(r"(\d+)")[0] - .astype(float) - ], - default=None, - ) - - play_df["yds_penalty"] = np.select( - [ - play_df.penalty_detail.isin(["Penalty Declined", "Penalty Offset"]), - play_df.yds_penalty.notna(), - (play_df.penalty_detail.notna()) & (play_df.yds_penalty.isna()) & (play_df.rush == True), - (play_df.penalty_detail.notna()) & (play_df.yds_penalty.isna()) & (play_df.int == True), - (play_df.penalty_detail.notna()) - & (play_df.yds_penalty.isna()) - & (play_df["pass"] == 1) - & (play_df["sack"] == False) - & (play_df["type.text"] != "Pass Incompletion"), - (play_df.penalty_detail.notna()) - & (play_df.yds_penalty.isna()) - & (play_df["pass"] == 1) - & (play_df["sack"] == False) - & (play_df["type.text"] == "Pass Incompletion"), - (play_df.penalty_detail.notna()) - & (play_df.yds_penalty.isna()) - & (play_df["pass"] == 1) - & (play_df["sack"] == True), - (play_df["type.text"] == "Penalty"), - ], - [ - 0, - play_df.yds_penalty.astype(float), - play_df.statYardage.astype(float) - play_df.yds_rushed.astype(float), - play_df.statYardage.astype(float) - play_df.yds_int_return.astype(float), - play_df.statYardage.astype(float) - play_df.yds_receiving.astype(float), - play_df.statYardage.astype(float), - play_df.statYardage.astype(float) - play_df.yds_sacked.astype(float), - play_df.statYardage.astype(float), - ], - default=None, + play_df = play_df.with_columns( + yds_rushed=pl.when((pl.col("rush") == True).and_(pl.col("text").str.contains("(?i)run for no gain"))) + .then(0) + .when((pl.col("rush") == True).and_(pl.col("text").str.contains("(?i)for no gain"))) + .then(0) + .when((pl.col("rush") == True).and_(pl.col("text").str.contains("(?i)run for a loss of"))) + .then(-1 * pl.col("text").str.extract(r"(?i)run for a loss of (\d+)").cast(pl.Int32)) + .when((pl.col("rush") == True).and_(pl.col("text").str.contains("(?i)rush for a loss of"))) + .then(-1 * pl.col("text").str.extract(r"(?i)rush for a loss of (\d+)").cast(pl.Int32)) + .when((pl.col("rush") == True).and_(pl.col("text").str.contains("(?i)run for"))) + .then(pl.col("text").str.extract(r"(?i)run for (\d+)").cast(pl.Int32)) + .when((pl.col("rush") == True).and_(pl.col("text").str.contains("(?i)rush for"))) + .then(pl.col("text").str.extract(r"(?i)rush for (\d+)").cast(pl.Int32)) + .when((pl.col("rush") == True).and_(pl.col("text").str.contains("(?i)Yd Run"))) + .then(pl.col("text").str.extract(r"(?i)(\d+) Yd Run").cast(pl.Int32)) + .when((pl.col("rush") == True).and_(pl.col("text").str.contains("(?i)Yd Rush"))) + .then(pl.col("text").str.extract(r"(?i)(\d+) Yd Rush").cast(pl.Int32)) + .when((pl.col("rush") == True).and_(pl.col("text").str.contains("(?i)Yard Rush"))) + .then(pl.col("text").str.extract(r"(?i)(\d+) Yard Rush").cast(pl.Int32)) + .when( + (pl.col("rush") == True) + .and_(pl.col("text").str.contains("(?i)rushed")) + .and_(pl.col("text").str.contains("(?i)touchdown") == False) + ) + .then(pl.col("text").str.extract(r"(?i)for (\d+) yards").cast(pl.Int32)) + .when( + (pl.col("rush") == True) + .and_(pl.col("text").str.contains("(?i)rushed")) + .and_(pl.col("text").str.contains("(?i)touchdown") == True) + ) + .then(pl.col("text").str.extract(r"(?i)for a (\d+) yard").cast(pl.Int32)) + .otherwise(None), + yds_receiving=pl.when( + (pl.col("pass") == True) + .and_(pl.col("text").str.contains(r"(?i)complete to")) + .and_(pl.col("text").str.contains(r"(?i)for no gain")) + ) + .then(0) + .when( + (pl.col("pass") == True) + .and_(pl.col("text").str.contains(r"(?i)complete to")) + .and_(pl.col("text").str.contains(r"(?i)for a loss of")) + ) + .then(-1 * pl.col("text").str.extract(r"(?i)for a loss of (\d+)").cast(pl.Int32)) + .when((pl.col("pass") == True).and_(pl.col("text").str.contains(r"(?i)complete to"))) + .then(pl.col("text").str.extract(r"(?i)for (\d+)").cast(pl.Int32)) + .when( + (pl.col("pass") == True).and_( + pl.col("text").str.contains(r"(?i)incomplete|(?i) sacked|(?i)intercepted|(?i)pass defensed") + ) + ) + .then(0) + .when((pl.col("pass") == True).and_(pl.col("text").str.contains(r"(?i)incompletion"))) + .then(0) + .when((pl.col("pass") == True).and_(pl.col("text").str.contains(r"(?i)Yd pass"))) + .then(pl.col("text").str.extract(r"(?i)(\d+) Yd pass").cast(pl.Int32)) + .otherwise(None), + yds_int_return=pl.when( + (pl.col("pass") == True) + .and_(pl.col("int_td") == True) + .and_(pl.col("text").str.contains(r"(?i)Yd Interception Return")) + ) + .then(pl.col("text").str.extract(r"(?i)(.+)Yd Interception Return").str.extract(r"(\d+)").cast(pl.Int32)) + .when( + (pl.col("pass") == True) + .and_(pl.col("int") == True) + .and_(pl.col("text").str.contains(r"(?i)for no gain")) + ) + .then(0) + .when( + (pl.col("pass") == True) + .and_(pl.col("int") == True) + .and_(pl.col("text").str.contains(r"(?i)for a loss of")) + ) + .then(-1 * pl.col("text").str.extract(r"(?i)for a loss of (\d+)").cast(pl.Int32)) + .when( + (pl.col("pass") == True).and_(pl.col("int") == True).and_(pl.col("text").str.contains(r"(?i)for a TD")) + ) + .then(pl.col("text").str.extract(r"(?i)return for (.+)").str.extract(r"(\d+)").cast(pl.Int32)) + .when((pl.col("pass") == True).and_(pl.col("int") == True)) + .then( + pl.col("text") + .str.replace("for a 1st", "") + .str.extract(r"(?i)for (.+)") + .str.extract(r"(\d+)") + .cast(pl.Int32) + ) + .otherwise(None), + yds_kickoff=pl.when(pl.col("kickoff_play") == True) + .then(pl.col("text").str.extract(r"(?i)kickoff for (.+)").str.extract(r"(\d+)").cast(pl.Int32)) + .otherwise(None), + yds_kickoff_return=pl.when( + (pl.col("kickoff_play") == True).and_(pl.col("kickoff_tb") == True).and_(pl.col("season") > 2013) + ) + .then(25) + .when((pl.col("kickoff_play") == True).and_(pl.col("kickoff_tb") == True).and_(pl.col("season") <= 2013)) + .then(20) + .when( + (pl.col("kickoff_play") == True) + .and_(pl.col("fumble_vec") == False) + .and_(pl.col("text").str.contains(r"(?i)for no gain|fair catch|fair caught")) + ) + .then(0) + .when( + (pl.col("kickoff_play") == True) + .and_(pl.col("fumble_vec") == False) + .and_(pl.col("text").str.contains(r"(?i)out-of-bounds|out of bounds")) + ) + .then(40) + .when((pl.col("kickoff_downed") == True).or_(pl.col("kickoff_fair_catch") == True)) + .then(0) + .when((pl.col("kickoff_play") == True).and_(pl.col("text").str.contains(r"(?i)returned by"))) + .then(pl.col("text").str.extract(r"(?i)returned by (.+)").str.extract(r"(\d+)").cast(pl.Int32)) + .when((pl.col("kickoff_play") == True).and_(pl.col("text").str.contains(r"(?i)return for"))) + .then(pl.col("text").str.extract(r"(?i)return for (.+)").str.extract(r"(\d+)").cast(pl.Int32)) + .otherwise(None), + yds_punted=pl.when((pl.col("punt") == True).and_(pl.col("punt_blocked") == True)) + .then(0) + .when(pl.col("punt") == True) + .then(pl.col("text").str.extract(r"(?i)punt for (.+)").str.extract(r"(\d+)").cast(pl.Int32)) + .otherwise(None), + yds_punt_return=pl.when((pl.col("punt") == True).and_(pl.col("punt_tb") == True)) + .then(20) + .when((pl.col("punt") == True).and_(pl.col("text").str.contains(r"(?i)fair catch|fair caught"))) + .then(0) + .when( + (pl.col("punt") == True).and_( + (pl.col("punt_downed") == True) + .or_(pl.col("punt_oob") == True) + .or_(pl.col("punt_fair_catch") == True) + ) + ) + .then(0) + .when((pl.col("punt") == True).and_(pl.col("text").str.contains(r"(?i)no return|no gain"))) + .then(0) + .when((pl.col("punt") == True).and_(pl.col("text").str.contains(r"(?i)returned \d+ yards"))) + .then(pl.col("text").str.extract(r"(?i)returned (.+)").str.extract(r"(\d+)").cast(pl.Int32)) + .when((pl.col("punt") == True).and_(pl.col("punt_blocked") == False)) + .then(pl.col("text").str.extract(r"(?i)returns for (.+)").str.extract(r"(\d+)").cast(pl.Int32)) + .when((pl.col("punt") == True).and_(pl.col("punt_blocked") == True)) + .then(pl.col("text").str.extract(r"(?i)return for (.+)").str.extract(r"(\d+)").cast(pl.Int32)) + .otherwise(None), + yds_fumble_return=pl.when((pl.col("fumble_vec") == True).and_(pl.col("kickoff_play") == False)) + .then(pl.col("text").str.extract(r"(?i)return for (.+)").str.extract(r"(\d+)").cast(pl.Int32)) + .otherwise(None), + yds_sacked=pl.when(pl.col("sack") == True) + .then(-1 * pl.col("text").str.extract(r"(?i)sacked (.+)").str.extract(r"(\d+)").cast(pl.Int32)) + .otherwise(None), + ).with_columns( + yds_penalty=pl.when(pl.col("penalty_detail").is_in(["Penalty Declined", "Penalty Offset"])) + .then(0) + .when(pl.col("yds_penalty").is_not_null()) + .then(pl.col("yds_penalty")) + .when( + (pl.col("penalty_detail").is_not_null()) + .and_(pl.col("yds_penalty").is_null()) + .and_(pl.col("rush") == True) + ) + .then(pl.col("statYardage") - pl.col("yds_rushed")) + .when( + (pl.col("penalty_detail").is_not_null()) + .and_(pl.col("yds_penalty").is_null()) + .and_(pl.col("int") == True) + ) + .then(pl.col("statYardage") - pl.col("yds_int_return")) + .when( + (pl.col("penalty_detail").is_not_null()) + .and_(pl.col("yds_penalty").is_null()) + .and_(pl.col("pass") == True) + .and_(pl.col("sack") == False) + .and_(pl.col("type.text") != "Pass Incompletion") + ) + .then(pl.col("statYardage") - pl.col("yds_receiving")) + .when( + (pl.col("penalty_detail").is_not_null()) + .and_(pl.col("yds_penalty").is_null()) + .and_(pl.col("pass") == True) + .and_(pl.col("sack") == False) + .and_(pl.col("type.text") == "Pass Incompletion") + ) + .then(pl.col("statYardage")) + .when( + (pl.col("penalty_detail").is_not_null()) + .and_(pl.col("yds_penalty").is_null()) + .and_(pl.col("pass") == True) + .and_(pl.col("sack") == True) + ) + .then(pl.col("statYardage") - pl.col("yds_sacked")) + .when(pl.col("type.text") == "Penalty") + .then(pl.col("statYardage")) + .otherwise(None), ) return play_df def __add_player_cols(self, play_df): - play_df.insert(0, "rush_player", None) - play_df.insert(0, "receiver_player", None) - play_df.insert(0, "pass_player", None) - play_df.insert(0, "sack_players", None) - play_df.insert(0, "sack_player1", None) - play_df.insert(0, "sack_player2", None) - play_df.insert(0, "interception_player", None) - play_df.insert(0, "pass_breakup_player", None) - play_df.insert(0, "fg_kicker_player", None) - play_df.insert(0, "fg_return_player", None) - play_df.insert(0, "fg_block_player", None) - play_df.insert(0, "punter_player", None) - play_df.insert(0, "punt_return_player", None) - play_df.insert(0, "punt_block_player", None) - play_df.insert(0, "punt_block_return_player", None) - play_df.insert(0, "kickoff_player", None) - play_df.insert(0, "kickoff_return_player", None) - play_df.insert(0, "fumble_player", None) - play_df.insert(0, "fumble_forced_player", None) - play_df.insert(0, "fumble_recovered_player", None) - play_df.insert(0, "rush_player_name", None) - play_df.insert(0, "receiver_player_name", None) - play_df.insert(0, "passer_player_name", None) - play_df.insert(0, "sack_player_name", None) - play_df.insert(0, "sack_player_name2", None) - play_df.insert(0, "interception_player_name", None) - play_df.insert(0, "pass_breakup_player_name", None) - play_df.insert(0, "fg_kicker_player_name", None) - play_df.insert(0, "fg_return_player_name", None) - play_df.insert(0, "fg_block_player_name", None) - play_df.insert(0, "punter_player_name", None) - play_df.insert(0, "punt_return_player_name", None) - play_df.insert(0, "punt_block_player_name", None) - play_df.insert(0, "punt_block_return_player_name", None) - play_df.insert(0, "kickoff_player_name", None) - play_df.insert(0, "kickoff_return_player_name", None) - play_df.insert(0, "fumble_player_name", None) - play_df.insert(0, "fumble_forced_player_name", None) - play_df.insert(0, "fumble_recovered_player_name", None) - - ## Extract player names - # RB names - play_df["rush_player"] = np.where( - (play_df.rush == 1), - play_df.text.str.extract( - r"(.{0,25} )run |(.{0,25} )\d{0,2} Yd Run|(.{0,25} )rush |(.{0,25} )rushed " - ).bfill(axis=1)[0], - None, - ) - play_df["rush_player"] = play_df.rush_player.str.replace(r" run | \d+ Yd Run| rush ", "", regex=True) - play_df["rush_player"] = play_df.rush_player.str.replace(r" \((.+)\)", "", regex=True) - - # QB names - play_df["pass_player"] = np.where( - (play_df["pass"] == 1) & (play_df["type.text"] != "Passing Touchdown"), - play_df.text.str.extract( - r"pass from (.*?) \(|(.{0,30} )pass |(.+) sacked by|(.+) sacked for|(.{0,30} )incomplete " - ).bfill(axis=1)[0], - play_df["pass_player"], - ) - play_df["pass_player"] = play_df.pass_player.str.replace( - r"pass | sacked by| sacked for| incomplete", "", regex=True - ) - - play_df["pass_player"] = np.where( - (play_df["pass"] == 1) & (play_df["type.text"] == "Passing Touchdown"), - play_df.text.str.extract("pass from(.+)")[0], - play_df["pass_player"], - ) - play_df["pass_player"] = play_df.pass_player.str.replace("pass from", "", regex=True) - play_df["pass_player"] = play_df.pass_player.str.replace(r"\(.+\)", "", regex=True) - play_df["pass_player"] = play_df.pass_player.str.replace(r" \,", "", regex=True) - - play_df["pass_player"] = np.where( - (play_df["type.text"] == "Passing Touchdown") & play_df.pass_player.isna(), - play_df.text.str.extract("(.+)pass(.+)? complete to")[0], - play_df["pass_player"], - ) - play_df["pass_player"] = play_df.pass_player.str.replace(r" pass complete to(.+)", "", regex=True) - play_df["pass_player"] = play_df.pass_player.str.replace(" pass complete to", "", regex=True) - - play_df["pass_player"] = np.where( - (play_df["type.text"] == "Passing Touchdown") & play_df.pass_player.isna(), - play_df.text.str.extract("(.+)pass,to")[0], - play_df["pass_player"], - ) - - play_df["pass_player"] = play_df.pass_player.str.replace(r" pass,to(.+)", "", regex=True) - play_df["pass_player"] = play_df.pass_player.str.replace(r" pass,to", "", regex=True) - play_df["pass_player"] = play_df.pass_player.str.replace(r" \((.+)\)", "", regex=True) - play_df["pass_player"] = np.where( - (play_df["pass"] == 1) & ((play_df.pass_player.str.strip().str.len == 0) | play_df.pass_player.isna()), - "TEAM", - play_df.pass_player, - ) - - play_df["receiver_player"] = np.where( - (play_df["pass"] == 1) & ~play_df.text.str.contains("sacked", case=False, flags=0, na=False, regex=True), - play_df.text.str.extract("to (.+)")[0], - None, - ) - - play_df["receiver_player"] = np.where( - play_df.text.str.contains("Yd pass", case=False, flags=0, na=False, regex=True), - play_df.text.str.extract("(.{0,25} )\\d{0,2} Yd pass", flags=re.IGNORECASE)[0], - play_df["receiver_player"], - ) - - play_df["receiver_player"] = np.where( - play_df.text.str.contains("Yd TD pass", case=False), - play_df.text.str.extract("(.{0,25} )\\d{0,2} Yd TD pass", flags=re.IGNORECASE)[0], - play_df["receiver_player"], - ) - - play_df["receiver_player"] = np.where( - (play_df["type.text"] == "Sack") - | (play_df["type.text"] == "Interception Return") - | (play_df["type.text"] == "Interception Return Touchdown") - | ( - play_df["type.text"].isin( - [ - "Fumble Recovery (Opponent) Touchdown", - "Fumble Recovery (Opponent)", - ] - ) - & play_df.text.str.contains("sacked", case=False) - ), - None, - play_df["receiver_player"], - ) - - play_df.receiver_player = play_df.receiver_player.str.replace("to ", "", case=False, regex=True) - play_df.receiver_player = play_df.receiver_player.str.replace("\\,.+", "", case=False, regex=True) - play_df.receiver_player = play_df.receiver_player.str.replace("for (.+)", "", case=False, regex=True) - play_df.receiver_player = play_df.receiver_player.str.replace(r" (\d{1,2})", "", case=False, regex=True) - play_df.receiver_player = play_df.receiver_player.str.replace(" Yd pass", "", case=False, regex=True) - play_df.receiver_player = play_df.receiver_player.str.replace(" Yd TD pass", "", case=False, regex=True) - play_df.receiver_player = play_df.receiver_player.str.replace("pass complete to", "", case=False, regex=True) - play_df.receiver_player = play_df.receiver_player.str.replace("penalty", "", case=False, regex=True) - play_df.receiver_player = play_df.receiver_player.str.replace(' "', "", case=False, regex=True) - play_df.receiver_player = np.where( - ~(play_df.receiver_player.str.contains("III", na=False)), - play_df.receiver_player.str.replace("[A-Z]{3,}", "", case=True, regex=True), - play_df.receiver_player, - ) - - play_df.receiver_player = play_df.receiver_player.str.replace(" &", "", case=True, regex=True) - play_df.receiver_player = play_df.receiver_player.str.replace("A&M", "", case=True, regex=False) - play_df.receiver_player = play_df.receiver_player.str.replace(" ST", "", case=True, regex=True) - play_df.receiver_player = play_df.receiver_player.str.replace(" GA", "", case=True, regex=True) - play_df.receiver_player = play_df.receiver_player.str.replace(" UL", "", case=True, regex=True) - play_df.receiver_player = play_df.receiver_player.str.replace(" FL", "", case=True, regex=True) - play_df.receiver_player = play_df.receiver_player.str.replace(" OH", "", case=True, regex=True) - play_df.receiver_player = play_df.receiver_player.str.replace(" NC", "", case=True, regex=True) - play_df.receiver_player = play_df.receiver_player.str.replace(' "', "", case=True, regex=True) - play_df.receiver_player = play_df.receiver_player.str.replace(" \\u00c9", "", case=True, regex=True) - play_df.receiver_player = play_df.receiver_player.str.replace(" fumbled,", "", case=False, regex=True) - play_df.receiver_player = play_df.receiver_player.str.replace("the (.+)", "", case=False, regex=True) - play_df.receiver_player = play_df.receiver_player.str.replace("pass incomplete to", "", case=False, regex=True) - play_df.receiver_player = play_df.receiver_player.str.replace( - "(.+)pass incomplete to", "", case=False, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace( - "(.+)pass incomplete", "", case=False, regex=True - ) - play_df.receiver_player = play_df.receiver_player.str.replace("pass incomplete", "", case=False, regex=True) - play_df.receiver_player = play_df.receiver_player.str.replace(r" \((.+)\)", "", regex=True) - - play_df["sack_players"] = np.where( - (play_df["sack"] == True) | (play_df["fumble_vec"] == True) & (play_df["pass"] == True), - play_df.text.str.extract("sacked by(.+)", flags=re.IGNORECASE)[0], - play_df.sack_players, - ) - - play_df["sack_players"] = play_df["sack_players"].str.replace("for (.+)", "", case=True, regex=True) - play_df["sack_players"] = play_df["sack_players"].str.replace("(.+) by ", "", case=True, regex=True) - play_df["sack_players"] = play_df["sack_players"].str.replace(" at the (.+)", "", case=True, regex=True) - play_df["sack_player1"] = play_df["sack_players"].str.replace("and (.+)", "", case=True, regex=True) - play_df["sack_player2"] = np.where( - play_df["sack_players"].str.contains("and (.+)"), - play_df["sack_players"].str.replace("(.+) and", "", case=True, regex=True), - None, - ) - - play_df["interception_player"] = np.where( - (play_df["type.text"] == "Interception Return") - | (play_df["type.text"] == "Interception Return Touchdown") & play_df["pass"] - == True, - play_df.text.str.extract("intercepted (.+)", flags=re.IGNORECASE)[0], - play_df.interception_player, - ) - - play_df["interception_player"] = np.where( - play_df.text.str.contains("Yd Interception Return", case=True, regex=True), - play_df.text.str.extract( - "(.{0,25} )\\d{0,2} Yd Interception Return|(.{0,25} )\\d{0,2} yd interception return", - flags=re.IGNORECASE, - ).bfill(axis=1)[0], - play_df.interception_player, - ) - play_df["interception_player"] = play_df["interception_player"].str.replace( - "return (.+)", "", case=True, regex=True - ) - play_df["interception_player"] = play_df["interception_player"].str.replace( - "(.+) intercepted", "", case=True, regex=True - ) - play_df["interception_player"] = play_df["interception_player"].str.replace( - "intercepted", "", case=True, regex=True - ) - play_df["interception_player"] = play_df["interception_player"].str.replace( - "Yd Interception Return", "", regex=True - ) - play_df["interception_player"] = play_df["interception_player"].str.replace( - "for a 1st down", "", case=True, regex=True - ) - play_df["interception_player"] = play_df["interception_player"].str.replace( - "(\\d{1,2})", "", case=True, regex=True - ) - play_df["interception_player"] = play_df["interception_player"].str.replace( - "for a TD", "", case=True, regex=True - ) - play_df["interception_player"] = play_df["interception_player"].str.replace( - "at the (.+)", "", case=True, regex=True - ) - play_df["interception_player"] = play_df["interception_player"].str.replace(" by ", "", case=True, regex=True) - - play_df["pass_breakup_player"] = np.where( - play_df["pass"] == True, - play_df.text.str.extract("broken up by (.+)"), - play_df.pass_breakup_player, - ) - play_df["pass_breakup_player"] = play_df["pass_breakup_player"].str.replace( - "(.+) broken up by", "", case=True, regex=True - ) - play_df["pass_breakup_player"] = play_df["pass_breakup_player"].str.replace( - "broken up by", "", case=True, regex=True - ) - play_df["pass_breakup_player"] = play_df["pass_breakup_player"].str.replace( - "Penalty(.+)", "", case=True, regex=True - ) - play_df["pass_breakup_player"] = play_df["pass_breakup_player"].str.replace( - "SOUTH FLORIDA", "", case=True, regex=True - ) - play_df["pass_breakup_player"] = play_df["pass_breakup_player"].str.replace( - "WEST VIRGINIA", "", case=True, regex=True - ) - play_df["pass_breakup_player"] = play_df["pass_breakup_player"].str.replace( - "MISSISSIPPI ST", "", case=True, regex=True - ) - play_df["pass_breakup_player"] = play_df["pass_breakup_player"].str.replace( - "CAMPBELL", "", case=True, regex=True - ) - play_df["pass_breakup_player"] = play_df["pass_breakup_player"].str.replace( - "COASTL CAROLINA", "", case=True, regex=True - ) - - play_df["punter_player"] = np.where( - play_df["type.text"].str.contains("Punt", regex=True), - play_df.text.str.extract(r"(.{0,30}) punt|Punt by (.{0,30})", flags=re.IGNORECASE).bfill(axis=1)[0], - play_df.punter_player, - ) - play_df["punter_player"] = play_df["punter_player"].str.replace(" punt", "", case=False, regex=True) - play_df["punter_player"] = play_df["punter_player"].str.replace(r" for(.+)", "", case=False, regex=True) - play_df["punter_player"] = play_df["punter_player"].str.replace("Punt by ", "", case=False, regex=True) - play_df["punter_player"] = play_df["punter_player"].str.replace(r"\((.+)\)", "", case=False, regex=True) - play_df["punter_player"] = play_df["punter_player"].str.replace(r" returned \d+", "", case=False, regex=True) - play_df["punter_player"] = play_df["punter_player"].str.replace(" returned", "", case=False, regex=True) - play_df["punter_player"] = play_df["punter_player"].str.replace(" no return", "", case=False, regex=True) - - play_df["punt_return_player"] = np.where( - play_df["type.text"].str.contains("Punt", regex=True), - play_df.text.str.extract( - r", (.{0,25}) returns|fair catch by (.{0,25})|, returned by (.{0,25})|yards by (.{0,30})| return by (.{0,25})", - flags=re.IGNORECASE, - ).bfill(axis=1)[0], - play_df.punt_return_player, - ) - play_df["punt_return_player"] = play_df["punt_return_player"].str.replace(", ", "", case=False, regex=True) - play_df["punt_return_player"] = play_df["punt_return_player"].str.replace( - " returns", "", case=False, regex=True - ) - play_df["punt_return_player"] = play_df["punt_return_player"].str.replace( - " returned", "", case=False, regex=True - ) - play_df["punt_return_player"] = play_df["punt_return_player"].str.replace( - " return", "", case=False, regex=True - ) - play_df["punt_return_player"] = play_df["punt_return_player"].str.replace( - "fair catch by", "", case=False, regex=True - ) - play_df["punt_return_player"] = play_df["punt_return_player"].str.replace( - r" at (.+)", "", case=False, regex=True - ) - play_df["punt_return_player"] = play_df["punt_return_player"].str.replace( - r" for (.+)", "", case=False, regex=True - ) - play_df["punt_return_player"] = play_df["punt_return_player"].str.replace( - r"(.+) by ", "", case=False, regex=True - ) - play_df["punt_return_player"] = play_df["punt_return_player"].str.replace( - r" to (.+)", "", case=False, regex=True - ) - play_df["punt_return_player"] = play_df["punt_return_player"].str.replace( - r"\((.+)\)", "", case=False, regex=True - ) - - play_df["punt_block_player"] = np.where( - play_df["type.text"].str.contains("Punt", case=True, regex=True), - play_df.text.str.extract("punt blocked by (.{0,25})| blocked by(.+)", flags=re.IGNORECASE).bfill(axis=1)[ - 0 - ], - play_df.punt_block_player, - ) - play_df["punt_block_player"] = play_df["punt_block_player"].str.replace( - r"punt blocked by |for a(.+)", "", case=True, regex=True - ) - play_df["punt_block_player"] = play_df["punt_block_player"].str.replace( - r"blocked by(.+)", "", case=True, regex=True - ) - play_df["punt_block_player"] = play_df["punt_block_player"].str.replace( - r"blocked(.+)", "", case=True, regex=True - ) - play_df["punt_block_player"] = play_df["punt_block_player"].str.replace(r" for(.+)", "", case=True, regex=True) - play_df["punt_block_player"] = play_df["punt_block_player"].str.replace(r",(.+)", "", case=True, regex=True) - play_df["punt_block_player"] = play_df["punt_block_player"].str.replace( - r"punt blocked by |for a(.+)", "", case=True, regex=True - ) - - play_df["punt_block_player"] = np.where( - play_df["type.text"].str.contains("yd return of blocked punt"), - play_df.text.str.extract("(.+) yd return of blocked"), - play_df.punt_block_player, - ) - play_df["punt_block_player"] = play_df["punt_block_player"].str.replace("blocked|Blocked", "", regex=True) - play_df["punt_block_player"] = play_df["punt_block_player"].str.replace(r"\\d+", "", regex=True) - play_df["punt_block_player"] = play_df["punt_block_player"].str.replace("yd return of", "", regex=True) - - play_df["punt_block_return_player"] = np.where( - (play_df["type.text"].str.contains("Punt", case=False, flags=0, na=False, regex=True)) - & ( - play_df.text.str.contains("blocked", case=False, flags=0, na=False, regex=True) - & play_df.text.str.contains("return", case=False, flags=0, na=False, regex=True) - ), - play_df.text.str.extract("(.+) return"), - play_df.punt_block_return_player, - ) - play_df["punt_block_return_player"] = play_df["punt_block_return_player"].str.replace( - "(.+)blocked by {punt_block_player}", "" - ) - play_df["punt_block_return_player"] = play_df["punt_block_return_player"].str.replace( - "blocked by {punt_block_player}", "" - ) - play_df["punt_block_return_player"] = play_df["punt_block_return_player"].str.replace( - "return(.+)", "", regex=True - ) - play_df["punt_block_return_player"] = play_df["punt_block_return_player"].str.replace("return", "", regex=True) - play_df["punt_block_return_player"] = play_df["punt_block_return_player"].str.replace( - "(.+)blocked by", "", regex=True - ) - play_df["punt_block_return_player"] = play_df["punt_block_return_player"].str.replace( - "for a TD(.+)|for a SAFETY(.+)", "", regex=True - ) - play_df["punt_block_return_player"] = play_df["punt_block_return_player"].str.replace( - "blocked by", "", regex=True - ) - play_df["punt_block_return_player"] = play_df["punt_block_return_player"].str.replace(", ", "", regex=True) - - play_df["kickoff_player"] = np.where( - play_df["type.text"].str.contains("Kickoff"), - play_df.text.str.extract("(.{0,25}) kickoff|(.{0,25}) on-side").bfill(axis=1)[0], - play_df.kickoff_player, - ) - play_df["kickoff_player"] = play_df["kickoff_player"].str.replace(" on-side| kickoff", "", regex=True) - - play_df["kickoff_return_player"] = np.where( - play_df["type.text"].str.contains("ickoff"), - play_df.text.str.extract( - ", (.{0,25}) return|, (.{0,25}) fumble|returned by (.{0,25})|touchback by (.{0,25})" - ).bfill(axis=1)[0], - play_df.kickoff_return_player, - ) - play_df["kickoff_return_player"] = play_df["kickoff_return_player"].str.replace( - ", ", "", case=False, regex=True - ) - play_df["kickoff_return_player"] = play_df["kickoff_return_player"].str.replace( - " return| fumble| returned by| for |touchback by ", - "", - case=False, - regex=True, - ) - play_df["kickoff_return_player"] = play_df["kickoff_return_player"].str.replace( - r"\((.+)\)(.+)", "", case=False, regex=True - ) - - play_df["fg_kicker_player"] = np.where( - play_df["type.text"].str.contains("Field Goal"), - play_df.text.str.extract( - "(.{0,25} )\\d{0,2} yd field goal| (.{0,25} )\\d{0,2} yd fg|(.{0,25} )\\d{0,2} yard field goal" - ).bfill(axis=1)[0], - play_df.fg_kicker_player, - ) - play_df["fg_kicker_player"] = play_df["fg_kicker_player"].str.replace( - " Yd Field Goal|Yd FG |yd FG| yd FG", "", case=False, regex=True - ) - play_df["fg_kicker_player"] = play_df["fg_kicker_player"].str.replace("(\\d{1,2})", "", case=False, regex=True) - - play_df["fg_block_player"] = np.where( - play_df["type.text"].str.contains("Field Goal"), - play_df.text.str.extract("blocked by (.{0,25})"), - play_df.fg_block_player, - ) - play_df["fg_block_player"] = play_df["fg_block_player"].str.replace(",(.+)", "", case=False, regex=True) - play_df["fg_block_player"] = play_df["fg_block_player"].str.replace("blocked by ", "", case=False, regex=True) - play_df["fg_block_player"] = play_df["fg_block_player"].str.replace(" (.)+", "", case=False, regex=True) - - play_df["fg_return_player"] = np.where( - (play_df["type.text"].str.contains("Field Goal")) - & (play_df["type.text"].str.contains("blocked by|missed")) - & (play_df["type.text"].str.contains("return")), - play_df.text.str.extract(" (.+)"), - play_df.fg_return_player, - ) - - play_df["fg_return_player"] = play_df["fg_return_player"].str.replace(",(.+)", "", case=False, regex=True) - play_df["fg_return_player"] = play_df["fg_return_player"].str.replace("return ", "", case=False, regex=True) - play_df["fg_return_player"] = play_df["fg_return_player"].str.replace("returned ", "", case=False, regex=True) - play_df["fg_return_player"] = play_df["fg_return_player"].str.replace(" for (.+)", "", case=False, regex=True) - play_df["fg_return_player"] = play_df["fg_return_player"].str.replace(" for (.+)", "", case=False, regex=True) - - play_df["fg_return_player"] = np.where( - play_df["type.text"].isin(["Missed Field Goal Return", "Missed Field Goal Return Touchdown"]), - play_df.text.str.extract("(.+)return"), - play_df.fg_return_player, - ) - play_df["fg_return_player"] = play_df["fg_return_player"].str.replace(" return", "", case=False, regex=True) - play_df["fg_return_player"] = play_df["fg_return_player"].str.replace("(.+),", "", case=False, regex=True) - - play_df["fumble_player"] = np.where( - play_df["text"].str.contains("fumble", case=False, flags=0, na=False, regex=True), - play_df["text"].str.extract("(.{0,25} )fumble"), - play_df.fumble_player, - ) - play_df["fumble_player"] = play_df["fumble_player"].str.replace(" fumble(.+)", "", case=False, regex=True) - play_df["fumble_player"] = play_df["fumble_player"].str.replace("fumble", "", case=False, regex=True) - play_df["fumble_player"] = play_df["fumble_player"].str.replace(" yds", "", case=False, regex=True) - play_df["fumble_player"] = play_df["fumble_player"].str.replace(" yd", "", case=False, regex=True) - play_df["fumble_player"] = play_df["fumble_player"].str.replace("yardline", "", case=False, regex=True) - play_df["fumble_player"] = play_df["fumble_player"].str.replace( - " yards| yard|for a TD|or a safety", "", case=False, regex=True - ) - play_df["fumble_player"] = play_df["fumble_player"].str.replace(" for ", "", case=False, regex=True) - play_df["fumble_player"] = play_df["fumble_player"].str.replace(" a safety", "", case=False, regex=True) - play_df["fumble_player"] = play_df["fumble_player"].str.replace("r no gain", "", case=False, regex=True) - play_df["fumble_player"] = play_df["fumble_player"].str.replace("(.+)(\\d{1,2})", "", case=False, regex=True) - play_df["fumble_player"] = play_df["fumble_player"].str.replace("(\\d{1,2})", "", case=False, regex=True) - play_df["fumble_player"] = play_df["fumble_player"].str.replace(", ", "", case=False, regex=True) - play_df["fumble_player"] = np.where(play_df["type.text"] == "Penalty", None, play_df.fumble_player) - - play_df["fumble_forced_player"] = np.where( - (play_df.text.str.contains("fumble", case=False, flags=0, na=False, regex=True)) - & (play_df.text.str.contains("forced by", case=False, flags=0, na=False, regex=True)), - play_df.text.str.extract("forced by(.{0,25})"), - play_df.fumble_forced_player, - ) - - play_df["fumble_forced_player"] = play_df["fumble_forced_player"].str.replace( - "(.+)forced by", "", case=False, regex=True - ) - play_df["fumble_forced_player"] = play_df["fumble_forced_player"].str.replace( - "forced by", "", case=False, regex=True - ) - play_df["fumble_forced_player"] = play_df["fumble_forced_player"].str.replace( - ", recove(.+)", "", case=False, regex=True - ) - play_df["fumble_forced_player"] = play_df["fumble_forced_player"].str.replace( - ", re(.+)", "", case=False, regex=True - ) - play_df["fumble_forced_player"] = play_df["fumble_forced_player"].str.replace( - ", fo(.+)", "", case=False, regex=True - ) - play_df["fumble_forced_player"] = play_df["fumble_forced_player"].str.replace( - ", r", "", case=False, regex=True - ) - play_df["fumble_forced_player"] = play_df["fumble_forced_player"].str.replace(", ", "", case=False, regex=True) - play_df["fumble_forced_player"] = np.where( - play_df["type.text"] == "Penalty", None, play_df.fumble_forced_player - ) - - play_df["fumble_recovered_player"] = np.where( - (play_df.text.str.contains("fumble", case=False, flags=0, na=False, regex=True)) - & (play_df.text.str.contains("recovered by", case=False, flags=0, na=False, regex=True)), - play_df.text.str.extract("recovered by(.{0,30})"), - play_df.fumble_recovered_player, - ) - - play_df["fumble_recovered_player"] = play_df["fumble_recovered_player"].str.replace( - "for a 1ST down", "", case=False, regex=True - ) - play_df["fumble_recovered_player"] = play_df["fumble_recovered_player"].str.replace( - "for a 1st down", "", case=False, regex=True - ) - play_df["fumble_recovered_player"] = play_df["fumble_recovered_player"].str.replace( - "(.+)recovered", "", case=False, regex=True - ) - play_df["fumble_recovered_player"] = play_df["fumble_recovered_player"].str.replace( - "(.+) by", "", case=False, regex=True - ) - play_df["fumble_recovered_player"] = play_df["fumble_recovered_player"].str.replace( - ", recove(.+)", "", case=False, regex=True - ) - play_df["fumble_recovered_player"] = play_df["fumble_recovered_player"].str.replace( - ", re(.+)", "", case=False, regex=True - ) - play_df["fumble_recovered_player"] = play_df["fumble_recovered_player"].str.replace( - "a 1st down", "", case=False, regex=True - ) - play_df["fumble_recovered_player"] = play_df["fumble_recovered_player"].str.replace( - " a 1st down", "", case=False, regex=True - ) - play_df["fumble_recovered_player"] = play_df["fumble_recovered_player"].str.replace( - ", for(.+)", "", case=False, regex=True - ) - play_df["fumble_recovered_player"] = play_df["fumble_recovered_player"].str.replace( - " for a", "", case=False, regex=True - ) - play_df["fumble_recovered_player"] = play_df["fumble_recovered_player"].str.replace( - " fo", "", case=False, regex=True - ) - play_df["fumble_recovered_player"] = play_df["fumble_recovered_player"].str.replace( - " , r", "", case=False, regex=True - ) - play_df["fumble_recovered_player"] = play_df["fumble_recovered_player"].str.replace( - ", r", "", case=False, regex=True - ) - play_df["fumble_recovered_player"] = play_df["fumble_recovered_player"].str.replace( - " (.+)", "", case=False, regex=True - ) - play_df["fumble_recovered_player"] = play_df["fumble_recovered_player"].str.replace( - " ,", "", case=False, regex=True - ) - play_df["fumble_recovered_player"] = play_df["fumble_recovered_player"].str.replace( - "penalty(.+)", "", case=False, regex=True - ) - play_df["fumble_recovered_player"] = play_df["fumble_recovered_player"].str.replace( - "for a 1ST down", "", case=False, regex=True - ) - play_df["fumble_recovered_player"] = np.where( - play_df["type.text"] == "Penalty", None, play_df.fumble_recovered_player - ) - - ## Extract player names - play_df["passer_player_name"] = play_df["pass_player"].str.strip() - play_df["rusher_player_name"] = play_df["rush_player"].str.strip() - play_df["receiver_player_name"] = play_df["receiver_player"].str.strip() - play_df["sack_player_name"] = play_df["sack_player1"].str.strip() - play_df["sack_player_name2"] = play_df["sack_player2"].str.strip() - play_df["pass_breakup_player_name"] = play_df["pass_breakup_player"].str.strip() - play_df["interception_player_name"] = play_df["interception_player"].str.strip() - play_df["fg_kicker_player_name"] = play_df["fg_kicker_player"].str.strip() - play_df["fg_block_player_name"] = play_df["fg_block_player"].str.strip() - play_df["fg_return_player_name"] = play_df["fg_return_player"].str.strip() - play_df["kickoff_player_name"] = play_df["kickoff_player"].str.strip() - play_df["kickoff_return_player_name"] = play_df["kickoff_return_player"].str.strip() - play_df["punter_player_name"] = play_df["punter_player"].str.strip() - play_df["punt_block_player_name"] = play_df["punt_block_player"].str.strip() - play_df["punt_return_player_name"] = play_df["punt_return_player"].str.strip() - play_df["punt_block_return_player_name"] = play_df["punt_block_return_player"].str.strip() - play_df["fumble_player_name"] = play_df["fumble_player"].str.strip() - play_df["fumble_forced_player_name"] = play_df["fumble_forced_player"].str.strip() - play_df["fumble_recovered_player_name"] = play_df["fumble_recovered_player"].str.strip() - - play_df.drop( - [ - "rush_player", - "receiver_player", - "pass_player", - "sack_player1", - "sack_player2", - "pass_breakup_player", - "interception_player", - "punter_player", - "fg_kicker_player", - "fg_block_player", - "fg_return_player", - "kickoff_player", - "kickoff_return_player", - "punt_return_player", - "punt_block_player", - "punt_block_return_player", - "fumble_player", - "fumble_forced_player", - "fumble_recovered_player", - ], - axis=1, - inplace=True, + play_df = ( + play_df.with_columns( + # --- RB Names ----- + rush_player=pl.when(pl.col("rush") == True) + .then( + pl.col("text") + .str.extract( + r"(?i)(.{0,25} )run |(?i)(.{0,25} )\d{0,2} Yd Run|(?i)(.{0,25} )rush |(?i)(.{0,25} )rushed " + ) + .str.replace(r"(?i) run |(?i) \d+ Yd Run|(?i) rush ", "") + .str.replace(r" \((.+)\)", "") + ) + .otherwise(None), + # --- QB Names ----- + pass_player=pl.when( + (pl.col("pass") == True) + .and_(pl.col("sack_vec") == False) + .and_(pl.col("type.text") != "Passing Touchdown") + ) + .then( + pl.col("text") + .str.extract( + r"(?i)(.{0,30} )pass |(?i)(.{0,30} )sacked by|(?i)(.{0,30} )sacked for|(?i)(.{0,30} )incomplete|(?i)pass from (.{0,30} ) \( " + ) + .str.replace(r"(?i)pass |(?i) sacked by|(?i) sacked for|(?i) incomplete", "") + ) + .when( + (pl.col("pass") == True) + .and_(pl.col("sack_vec") == True) + .and_(pl.col("type.text") != "Passing Touchdown") + ) + .then( + pl.col("text") + .str.extract(r"(?i)(.{0,30} )sacked by|(?i)(.{0,30} )sacked for") + .str.replace(r"(?i)pass |(?i) sacked by|(?i) sacked for|(?i) incomplete", "") + ) + .when((pl.col("pass") == True).and_(pl.col("type.text") == "Passing Touchdown")) + .then( + pl.col("text") + .str.extract(r"(?i)pass from(.+)") + .str.replace(r"pass from", "") + # .str.replace(r"\((.+)\)", "") + .str.replace(r" \,", "") + ) + .otherwise(None), + ) + .with_columns( + pass_player=pl.when((pl.col("type.text") == "Passing Touchdown").and_(pl.col("pass_player").is_null())) + .then( + pl.col("text") + .str.extract(r"(.+)pass(.+)? complete to") + .str.replace(r" pass complete to(.+)", "") + .str.replace(r" pass complete to", "") + ) + .otherwise(pl.col("pass_player")) + ) + .with_columns( + pass_player=pl.when((pl.col("type.text") == "Passing Touchdown").and_(pl.col("pass_player").is_null())) + .then( + pl.col("text") + .str.extract(r"(.+)pass,to") + .str.replace(r" pass,to(.+)", "") + .str.replace(r" pass,to", "") + .str.replace(r" \((.+)\)", "") + ) + .otherwise(pl.col("pass_player")) + ) + .with_columns( + pass_player=pl.when( + (pl.col("pass") == True).and_( + ((pl.col("pass_player").str.strip().str.n_chars() == 0).or_(pl.col("pass_player").is_null())) + ) + ) + .then(pl.lit("TEAM")) + .otherwise(pl.col("pass_player")), + # --- WR Names ----- + receiver_player=pl.when( + (pl.col("pass") == True).and_(pl.col("text").str.contains(r"(?i)sacked") == False) + ) + .then(pl.col("text").str.extract(r"(?i)to (.+)")) + .when(pl.col("text").str.contains(r"(?i)Yd pass")) + .then(pl.col("text").str.extract(r"(?i)(.{0,25} )\d{0,2} Yd pass")) + .when(pl.col("text").str.contains(r"(?i)Yd TD pass")) + .then(pl.col("text").str.extract(r"(?i)(.{0,25} )\d{0,2} Yd TD pass")) + .otherwise(None), + ) + .with_columns( + receiver_player=pl.when( + (pl.col("type.text") == "Sack") + .or_(pl.col("type.text") == "Interception Return") + .or_(pl.col("type.text") == "Interception Return Touchdown") + .or_( + ( + pl.col("type.text").is_in( + ["Fumble Recovery (Opponent) Touchdown", "Fumble Recovery (Opponent)"] + ) + ).and_(pl.col("text").str.contains(r"(?i)sacked")) + ) + ) + .then(None) + .otherwise( + pl.col("receiver_player") + .str.replace(r"to ", "") + .str.replace(r"(?i)\\,.+", "") + .str.replace(r"(?i)for (.+)", "") + .str.replace(r"(?i) (\d{1,2})", "") + .str.replace(r"(?i) Yd pass", "") + .str.replace(r"(?i) Yd TD pass", "") + .str.replace(r"(?i)pass complete to", "") + .str.replace(r"(?i)penalty", "") + .str.replace(r'(?i) "', "") + ) + ) + .with_columns( + receiver_player=pl.when(pl.col("receiver_player").str.contains(r"(?i)III") == True) + .then(pl.col("receiver_player").str.replace(r"(?i)[A-Z]{3,}", "")) + .otherwise(pl.col("receiver_player")) + ) + .with_columns( + receiver_player=pl.col("receiver_player") + .str.replace(r"(?i) &", "") + .str.replace(r"(?i)A&M", "") + .str.replace(r"(?i) ST", "") + .str.replace(r"(?i) GA", "") + .str.replace(r"(?i) UL", "") + .str.replace(r"(?i) FL", "") + .str.replace(r"(?i) OH", "") + .str.replace(r"(?i) NC", "") + .str.replace(r'(?i) "', "") + .str.replace(r"(?i) \\u00c9", "") + .str.replace(r"(?i) fumbled,", "") + .str.replace(r"(?i)the (.+)", "") + .str.replace(r"(?i)pass incomplete to", "") + .str.replace(r"(?i)(.+)pass incomplete", "") + .str.replace(r"(?i)pass incomplete", "") + .str.replace(r"(?i) \((.+)\)", ""), + # --- Sack Names ----- + sack_players=pl.when( + (pl.col("sack") == True).or_((pl.col("fumble_vec") == True).and_(pl.col("pass") == True)) + ) + .then( + pl.col("text") + .str.extract(r"(?i)sacked by(.+)") + .str.replace(r"for (.+)", "") + .str.replace(r"(.+) by ", "") + .str.replace(r" at the (.+)", "") + ) + .otherwise(None), + ) + .with_columns( + sack_player1=pl.col("sack_players").str.replace(r"and (.+)", ""), + sack_player2=pl.when(pl.col("sack_players").str.contains(r"and (.+)")) + .then(pl.col("sack_players").str.replace(r"(.+) and", "")) + .otherwise(None), + # --- Interception Names ----- + interception_player=pl.when( + ( + (pl.col("type.text") == "Interception Return").or_( + pl.col("type.text") == "Interception Return Touchdown" + ) + ).and_(pl.col("pass") == True) + ) + .then(pl.col("text").str.extract(r"(?i)intercepted (.+)")) + .when(pl.col("text").str.contains(r"Yd Interception Return")) + .then( + pl.col("text") + .str.extract( + r"(?i)(.{0,25} )\\d{0,2} Yd Interception Return|(?i)(.{0,25} )\\d{0,2} yd interception return" + ) + .str.replace(r"return (.+)", "") + .str.replace(r"(.+) intercepted", "") + .str.replace(r"intercepted", "") + .str.replace(r"Yd Interception Return", "") + .str.replace(r"for a 1st down", "") + .str.replace(r"(\\d{1,2})", "") + .str.replace(r"for a TD", "") + .str.replace(r"at the (.+)", "") + .str.replace(r" by ", "") + ) + .otherwise(None), + # --- Pass Breakup Players ---- + pass_breakup_player=pl.when(pl.col("pass") == True) + .then( + pl.col("text") + .str.extract(r"(?i)broken up by (.+)") + .str.replace(r"(.+) broken up by", "") + .str.replace(r"broken up by", "") + .str.replace(r"Penalty(.+)", "") + .str.replace(r"SOUTH FLORIDA", "") + .str.replace(r"WEST VIRGINIA", "") + .str.replace(r"MISSISSIPPI ST", "") + .str.replace(r"CAMPBELL", "") + .str.replace(r"COASTL CAROLINA", "") + ) + .otherwise(None), + # --- Punter Names ---- + punter_player=pl.when(pl.col("type.text").str.contains("Punt")) + .then( + pl.col("text") + .str.extract(r"(?i)(.{0,30}) punt|(?i)Punt by (.{0,30})") + .str.replace(r"(?i) punt", "") + .str.replace(r"(?i) for(.+)", "") + .str.replace(r"(?i)Punt by ", "") + .str.replace(r"(?i)\((.+)\)", "") + .str.replace(r"(?i) returned \d+", "") + .str.replace(r"(?i) returned", "") + .str.replace(r"(?i) no return", "") + ) + .otherwise(None), + # --- Punt Returner Names ---- + punt_return_player=pl.when(pl.col("type.text").str.contains("Punt")) + .then( + pl.col("text") + .str.extract( + r"(?i), (.{0,25}) returns|(?i)fair catch by (.{0,25})|(?i), returned by (.{0,25})|(?i)yards by (.{0,30})|(?i) return by (.{0,25})" + ) + .str.replace(r"(?i), ", "") + .str.replace(r"(?i) returns", "") + .str.replace(r"(?i) returned", "") + .str.replace(r"(?i) return", "") + .str.replace(r"(?i)fair catch by", "") + .str.replace(r"(?i) at (.+)", "") + .str.replace(r"(?i) for (.+)", "") + .str.replace(r"(?i)(.+) by ", "") + .str.replace(r"(?i) to (.+)", "") + .str.replace(r"(?i)\((.+)\)", "") + ) + .otherwise(None), + # --- Punt Blocker Names ---- + punt_block_player=pl.when(pl.col("type.text").str.contains("Punt")) + .then( + pl.col("text") + .str.extract(r"(?i)punt blocked by (.{0,25})|(?i)blocked by(.+)") + .str.replace(r"punt blocked by |for a(.+)", "") + .str.replace(r"blocked by(.+)", "") + .str.replace(r"blocked(.+)", "") + .str.replace(r" for(.+)", "") + .str.replace(r",(.+)", "") + .str.replace(r"punt blocked by |for a(.+)", "") + ) + .otherwise(None), + ) + .with_columns( + punt_block_player=pl.when((pl.col("type.text").str.contains(r"(?i)yd return of blocked punt"))) + .then( + pl.col("text") + .str.extract(r"(?i)(.+) yd return of blocked") + .str.replace(r"(?i)blocked|(?i)Blocked", "") + .str.replace(r"(?i)\\d+", "") + .str.replace(r"(?i)yd return of", "") + ) + .otherwise(pl.col("punt_block_player")), + # --- Punt Block Returner Names ---- + punt_block_return_player=pl.when( + (pl.col("type.text").str.contains(r"Punt")) + .and_(pl.col("text").str.contains(r"(?i)blocked")) + .and_(pl.col("text").str.contains(r"(?i)return")) + ) + .then(pl.col("text").str.extract(r"(?i)(.+) return")) + .otherwise(None), + ) + .with_columns( + punt_block_return_player=pl.struct(["punt_block_player", "punt_block_return_player"]).apply( + lambda cols: cols["punt_block_return_player"] + .str.replace(r"(?i)(.+)blocked by", "") + .str.replace(pl.format(r"(?i)blocked by {}", cols["punt_block_player"]), ""), + return_dtype=pl.Utf8, + ) + ) + .with_columns( + punt_block_return_player=pl.col("punt_block_return_player") + .str.replace(r"(?i)return(.+)", "") + .str.replace(r"(?i)return", "") + .str.replace(r"for a TD(.+)|for a SAFETY(.+)", "") + .str.replace(r"(?i)blocked by ", "") + .str.replace(r", ", ""), + # --- Kickoff Names ---- + kickoff_player=pl.when(pl.col("type.text").str.contains(r"(?i)kickoff")) + .then( + pl.col("text") + .str.extract(r"(?i)(.{0,25}) kickoff|(.{0,25}) on-side") + .str.replace(r"(?i) on-side| kickoff", "") + ) + .otherwise(None), + # --- Kickoff Returner Names ---- + kickoff_return_player=pl.when(pl.col("type.text").str.contains(r"(?i)ickoff")) + .then( + pl.col("text") + .str.extract( + r"(?i), (.{0,25}) return|(?i), (.{0,25}) fumble|(?i)returned by (.{0,25})|(?i)touchback by (.{0,25})" + ) + .str.replace(r", ", "") + .str.replace(r"(?i) return|(?i) fumble|(?i) returned by|(?i) for |(?i)touchback by ", "") + .str.replace(r"\((.+)\)(.+)", "") + ) + .otherwise(None), + # --- Field Goal Kicker Names ---- + fg_kicker_player=pl.when(pl.col("type.text").str.contains(r"(?i)Field Goal")) + .then( + pl.col("text") + .str.extract( + r"(?i)(.{0,25} )\\d{0,2} yd field goal|(?i)(.{0,25} )\\d{0,2} yd fg|(?i)(.{0,25} )\\d{0,2} yard field goal" + ) + .str.replace(r"(?i) Yd Field Goal|(?i)Yd FG |(?i)yd FG|(?i) yd FG", "") + .str.replace(r"(\\d{1,2})", "") + ) + .otherwise(None), + # --- Field Goal Blocker Names ---- + fg_block_player=pl.when(pl.col("type.text").str.contains(r"(?i)Field Goal")) + .then( + pl.col("text") + .str.extract(r"(?i)blocked by (.{0,25})") + .str.replace(r",(.+)", "") + .str.replace(r"blocked by ", "") + .str.replace(r" (.)+", "") + ) + .otherwise(None), + # --- Field Goal Returner Names ---- + fg_return_player=pl.when( + (pl.col("type.text").str.contains(r"(?i)Field Goal")) + .and_(pl.col("text").str.contains(r"(?i)blocked by|missed")) + .and_(pl.col("text").str.contains(r"(?i)return")) + ) + .then( + pl.col("text") + .str.extract(r"(?i) (.+)") + .str.replace(r"(?i),(.+)", "") + .str.replace(r"(?i)return ", "") + .str.replace(r"(?i)returned ", "") + .str.replace(r"(?i) for (.+)", "") + .str.replace(r"(?i) for (.+)", "") + ) + .otherwise(None), + ) + .with_columns( + fg_return_player=pl.when( + (pl.col("type.text").is_in(["Missed Field Goal Return", "Missed Field Goal Return Touchdown"])) + ) + .then( + pl.col("text") + .str.extract(r"(?i)(.+)return") + .str.replace(r"(?i) return", "") + .str.replace(r"(?i)(.+),", "") + ) + .otherwise(pl.col("fg_return_player")), + # --- Fumble Recovery Names ---- + fumble_player=pl.when(pl.col("text").str.contains(r"(?i)fumble")) + .then( + pl.col("text") + .str.extract(r"(?i)(.{0,25} )fumble|(?i)(.{0,25} )fumble") + .str.replace(r"(?i) fumble(.+)", "") + .str.replace(r"(?i)fumble", "") + .str.replace(r"(?i) yds", "") + .str.replace(r"(?i) yd", "") + .str.replace(r"(?i)yardline", "") + .str.replace(r"(?i) yards|(?i) yard|(?i)for a TD|(?i)or a safety", "") + .str.replace(r"(?i) for ", "") + .str.replace(r"(?i) a safety", "") + .str.replace(r"(?i)r no gain", "") + .str.replace(r"(?i)(.+)(\\d{1,2})", "") + .str.replace(r"(?i)(\\d{1,2})", "") + .str.replace(r", ", "") + ) + .otherwise(None), + ) + .with_columns( + fumble_player=pl.when(pl.col("type.text") == "Penalty").then(None).otherwise(pl.col("fumble_player")), + # --- Forced Fumble Names ---- + fumble_forced_player=pl.when( + (pl.col("text").str.contains(r"(?i)fumble")).and_(pl.col("text").str.contains(r"(?i)forced by")) + ) + .then( + pl.col("text") + .str.extract(r"(?i)forced by(.{0,25})") + .str.replace(r"(?i)(.+)forced by", "") + .str.replace(r"(?i)forced by", "") + .str.replace(r"(?i), recove(.+)", "") + .str.replace(r"(?i), re(.+)", "") + .str.replace(r"(?i), fo(.+)", "") + .str.replace(r"(?i), r", "") + .str.replace(r"(?i), ", "") + ) + .otherwise(None), + ) + .with_columns( + fumble_forced_player=pl.when(pl.col("type.text") == "Penalty") + .then(None) + .otherwise(pl.col("fumble_forced_player")), + # --- Fumble Recovered Names ---- + fumble_recovered_player=pl.when( + (pl.col("text").str.contains(r"(?i)fumble")).and_(pl.col("text").str.contains(r"(?i)recovered by")) + ) + .then( + pl.col("text") + .str.extract(r"(?i)recovered by(.{0,30})") + .str.replace(r"(?i)for a 1ST down", "") + .str.replace(r"(?i)for a 1st down", "") + .str.replace(r"(?i)(.+)recovered", "") + .str.replace(r"(?i)(.+) by", "") + .str.replace(r"(?i), recove(.+)", "") + .str.replace(r"(?i), re(.+)", "") + .str.replace(r"(?i)a 1st down", "") + .str.replace(r"(?i) a 1st down", "") + .str.replace(r"(?i), for(.+)", "") + .str.replace(r"(?i) for a", "") + .str.replace(r"(?i) fo", "") + .str.replace(r"(?i) , r", "") + .str.replace(r"(?i), r", "") + .str.replace(r"(?i) (.+)", "") + .str.replace(r"(?i) ,", "") + .str.replace(r"(?i)penalty(.+)", "") + .str.replace(r"(?i)for a 1ST down", "") + ) + .otherwise(None), + ) + .with_columns( + fumble_recovered_player=pl.when(pl.col("type.text") == "Penalty") + .then(None) + .otherwise(pl.col("fumble_recovered_player")), + ) + .with_columns( + ## Extract player names + passer_player_name=pl.col("pass_player").str.strip(), + rusher_player_name=pl.col("rush_player").str.strip(), + receiver_player_name=pl.col("receiver_player").str.strip(), + sack_player_name=pl.col("sack_player1").str.strip(), + sack_player_name2=pl.col("sack_player2").str.strip(), + pass_breakup_player_name=pl.col("pass_breakup_player").str.strip(), + interception_player_name=pl.col("interception_player").str.strip(), + fg_kicker_player_name=pl.col("fg_kicker_player").str.strip(), + fg_block_player_name=pl.col("fg_block_player").str.strip(), + fg_return_player_name=pl.col("fg_return_player").str.strip(), + kickoff_player_name=pl.col("kickoff_player").str.strip(), + kickoff_return_player_name=pl.col("kickoff_return_player").str.strip(), + punter_player_name=pl.col("punter_player").str.strip(), + punt_block_player_name=pl.col("punt_block_player").str.strip(), + punt_return_player_name=pl.col("punt_return_player").str.strip(), + punt_block_return_player_name=pl.col("punt_block_return_player").str.strip(), + fumble_player_name=pl.col("fumble_player").str.strip(), + fumble_forced_player_name=pl.col("fumble_forced_player").str.strip(), + fumble_recovered_player_name=pl.col("fumble_recovered_player").str.strip(), + ) + .drop( + [ + "rush_player", + "receiver_player", + "pass_player", + "sack_player1", + "sack_player2", + "pass_breakup_player", + "interception_player", + "punter_player", + "fg_kicker_player", + "fg_block_player", + "fg_return_player", + "kickoff_player", + "kickoff_return_player", + "punt_return_player", + "punt_block_player", + "punt_block_return_player", + "fumble_player", + "fumble_forced_player", + "fumble_recovered_player", + ] + ) ) return play_df def __after_cols(self, play_df): - play_df["new_down"] = np.select( - [ - (play_df["type.text"] == "Timeout"), - # 8 cases with three T/F penalty flags - # 4 cases in 1 - (play_df["type.text"].isin(penalty)) & (play_df["penalty_1st_conv"] == True), - # offsetting penalties, no penalties declined, no 1st down by penalty (1 case) - (play_df["type.text"].isin(penalty)) - & (play_df["penalty_1st_conv"] == False) - & (play_df["penalty_offset"] == True) - & (play_df["penalty_declined"] == False), - # offsetting penalties, penalty declined true, no 1st down by penalty - # seems like it would be a regular play at that point (1 case, split in three) - (play_df["type.text"].isin(penalty)) - & (play_df["penalty_1st_conv"] == False) - & (play_df["penalty_offset"] == True) - & (play_df["penalty_declined"] == True) - & (play_df["statYardage"] < play_df["start.distance"]) - & (play_df["start.down"] <= 3), - (play_df["type.text"].isin(penalty)) - & (play_df["penalty_1st_conv"] == False) - & (play_df["penalty_offset"] == True) - & (play_df["penalty_declined"] == True) - & (play_df["statYardage"] < play_df["start.distance"]) - & (play_df["start.down"] == 4), - (play_df["type.text"].isin(penalty)) - & (play_df["penalty_1st_conv"] == False) - & (play_df["penalty_offset"] == True) - & (play_df["penalty_declined"] == True) - & (play_df["statYardage"] >= play_df["start.distance"]), - # only penalty declined true, same logic as prior (1 case, split in three) - (play_df["type.text"].isin(penalty)) - & (play_df["penalty_1st_conv"] == False) - & (play_df["penalty_offset"] == False) - & (play_df["penalty_declined"] == True) - & (play_df["statYardage"] < play_df["start.distance"]) - & (play_df["start.down"] <= 3), - (play_df["type.text"].isin(penalty)) - & (play_df["penalty_1st_conv"] == False) - & (play_df["penalty_offset"] == False) - & (play_df["penalty_declined"] == True) - & (play_df["statYardage"] < play_df["start.distance"]) - & (play_df["start.down"] == 4), - (play_df["type.text"].isin(penalty)) - & (play_df["penalty_1st_conv"] == False) - & (play_df["penalty_offset"] == False) - & (play_df["penalty_declined"] == True) - & (play_df["statYardage"] >= play_df["start.distance"]), - ], - [ - play_df["start.down"], - 1, - play_df["start.down"], - play_df["start.down"] + 1, - 1, - 1, - play_df["start.down"] + 1, - 1, - 1, - ], - default=play_df["start.down"], - ) - play_df["new_distance"] = np.select( - [ - (play_df["type.text"] == "Timeout"), - # 8 cases with three T/F penalty flags - # 4 cases in 1 - (play_df["type.text"].isin(penalty)) & (play_df["penalty_1st_conv"] == True), - # offsetting penalties, no penalties declined, no 1st down by penalty (1 case) - (play_df["type.text"].isin(penalty)) - & (play_df["penalty_1st_conv"] == False) - & (play_df["penalty_offset"] == True) - & (play_df["penalty_declined"] == False), - # offsetting penalties, penalty declined true, no 1st down by penalty - # seems like it would be a regular play at that point (1 case, split in three) - (play_df["type.text"].isin(penalty)) - & (play_df["penalty_1st_conv"] == False) - & (play_df["penalty_offset"] == True) - & (play_df["penalty_declined"] == True) - & (play_df["statYardage"] < play_df["start.distance"]) - & (play_df["start.down"] <= 3), - (play_df["type.text"].isin(penalty)) - & (play_df["penalty_1st_conv"] == False) - & (play_df["penalty_offset"] == True) - & (play_df["penalty_declined"] == True) - & (play_df["statYardage"] < play_df["start.distance"]) - & (play_df["start.down"] == 4), - (play_df["type.text"].isin(penalty)) - & (play_df["penalty_1st_conv"] == False) - & (play_df["penalty_offset"] == True) - & (play_df["penalty_declined"] == True) - & (play_df["statYardage"] >= play_df["start.distance"]), - # only penalty declined true, same logic as prior (1 case, split in three) - (play_df["type.text"].isin(penalty)) - & (play_df["penalty_1st_conv"] == False) - & (play_df["penalty_offset"] == False) - & (play_df["penalty_declined"] == True) - & (play_df["statYardage"] < play_df["start.distance"]) - & (play_df["start.down"] <= 3), - (play_df["type.text"].isin(penalty)) - & (play_df["penalty_1st_conv"] == False) - & (play_df["penalty_offset"] == False) - & (play_df["penalty_declined"] == True) - & (play_df["statYardage"] < play_df["start.distance"]) - & (play_df["start.down"] == 4), - (play_df["type.text"].isin(penalty)) - & (play_df["penalty_1st_conv"] == False) - & (play_df["penalty_offset"] == False) - & (play_df["penalty_declined"] == True) - & (play_df["statYardage"] >= play_df["start.distance"]), - ], - [ - play_df["start.distance"], - 10, - play_df["start.distance"], - play_df["start.distance"] - play_df["statYardage"], - 10, - 10, - play_df["start.distance"] - play_df["statYardage"], - 10, - 10, - ], - default=play_df["start.distance"], - ) - - play_df["middle_8"] = np.where( - (play_df["start.adj_TimeSecsRem"] >= 1560) & (play_df["start.adj_TimeSecsRem"] <= 2040), - True, - False, - ) - play_df["rz_play"] = np.where(play_df["start.yardsToEndzone"] <= 20, True, False) - play_df["scoring_opp"] = np.where(play_df["start.yardsToEndzone"] <= 40, True, False) - play_df["stuffed_run"] = np.where((play_df.rush == True) & (play_df.yds_rushed <= 0), True, False) - play_df["stopped_run"] = np.where((play_df.rush == True) & (play_df.yds_rushed <= 2), True, False) - play_df["opportunity_run"] = np.where((play_df.rush == True) & (play_df.yds_rushed >= 4), True, False) - play_df["highlight_run"] = np.where((play_df.rush == True) & (play_df.yds_rushed >= 8), True, False) - - play_df["adj_rush_yardage"] = np.select( - [ - (play_df.rush == True) & (play_df.yds_rushed > 10), - (play_df.rush == True) & (play_df.yds_rushed <= 10), - ], - [10, play_df.yds_rushed], - default=None, - ) - play_df["line_yards"] = np.select( - [ - (play_df.rush == 1) & (play_df.yds_rushed < 0), - (play_df.rush == 1) & (play_df.yds_rushed >= 0) & (play_df.yds_rushed <= 4), - (play_df.rush == 1) & (play_df.yds_rushed >= 5) & (play_df.yds_rushed <= 10), - (play_df.rush == 1) & (play_df.yds_rushed >= 11), - ], - [ - 1.2 * play_df.adj_rush_yardage, - play_df.adj_rush_yardage, - 0.5 * play_df.adj_rush_yardage, - 0.0, - ], - default=None, - ) - - play_df["second_level_yards"] = np.select( - [(play_df.rush == 1) & (play_df.yds_rushed >= 5), (play_df.rush == 1)], - [(0.5 * (play_df.adj_rush_yardage - 5)), 0], - default=None, - ) - - play_df["open_field_yards"] = np.select( - [(play_df.rush == 1) & (play_df.yds_rushed > 10), (play_df.rush == 1)], - [(play_df.yds_rushed - play_df.adj_rush_yardage), 0], - default=None, - ) - - play_df["highlight_yards"] = play_df["second_level_yards"] + play_df["open_field_yards"] - - play_df["opp_highlight_yards"] = np.select( - [ - (play_df.opportunity_run == True), - (play_df.opportunity_run == False) & (play_df.rush == 1), - ], - [play_df["highlight_yards"], 0.0], - default=None, - ) - - play_df["short_rush_success"] = np.where( - (play_df["start.distance"] < 2) - & (play_df.rush == True) - & (play_df.statYardage >= play_df["start.distance"]), - True, - False, - ) - play_df["short_rush_attempt"] = np.where((play_df["start.distance"] < 2) & (play_df.rush == True), True, False) - play_df["power_rush_success"] = np.where( - (play_df["start.distance"] < 2) - & (play_df["start.down"].isin([3, 4])) - & (play_df.rush == True) - & (play_df.statYardage >= play_df["start.distance"]), - True, - False, - ) - play_df["power_rush_attempt"] = np.where( - (play_df["start.distance"] < 2) & (play_df["start.down"].isin([3, 4])) & (play_df.rush == True), - True, - False, - ) - play_df["early_down"] = np.where( - ((play_df.down_1 == True) | (play_df.down_2 == True)) & (play_df.scrimmage_play == True), - True, - False, - ) - play_df["late_down"] = np.where( - (play_df.early_down == False) & (play_df.scrimmage_play == True), - True, - False, - ) - play_df["early_down_pass"] = np.where((play_df["pass"] == 1) & (play_df.early_down == True), True, False) - play_df["early_down_rush"] = np.where((play_df["rush"] == 1) & (play_df.early_down == True), True, False) - play_df["late_down_pass"] = np.where((play_df["pass"] == 1) & (play_df.late_down == True), True, False) - play_df["late_down_rush"] = np.where((play_df["rush"] == 1) & (play_df.late_down == True), True, False) - play_df["standard_down"] = np.select( - [ - (play_df.scrimmage_play == True) & (play_df.down_1 == True), - (play_df.scrimmage_play == True) & (play_df.down_2 == True) & (play_df["start.distance"] < 8), - (play_df.scrimmage_play == True) & (play_df.down_3 == True) & (play_df["start.distance"] < 5), - (play_df.scrimmage_play == True) & (play_df.down_4 == True) & (play_df["start.distance"] < 5), - ], - [True, True, True, True], - default=False, - ) - play_df["passing_down"] = np.select( - [ - (play_df.scrimmage_play == True) & (play_df.down_2 == True) & (play_df["start.distance"] >= 8), - (play_df.scrimmage_play == True) & (play_df.down_3 == True) & (play_df["start.distance"] >= 5), - (play_df.scrimmage_play == True) & (play_df.down_4 == True) & (play_df["start.distance"] >= 5), - ], - [True, True, True], - default=False, - ) - play_df["TFL"] = np.select( - [ - (play_df["type.text"] != "Penalty") & (play_df.sp == False) & (play_df.statYardage < 0), - (play_df["sack_vec"] == True), - ], - [True, True], - default=False, - ) - play_df["TFL_pass"] = np.where((play_df["TFL"] == True) & (play_df["pass"] == True), True, False) - play_df["TFL_rush"] = np.where((play_df["TFL"] == True) & (play_df["rush"] == True), True, False) - play_df["havoc"] = np.select( - [ - (play_df["pass_breakup"] == True), - (play_df["TFL"] == True), - (play_df["int"] == True), - (play_df["forced_fumble"] == True), - ], - [True, True, True, True], - default=False, + play_df = ( + play_df.with_columns( + new_down=pl.when(pl.col("type.text") == "Timeout") + .then(pl.col("start.down")) + .when((pl.col("type.text").is_in(penalty)).and_(pl.col("penalty_1st_conv") == True)) + .then(1) + .when( + (pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == True) + .and_(pl.col("penalty_declined") == False) + ) + .then(pl.col("start.down")) + .when( + (pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == True) + .and_(pl.col("penalty_declined") == True) + .and_(pl.col("statYardage") < pl.col("start.distance")) + .and_(pl.col("start.down") <= 3) + ) + .then(pl.col("start.down") + 1) + .when( + (pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == True) + .and_(pl.col("penalty_declined") == True) + .and_(pl.col("statYardage") < pl.col("start.distance")) + .and_(pl.col("start.down") == 4) + ) + .then(1) + .when( + (pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == True) + .and_(pl.col("penalty_declined") == True) + .and_(pl.col("statYardage") >= pl.col("start.distance")) + ) + .then(1) + .when( + (pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == False) + .and_(pl.col("penalty_declined") == True) + .and_(pl.col("statYardage") < pl.col("start.distance")) + .and_(pl.col("start.down") <= 3) + ) + .then(pl.col("start.down") + 1) + .when( + (pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == False) + .and_(pl.col("penalty_declined") == True) + .and_(pl.col("statYardage") < pl.col("start.distance")) + .and_(pl.col("start.down") == 4) + ) + .then(1) + .when( + (pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == False) + .and_(pl.col("penalty_declined") == True) + .and_(pl.col("statYardage") >= pl.col("start.distance")) + ) + .then(1) + .otherwise(pl.col("start.down")), + new_distance=pl.when(pl.col("type.text") == "Timeout") + .then(pl.col("start.distance")) + .when((pl.col("type.text").is_in(penalty)).and_(pl.col("penalty_1st_conv") == True)) + .then(10) + .when( + (pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == True) + .and_(pl.col("penalty_declined") == False) + ) + .then(pl.col("start.distance")) + .when( + (pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == True) + .and_(pl.col("penalty_declined") == True) + .and_(pl.col("statYardage") < pl.col("start.distance")) + .and_(pl.col("start.down") <= 3) + ) + .then(pl.col("start.distance")) + .when( + (pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == True) + .and_(pl.col("penalty_declined") == True) + .and_(pl.col("statYardage") < pl.col("start.distance")) + .and_(pl.col("start.down") == 4) + ) + .then(pl.col("start.distance")) + .when( + (pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == True) + .and_(pl.col("penalty_declined") == True) + .and_(pl.col("statYardage") >= pl.col("start.distance")) + ) + .then(pl.col("start.distance")) + .when( + (pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == False) + .and_(pl.col("penalty_declined") == True) + .and_(pl.col("statYardage") < pl.col("start.distance")) + .and_(pl.col("start.down") <= 3) + ) + .then(pl.col("start.distance")) + .when( + (pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == False) + .and_(pl.col("penalty_declined") == True) + .and_(pl.col("statYardage") < pl.col("start.distance")) + .and_(pl.col("start.down") == 4) + ) + .then(pl.col("start.distance")) + .when( + (pl.col("type.text").is_in(penalty)) + .and_(pl.col("penalty_1st_conv") == False) + .and_(pl.col("penalty_offset") == False) + .and_(pl.col("penalty_declined") == True) + .and_(pl.col("statYardage") >= pl.col("start.distance")) + ) + .then(pl.col("start.distance")) + .otherwise(pl.col("start.distance")), + middle_8=pl.when( + (pl.col("start.adj_TimeSecsRem") >= 1560).and_(pl.col("start.adj_TimeSecsRem") <= 2040) + ) + .then(True) + .otherwise(False), + rz_play=pl.when(pl.col("start.yardLine") <= 20).then(True).otherwise(False), + under_2=pl.when(pl.col("start.TimeSecsRem") <= 120).then(True).otherwise(False), + goal_to_go=pl.when(pl.col("start.yardLine") <= 10).then(True).otherwise(False), + scoring_opp=pl.when(pl.col("start.yardLine") <= 40).then(True).otherwise(False), + stuffed_run=pl.when((pl.col("type.text") == "Rush").and_(pl.col("yds_rushed") <= 0)) + .then(True) + .otherwise(False), + stopped_run=pl.when((pl.col("type.text") == "Rush").and_(pl.col("yds_rushed") <= 2)) + .then(True) + .otherwise(False), + opportunity_run=pl.when((pl.col("type.text") == "Rush").and_(pl.col("yds_rushed") <= 4)) + .then(True) + .otherwise(False), + highlight_run=pl.when((pl.col("type.text") == "Rush").and_(pl.col("yds_rushed") >= 8)) + .then(True) + .otherwise(False), + adj_rush_yardage=pl.when((pl.col("type.text") == "Rush").and_(pl.col("yds_rushed") > 8)) + .then(8) + .when((pl.col("type.text") == "Rush").and_(pl.col("yds_rushed") <= 8)) + .then(pl.col("yds_rushed")) + .otherwise(None), + ) + .with_columns( + line_yards=pl.when((pl.col("rush") == True).and_(pl.col("yds_rushed") < 0)) + .then(1.2 * pl.col("adj_rush_yardage")) + .when((pl.col("rush") == True).and_(pl.col("yds_rushed") >= 0).and_(pl.col("yds_rushed") <= 3)) + .then(pl.col("adj_rush_yardage")) + .when((pl.col("rush") == True).and_(pl.col("yds_rushed") >= 4).and_(pl.col("yds_rushed") <= 8)) + .then(3 + 0.5 * (pl.col("adj_rush_yardage") - 3)) + .when((pl.col("rush") == True).and_(pl.col("yds_rushed") >= 8)) + .then(5.5) + .otherwise(None), + second_level_yards=pl.when((pl.col("rush") == True).and_(pl.col("yds_rushed") >= 4)) + .then(0.5 * (pl.col("adj_rush_yardage") - 4)) + .when(pl.col("rush") == True) + .then(0) + .otherwise(None), + open_field_yards=pl.when((pl.col("rush") == True).and_(pl.col("yds_rushed") > 8)) + .then(pl.col("yds_rushed") - pl.col("adj_rush_yardage")) + .when(pl.col("rush") == True) + .then(0) + .otherwise(None), + ) + .with_columns( + highlight_yards=pl.col("second_level_yards") + pl.col("open_field_yards"), + ) + .with_columns( + opp_highlight_yards=pl.when(pl.col("opportunity_run") == True) + .then(pl.col("highlight_yards")) + .when((pl.col("opportunity_run") == False).and_(pl.col("rush") == True)) + .then(0) + .otherwise(None), + short_rush_success=pl.when( + (pl.col("start.distance") < 2) + .and_(pl.col("rush") == True) + .and_(pl.col("statYardage") >= pl.col("start.distance")) + ) + .then(True) + .when( + (pl.col("start.distance") < 2) + .and_(pl.col("rush") == True) + .and_(pl.col("statYardage") < pl.col("start.distance")) + ) + .then(False) + .otherwise(None), + short_rush_attempt=pl.when((pl.col("start.distance") < 2).and_(pl.col("rush") == True)) + .then(True) + .when((pl.col("start.distance") >= 2).and_(pl.col("rush") == True)) + .then(False) + .otherwise(None), + power_rush_success=pl.when( + (pl.col("start.distance") < 2) + .and_(pl.col("rush") == True) + .and_(pl.col("start.down").is_in([3, 4])) + .and_(pl.col("statYardage") >= pl.col("start.distance")) + ) + .then(True) + .when( + (pl.col("start.distance") < 2) + .and_(pl.col("rush") == True) + .and_(pl.col("start.down").is_in([3, 4])) + .and_(pl.col("statYardage") < pl.col("start.distance")) + ) + .then(False) + .otherwise(None), + power_rush_attempt=pl.when( + (pl.col("start.distance") < 2) + .and_(pl.col("rush") == True) + .and_(pl.col("start.down").is_in([3, 4])) + ) + .then(True) + .when( + (pl.col("start.distance") < 2) + .and_(pl.col("rush") == True) + .and_(pl.col("start.down").is_in([3, 4])) + ) + .then(False) + .otherwise(None), + early_down=pl.when( + ((pl.col("down_1") == True).or_(pl.col("down_2") == True)).and_(pl.col("scrimmage_play") == True) + ) + .then(True) + .otherwise(False), + late_down=pl.when( + ((pl.col("down_3") == True).or_(pl.col("down_4"))).and_(pl.col("scrimmage_play") == True) + ) + .then(True) + .otherwise(False), + ) + .with_columns( + early_down_pass=pl.when((pl.col("pass") == True).and_(pl.col("early_down") == True)) + .then(True) + .otherwise(False), + early_down_rush=pl.when((pl.col("rush") == True).and_(pl.col("early_down") == True)) + .then(True) + .otherwise(False), + late_down_pass=pl.when((pl.col("pass") == True).and_(pl.col("late_down") == True)) + .then(True) + .otherwise(False), + late_down_rush=pl.when((pl.col("rush") == True).and_(pl.col("late_down") == True)) + .then(True) + .otherwise(False), + standard_down=pl.when((pl.col("scrimmage_play") == True).and_(pl.col("down_1") == True)) + .then(True) + .when( + (pl.col("scrimmage_play") == True) + .and_(pl.col("down_2") == True) + .and_(pl.col("start.distance") < 8) + ) + .then(True) + .when( + (pl.col("scrimmage_play") == True) + .and_(pl.col("down_3") == True) + .and_(pl.col("start.distance") < 5) + ) + .then(True) + .when( + (pl.col("scrimmage_play") == True) + .and_(pl.col("down_4") == True) + .and_(pl.col("start.distance") < 5) + ) + .then(True) + .otherwise(False), + passing_down=pl.when( + (pl.col("scrimmage_play") == True) + .and_(pl.col("down_2") == True) + .and_(pl.col("start.distance") >= 8) + ) + .then(True) + .when( + (pl.col("scrimmage_play") == True) + .and_(pl.col("down_3") == True) + .and_(pl.col("start.distance") >= 5) + ) + .then(True) + .when( + (pl.col("scrimmage_play") == True) + .and_(pl.col("down_4") == True) + .and_(pl.col("start.distance") >= 5) + ) + .then(True) + .otherwise(False), + TFL=pl.when( + (pl.col("type.text") != "Penalty").and_(pl.col("sp") == False).and_(pl.col("statYardage") < 0) + ) + .then(True) + .when(pl.col("sack_vec") == True) + .then(True) + .otherwise(False), + ) + .with_columns( + TFL_pass=pl.when((pl.col("TFL") == True).and_(pl.col("pass") == True)).then(True).otherwise(False), + TFL_rush=pl.when((pl.col("TFL") == True).and_(pl.col("rush") == True)).then(True).otherwise(False), + havoc=pl.when(pl.col("pass_breakup") == True) + .then(True) + .when(pl.col("TFL") == True) + .then(True) + .when(pl.col("int") == True) + .then(True) + .when(pl.col("forced_fumble") == True) + .then(True) + .otherwise(False), + ) ) return play_df def __add_spread_time(self, play_df): - play_df["start.pos_team_spread"] = np.where( - (play_df["start.pos_team.id"] == play_df["homeTeamId"]), - play_df["homeTeamSpread"], - -1 * play_df["homeTeamSpread"], - ) - play_df["start.elapsed_share"] = ((3600 - play_df["start.adj_TimeSecsRem"]) / 3600).clip(0, 3600) - play_df["start.spread_time"] = play_df["start.pos_team_spread"] * np.exp(-4 * play_df["start.elapsed_share"]) - play_df["end.pos_team_spread"] = np.where( - (play_df["end.pos_team.id"] == play_df["homeTeamId"]), - play_df["homeTeamSpread"], - -1 * play_df["homeTeamSpread"], - ) - play_df["end.pos_team_spread"] = np.where( - (play_df["end.pos_team.id"] == play_df["homeTeamId"]), - play_df["homeTeamSpread"], - -1 * play_df["homeTeamSpread"], + play_df = ( + play_df.with_columns( + pl.when(pl.col("start.pos_team.id") == pl.col("homeTeamId")) + .then(pl.col("homeTeamSpread")) + .otherwise(-1 * pl.col("homeTeamSpread")) + .alias("start.pos_team_spread"), + ((3600 - pl.col("start.adj_TimeSecsRem")) / 3600).clip(0, 3600).alias("start.elapsed_share"), + ) + .with_columns( + (pl.col("start.pos_team_spread") * np.exp(-4 * pl.col("start.elapsed_share"))).alias( + "start.spread_time" + ), + pl.when(pl.col("end.pos_team.id") == pl.col("homeTeamId")) + .then(pl.col("homeTeamSpread")) + .otherwise(-1 * pl.col("homeTeamSpread")) + .alias("end.pos_team_spread"), + ((3600 - pl.col("end.adj_TimeSecsRem")) / 3600).clip(0, 3600).alias("end.elapsed_share"), + ) + .with_columns( + (pl.col("end.pos_team_spread") * np.exp(-4 * pl.col("end.elapsed_share"))).alias("end.spread_time"), + ) ) - play_df["end.elapsed_share"] = ((3600 - play_df["end.adj_TimeSecsRem"]) / 3600).clip(0, 3600) - play_df["end.spread_time"] = play_df["end.pos_team_spread"] * np.exp(-4 * play_df["end.elapsed_share"]) return play_df def __calculate_ep_exp_val(self, matrix): @@ -3156,25 +3083,38 @@ def __calculate_ep_exp_val(self, matrix): ) def __process_epa(self, play_df): - play_df.loc[play_df["type.text"].isin(kickoff_vec), "down"] = 1 - play_df.loc[play_df["type.text"].isin(kickoff_vec), "start.down"] = 1 - play_df.loc[play_df["type.text"].isin(kickoff_vec), "down_1"] = True - play_df.loc[play_df["type.text"].isin(kickoff_vec), "down_2"] = False - play_df.loc[play_df["type.text"].isin(kickoff_vec), "down_3"] = False - play_df.loc[play_df["type.text"].isin(kickoff_vec), "down_4"] = False - play_df.loc[play_df["type.text"].isin(kickoff_vec), "distance"] = 10 - play_df.loc[play_df["type.text"].isin(kickoff_vec), "start.distance"] = 10 - play_df["start.yardsToEndzone.touchback"] = 99 - play_df.loc[ - (play_df["type.text"].isin(kickoff_vec)) & (play_df["season"] > 2013), - "start.yardsToEndzone.touchback", - ] = 75 - play_df.loc[ - (play_df["type.text"].isin(kickoff_vec)) & (play_df["season"] <= 2013), - "start.yardsToEndzone.touchback", - ] = 80 + play_df = ( + play_df.with_columns( + down=pl.when(pl.col("type.text").is_in(kickoff_vec)).then(1).otherwise(pl.col("start.down")), + down_1=pl.when(pl.col("type.text").is_in(kickoff_vec)).then(True).otherwise(pl.col("down_1")), + down_2=pl.when(pl.col("type.text").is_in(kickoff_vec)).then(False).otherwise(pl.col("down_2")), + down_3=pl.when(pl.col("type.text").is_in(kickoff_vec)).then(False).otherwise(pl.col("down_3")), + down_4=pl.when(pl.col("type.text").is_in(kickoff_vec)).then(False).otherwise(pl.col("down_4")), + distance=pl.when(pl.col("type.text").is_in(kickoff_vec)).then(10).otherwise(pl.col("start.distance")), + ) + .with_columns( + pl.when(pl.col("type.text").is_in(kickoff_vec)) + .then(1) + .otherwise(pl.col("start.down")) + .alias("start.down"), + pl.when(pl.col("type.text").is_in(kickoff_vec)) + .then(10) + .otherwise(pl.col("start.distance")) + .alias("start.distance"), + pl.lit(99).alias("start.yardsToEndzone.touchback"), + ) + .with_columns( + pl.when((pl.col("type.text").is_in(kickoff_vec)).and_(pl.col("season") > 2013)) + .then(75) + .when((pl.col("type.text").is_in(kickoff_vec)).and_(pl.col("season") <= 2013)) + .then(80) + .otherwise(pl.col("start.yardsToEndzone")) + .alias("start.yardsToEndzone.touchback"), + ) + ) start_touchback_data = play_df[ep_start_touchback_columns] + start_touchback_data.columns = ep_final_names # self.logger.info(start_data.iloc[[36]].to_json(orient="records")) @@ -3190,26 +3130,64 @@ def __process_epa(self, play_df): EP_start_parts = ep_model.predict(dtest_start) EP_start = self.__calculate_ep_exp_val(EP_start_parts) - play_df.loc[play_df["end.TimeSecsRem"] <= 0, "end.TimeSecsRem"] = 0 - play_df.loc[ - (play_df["end.TimeSecsRem"] <= 0) & (play_df.period < 5), - "end.yardsToEndzone", - ] = 99 - play_df.loc[(play_df["end.TimeSecsRem"] <= 0) & (play_df.period < 5), "down_1_end"] = True - play_df.loc[(play_df["end.TimeSecsRem"] <= 0) & (play_df.period < 5), "down_2_end"] = False - play_df.loc[(play_df["end.TimeSecsRem"] <= 0) & (play_df.period < 5), "down_3_end"] = False - play_df.loc[(play_df["end.TimeSecsRem"] <= 0) & (play_df.period < 5), "down_4_end"] = False - - play_df.loc[play_df["end.yardsToEndzone"] >= 100, "end.yardsToEndzone"] = 99 - play_df.loc[play_df["end.yardsToEndzone"] <= 0, "end.yardsToEndzone"] = 99 - - play_df.loc[play_df.kickoff_tb == True, "end.yardsToEndzone"] = 75 - play_df.loc[play_df.kickoff_tb == True, "end.down"] = 1 - play_df.loc[play_df.kickoff_tb == True, "end.distance"] = 10 - - play_df.loc[play_df.punt_tb == True, "end.down"] = 1 - play_df.loc[play_df.punt_tb == True, "end.distance"] = 10 - play_df.loc[play_df.punt_tb == True, "end.yardsToEndzone"] = 80 + play_df = ( + play_df.with_columns( + pl.when(pl.col("end.TimeSecsRem") <= 0) + .then(0) + .otherwise(pl.col("end.TimeSecsRem")) + .alias("end.TimeSecsRem"), + ) + .with_columns( + pl.when((pl.col("end.TimeSecsRem") <= 0).and_(pl.col("period") < 5)) + .then(99) + .otherwise(pl.col("end.yardsToEndzone")) + .alias("end.yardsToEndzone"), + pl.when((pl.col("end.TimeSecsRem") <= 0).and_(pl.col("period") < 5)) + .then(True) + .otherwise(pl.col("down_1_end")) + .alias("down_1_end"), + pl.when((pl.col("end.TimeSecsRem") <= 0).and_(pl.col("period") < 5)) + .then(False) + .otherwise(pl.col("down_2_end")) + .alias("down_2_end"), + pl.when((pl.col("end.TimeSecsRem") <= 0).and_(pl.col("period") < 5)) + .then(False) + .otherwise(pl.col("down_3_end")) + .alias("down_3_end"), + pl.when((pl.col("end.TimeSecsRem") <= 0).and_(pl.col("period") < 5)) + .then(False) + .otherwise(pl.col("down_4_end")) + .alias("down_4_end"), + ) + .with_columns( + pl.when(pl.col("end.yardsToEndzone") >= 100) + .then(99) + .otherwise(pl.col("end.yardsToEndzone")) + .alias("end.yardsToEndzone"), + ) + .with_columns( + pl.when(pl.col("end.yardsToEndzone") <= 0) + .then(99) + .otherwise(pl.col("end.yardsToEndzone")) + .alias("end.yardsToEndzone"), + ) + .with_columns( + pl.when(pl.col("kickoff_tb") == True) + .then(75) + .otherwise(pl.col("end.yardsToEndzone")) + .alias("end.yardsToEndzone"), + pl.when(pl.col("kickoff_tb") == True).then(1).otherwise(pl.col("end.down")).alias("end.down"), + pl.when(pl.col("kickoff_tb") == True).then(10).otherwise(pl.col("end.distance")).alias("end.distance"), + ) + .with_columns( + pl.when(pl.col("punt_tb") == True).then(1).otherwise(pl.col("end.down")).alias("end.down"), + pl.when(pl.col("punt_tb") == True).then(10).otherwise(pl.col("end.distance")).alias("end.distance"), + pl.when(pl.col("punt_tb") == True) + .then(80) + .otherwise(pl.col("end.yardsToEndzone")) + .alias("end.yardsToEndzone"), + ) + ) end_data = play_df[ep_end_columns] end_data.columns = ep_final_names @@ -3219,421 +3197,424 @@ def __process_epa(self, play_df): EP_end = self.__calculate_ep_exp_val(EP_end_parts) - play_df["EP_start_touchback"] = EP_start_touchback - play_df["EP_start"] = EP_start - play_df["EP_end"] = EP_end - kick = "kick)" - play_df["EP_start"] = np.where( - play_df["type.text"].isin( - [ - "Extra Point Good", - "Extra Point Missed", - "Two-Point Conversion Good", - "Two-Point Conversion Missed", - "Two Point Pass", - "Two Point Rush", - "Blocked PAT", - ] - ), - 0.92, - play_df["EP_start"], + play_df = play_df.with_columns( + EP_start_touchback=pl.lit(EP_start_touchback), + EP_start=pl.lit(EP_start), + EP_end=pl.lit(EP_end), ) - play_df.EP_end = np.select( - [ + + kick = "kick)" + play_df = ( + play_df.with_columns( + EP_start=pl.when( + pl.col("type.text").is_in( + [ + "Extra Point Good", + "Extra Point Missed", + "Two-Point Conversion Good", + "Two-Point Conversion Missed", + "Two Point Pass", + "Two Point Rush", + "Blocked PAT", + ] + ) + ) + .then(0.92) + .otherwise(pl.col("EP_start")), + ) + .with_columns( # End of Half - ( - play_df["type.text"] - .str.lower() - .str.contains("end of game", case=False, flags=0, na=False, regex=True) - ) - | ( - play_df["type.text"] - .str.lower() - .str.contains("end of game", case=False, flags=0, na=False, regex=True) - ) - | ( - play_df["type.text"] - .str.lower() - .str.contains("end of half", case=False, flags=0, na=False, regex=True) - ) - | ( - play_df["type.text"] - .str.lower() - .str.contains("end of half", case=False, flags=0, na=False, regex=True) - ), - # Def 2pt conversion is its own play - (play_df["type.text"].isin(["Defensive 2pt Conversion"])), + EP_end=pl.when( + (pl.col("type.text").str.to_lowercase().str.contains(r"end of game")).or_( + pl.col("type.text").str.to_lowercase().str.contains(r"end of half") + ) + ) + .then(0) + # Defensive 2pt Conversion + .when(pl.col("type.text").is_in(["Defensive 2pt Conversion"])) + .then(-2) # Safeties - ( - (play_df["type.text"].isin(defense_score_vec)) - & (play_df["text"].str.lower().str.contains("safety", case=False, regex=True)) - ), + .when( + (pl.col("type.text").is_in(defense_score_vec)).and_( + pl.col("text").str.to_lowercase().str.contains(r"(?i)safety") + ) + ) + .then(-2) # Defense TD + Successful Two-Point Conversion - ( - (play_df["type.text"].isin(defense_score_vec)) - & (play_df["text"].str.lower().str.contains("conversion", case=False, regex=False)) - & (~play_df["text"].str.lower().str.contains(r"failed\s?\)", case=False, regex=True)) - ), + .when( + (pl.col("type.text").is_in(defense_score_vec)) + .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)conversion")) + .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)failed") == False) + ) + .then(-8) # Defense TD + Failed Two-Point Conversion - ( - (play_df["type.text"].isin(defense_score_vec)) - & (play_df["text"].str.lower().str.contains("conversion", case=False, regex=False)) - & (play_df["text"].str.lower().str.contains(r"failed\s?\)", case=False, regex=True)) - ), + .when( + (pl.col("type.text").is_in(defense_score_vec)) + .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)conversion")) + .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)failed")) + ) + .then(-6) # Defense TD + Kick/PAT Missed - ( - (play_df["type.text"].isin(defense_score_vec)) - & (play_df["text"].str.contains("PAT", case=True, regex=False)) - & (play_df["text"].str.lower().str.contains(r"missed\s?\)", case=False, regex=True)) - ), + .when( + (pl.col("type.text").is_in(defense_score_vec)) + .and_(pl.col("text").str.to_lowercase().str.contains(r"PAT")) + .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)missed")) + ) + .then(-6) # Defense TD + Kick/PAT Good - ( - (play_df["type.text"].isin(defense_score_vec)) - & (play_df["text"].str.lower().str.contains(kick, case=False, regex=False)) - ), + .when( + (pl.col("type.text").is_in(defense_score_vec)).and_( + pl.col("text").str.to_lowercase().str.contains(r"kick\)") + ) + ) + .then(-7) # Defense TD - (play_df["type.text"].isin(defense_score_vec)), + .when(pl.col("type.text").is_in(defense_score_vec)) + .then(-6.92) # Offense TD + Failed Two-Point Conversion - ( - (play_df["type.text"].isin(offense_score_vec)) - & (play_df["text"].str.lower().str.contains("conversion", case=False, regex=False)) - & (play_df["text"].str.lower().str.contains(r"failed\s?\)", case=False, regex=True)) - ), + .when( + (pl.col("type.text").is_in(offense_score_vec)) + .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)conversion")) + .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)failed")) + ) + .then(6) # Offense TD + Successful Two-Point Conversion - ( - (play_df["type.text"].isin(offense_score_vec)) - & (play_df["text"].str.lower().str.contains("conversion", case=False, regex=False)) - & (~play_df["text"].str.lower().str.contains(r"failed\s?\)", case=False, regex=True)) - ), + .when( + (pl.col("type.text").is_in(offense_score_vec)) + .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)conversion")) + .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)failed") == False) + ) + .then(8) # Offense Made FG - ( - (play_df["type.text"].isin(offense_score_vec)) - & ( - play_df["type.text"] - .str.lower() - .str.contains("field goal", case=False, flags=0, na=False, regex=True) - ) - & ( - play_df["type.text"] - .str.lower() - .str.contains("good", case=False, flags=0, na=False, regex=True) - ) - ), - # Missed FG -- Not Needed - # (play_df["type.text"].isin(offense_score_vec)) & - # (play_df["type.text"].str.lower().str.contains('field goal', case=False, flags=0, na=False, regex=True)) & - # (~play_df["type.text"].str.lower().str.contains('good', case=False, flags=0, na=False, regex=True)), + .when( + (pl.col("type.text").is_in(offense_score_vec)) + .and_(pl.col("type.text").str.to_lowercase().str.contains(r"(?i)field goal")) + .and_(pl.col("type.text").str.to_lowercase().str.contains(r"(?i)good")) + ) + .then(3) # Offense TD + Kick/PAT Missed - ( - (play_df["type.text"].isin(offense_score_vec)) - & (~play_df["text"].str.lower().str.contains("conversion", case=False, regex=False)) - & ((play_df["text"].str.contains("PAT", case=True, regex=False))) - & ((play_df["text"].str.lower().str.contains(r"missed\s?\)", case=False, regex=True))) - ), - # Offense TD + Kick PAT Good - ( - (play_df["type.text"].isin(offense_score_vec)) - & (play_df["text"].str.lower().str.contains(kick, case=False, regex=False)) - ), + .when( + (pl.col("type.text").is_in(offense_score_vec)) + .and_(pl.col("text").str.to_lowercase().str.contains(r"PAT")) + .and_(pl.col("text").str.to_lowercase().str.contains(r"(?i)missed")) + ) + .then(6) + # Offense TD + Kick/PAT Good + .when( + (pl.col("type.text").is_in(offense_score_vec)).and_( + pl.col("text").str.to_lowercase().str.contains(r"kick\)") + ) + ) + .then(7) # Offense TD - (play_df["type.text"].isin(offense_score_vec)), - # Extra Point Good (pre-2014 data) - (play_df["type.text"] == "Extra Point Good"), - # Extra Point Missed (pre-2014 data) - (play_df["type.text"] == "Extra Point Missed"), - # Extra Point Blocked (pre-2014 data) - (play_df["type.text"] == "Blocked PAT"), - # Two-Point Good (pre-2014 data) - (play_df["type.text"] == "Two-Point Conversion Good"), - # Two-Point Missed (pre-2014 data) - (play_df["type.text"] == "Two-Point Conversion Missed"), - # Two-Point No Good (pre-2014 data) - ( - ((play_df["type.text"] == "Two Point Pass") | (play_df["type.text"] == "Two Point Rush")) - & (play_df["text"].str.lower().str.contains("no good", case=False, regex=False)) - ), - # Two-Point Good (pre-2014 data) - ( - ((play_df["type.text"] == "Two Point Pass") | (play_df["type.text"] == "Two Point Rush")) - & (~play_df["text"].str.lower().str.contains("no good", case=False, regex=False)) - ), + .when(pl.col("type.text").is_in(offense_score_vec)) + .then(6.92) + # Extra Point Good + .when(pl.col("type.text").is_in(["Extra Point Good"])) + .then(1) + # Extra Point Missed + .when(pl.col("type.text").is_in(["Extra Point Missed"])) + .then(0) + # Two-Point Conversion Good + .when(pl.col("type.text").is_in(["Two-Point Conversion Good"])) + .then(2) + # Two-Point Conversion Missed + .when(pl.col("type.text").is_in(["Two-Point Conversion Missed"])) + .then(0) + # Two Point Pass/Rush Missed (Pre-2014 Data) + .when( + (pl.col("type.text").is_in(["Two Point Pass", "Two Point Rush"])).and_( + pl.col("text").str.to_lowercase().str.contains(r"(?i)no good") + ) + ) + .then(0) + # Two Point Pass/Rush Good (Pre-2014 Data) + .when( + (pl.col("type.text").is_in(["Two Point Pass", "Two Point Rush"])).and_( + pl.col("text").str.to_lowercase().str.contains(r"(?i)no good") == False + ) + ) + .then(2) + # Blocked PAT + .when(pl.col("type.text").is_in(["Blocked PAT"])) + .then(0) # Flips for Turnovers that aren't kickoffs - ( - ((play_df["type.text"].isin(end_change_vec)) | (play_df.downs_turnover == True)) - & (play_df.kickoff_play == False) - ), - # Flips for Turnovers that are on kickoffs - (play_df["type.text"].isin(kickoff_turnovers)), - ], - [ - 0, - -2, - -2, - -6, - -8, - -6, - -7, - -6.92, - 6, - 8, - 3, - 6, - 7, - 6.92, - 1, - 0, - 0, - 2, - 0, - 0, - 2, - (play_df.EP_end * -1), - (play_df.EP_end * -1), - ], - default=play_df.EP_end, - ) - play_df["lag_EP_end"] = play_df["EP_end"].shift(1) - play_df["lag_change_of_pos_team"] = play_df.change_of_pos_team.shift(1) - play_df["lag_change_of_pos_team"] = np.where( - play_df["lag_change_of_pos_team"].isna(), - False, - play_df["lag_change_of_pos_team"], - ) - play_df["EP_between"] = np.where( - play_df.lag_change_of_pos_team == True, - play_df["EP_start"] + play_df["lag_EP_end"], - play_df["EP_start"] - play_df["lag_EP_end"], - ) - play_df["EP_start"] = np.where( - (play_df["type.text"].isin(["Timeout", "End Period"])) & (play_df["lag_change_of_pos_team"] == False), - play_df["lag_EP_end"], - play_df["EP_start"], - ) - play_df["EP_start"] = np.where( - (play_df["type.text"].isin(kickoff_vec)), - play_df["EP_start_touchback"], - play_df["EP_start"], - ) - play_df["EP_end"] = np.where((play_df["type.text"] == "Timeout"), play_df["EP_start"], play_df["EP_end"]) - play_df["EPA"] = np.select( - [ - (play_df["type.text"] == "Timeout"), - (play_df["scoring_play"] == False) & (play_df["end_of_half"] == True), - (play_df["type.text"].isin(kickoff_vec)) & (play_df["penalty_in_text"] == True), - (play_df["penalty_in_text"] == True) - & (play_df["type.text"] != "Penalty") - & (~play_df["type.text"].isin(kickoff_vec)), - ], - [ - 0, - -1 * play_df["EP_start"], - play_df["EP_end"] - play_df["EP_start"], - (play_df["EP_end"] - play_df["EP_start"] + play_df["EP_between"]), - ], - default=(play_df["EP_end"] - play_df["EP_start"]), - ) - play_df["def_EPA"] = -1 * play_df["EPA"] - # ----- EPA Summary flags ------ - play_df["EPA_scrimmage"] = np.select([(play_df.scrimmage_play == True)], [play_df.EPA], default=None) - play_df["EPA_rush"] = np.select( - [ - (play_df.rush == True) & (play_df["penalty_in_text"] == True), - (play_df.rush == True) & (play_df["penalty_in_text"] == False), - ], - [play_df.EPA, play_df.EPA], - default=None, - ) - play_df["EPA_pass"] = np.where((play_df["pass"] == True), play_df.EPA, None) - - play_df["EPA_explosive"] = np.where( - ((play_df["pass"] == True) & (play_df["EPA"] >= 2.4)) - | (((play_df["rush"] == True) & (play_df["EPA"] >= 1.8))), - True, - False, - ) - play_df["EPA_non_explosive"] = np.where((play_df["EPA_explosive"] == False), play_df.EPA, None) - - play_df["EPA_explosive_pass"] = np.where(((play_df["pass"] == True) & (play_df["EPA"] >= 2.4)), True, False) - play_df["EPA_explosive_rush"] = np.where((((play_df["rush"] == True) & (play_df["EPA"] >= 1.8))), True, False) - - play_df["first_down_created"] = np.where( - (play_df.scrimmage_play == True) - & (play_df["end.down"] == 1) - & (play_df["start.pos_team.id"] == play_df["end.pos_team.id"]), - True, - False, - ) - - play_df["EPA_success"] = np.where(play_df.EPA > 0, True, False) - play_df["EPA_success_early_down"] = np.where((play_df.EPA > 0) & (play_df.early_down == True), True, False) - play_df["EPA_success_early_down_pass"] = np.where( - (play_df["pass"] == True) & (play_df.EPA > 0) & (play_df.early_down == True), - True, - False, - ) - play_df["EPA_success_early_down_rush"] = np.where( - (play_df["rush"] == True) & (play_df.EPA > 0) & (play_df.early_down == True), - True, - False, - ) - play_df["EPA_success_late_down"] = np.where((play_df.EPA > 0) & (play_df.late_down == True), True, False) - play_df["EPA_success_late_down_pass"] = np.where( - (play_df["pass"] == True) & (play_df.EPA > 0) & (play_df.late_down == True), - True, - False, - ) - play_df["EPA_success_late_down_rush"] = np.where( - (play_df["rush"] == True) & (play_df.EPA > 0) & (play_df.late_down == True), - True, - False, - ) - play_df["EPA_success_standard_down"] = np.where( - (play_df.EPA > 0) & (play_df.standard_down == True), True, False - ) - play_df["EPA_success_passing_down"] = np.where((play_df.EPA > 0) & (play_df.passing_down == True), True, False) - play_df["EPA_success_pass"] = np.where((play_df.EPA > 0) & (play_df["pass"] == True), True, False) - play_df["EPA_success_rush"] = np.where((play_df.EPA > 0) & (play_df.rush == True), True, False) - play_df["EPA_success_EPA"] = np.where(play_df.EPA > 0, play_df.EPA, None) - play_df["EPA_success_standard_down_EPA"] = np.where( - (play_df.EPA > 0) & (play_df.standard_down == True), play_df.EPA, None - ) - play_df["EPA_success_passing_down_EPA"] = np.where( - (play_df.EPA > 0) & (play_df.passing_down == True), play_df.EPA, None - ) - play_df["EPA_success_pass_EPA"] = np.where((play_df.EPA > 0) & (play_df["pass"] == True), play_df.EPA, None) - play_df["EPA_success_rush_EPA"] = np.where((play_df.EPA > 0) & (play_df.rush == True), True, False) - play_df["EPA_middle_8_success"] = np.where((play_df.EPA > 0) & (play_df["middle_8"] == True), True, False) - play_df["EPA_middle_8_success_pass"] = np.where( - (play_df["pass"] == True) & (play_df.EPA > 0) & (play_df["middle_8"] == True), - True, - False, - ) - play_df["EPA_middle_8_success_rush"] = np.where( - (play_df["rush"] == True) & (play_df.EPA > 0) & (play_df["middle_8"] == True), - True, - False, - ) - play_df["EPA_penalty"] = np.select( - [ - (play_df["type.text"].isin(["Penalty", "Penalty (Kickoff)"])), - (play_df["penalty_in_text"] == True), - ], - [play_df["EPA"], play_df["EP_end"] - play_df["EP_start"]], - default=None, - ) - play_df["EPA_sp"] = np.where( - (play_df.fg_attempt == True) | (play_df.punt == True) | (play_df.kickoff_play == True), - play_df["EPA"], - False, + .when( + ((pl.col("type.text").is_in(end_change_vec)).or_(pl.col("downs_turnover") == True)).and_( + pl.col("type.text").is_in(kickoff_vec) == False + ) + ) + .then(pl.col("EP_end") * -1) + # Flips for Turnovers that are kickoffs + .when(pl.col("type.text").is_in(kickoff_turnovers)) + .then(pl.col("EP_end") * -1) + # Onside kicks + .when((pl.col("kickoff_onside") == True).and_(pl.col("change_of_pos_team") == True)) + .then(pl.col("EP_end") * -1) + .otherwise(pl.col("EP_end")) + ) + .with_columns( + lag_EP_end=pl.col("EP_end").shift(1), + lag_change_of_pos_team=pl.col("change_of_pos_team").shift(1), + ) + .with_columns( + lag_change_of_pos_team=pl.when(pl.col("lag_change_of_pos_team").is_null()) + .then(False) + .otherwise(pl.col("lag_change_of_pos_team")), + ) + .with_columns( + EP_between=pl.when(pl.col("lag_change_of_pos_team") == True) + .then(pl.col("EP_start") + pl.col("lag_EP_end")) + .otherwise(pl.col("EP_start") - pl.col("lag_EP_end")), + EP_start=pl.when( + (pl.col("type.text").is_in(["Timeout", "End Period"])).and_( + pl.col("lag_change_of_pos_team") == False + ) + ) + .then(pl.col("lag_EP_end")) + .otherwise(pl.col("EP_start")), + ) + .with_columns( + EP_start=pl.when(pl.col("type.text").is_in(kickoff_vec)) + .then(pl.col("EP_start_touchback")) + .otherwise(pl.col("EP_start")), + ) + .with_columns( + EP_end=pl.when(pl.col("type.text").is_in(["Timeout"])) + .then(pl.col("EP_start")) + .otherwise(pl.col("EP_end")), + ) + .with_columns( + EPA=pl.when(pl.col("type.text").is_in(["Timeout"])) + .then(0) + .when((pl.col("scoring_play") == False).and_(pl.col("end_of_half") == True)) + .then(-1 * pl.col("EP_start")) + .when((pl.col("type.text").is_in(kickoff_vec)).and_(pl.col("penalty_in_text") == True)) + .then(pl.col("EP_end") - pl.col("EP_start")) + .when( + (pl.col("penalty_in_text") == True) + .and_(pl.col("type.text").is_in(["Penalty"]) == False) + .and_(pl.col("type.text").is_in(kickoff_vec) == False) + ) + .then(pl.col("EP_end") - pl.col("EP_start") + pl.col("EP_between")) + .otherwise(pl.col("EP_end") - pl.col("EP_start")), + ) + .with_columns( + def_EPA=pl.col("EPA") * -1, + # --- EPA Summary flags ---- + EPA_scrimmage=pl.when(pl.col("scrimmage_play") == True).then(pl.col("EPA")).otherwise(None), + EPA_rush=pl.when((pl.col("rush") == True).and_(pl.col("penalty_in_text") == True)) + .then(pl.col("EPA")) + .when((pl.col("rush") == True).and_(pl.col("penalty_in_text") == False)) + .then(pl.col("EPA")) + .otherwise(None), + EPA_pass=pl.when(pl.col("pass") == True).then(pl.col("EPA")).otherwise(None), + EPA_explosive=pl.when((pl.col("pass") == True).and_(pl.col("EPA") >= 2.4)) + .then(True) + .when(((pl.col("rush") == True).and_(pl.col("EPA") >= 1.8))) + .then(True) + .otherwise(False), + ) + .with_columns( + EPA_non_explosive=pl.when(pl.col("EPA_explosive") == False).then(pl.col("EPA")).otherwise(None), + EPA_explosive_pass=pl.when((pl.col("pass") == True).and_(pl.col("EPA") >= 2.4)) + .then(True) + .otherwise(False), + EPA_explosive_rush=pl.when((pl.col("rush") == True).and_(pl.col("EPA") >= 1.8)) + .then(True) + .otherwise(False), + first_down_created=pl.when( + (pl.col("scrimmage_play") == True) + .and_(pl.col("end.down") == 1) + .and_(pl.col("start.pos_team.id") == pl.col("end.pos_team.id")) + ) + .then(True) + .otherwise(False), + EPA_success=pl.when(pl.col("EPA") > 0).then(True).otherwise(False), + EPA_success_early_down=pl.when((pl.col("EPA") > 0).and_(pl.col("early_down") == True)) + .then(True) + .otherwise(False), + EPA_success_early_down_pass=pl.when( + (pl.col("pass") == True).and_(pl.col("EPA") > 0).and_(pl.col("early_down") == True) + ) + .then(True) + .otherwise(False), + EPA_success_early_down_rush=pl.when( + (pl.col("rush") == True).and_(pl.col("EPA") > 0).and_(pl.col("early_down") == True) + ) + .then(True) + .otherwise(False), + EPA_success_late_down=pl.when((pl.col("EPA") > 0).and_(pl.col("late_down") == True)) + .then(True) + .otherwise(False), + EPA_success_late_down_pass=pl.when( + (pl.col("pass") == True).and_(pl.col("EPA") > 0).and_(pl.col("late_down") == True) + ) + .then(True) + .otherwise(False), + EPA_success_late_down_rush=pl.when( + (pl.col("rush") == True).and_(pl.col("EPA") > 0).and_(pl.col("late_down") == True) + ) + .then(True) + .otherwise(False), + EPA_success_standard_down=pl.when((pl.col("EPA") > 0).and_(pl.col("standard_down") == True)) + .then(True) + .otherwise(False), + EPA_success_passing_down=pl.when((pl.col("EPA") > 0).and_(pl.col("passing_down") == True)) + .then(True) + .otherwise(False), + EPA_success_pass=pl.when((pl.col("EPA") > 0).and_(pl.col("pass") == True)).then(True).otherwise(False), + EPA_success_rush=pl.when((pl.col("EPA") > 0).and_(pl.col("rush") == True)).then(True).otherwise(False), + EPA_success_EPA=pl.when(pl.col("EPA") > 0).then(pl.col("EPA")).otherwise(None), + EPA_success_standard_down_EPA=pl.when((pl.col("EPA") > 0).and_(pl.col("standard_down") == True)) + .then(pl.col("EPA")) + .otherwise(None), + EPA_success_passing_down_EPA=pl.when((pl.col("EPA") > 0).and_(pl.col("passing_down") == True)) + .then(pl.col("EPA")) + .otherwise(None), + EPA_success_pass_EPA=pl.when((pl.col("EPA") > 0).and_(pl.col("pass") == True)) + .then(pl.col("EPA")) + .otherwise(None), + EPA_success_rush_EPA=pl.when((pl.col("EPA") > 0).and_(pl.col("rush") == True)) + .then(pl.col("EPA")) + .otherwise(None), + EPA_middle_8_success=pl.when((pl.col("EPA") > 0).and_(pl.col("middle_8") == True)) + .then(True) + .otherwise(False), + EPA_middle_8_success_pass=pl.when( + (pl.col("pass") == True).and_(pl.col("EPA") > 0).and_(pl.col("middle_8") == True) + ) + .then(True) + .otherwise(False), + EPA_middle_8_success_rush=pl.when( + (pl.col("rush") == True).and_(pl.col("EPA") > 0).and_(pl.col("middle_8") == True) + ) + .then(True) + .otherwise(False), + EPA_penalty=pl.when(pl.col("type.text").is_in(["Penalty", "Penalty (Kickoff)"])) + .then(pl.col("EPA")) + .when(pl.col("penalty_in_text") == True) + .then(pl.col("EP_end") - pl.col("EP_start")) + .otherwise(None), + EPA_sp=pl.when( + (pl.col("fg_attempt") == True).or_(pl.col("punt") == True).or_(pl.col("kickoff_play") == True) + ) + .then(pl.col("EPA")) + .otherwise(False), + EPA_fg=pl.when(pl.col("fg_attempt") == True).then(pl.col("EPA")).otherwise(None), + EPA_punt=pl.when(pl.col("punt") == True).then(pl.col("EPA")).otherwise(None), + EPA_kickoff=pl.when(pl.col("kickoff_play") == True).then(pl.col("EPA")).otherwise(None), + ) ) - play_df["EPA_fg"] = np.where((play_df.fg_attempt == True), play_df["EPA"], None) - play_df["EPA_punt"] = np.where((play_df.punt == True), play_df["EPA"], None) - play_df["EPA_kickoff"] = np.where((play_df.kickoff_play == True), play_df["EPA"], None) return play_df def __process_qbr(self, play_df): - play_df["qbr_epa"] = np.select( - [ - (play_df.EPA < -5.0), - (play_df.fumble_vec == True), - ], - [-5.0, -3.5], - default=play_df.EPA, - ) - - play_df["weight"] = np.select( - [ - (play_df.home_wp_before < 0.1), - (play_df.home_wp_before >= 0.1) & (play_df.home_wp_before < 0.2), - (play_df.home_wp_before >= 0.8) & (play_df.home_wp_before < 0.9), - (play_df.home_wp_before > 0.9), - ], - [0.6, 0.9, 0.9, 0.6], - default=1, - ) - play_df["non_fumble_sack"] = (play_df["sack_vec"] == True) & (play_df["fumble_vec"] == False) - - play_df["sack_epa"] = np.where(play_df["non_fumble_sack"] == True, play_df["qbr_epa"], np.NaN) - play_df["pass_epa"] = np.where(play_df["pass"] == True, play_df["qbr_epa"], np.NaN) - play_df["rush_epa"] = np.where(play_df["rush"] == True, play_df["qbr_epa"], np.NaN) - play_df["pen_epa"] = np.where(play_df["penalty_flag"] == True, play_df["qbr_epa"], np.NaN) - - play_df["sack_weight"] = np.where(play_df["non_fumble_sack"] == True, play_df["weight"], np.NaN) - play_df["pass_weight"] = np.where(play_df["pass"] == True, play_df["weight"], np.NaN) - play_df["rush_weight"] = np.where(play_df["rush"] == True, play_df["weight"], np.NaN) - play_df["pen_weight"] = np.where(play_df["penalty_flag"] == True, play_df["weight"], np.NaN) - - play_df["action_play"] = play_df.EPA != 0 - play_df["athlete_name"] = np.select( - [ - play_df.passer_player_name.notna(), - play_df.rusher_player_name.notna(), - ], - [play_df.passer_player_name, play_df.rusher_player_name], - default=None, + play_df = ( + play_df.with_columns( + qbr_epa=pl.when(pl.col("EPA") < -5.0) + .then(-5.0) + .when(pl.col("fumble_vec") == True) + .then(-3.5) + .otherwise(pl.col("EPA")), + weight=pl.when(pl.col("home_wp_before") < 0.1) + .then(0.6) + .when((pl.col("home_wp_before") >= 0.1).and_(pl.col("home_wp_before") < 0.2)) + .then(0.9) + .when((pl.col("home_wp_before") >= 0.8).and_(pl.col("home_wp_before") < 0.9)) + .then(0.9) + .when(pl.col("home_wp_before") > 0.9) + .then(0.6) + .otherwise(1), + non_fumble_sack=pl.when((pl.col("sack_vec") == True).and_(pl.col("fumble_vec") == False)) + .then(True) + .otherwise(False), + ) + .with_columns( + sack_epa=pl.when(pl.col("non_fumble_sack") == True).then(pl.col("qbr_epa")).otherwise(None), + pass_epa=pl.when(pl.col("pass") == True).then(pl.col("qbr_epa")).otherwise(None), + rush_epa=pl.when(pl.col("rush") == True).then(pl.col("qbr_epa")).otherwise(None), + pen_epa=pl.when(pl.col("penalty_flag") == True).then(pl.col("qbr_epa")).otherwise(None), + ) + .with_columns( + sack_weight=pl.when(pl.col("non_fumble_sack") == True).then(pl.col("weight")).otherwise(None), + pass_weight=pl.when(pl.col("pass") == True).then(pl.col("weight")).otherwise(None), + rush_weight=pl.when(pl.col("rush") == True).then(pl.col("weight")).otherwise(None), + pen_weight=pl.when(pl.col("penalty_flag") == True).then(pl.col("weight")).otherwise(None), + ) + .with_columns( + action_play=pl.col("EPA") != 0, + athlete_name=pl.when(pl.col("passer_player_name").is_not_null()) + .then(pl.col("passer_player_name")) + .when(pl.col("rusher_player_name").is_not_null()) + .then(pl.col("rusher_player_name")) + .otherwise(None), + ) ) return play_df def __process_wpa(self, play_df): # ---- prepare variables for wp_before calculations ---- - play_df["start.ExpScoreDiff_touchback"] = np.select( - [(play_df["type.text"].isin(kickoff_vec))], - [play_df["pos_score_diff_start"] + play_df["EP_start_touchback"]], - default=0.000, - ) - play_df["start.ExpScoreDiff"] = np.select( - [ - (play_df["penalty_in_text"] == True) & (play_df["type.text"] != "Penalty"), - (play_df["type.text"] == "Timeout") & (play_df["lag_scoringPlay"] == True), - ], - [ - play_df["pos_score_diff_start"] + play_df["EP_start"] - play_df["EP_between"], - (play_df["pos_score_diff_start"] + 0.92), - ], - default=play_df["pos_score_diff_start"] + play_df.EP_start, - ) - play_df["start.ExpScoreDiff_Time_Ratio_touchback"] = play_df["start.ExpScoreDiff_touchback"] / ( - play_df["start.adj_TimeSecsRem"] + 1 - ) - play_df["start.ExpScoreDiff_Time_Ratio"] = play_df["start.ExpScoreDiff"] / ( - play_df["start.adj_TimeSecsRem"] + 1 - ) - - # ---- prepare variables for wp_after calculations ---- - play_df["end.ExpScoreDiff"] = np.select( - [ - # Flips for Turnovers that aren't kickoffs - ( - ((play_df["type.text"].isin(end_change_vec)) | (play_df.downs_turnover == True)) - & (play_df.kickoff_play == False) - & (play_df["scoringPlay"] == False) + play_df = ( + play_df.with_columns( + pl.when(pl.col("type.text").is_in(kickoff_vec)) + .then(pl.col("pos_score_diff_start") + pl.col("EP_start_touchback")) + .otherwise(0.000) + .alias("start.ExpScoreDiff_touchback"), + pl.when((pl.col("penalty_in_text") == True).and_(pl.col("type.text").is_in(["Penalty"]) == False)) + .then(pl.col("pos_score_diff_start") + pl.col("EP_start") - pl.col("EP_between")) + .when((pl.col("type.text") == "Timeout").and_(pl.col("lag_scoringPlay") == True)) + .then(pl.col("pos_score_diff_start") + 0.92) + .otherwise(pl.col("pos_score_diff_start") + pl.col("EP_start")) + .alias("start.ExpScoreDiff"), + ) + .with_columns( + (pl.col("start.ExpScoreDiff_touchback") / (pl.col("start.adj_TimeSecsRem") + 1)).alias( + "start.ExpScoreDiff_Time_Ratio_touchback" ), - # Flips for Turnovers that are on kickoffs - (play_df["type.text"].isin(kickoff_turnovers)) & (play_df["scoringPlay"] == False), - (play_df["scoringPlay"] == False) & (play_df["type.text"] != "Timeout"), - (play_df["scoringPlay"] == False) & (play_df["type.text"] == "Timeout"), - (play_df["scoringPlay"] == True) - & (play_df["td_play"] == True) - & (play_df["type.text"].isin(defense_score_vec)) - & (play_df.season <= 2013), - (play_df["scoringPlay"] == True) - & (play_df["td_play"] == True) - & (play_df["type.text"].isin(offense_score_vec)) - & (play_df.season <= 2013), - (play_df["type.text"] == "Timeout") & (play_df["lag_scoringPlay"] == True) & (play_df.season <= 2013), - ], - [ - play_df["pos_score_diff_end"] - play_df.EP_end, - play_df["pos_score_diff_end"] + play_df.EP_end, - play_df["pos_score_diff_end"] + play_df.EP_end, - play_df["pos_score_diff_end"] + play_df.EP_end, - play_df["pos_score_diff_end"] + 0.92, - play_df["pos_score_diff_end"] + 0.92, - play_df["pos_score_diff_end"] + 0.92, - ], - default=play_df["pos_score_diff_end"], + (pl.col("start.ExpScoreDiff") / (pl.col("start.adj_TimeSecsRem") + 1)).alias( + "start.ExpScoreDiff_Time_Ratio" + ), + # ---- prepare variables for wp_after calculations ---- + pl.when( + ((pl.col("type.text").is_in(end_change_vec)).or_(pl.col("downs_turnover") == True)) + .and_(pl.col("kickoff_play") == False) + .and_(pl.col("scoringPlay") == False) + ) + .then(pl.col("pos_score_diff_end") - pl.col("EP_end")) + .when(pl.col("type.text").is_in(kickoff_turnovers).and_(pl.col("scoringPlay") == False)) + .then(pl.col("pos_score_diff_end") + pl.col("EP_end")) + .when((pl.col("scoringPlay") == False).and_(pl.col("type.text") != "Timeout")) + .then(pl.col("pos_score_diff_end") + pl.col("EP_end")) + .when((pl.col("scoringPlay") == False).and_(pl.col("type.text") == "Timeout")) + .then(pl.col("pos_score_diff_end") + pl.col("EP_end")) + .when( + (pl.col("scoringPlay") == True) + .and_(pl.col("td_play") == True) + .and_(pl.col("type.text").is_in(defense_score_vec)) + .and_(pl.col("season") <= 2013) + ) + .then(pl.col("pos_score_diff_end") - 0.92) + .when( + (pl.col("scoringPlay") == True) + .and_(pl.col("td_play") == True) + .and_(pl.col("type.text").is_in(offense_score_vec)) + .and_(pl.col("season") <= 2013) + ) + .then(pl.col("pos_score_diff_end") + 0.92) + .when( + (pl.col("type.text") == "Timeout") + .and_(pl.col("lag_scoringPlay") == True) + .and_(pl.col("season") <= 2013) + ) + .then(pl.col("pos_score_diff_end") + 0.92) + .otherwise(pl.col("pos_score_diff_end")) + .alias("end.ExpScoreDiff"), + ) + .with_columns( + (pl.col("end.ExpScoreDiff") / (pl.col("end.adj_TimeSecsRem") + 1)).alias("end.ExpScoreDiff_Time_Ratio") + ) ) - play_df["end.ExpScoreDiff_Time_Ratio"] = play_df["end.ExpScoreDiff"] / (play_df["end.adj_TimeSecsRem"] + 1) + # ---- wp_before ---- start_touchback_data = play_df[wp_start_touchback_columns] start_touchback_data.columns = wp_final_names @@ -3645,24 +3626,7 @@ def __process_wpa(self, play_df): # self.logger.info(start_data.iloc[[36]].to_json(orient="records")) dtest_start = DMatrix(start_data) WP_start = wp_model.predict(dtest_start) - play_df["wp_before"] = WP_start - play_df["wp_touchback"] = WP_start_touchback - play_df["wp_before"] = np.where( - play_df["type.text"].isin(kickoff_vec), - play_df["wp_touchback"], - play_df["wp_before"], - ) - play_df["def_wp_before"] = 1 - play_df.wp_before - play_df["home_wp_before"] = np.where( - play_df["start.pos_team.id"] == play_df["homeTeamId"], - play_df.wp_before, - play_df.def_wp_before, - ) - play_df["away_wp_before"] = np.where( - play_df["start.pos_team.id"] != play_df["homeTeamId"], - play_df.wp_before, - play_df.def_wp_before, - ) + # ---- wp_after ---- end_data = play_df[wp_end_columns] end_data.columns = wp_final_names @@ -3670,395 +3634,481 @@ def __process_wpa(self, play_df): dtest_end = DMatrix(end_data) WP_end = wp_model.predict(dtest_end) - play_df["lead_wp_before"] = play_df["wp_before"].shift(-1) - play_df["lead_wp_before2"] = play_df["wp_before"].shift(-2) - - play_df["wp_after"] = WP_end - game_complete = self.json["teamInfo"]["status"]["type"]["completed"] - play_df["wp_after"] = np.select( - [ - (play_df["type.text"] == "Timeout"), - game_complete - & ((play_df.lead_play_type.isna()) | (play_df.game_play_number == max(play_df.game_play_number))) - & (play_df.pos_score_diff_end > 0), - game_complete - & ((play_df.lead_play_type.isna()) | (play_df.game_play_number == max(play_df.game_play_number))) - & (play_df.pos_score_diff_end < 0), - (play_df.end_of_half == 1) - & (play_df["start.pos_team.id"] == play_df.lead_pos_team) - & (play_df["type.text"] != "Timeout"), - (play_df.end_of_half == 1) - & (play_df["start.pos_team.id"] != play_df["end.pos_team.id"]) - & (play_df["type.text"] != "Timeout"), - (play_df.end_of_half == 1) - & (play_df["start.pos_team_receives_2H_kickoff"] == False) - & (play_df["type.text"] == "Timeout"), - (play_df.lead_play_type.isin(["End Period", "End of Half"])) & (play_df.change_of_pos_team == 0), - (play_df.lead_play_type.isin(["End Period", "End of Half"])) & (play_df.change_of_pos_team == 1), - (play_df["kickoff_onside"] == True) - & (play_df["start.def_pos_team.id"] == play_df["end.pos_team.id"]), # onside recovery - (play_df["start.pos_team.id"] != play_df["end.pos_team.id"]), - ], - [ - play_df.wp_before, - 1.0, - 0.0, - play_df.lead_wp_before, - (1 - play_df.lead_wp_before), - play_df.wp_after, - play_df.lead_wp_before, - (1 - play_df.lead_wp_before), - play_df.wp_after, - (1 - play_df.wp_after), - ], - default=play_df.wp_after, - ) - - play_df["def_wp_after"] = 1 - play_df.wp_after - play_df["home_wp_after"] = np.where( - play_df["end.pos_team.id"] == play_df["homeTeamId"], - play_df.wp_after, - play_df.def_wp_after, - ) - play_df["away_wp_after"] = np.where( - play_df["end.pos_team.id"] != play_df["homeTeamId"], - play_df.wp_after, - play_df.def_wp_after, + play_df = ( + play_df.with_columns( + wp_before=pl.lit(WP_start), wp_touchback=pl.lit(WP_start_touchback), wp_after=pl.lit(WP_end) + ) + .with_columns( + wp_before=pl.when(pl.col("type.text").is_in(kickoff_vec)) + .then(pl.col("wp_touchback")) + .otherwise(pl.col("wp_before")), + ) + .with_columns( + def_wp_before=1 - pl.col("wp_before"), + ) + .with_columns( + home_wp_before=pl.when(pl.col("start.pos_team.id") == pl.col("homeTeamId")) + .then(pl.col("wp_before")) + .otherwise(pl.col("def_wp_before")), + away_wp_before=pl.when(pl.col("start.pos_team.id") != pl.col("homeTeamId")) + .then(pl.col("wp_before")) + .otherwise(pl.col("def_wp_before")), + ) + .with_columns( + lead_wp_before=pl.col("wp_before").shift(-1), + lead_wp_before2=pl.col("wp_before").shift(-2), + ) + .with_columns( + wp_after=pl.when(pl.col("type.text").is_in(["Timeout"])) + .then(pl.col("wp_before")) + .when( + (pl.col("status_type_completed") == True) + .and_( + (pl.col("lead_play_type").is_null()).or_( + pl.col("game_play_number") == pl.col("game_play_number").max() + ) + ) + .and_(pl.col("pos_score_diff_end") > 0) + ) + .then(1.0) + .when( + (pl.col("status_type_completed") == True) + .and_( + (pl.col("lead_play_type").is_null()).or_( + pl.col("game_play_number") == pl.col("game_play_number").max() + ) + ) + .and_(pl.col("pos_score_diff_end") < 0) + ) + .then(0.0) + .when( + (pl.col("end_of_half") == True) + .and_(pl.col("start.pos_team.id") == pl.col("lead_pos_team")) + .and_(pl.col("type.text") != "Timeout") + ) + .then(pl.col("lead_wp_before")) + .when( + (pl.col("end_of_half") == True) + .and_(pl.col("start.pos_team.id") != pl.col("end.pos_team.id")) + .and_(pl.col("type.text") != "Timeout") + ) + .then(1 - pl.col("lead_wp_before")) + .when( + (pl.col("end_of_half") == True) + .and_(pl.col("start.pos_team_receives_2H_kickoff") == False) + .and_(pl.col("type.text") == "Timeout") + ) + .then(pl.col("wp_after")) + .when( + (pl.col("lead_play_type").is_in(["End Period", "End of Half"])).and_( + pl.col("change_of_pos_team") == False + ) + ) + .then(pl.col("lead_wp_before")) + .when( + (pl.col("lead_play_type").is_in(["End Period", "End of Half"])).and_( + pl.col("change_of_pos_team") == True + ) + ) + .then(1 - pl.col("lead_wp_before")) + .when((pl.col("kickoff_onside") == True).and_(pl.col("change_of_pos_team") == True)) + .then(pl.col("wp_after")) + .when(pl.col("start.pos_team.id") != pl.col("end.pos_team.id")) + .then(1 - pl.col("wp_after")) + .otherwise(pl.col("wp_after")), + ) + .with_columns( + def_wp_after=1 - pl.col("wp_after"), + ) + .with_columns( + home_wp_after=pl.when(pl.col("end.pos_team.id") == pl.col("homeTeamId")) + .then(pl.col("wp_after")) + .otherwise(pl.col("def_wp_after")), + away_wp_after=pl.when(pl.col("end.pos_team.id") != pl.col("homeTeamId")) + .then(pl.col("wp_after")) + .otherwise(pl.col("def_wp_after")), + ) + .with_columns( + wpa=pl.col("wp_after") - pl.col("wp_before"), + ) ) - - play_df["wpa"] = play_df.wp_after - play_df.wp_before return play_df def __add_drive_data(self, play_df): base_groups = play_df.groupby(["drive.id"]) - play_df["drive_start"] = np.where( - play_df["start.pos_team.id"] == play_df["homeTeamId"], - 100 - play_df["drive.start.yardLine"], - play_df["drive.start.yardLine"], - ) - play_df["drive_stopped"] = play_df["drive.result"].str.contains( - "punt|fumble|interception|downs", regex=True, case=False - ) - play_df["drive_start"] = play_df["drive_start"].astype(float) - play_df["drive_play_index"] = base_groups["scrimmage_play"].apply(lambda x: x.cumsum()) - play_df["drive_offense_plays"] = np.where( - (play_df["sp"] == False) & (play_df["scrimmage_play"] == True), - play_df["play"].astype(int), - 0, - ) - play_df["prog_drive_EPA"] = base_groups["EPA_scrimmage"].apply(lambda x: x.cumsum()) - play_df["prog_drive_WPA"] = base_groups["wpa"].apply(lambda x: x.cumsum()) - play_df["drive_offense_yards"] = np.where( - (play_df["sp"] == False) & (play_df["scrimmage_play"] == True), - play_df["statYardage"], - 0, + play_df = ( + play_df.with_columns( + drive_start=pl.when(pl.col("start.pos_team.id") == pl.col("homeTeamId")) + .then(100 - pl.col("drive.start.yardLine")) + .otherwise(pl.col("drive.start.yardLine")), + drive_stopped=pl.when(pl.col("drive.result").is_null()) + .then(False) + .otherwise( + pl.col("drive.result").str.to_lowercase().str.contains(r"(?i)punt|fumble|interception|downs") + ), + ) + .with_columns( + drive_start=pl.col("drive_start").cast(pl.Float32), + ) + .with_columns( + drive_play_index=pl.col("scrimmage_play").cumsum().over("drive.id"), + ) + .with_columns( + drive_offense_plays=pl.when((pl.col("sp") == False).and_(pl.col("scrimmage_play") == True)) + .then(pl.col("play").cast(pl.Int32)) + .otherwise(0), + prog_drive_EPA=pl.col("EPA_scrimmage").cumsum().over("drive.id"), + prog_drive_WPA=pl.col("wpa").cumsum().over("drive.id"), + drive_offense_yards=pl.when((pl.col("sp") == False).and_(pl.col("scrimmage_play") == True)) + .then(pl.col("statYardage")) + .otherwise(0), + ) + .with_columns( + drive_total_yards=pl.col("drive_offense_yards").cumsum().over("drive.id"), + ) ) - play_df["drive_total_yards"] = play_df.groupby(["drive.id"])["drive_offense_yards"].apply(lambda x: x.cumsum()) return play_df - def create_box_score(self): + def __cast_box_score_column(self, play_df, column, target_type): + if column in play_df.columns: + play_df = play_df.with_columns(pl.col(column).cast(target_type).alias(column)) + else: + play_df = play_df.with_columns((pl.Null).alias(column)) + return play_df + + def create_box_score(self, play_df): + # have to run the pipeline before pulling this in if self.ran_pipeline == False: self.run_processing_pipeline() - # have to run the pipeline before pulling this in - self.plays_json["completion"] = self.plays_json["completion"].astype(float) - self.plays_json["pass_attempt"] = self.plays_json["pass_attempt"].astype(float) - self.plays_json["target"] = self.plays_json["target"].astype(float) - self.plays_json["yds_receiving"] = self.plays_json["yds_receiving"].astype(float) - self.plays_json["yds_rushed"] = self.plays_json["yds_rushed"].astype(float) - self.plays_json["rush"] = self.plays_json["rush"].astype(float) - self.plays_json["rush_td"] = self.plays_json["rush_td"].astype(float) - self.plays_json["pass"] = self.plays_json["pass"].astype(float) - self.plays_json["pass_td"] = self.plays_json["pass_td"].astype(float) - self.plays_json["EPA"] = self.plays_json["EPA"].astype(float) - self.plays_json["wpa"] = self.plays_json["wpa"].astype(float) - self.plays_json["int"] = self.plays_json["int"].astype(float) - self.plays_json["int_td"] = self.plays_json["int_td"].astype(float) - self.plays_json["def_EPA"] = self.plays_json["def_EPA"].astype(float) - self.plays_json["EPA_rush"] = self.plays_json["EPA_rush"].astype(float) - self.plays_json["EPA_pass"] = self.plays_json["EPA_pass"].astype(float) - self.plays_json["EPA_success"] = self.plays_json["EPA_success"].astype(float) - self.plays_json["EPA_success_pass"] = self.plays_json["EPA_success_pass"].astype(float) - self.plays_json["EPA_success_rush"] = self.plays_json["EPA_success_rush"].astype(float) - self.plays_json["EPA_success_standard_down"] = self.plays_json["EPA_success_standard_down"].astype(float) - self.plays_json["EPA_success_passing_down"] = self.plays_json["EPA_success_passing_down"].astype(float) - self.plays_json["middle_8"] = self.plays_json["middle_8"].astype(float) - self.plays_json["rz_play"] = self.plays_json["rz_play"].astype(float) - self.plays_json["scoring_opp"] = self.plays_json["scoring_opp"].astype(float) - self.plays_json["stuffed_run"] = self.plays_json["stuffed_run"].astype(float) - self.plays_json["stopped_run"] = self.plays_json["stopped_run"].astype(float) - self.plays_json["opportunity_run"] = self.plays_json["opportunity_run"].astype(float) - self.plays_json["highlight_run"] = self.plays_json["highlight_run"].astype(float) - self.plays_json["short_rush_success"] = self.plays_json["short_rush_success"].astype(float) - self.plays_json["short_rush_attempt"] = self.plays_json["short_rush_attempt"].astype(float) - self.plays_json["power_rush_success"] = self.plays_json["power_rush_success"].astype(float) - self.plays_json["power_rush_attempt"] = self.plays_json["power_rush_attempt"].astype(float) - self.plays_json["EPA_explosive"] = self.plays_json["EPA_explosive"].astype(float) - self.plays_json["EPA_explosive_pass"] = self.plays_json["EPA_explosive_pass"].astype(float) - self.plays_json["EPA_explosive_rush"] = self.plays_json["EPA_explosive_rush"].astype(float) - self.plays_json["standard_down"] = self.plays_json["standard_down"].astype(float) - self.plays_json["passing_down"] = self.plays_json["passing_down"].astype(float) - self.plays_json["fumble_vec"] = self.plays_json["fumble_vec"].astype(float) - self.plays_json["sack"] = self.plays_json["sack"].astype(float) - self.plays_json["penalty_flag"] = self.plays_json["penalty_flag"].astype(float) - self.plays_json["play"] = self.plays_json["play"].astype(float) - self.plays_json["scrimmage_play"] = self.plays_json["scrimmage_play"].astype(float) - self.plays_json["sp"] = self.plays_json["sp"].astype(float) - self.plays_json["kickoff_play"] = self.plays_json["kickoff_play"].astype(float) - self.plays_json["punt"] = self.plays_json["punt"].astype(float) - self.plays_json["fg_attempt"] = self.plays_json["fg_attempt"].astype(float) - self.plays_json["EPA_penalty"] = self.plays_json["EPA_penalty"].astype(float) - self.plays_json["EPA_sp"] = self.plays_json["EPA_sp"].astype(float) - self.plays_json["EPA_fg"] = self.plays_json["EPA_fg"].astype(float) - self.plays_json["EPA_punt"] = self.plays_json["EPA_punt"].astype(float) - self.plays_json["EPA_kickoff"] = self.plays_json["EPA_kickoff"].astype(float) - self.plays_json["TFL"] = self.plays_json["TFL"].astype(float) - self.plays_json["TFL_pass"] = self.plays_json["TFL_pass"].astype(float) - self.plays_json["TFL_rush"] = self.plays_json["TFL_rush"].astype(float) - self.plays_json["havoc"] = self.plays_json["havoc"].astype(float) - pass_box = self.plays_json[(self.plays_json["pass"] == True) & (self.plays_json.scrimmage_play == True)] - rush_box = self.plays_json[(self.plays_json["rush"] == True) & (self.plays_json.scrimmage_play == True)] + box_score_columns = [ + "completion", + "target", + "yds_receiving", + "yds_rushed", + "rush", + "rush_td", + "pass", + "pass_td", + "EPA", + "wpa", + "int", + "int_td", + "def_EPA", + "EPA_rush", + "EPA_pass", + "EPA_success", + "EPA_success_pass", + "EPA_success_rush", + "EPA_success_standard_down", + "EPA_success_passing_down", + "middle_8", + "rz_play", + "scoring_opp", + "stuffed_run", + "stopped_run", + "opportunity_run", + "highlight_run", + "short_rush_success", + "short_rush_attempt", + "power_rush_success", + "power_rush_attempt", + "EPA_explosive", + "EPA_explosive_pass", + "EPA_explosive_rush", + "standard_down", + "passing_down", + "fumble_vec", + "sack", + "penalty_flag", + "play", + "scrimmage_play", + "sp", + "kickoff_play", + "punt", + "fg_attempt", + "EPA_penalty", + "EPA_sp", + "EPA_fg", + "EPA_punt", + "EPA_kickoff", + "TFL", + "TFL_pass", + "TFL_rush", + "havoc", + ] + for item in box_score_columns: + self.__cast_box_score_column(play_df, item, pl.Float32) + + pass_box = play_df.filter((pl.col("pass") == True) & (pl.col("scrimmage_play") == True)) + rush_box = play_df.filter((pl.col("rush") == True) & (pl.col("scrimmage_play") == True)) # pass_box.yds_receiving.fillna(0.0, inplace=True) passer_box = ( - pass_box[(pass_box["pass"] == True) & (pass_box["scrimmage_play"] == True)] - .fillna(0.0) - .groupby(by=["pos_team", "passer_player_name"], as_index=False) + pass_box.fill_null(0.0) + .groupby(by=["pos_team", "passer_player_name"]) .agg( - Comp=("completion", sum), - Att=("pass_attempt", sum), - Yds=("yds_receiving", sum), - Pass_TD=("pass_td", sum), - Int=("int", sum), - YPA=("yds_receiving", mean), - EPA=("EPA", sum), - EPA_per_Play=("EPA", mean), - WPA=("wpa", sum), - SR=("EPA_success", mean), - Sck=("sack_vec", sum), - ) - .round(2) + Comp=pl.col("completion").sum(), + Att=pl.col("pass_attempt").sum(), + Yds=pl.col("yds_receiving").sum(), + Pass_TD=pl.col("pass_td").sum(), + Int=pl.col("int").sum(), + YPA=pl.col("yds_receiving").mean(), + EPA=pl.col("EPA").sum(), + EPA_per_Play=pl.col("EPA").mean(), + WPA=pl.col("wpa").sum(), + SR=pl.col("EPA_success").mean(), + Sck=pl.col("sack_vec").sum(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns(pos_team=pl.col("pos_team").cast(pl.Int32)) ) - passer_box = passer_box.replace({np.nan: None}) - qbs_list = passer_box.passer_player_name.to_list() - - def weighted_mean(s, df, wcol): - s = s[s.notna() == True] - # self.logger.info(s) - if len(s) == 0: - return 0 - return np.average(s, weights=df.loc[s.index, wcol]) + # passer_box = passer_box.replace(pl.all(), pl.Null) + qbs_list = passer_box["passer_player_name"].to_list() - pass_qbr_box = self.plays_json[ - (self.plays_json.athlete_name.notna() == True) - & (self.plays_json.scrimmage_play == True) - & (self.plays_json.athlete_name.isin(qbs_list)) - ] - pass_qbr = pass_qbr_box.groupby(by=["pos_team", "athlete_name"], as_index=False).agg( - qbr_epa=("qbr_epa", partial(weighted_mean, df=pass_qbr_box, wcol="weight")), - sack_epa=("sack_epa", partial(weighted_mean, df=pass_qbr_box, wcol="sack_weight")), - pass_epa=("pass_epa", partial(weighted_mean, df=pass_qbr_box, wcol="pass_weight")), - rush_epa=("rush_epa", partial(weighted_mean, df=pass_qbr_box, wcol="rush_weight")), - pen_epa=("pen_epa", partial(weighted_mean, df=pass_qbr_box, wcol="pen_weight")), - spread=("start.pos_team_spread", lambda x: x.iloc[0]), + pass_qbr_box = play_df.filter( + (pl.col("athlete_name").is_not_null() == True) + & (pl.col("scrimmage_play") == True) + & (pl.col("athlete_name").is_in(qbs_list)) + ) + pass_qbr_box_df = pass_qbr_box.groupby(by=["pos_team", "athlete_name"]) + pass_qbr = ( + pass_qbr_box.groupby(by=["pos_team", "athlete_name"]) + .agg( + qbr_epa=(pl.col("qbr_epa") * pl.col("weight")).sum() / pl.col("weight").sum(), + sack_epa=(pl.col("sack_epa") * pl.col("sack_weight")).sum() / pl.col("sack_weight").sum(), + pass_epa=(pl.col("pass_epa") * pl.col("pass_weight")).sum() / pl.col("pass_weight").sum(), + rush_epa=(pl.col("rush_epa") * pl.col("rush_weight")).sum() / pl.col("rush_weight").sum(), + pen_epa=(pl.col("pen_epa") * pl.col("pen_weight")).sum() / pl.col("pen_weight").sum(), + spread=(pl.col("start.pos_team_spread").first()), + ) + .fill_null(0.0) + .with_columns(pos_team=pl.col("pos_team").cast(pl.Int32)) ) - # self.logger.info(pass_qbr) + # # self.logger.info(pass_qbr) dtest_qbr = DMatrix(pass_qbr[qbr_vars]) qbr_result = qbr_model.predict(dtest_qbr) - pass_qbr["exp_qbr"] = qbr_result - passer_box = pd.merge( - passer_box, pass_qbr, left_on=["passer_player_name", "pos_team"], right_on=["athlete_name", "pos_team"] + pass_qbr = pass_qbr.with_columns(exp_qbr=pl.lit(qbr_result)) + passer_box = passer_box.join( + pass_qbr, left_on=["passer_player_name", "pos_team"], right_on=["athlete_name", "pos_team"] ) rusher_box = ( - rush_box.fillna(0.0) - .groupby(by=["pos_team", "rusher_player_name"], as_index=False) + rush_box.fill_null(0.0) + .groupby(by=["pos_team", "rusher_player_name"]) .agg( - Car=("rush", sum), - Yds=("yds_rushed", sum), - Rush_TD=("rush_td", sum), - YPC=("yds_rushed", mean), - EPA=("EPA", sum), - EPA_per_Play=("EPA", mean), - WPA=("wpa", sum), - SR=("EPA_success", mean), - Fum=("fumble_vec", sum), - Fum_Lost=("fumble_lost", sum), - ) - .round(2) + Car=pl.col("rush").sum(), + Yds=pl.col("yds_rushed").sum(), + Rush_TD=pl.col("rush_td").sum(), + YPC=pl.col("yds_rushed").mean(), + EPA=pl.col("EPA").sum(), + EPA_per_Play=pl.col("EPA").mean(), + WPA=pl.col("wpa").sum(), + SR=pl.col("EPA_success").mean(), + Fum=pl.col("fumble_vec").sum(), + Fum_Lost=pl.col("fumble_lost").sum(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns(pos_team=pl.col("pos_team").cast(pl.Int32)) ) - rusher_box = rusher_box.replace({np.nan: None}) + # rusher_box = rusher_box.replace({np.nan: None}) receiver_box = ( - pass_box.groupby(by=["pos_team", "receiver_player_name"], as_index=False) + pass_box.fill_null(0.0) + .groupby(by=["pos_team", "receiver_player_name"]) .agg( - Rec=("completion", sum), - Tar=("target", sum), - Yds=("yds_receiving", sum), - Rec_TD=("pass_td", sum), - YPT=("yds_receiving", mean), - EPA=("EPA", sum), - EPA_per_Play=("EPA", mean), - WPA=("wpa", sum), - SR=("EPA_success", mean), - Fum=("fumble_vec", sum), - Fum_Lost=("fumble_lost", sum), - ) - .round(2) + Rec=pl.col("completion").sum(), + Tar=pl.col("target").sum(), + Yds=pl.col("yds_receiving").sum(), + Rec_TD=pl.col("pass_td").sum(), + YPT=pl.col("yds_receiving").mean(), + EPA=pl.col("EPA").sum(), + EPA_per_Play=pl.col("EPA").mean(), + WPA=pl.col("wpa").sum(), + SR=pl.col("EPA_success").mean(), + Fum=pl.col("fumble_vec").sum(), + Fum_Lost=pl.col("fumble_lost").sum(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns(pos_team=pl.col("pos_team").cast(pl.Int32)) ) - receiver_box = receiver_box.replace({np.nan: None}) team_base_box = ( - self.plays_json.groupby(by=["pos_team"], as_index=False) + play_df.groupby(by=["pos_team"]) .agg( - EPA_plays=("play", sum), - total_yards=("statYardage", sum), - EPA_overall_total=("EPA", sum), + EPA_plays=pl.col("play").sum(), + total_yards=pl.col("statYardage").sum(), + EPA_overall_total=pl.col("EPA").sum(), ) - .round(2) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns(pos_team=pl.col("pos_team").cast(pl.Int32)) ) team_pen_box = ( - self.plays_json[(self.plays_json.penalty_flag == True)] - .groupby(by=["pos_team"], as_index=False) + play_df.filter(pl.col("penalty_flag") == True) + .groupby(by=["pos_team"]) .agg( - total_pen_yards=("statYardage", sum), - EPA_penalty=("EPA_penalty", sum), + total_pen_yards=pl.col("statYardage").sum(), + EPA_penalty=pl.col("EPA_penalty").sum(), + penalty_first_downs_created=pl.col("penalty_1st_conv").sum(), + penalty_first_downs_created_rate=pl.col("penalty_1st_conv").mean(), ) - .round(2) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns(pos_team=pl.col("pos_team").cast(pl.Int32)) ) team_scrimmage_box = ( - self.plays_json[(self.plays_json.scrimmage_play == True)] - .groupby(by=["pos_team"], as_index=False) + play_df.filter(pl.col("scrimmage_play") == True) + .groupby(by=["pos_team"]) .agg( - scrimmage_plays=("scrimmage_play", sum), - EPA_overall_off=("EPA", sum), - EPA_overall_offense=("EPA", sum), - EPA_per_play=("EPA", mean), - EPA_non_explosive=("EPA_non_explosive", sum), - EPA_non_explosive_per_play=("EPA_non_explosive", mean), - EPA_explosive=("EPA_explosive", sum), - EPA_explosive_rate=("EPA_explosive", mean), - passes_rate=("pass", mean), - off_yards=("statYardage", sum), - total_off_yards=("statYardage", sum), - yards_per_play=("statYardage", mean), - ) - .round(2) + scrimmage_plays=pl.col("scrimmage_play").sum(), + EPA_overall_off=pl.col("EPA").sum(), + EPA_overall_offense=pl.col("EPA").sum(), + EPA_per_play=pl.col("EPA").mean(), + EPA_non_explosive=pl.col("EPA_non_explosive").sum(), + EPA_non_explosive_per_play=pl.col("EPA_non_explosive").mean(), + EPA_explosive=pl.col("EPA_explosive").sum(), + EPA_explosive_rate=pl.col("EPA_explosive").mean(), + passes_rate=pl.col("pass").mean(), + off_yards=pl.col("statYardage").sum(), + total_off_yards=pl.col("statYardage").sum(), + yards_per_play=pl.col("statYardage").mean(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns(pos_team=pl.col("pos_team").cast(pl.Int32)) ) team_sp_box = ( - self.plays_json[(self.plays_json.sp == True)] - .groupby(by=["pos_team"], as_index=False) + play_df.filter(pl.col("sp") == True) + .groupby(by=["pos_team"]) .agg( - special_teams_plays=("sp", sum), - EPA_sp=("EPA_sp", sum), - EPA_special_teams=("EPA_sp", sum), - EPA_fg=("EPA_fg", sum), - EPA_punt=("EPA_punt", sum), - kickoff_plays=("kickoff_play", sum), - EPA_kickoff=("EPA_kickoff", sum), - ) - .round(2) + special_teams_plays=pl.col("sp").sum(), + EPA_sp=pl.col("EPA_sp").sum(), + EPA_special_teams=pl.col("EPA_sp").sum(), + field_goals=pl.col("fg_attempt").sum(), + EPA_fg=pl.col("EPA_fg").sum(), + punt_plays=pl.col("punt_play").sum(), + EPA_punt=pl.col("EPA_punt").sum(), + kickoff_plays=pl.col("kickoff_play").sum(), + EPA_kickoff=pl.col("EPA_kickoff").sum(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns(pos_team=pl.col("pos_team").cast(pl.Int32)) ) team_scrimmage_box_pass = ( - self.plays_json[(self.plays_json["pass"] == True) & (self.plays_json["scrimmage_play"] == True)] - .fillna(0) - .groupby(by=["pos_team"], as_index=False) + play_df.filter((pl.col("pass") == True) & (pl.col("scrimmage_play") == True)) + .fill_null(0.0) + .groupby(by=["pos_team"]) .agg( - passes=("pass", sum), - pass_yards=("yds_receiving", sum), - yards_per_pass=("yds_receiving", mean), - EPA_passing_overall=("EPA", sum), - EPA_passing_per_play=("EPA", mean), - EPA_explosive_passing=("EPA_explosive", sum), - EPA_explosive_passing_rate=("EPA_explosive", mean), - EPA_non_explosive_passing=("EPA_non_explosive", sum), - EPA_non_explosive_passing_per_play=("EPA_non_explosive", mean), - ) - .round(2) + passes=pl.col("pass").sum(), + pass_yards=pl.col("yds_receiving").sum(), + yards_per_pass=pl.col("yds_receiving").mean(), + passing_first_downs_created=pl.col("first_down_created").sum(), + passing_first_downs_created_rate=pl.col("first_down_created").mean(), + EPA_passing_overall=pl.col("EPA").sum(), + EPA_passing_per_play=pl.col("EPA").mean(), + EPA_explosive_passing=pl.col("EPA_explosive").sum(), + EPA_explosive_passing_rate=pl.col("EPA_explosive").mean(), + EPA_non_explosive_passing=pl.col("EPA_non_explosive").sum(), + EPA_non_explosive_passing_per_play=pl.col("EPA_non_explosive").mean(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns(pos_team=pl.col("pos_team").cast(pl.Int32)) ) team_scrimmage_box_rush = ( - self.plays_json[(self.plays_json["rush"] == True) & (self.plays_json["scrimmage_play"] == True)] - .groupby(by=["pos_team"], as_index=False) + play_df.filter((pl.col("rush") == True) & (pl.col("scrimmage_play") == True)) + .fill_null(0.0) + .groupby(by=["pos_team"]) .agg( - EPA_rushing_overall=("EPA", sum), - EPA_rushing_per_play=("EPA", mean), - EPA_explosive_rushing=("EPA_explosive", sum), - EPA_explosive_rushing_rate=("EPA_explosive", mean), - EPA_non_explosive_rushing=("EPA_non_explosive", sum), - EPA_non_explosive_rushing_per_play=("EPA_non_explosive", mean), - rushes=("rush", sum), - rush_yards=("yds_rushed", sum), - yards_per_rush=("yds_rushed", mean), - rushing_power_rate=("power_rush_attempt", mean), - ) - .round(2) + rushes=pl.col("rush").sum(), + rush_yards=pl.col("yds_rushed").sum(), + yards_per_rush=pl.col("yds_rushed").mean(), + rushing_power_rate=pl.col("power_rush_attempt").mean(), + rushing_first_downs_created=pl.col("first_down_created").sum(), + rushing_first_downs_created_rate=pl.col("first_down_created").mean(), + EPA_rushing_overall=pl.col("EPA").sum(), + EPA_rushing_per_play=pl.col("EPA").mean(), + EPA_explosive_rushing=pl.col("EPA_explosive").sum(), + EPA_explosive_rushing_rate=pl.col("EPA_explosive").mean(), + EPA_non_explosive_rushing=pl.col("EPA_non_explosive").sum(), + EPA_non_explosive_rushing_per_play=pl.col("EPA_non_explosive").mean(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns(pos_team=pl.col("pos_team").cast(pl.Int32)) ) team_rush_base_box = ( - self.plays_json[(self.plays_json["scrimmage_play"] == True)] - .groupby(by=["pos_team"], as_index=False) + play_df.filter((pl.col("scrimmage_play") == True)) + .fill_null(0.0) + .groupby(by=["pos_team"]) .agg( - rushes_rate=("rush", mean), - first_downs_created=("first_down_created", sum), - first_downs_created_rate=("first_down_created", mean), + rushes_rate=pl.col("rush").mean(), + first_downs_created=pl.col("first_down_created").sum(), + first_downs_created_rate=pl.col("first_down_created").mean(), ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns(pos_team=pl.col("pos_team").cast(pl.Int32)) ) + team_rush_power_box = ( - self.plays_json[(self.plays_json["power_rush_attempt"] == True)] - .groupby(by=["pos_team"], as_index=False) + play_df.filter((pl.col("power_rush_attempt") == True) & (pl.col("scrimmage_play") == True)) + .fill_null(0.0) + .groupby(by=["pos_team"]) .agg( - EPA_rushing_power=("EPA", sum), - EPA_rushing_power_per_play=("EPA", mean), - rushing_power_success=("power_rush_success", sum), - rushing_power_success_rate=("power_rush_success", mean), - rushing_power=("power_rush_attempt", sum), + EPA_rushing_power=pl.col("EPA").sum(), + EPA_rushing_power_per_play=pl.col("EPA").mean(), + rushing_power_success=pl.col("power_rush_success").sum(), + rushing_power_success_rate=pl.col("power_rush_success").mean(), + rushing_power=pl.col("power_rush_attempt").sum(), ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns(pos_team=pl.col("pos_team").cast(pl.Int32)) + ) + + play_df = play_df.with_columns( + opp_highlight_yards=pl.col("opp_highlight_yards").cast(pl.Float32), + highlight_yards=pl.col("highlight_yards").cast(pl.Float32), + line_yards=pl.col("line_yards").cast(pl.Float32), + second_level_yards=pl.col("second_level_yards").cast(pl.Float32), + open_field_yards=pl.col("open_field_yards").cast(pl.Float32), ) - self.plays_json.opp_highlight_yards = self.plays_json.opp_highlight_yards.astype(float) - self.plays_json.highlight_yards = self.plays_json.highlight_yards.astype(float) - self.plays_json.line_yards = self.plays_json.line_yards.astype(float) - self.plays_json.second_level_yards = self.plays_json.second_level_yards.astype(float) - self.plays_json.open_field_yards = self.plays_json.open_field_yards.astype(float) team_rush_box = ( - self.plays_json[(self.plays_json["rush"] == True) & (self.plays_json["scrimmage_play"] == True)] - .fillna(0) - .groupby(by=["pos_team"], as_index=False) + play_df.filter((pl.col("rush") == True) & (pl.col("scrimmage_play") == True)) + .fill_null(0.0) + .groupby(by=["pos_team"]) .agg( - rushing_stuff=("stuffed_run", sum), - rushing_stuff_rate=("stuffed_run", mean), - rushing_stopped=("stopped_run", sum), - rushing_stopped_rate=("stopped_run", mean), - rushing_opportunity=("opportunity_run", sum), - rushing_opportunity_rate=("opportunity_run", mean), - rushing_highlight=("highlight_run", sum), - rushing_highlight_rate=("highlight_run", mean), - rushing_highlight_yards=("highlight_yards", sum), - line_yards=("line_yards", sum), - line_yards_per_carry=("line_yards", mean), - second_level_yards=("second_level_yards", sum), - open_field_yards=("open_field_yards", sum), - ) - .round(2) + rushing_stuff=pl.col("stuffed_run").sum(), + rushing_stuff_rate=pl.col("stuffed_run").mean(), + rushing_stopped=pl.col("stopped_run").sum(), + rushing_stopped_rate=pl.col("stopped_run").mean(), + rushing_opportunity=pl.col("opportunity_run").sum(), + rushing_opportunity_rate=pl.col("opportunity_run").mean(), + rushing_highlight=pl.col("highlight_run").sum(), + rushing_highlight_rate=pl.col("highlight_run").mean(), + rushing_highlight_yards=pl.col("highlight_yards").sum(), + line_yards=pl.col("line_yards").sum(), + line_yards_per_carry=pl.col("line_yards").mean(), + second_level_yards=pl.col("second_level_yards").sum(), + open_field_yards=pl.col("open_field_yards").sum(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns(pos_team=pl.col("pos_team").cast(pl.Int32)) ) team_rush_opp_box = ( - self.plays_json[ - (self.plays_json["rush"] == True) - & (self.plays_json["scrimmage_play"] == True) - & (self.plays_json.opportunity_run == True) - ] - .fillna(0) - .groupby(by=["pos_team"], as_index=False) + play_df.filter( + (pl.col("rush") == True) & (pl.col("scrimmage_play") == True) & (pl.col("opportunity_run") == True) + ) + .fill_null(0.0) + .groupby(by=["pos_team"]) .agg( - rushing_highlight_yards_per_opp=("opp_highlight_yards", mean), + rushing_highlight_yards_per_opp=pl.col("opp_highlight_yards").mean(), ) - .round(2) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns(pos_team=pl.col("pos_team").cast(pl.Int32)) ) team_data_frames = [ @@ -4073,166 +4123,235 @@ def weighted_mean(s, df, wcol): team_rush_power_box, team_rush_box, ] - team_box = reduce(lambda left, right: pd.merge(left, right, on=["pos_team"], how="outer"), team_data_frames) - team_box = team_box.replace({np.nan: None}) + team_box = reduce(lambda left, right: left.join(right, on=["pos_team"], how="outer"), team_data_frames) situation_box_normal = ( - self.plays_json[(self.plays_json.scrimmage_play == True)] - .groupby(by=["pos_team"], as_index=False) + play_df.filter(pl.col("scrimmage_play") == True) + .groupby(by=["pos_team"]) + .agg( + EPA_success=pl.col("EPA_success").sum(), + EPA_success_rate=pl.col("EPA_success").mean(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns(pos_team=pl.col("pos_team").cast(pl.Int32)) + ) + + situation_box_rz = ( + play_df.filter((pl.col("rz_play") == True) & (pl.col("scrimmage_play") == True)) + .groupby(by=["pos_team"]) .agg( - EPA_success=("EPA_success", sum), - EPA_success_rate=("EPA_success", mean), + EPA_success_rz=pl.col("EPA_success").sum(), + EPA_success_rate_rz=pl.col("EPA_success").mean(), ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns(pos_team=pl.col("pos_team").cast(pl.Int32)) + ) + + situation_box_third = ( + play_df.filter((pl.col("start.down") == 3) & (pl.col("scrimmage_play") == True)) + .groupby(by=["pos_team"]) + .agg( + EPA_success_third=pl.col("EPA_success").sum(), + EPA_success_rate_third=pl.col("EPA_success").mean(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns(pos_team=pl.col("pos_team").cast(pl.Int32)) ) situation_box_pass = ( - self.plays_json[(self.plays_json["pass"] == True) & (self.plays_json.scrimmage_play == True)] - .groupby(by=["pos_team"], as_index=False) + play_df.filter((pl.col("pass") == True) & (pl.col("scrimmage_play") == True)) + .groupby(by=["pos_team"]) .agg( - EPA_success_pass=("EPA_success", sum), - EPA_success_pass_rate=("EPA_success", mean), + EPA_success_pass=pl.col("EPA_success").sum(), + EPA_success_pass_rate=pl.col("EPA_success").mean(), ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns(pos_team=pl.col("pos_team").cast(pl.Int32)) ) situation_box_rush = ( - self.plays_json[(self.plays_json["rush"] == True) & (self.plays_json.scrimmage_play == True)] - .groupby(by=["pos_team"], as_index=False) + play_df.filter((pl.col("rush") == True) & (pl.col("scrimmage_play") == True)) + .groupby(by=["pos_team"]) .agg( - EPA_success_rush=("EPA_success", sum), - EPA_success_rush_rate=("EPA_success", mean), + EPA_success_rush=pl.col("EPA_success").sum(), + EPA_success_rush_rate=pl.col("EPA_success").mean(), ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns(pos_team=pl.col("pos_team").cast(pl.Int32)) ) situation_box_middle8 = ( - self.plays_json[(self.plays_json["middle_8"] == True) & (self.plays_json.scrimmage_play == True)] - .groupby(by=["pos_team"], as_index=False) + play_df.filter((pl.col("middle_8") == True) & (pl.col("scrimmage_play") == True)) + .groupby(by=["pos_team"]) .agg( - middle_8=("middle_8", sum), - middle_8_pass_rate=("pass", mean), - middle_8_rush_rate=("rush", mean), - EPA_middle_8=("EPA", sum), - EPA_middle_8_per_play=("EPA", mean), - EPA_middle_8_success=("EPA_success", sum), - EPA_middle_8_success_rate=("EPA_success", mean), + middle_8=pl.col("middle_8").sum(), + middle_8_pass_rate=pl.col("pass").mean(), + middle_8_rush_rate=pl.col("rush").mean(), + EPA_middle_8=pl.col("EPA").sum(), + EPA_middle_8_per_play=pl.col("EPA").mean(), + EPA_middle_8_success=pl.col("EPA_success").sum(), + EPA_middle_8_success_rate=pl.col("EPA_success").mean(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns( + pos_team=pl.col("pos_team").cast(pl.Int32), ) ) situation_box_middle8_pass = ( - self.plays_json[ - (self.plays_json["pass"] == True) - & (self.plays_json["middle_8"] == True) - & (self.plays_json.scrimmage_play == True) - ] - .groupby(by=["pos_team"], as_index=False) + play_df.filter( + (pl.col("pass") == True) & (pl.col("middle_8") == True) & (pl.col("scrimmage_play") == True) + ) + .groupby(by=["pos_team"]) .agg( - middle_8_pass=("pass", sum), - EPA_middle_8_pass=("EPA", sum), - EPA_middle_8_pass_per_play=("EPA", mean), - EPA_middle_8_success_pass=("EPA_success", sum), - EPA_middle_8_success_pass_rate=("EPA_success", mean), + middle_8_pass=pl.col("pass").sum(), + EPA_middle_8_pass=pl.col("EPA").sum(), + EPA_middle_8_pass_per_play=pl.col("EPA").mean(), + EPA_middle_8_success_pass=pl.col("EPA_success").sum(), + EPA_middle_8_success_pass_rate=pl.col("EPA_success").mean(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns( + pos_team=pl.col("pos_team").cast(pl.Int32), ) ) situation_box_middle8_rush = ( - self.plays_json[ - (self.plays_json["rush"] == True) - & (self.plays_json["middle_8"] == True) - & (self.plays_json.scrimmage_play == True) - ] - .groupby(by=["pos_team"], as_index=False) + play_df.filter( + (pl.col("rush") == True) & (pl.col("middle_8") == True) & (pl.col("scrimmage_play") == True) + ) + .groupby(by=["pos_team"]) .agg( - middle_8_rush=("rush", sum), - EPA_middle_8_rush=("EPA", sum), - EPA_middle_8_rush_per_play=("EPA", mean), - EPA_middle_8_success_rush=("EPA_success", sum), - EPA_middle_8_success_rush_rate=("EPA_success", mean), + middle_8_rush=pl.col("rush").sum(), + EPA_middle_8_rush=pl.col("EPA").sum(), + EPA_middle_8_rush_per_play=pl.col("EPA").mean(), + EPA_middle_8_success_rush=pl.col("EPA_success").sum(), + EPA_middle_8_success_rush_rate=pl.col("EPA_success").mean(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns( + pos_team=pl.col("pos_team").cast(pl.Int32), ) ) situation_box_early = ( - self.plays_json[(self.plays_json.early_down == True)] - .groupby(by=["pos_team"], as_index=False) + play_df.filter((pl.col("early_down") == True) & (pl.col("scrimmage_play") == True)) + .groupby(by=["pos_team"]) .agg( - EPA_success_early_down=("EPA_success", sum), - EPA_success_early_down_rate=("EPA_success", mean), - early_downs=("early_down", sum), - early_down_pass_rate=("pass", mean), - early_down_rush_rate=("rush", mean), - EPA_early_down=("EPA", sum), - EPA_early_down_per_play=("EPA", mean), - early_down_first_down=("first_down_created", sum), - early_down_first_down_rate=("first_down_created", mean), + EPA_success_early_down=pl.col("EPA_success").sum(), + EPA_success_early_down_rate=pl.col("EPA_success").mean(), + early_downs=pl.col("early_down").sum(), + early_down_pass_rate=pl.col("pass").mean(), + early_down_rush_rate=pl.col("rush").mean(), + EPA_early_down=pl.col("EPA").sum(), + EPA_early_down_per_play=pl.col("EPA").mean(), + early_down_first_down=pl.col("first_down_created").sum(), + early_down_first_down_rate=pl.col("first_down_created").mean(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns( + pos_team=pl.col("pos_team").cast(pl.Int32), ) ) situation_box_early_pass = ( - self.plays_json[(self.plays_json["pass"] == True) & (self.plays_json.early_down == True)] - .groupby(by=["pos_team"], as_index=False) + play_df.filter( + (pl.col("pass") == True) & (pl.col("early_down") == True) & (pl.col("scrimmage_play") == True) + ) + .groupby(by=["pos_team"]) .agg( - early_down_pass=("pass", sum), - EPA_early_down_pass=("EPA", sum), - EPA_early_down_pass_per_play=("EPA", mean), - EPA_success_early_down_pass=("EPA_success", sum), - EPA_success_early_down_pass_rate=("EPA_success", mean), + early_down_pass=pl.col("pass").sum(), + EPA_early_down_pass=pl.col("EPA").sum(), + EPA_early_down_pass_per_play=pl.col("EPA").mean(), + EPA_success_early_down_pass=pl.col("EPA_success").sum(), + EPA_success_early_down_pass_rate=pl.col("EPA_success").mean(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns( + pos_team=pl.col("pos_team").cast(pl.Int32), ) ) situation_box_early_rush = ( - self.plays_json[(self.plays_json["rush"] == True) & (self.plays_json.early_down == True)] - .groupby(by=["pos_team"], as_index=False) + play_df.filter( + (pl.col("rush") == True) & (pl.col("early_down") == True) & (pl.col("scrimmage_play") == True) + ) + .groupby(by=["pos_team"]) .agg( - early_down_rush=("rush", sum), - EPA_early_down_rush=("EPA", sum), - EPA_early_down_rush_per_play=("EPA", mean), - EPA_success_early_down_rush=("EPA_success", sum), - EPA_success_early_down_rush_rate=("EPA_success", mean), + early_down_rush=pl.col("rush").sum(), + EPA_early_down_rush=pl.col("EPA").sum(), + EPA_early_down_rush_per_play=pl.col("EPA").mean(), + EPA_success_early_down_rush=pl.col("EPA_success").sum(), + EPA_success_early_down_rush_rate=pl.col("EPA_success").mean(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns( + pos_team=pl.col("pos_team").cast(pl.Int32), ) ) situation_box_late = ( - self.plays_json[(self.plays_json.late_down == True)] - .groupby(by=["pos_team"], as_index=False) + play_df.filter((pl.col("late_down") == True) & (pl.col("scrimmage_play") == True)) + .groupby(by=["pos_team"]) .agg( - EPA_success_late_down=("EPA_success_late_down", sum), - EPA_success_late_down_pass=("EPA_success_late_down_pass", sum), - EPA_success_late_down_rush=("EPA_success_late_down_rush", sum), - late_downs=("late_down", sum), - late_down_pass=("late_down_pass", sum), - late_down_rush=("late_down_rush", sum), - EPA_late_down=("EPA", sum), - EPA_late_down_per_play=("EPA", mean), - EPA_success_late_down_rate=("EPA_success_late_down", mean), - EPA_success_late_down_pass_rate=("EPA_success_late_down_pass", mean), - EPA_success_late_down_rush_rate=("EPA_success_late_down_rush", mean), - late_down_pass_rate=("late_down_pass", mean), - late_down_rush_rate=("late_down_rush", mean), + EPA_success_late_down=pl.col("EPA_success_late_down").sum(), + EPA_success_late_down_pass=pl.col("EPA_success_late_down_pass").sum(), + EPA_success_late_down_rush=pl.col("EPA_success_late_down_rush").sum(), + late_downs=pl.col("late_down").sum(), + late_down_pass=pl.col("late_down_pass").sum(), + late_down_rush=pl.col("late_down_rush").sum(), + EPA_late_down=pl.col("EPA").sum(), + EPA_late_down_per_play=pl.col("EPA").mean(), + EPA_success_late_down_rate=pl.col("EPA_success_late_down").mean(), + EPA_success_late_down_pass_rate=pl.col("EPA_success_late_down_pass").mean(), + EPA_success_late_down_rush_rate=pl.col("EPA_success_late_down_rush").mean(), + late_down_pass_rate=pl.col("late_down_pass").mean(), + late_down_rush_rate=pl.col("late_down_rush").mean(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns( + pos_team=pl.col("pos_team").cast(pl.Int32), ) ) situation_box_standard = ( - self.plays_json[self.plays_json.standard_down == True] - .groupby(by=["pos_team"], as_index=False) + play_df.filter((pl.col("standard_down") == True) & (pl.col("scrimmage_play") == True)) + .groupby(by=["pos_team"]) .agg( - EPA_success_standard_down=("EPA_success_standard_down", sum), - EPA_success_standard_down_rate=("EPA_success_standard_down", mean), - EPA_standard_down=("EPA_success_standard_down", sum), - EPA_standard_down_per_play=("EPA_success_standard_down", mean), + EPA_success_standard_down=pl.col("EPA_success").sum(), + EPA_success_standard_down_rate=pl.col("EPA_success").mean(), + EPA_standard_down=pl.col("EPA").sum(), + EPA_standard_down_per_play=pl.col("EPA").mean(), + standard_downs=pl.col("standard_down").sum(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns( + pos_team=pl.col("pos_team").cast(pl.Int32), ) ) + situation_box_passing = ( - self.plays_json[self.plays_json.passing_down == True] - .groupby(by=["pos_team"], as_index=False) + play_df.filter((pl.col("passing_down") == True) & (pl.col("scrimmage_play") == True)) + .groupby(by=["pos_team"]) .agg( - EPA_success_passing_down=("EPA_success_passing_down", sum), - EPA_success_passing_down_rate=("EPA_success_passing_down", mean), - EPA_passing_down=("EPA_success_standard_down", sum), - EPA_passing_down_per_play=("EPA_success_standard_down", mean), + EPA_success_passing_down=pl.col("EPA_success").sum(), + EPA_success_passing_down_rate=pl.col("EPA_success").mean(), + EPA_passing_down=pl.col("EPA").sum(), + EPA_passing_down_per_play=pl.col("EPA").mean(), + passing_downs=pl.col("passing_down").sum(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns( + pos_team=pl.col("pos_team").cast(pl.Int32), ) ) + situation_data_frames = [ situation_box_normal, situation_box_pass, situation_box_rush, + situation_box_rz, + situation_box_third, situation_box_early, situation_box_early_pass, situation_box_early_rush, @@ -4243,94 +4362,108 @@ def weighted_mean(s, df, wcol): situation_box_standard, situation_box_passing, ] + situation_box = reduce( - lambda left, right: pd.merge(left, right, on=["pos_team"], how="outer"), situation_data_frames + lambda left, right: left.join(right, on=["pos_team"], how="outer"), situation_data_frames + ) + + play_df = play_df.with_columns( + drive_stopped=pl.col("drive_stopped").cast(pl.Float32), + drive_start=pl.col("drive_start").cast(pl.Float32), ) - situation_box = situation_box.replace({np.nan: None}) - self.plays_json.drive_stopped = self.plays_json.drive_stopped.astype(float) def_base_box = ( - self.plays_json[(self.plays_json.scrimmage_play == True)] - .groupby(by=["def_pos_team"], as_index=False) + play_df.filter(pl.col("scrimmage_play") == True) + .groupby(by=["def_pos_team"]) .agg( - scrimmage_plays=("scrimmage_play", sum), - TFL=("TFL", sum), - TFL_pass=("TFL_pass", sum), - TFL_rush=("TFL_rush", sum), - havoc_total=("havoc", sum), - havoc_total_rate=("havoc", mean), - fumbles=("forced_fumble", sum), - def_int=("int", sum), - drive_stopped_rate=("drive_stopped", mean), + scrimmage_plays=pl.col("scrimmage_play").sum(), + TFL=pl.col("TFL").sum(), + TFL_pass=pl.col("TFL_pass").sum(), + TFL_rush=pl.col("TFL_rush").sum(), + havoc_total=pl.col("havoc").sum(), + havoc_total_rate=pl.col("havoc").mean(), + fumbles=pl.col("forced_fumble").sum(), + def_int=pl.col("int").sum(), + drive_stopped_rate=100 * pl.col("drive_stopped").mean(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns( + def_pos_team=pl.col("def_pos_team").cast(pl.Int32), ) ) - def_base_box.drive_stopped_rate = 100 * def_base_box.drive_stopped_rate - def_base_box = def_base_box.replace({np.nan: None}) def_box_havoc_pass = ( - self.plays_json[(self.plays_json.scrimmage_play == True) & (self.plays_json["pass"] == True)] - .groupby(by=["def_pos_team"], as_index=False) + play_df.filter((pl.col("pass") == True) & (pl.col("scrimmage_play") == True)) + .groupby(by=["def_pos_team"]) .agg( - num_pass_plays=("pass", sum), - havoc_total_pass=("havoc", sum), - havoc_total_pass_rate=("havoc", mean), - sacks=("sack_vec", sum), - sacks_rate=("sack_vec", mean), - pass_breakups=("pass_breakup", sum), + num_pass_plays=pl.col("pass").sum(), + havoc_total_pass=pl.col("havoc").sum(), + havoc_total_pass_rate=pl.col("havoc").mean(), + sacks=pl.col("sack_vec").sum(), + sacks_rate=pl.col("sack_vec").mean(), + pass_breakups=pl.col("pass_breakup").sum(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns( + def_pos_team=pl.col("def_pos_team").cast(pl.Int32), ) ) - def_box_havoc_pass = def_box_havoc_pass.replace({np.nan: None}) def_box_havoc_rush = ( - self.plays_json[(self.plays_json.scrimmage_play == True) & (self.plays_json["rush"] == True)] - .groupby(by=["def_pos_team"], as_index=False) + play_df.filter((pl.col("rush") == True) & (pl.col("scrimmage_play") == True)) + .groupby(by=["def_pos_team"]) .agg( - havoc_total_rush=("havoc", sum), - havoc_total_rush_rate=("havoc", mean), + havoc_total_rush=pl.col("havoc").sum(), + havoc_total_rush_rate=pl.col("havoc").mean(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns( + def_pos_team=pl.col("def_pos_team").cast(pl.Int32), ) ) - def_box_havoc_rush = def_box_havoc_rush.replace({np.nan: None}) def_data_frames = [def_base_box, def_box_havoc_pass, def_box_havoc_rush] - def_box = reduce(lambda left, right: pd.merge(left, right, on=["def_pos_team"], how="outer"), def_data_frames) - def_box = def_box.replace({np.nan: None}) - def_box_json = json.loads(def_box.to_json(orient="records")) + def_box = reduce(lambda left, right: left.join(right, on=["def_pos_team"], how="outer"), def_data_frames) + def_box_json = json.loads(def_box.write_json(row_oriented=True)) turnover_box = ( - self.plays_json[(self.plays_json.scrimmage_play == True)] - .groupby(by=["pos_team"], as_index=False) + play_df.filter(pl.col("scrimmage_play") == True) + .groupby(by=["pos_team"]) .agg( - fumbles_lost=("fumble_lost", sum), - fumbles_recovered=("fumble_recovered", sum), - total_fumbles=("fumble_vec", sum), - Int=("int", sum), + pass_breakups=pl.col("pass_breakup").sum(), + fumbles_lost=pl.col("fumble_lost").sum(), + fumbles_recovered=pl.col("fumble_recovered").sum(), + total_fumbles=pl.col("fumble_vec").sum(), + Int=pl.col("int").sum(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns( + pos_team=pl.col("pos_team").cast(pl.Int32), ) - .round(2) ) - turnover_box = turnover_box.replace({np.nan: None}) - turnover_box_json = json.loads(turnover_box.to_json(orient="records")) + turnover_box_json = json.loads(turnover_box.write_json(row_oriented=True)) if len(turnover_box_json) < 2: for i in range(len(turnover_box_json), 2): turnover_box_json.append({}) - total_fumbles = reduce(lambda x, y: x + y, map(lambda x: x.get("total_fumbles", 0), turnover_box_json)) + turnover_box_json[0]["Int"] = int(turnover_box_json[0].get("Int", 0)) + turnover_box_json[1]["Int"] = int(turnover_box_json[1].get("Int", 0)) - away_passes_def = turnover_box_json[1].get("pass_breakups", 0) + away_passes_def = turnover_box_json[0].get("pass_breakups", 0) away_passes_int = turnover_box_json[0].get("Int", 0) - turnover_box_json[0]["expected_turnovers"] = (0.5 * total_fumbles) + ( + away_fumbles = turnover_box_json[0].get("total_fumbles", 0) + turnover_box_json[0]["expected_turnovers"] = (0.5 * away_fumbles) + ( 0.22 * (away_passes_def + away_passes_int) ) - home_passes_def = turnover_box_json[0].get("pass_breakups", 0) + home_passes_def = turnover_box_json[1].get("pass_breakups", 0) home_passes_int = turnover_box_json[1].get("Int", 0) - turnover_box_json[1]["expected_turnovers"] = (0.5 * total_fumbles) + ( + home_fumbles = turnover_box_json[1].get("total_fumbles", 0) + turnover_box_json[1]["expected_turnovers"] = (0.5 * home_fumbles) + ( 0.22 * (home_passes_def + home_passes_int) ) - turnover_box_json[0]["Int"] = int(turnover_box_json[0].get("Int", 0)) - turnover_box_json[1]["Int"] = int(turnover_box_json[1].get("Int", 0)) - turnover_box_json[0]["expected_turnover_margin"] = ( turnover_box_json[1]["expected_turnovers"] - turnover_box_json[0]["expected_turnovers"] ) @@ -4338,8 +4471,8 @@ def weighted_mean(s, df, wcol): turnover_box_json[0]["expected_turnovers"] - turnover_box_json[1]["expected_turnovers"] ) - away_to = turnover_box_json[0]["fumbles_lost"] + turnover_box_json[0]["Int"] - home_to = turnover_box_json[1]["fumbles_lost"] + turnover_box_json[1]["Int"] + away_to = turnover_box_json[0].get("fumbles_lost", 0) + turnover_box_json[0]["Int"] + home_to = turnover_box_json[1].get("fumbles_lost", 0) + turnover_box_json[1]["Int"] turnover_box_json[0]["turnovers"] = away_to turnover_box_json[1]["turnovers"] = home_to @@ -4354,43 +4487,43 @@ def weighted_mean(s, df, wcol): turnover_box_json[1]["turnover_margin"] - turnover_box_json[1]["expected_turnover_margin"] ) - self.plays_json.drive_start = self.plays_json.drive_start.astype(float) drives_data = ( - self.plays_json[(self.plays_json.scrimmage_play == True)] - .groupby(by=["pos_team"], as_index=False) + play_df.filter(pl.col("scrimmage_play") == True) + .groupby(by=["pos_team"]) .agg( - drive_total_available_yards=("drive_start", sum), - drive_total_gained_yards=("drive.yards", sum), - avg_field_position=("drive_start", mean), - plays_per_drive=("drive.offensivePlays", mean), - yards_per_drive=("drive.yards", mean), - drives=("drive.id", pd.Series.nunique), + drive_total_available_yards=pl.col("drive_start").sum(), + drive_total_gained_yards=pl.col("drive.yards").sum(), + avg_field_position=pl.col("drive_start").mean(), + plays_per_drive=pl.col("drive.offensivePlays").mean(), + yards_per_drive=pl.col("drive.yards").mean(), + drives=pl.col("drive.id").n_unique(), + drive_total_gained_yards_rate=100 * pl.col("drive.yards").sum() / pl.col("drive_start").sum(), + ) + .with_columns(pl.col(pl.Float32).round(2)) + .with_columns( + pos_team=pl.col("pos_team").cast(pl.Int32), ) ) - drives_data["drive_total_gained_yards_rate"] = ( - 100 * drives_data.drive_total_gained_yards / drives_data.drive_total_available_yards - ).round(2) return { - "pass": json.loads(passer_box.to_json(orient="records")), - "rush": json.loads(rusher_box.to_json(orient="records")), - "receiver": json.loads(receiver_box.to_json(orient="records")), - "team": json.loads(team_box.to_json(orient="records")), - "situational": json.loads(situation_box.to_json(orient="records")), + "pass": json.loads(passer_box.write_json(row_oriented=True)), + "rush": json.loads(rusher_box.write_json(row_oriented=True)), + "receiver": json.loads(receiver_box.write_json(row_oriented=True)), + "team": json.loads(team_box.write_json(row_oriented=True)), + "situational": json.loads(situation_box.write_json(row_oriented=True)), "defensive": def_box_json, "turnover": turnover_box_json, - "drives": json.loads(drives_data.to_json(orient="records")), + "drives": json.loads(drives_data.write_json(row_oriented=True)), } def run_processing_pipeline(self): if self.ran_pipeline == False: pbp_txt = self.__helper_nfl_pbp_drives(self.json) - pbp_txt["plays"]["week"] = pbp_txt["header"]["week"] self.plays_json = pbp_txt["plays"] pbp_json = { - "gameId": self.gameId, - "plays": np.array(self.plays_json).tolist(), + "gameId": int(self.gameId), + "plays": self.plays_json, "season": pbp_txt["season"], "week": pbp_txt["header"]["week"], "gameInfo": pbp_txt["gameInfo"], @@ -4413,27 +4546,31 @@ def run_processing_pipeline(self): "videos": np.array(pbp_txt["videos"]).tolist(), } self.json = pbp_json - self.plays_json = pd.DataFrame(pbp_txt["plays"].to_dict(orient="records")) + self.plays_json = pbp_txt["plays"] if pbp_json.get("header").get("competitions")[0].get("playByPlaySource") != "none": - self.plays_json = self.__add_downs_data(self.plays_json) - self.plays_json = self.__add_play_type_flags(self.plays_json) - self.plays_json = self.__add_rush_pass_flags(self.plays_json) - self.plays_json = self.__add_team_score_variables(self.plays_json) - self.plays_json = self.__add_new_play_types(self.plays_json) - self.plays_json = self.__setup_penalty_data(self.plays_json) - self.plays_json = self.__add_play_category_flags(self.plays_json) - self.plays_json = self.__add_yardage_cols(self.plays_json) - self.plays_json = self.__add_player_cols(self.plays_json) - self.plays_json = self.__after_cols(self.plays_json) - self.plays_json = self.__add_spread_time(self.plays_json) - self.plays_json = self.__process_epa(self.plays_json) - self.plays_json = self.__process_wpa(self.plays_json) - self.plays_json = self.__add_drive_data(self.plays_json) - self.plays_json = self.__process_qbr(self.plays_json) - self.plays_json = self.plays_json.replace({np.nan: None}) + self.plays_json = ( + self.plays_json.pipe(self.__add_downs_data) + .pipe(self.__add_play_type_flags) + .pipe(self.__add_rush_pass_flags) + .pipe(self.__add_team_score_variables) + .pipe(self.__add_new_play_types) + .pipe(self.__setup_penalty_data) + .pipe(self.__add_play_category_flags) + .pipe(self.__add_yardage_cols) + .pipe(self.__add_player_cols) + .pipe(self.__after_cols) + .pipe(self.__add_spread_time) + .pipe(self.__process_epa) + .pipe(self.__process_wpa) + .pipe(self.__add_drive_data) + .pipe(self.__process_qbr) + ) + self.ran_pipeline = True + advBoxScore = self.create_box_score(self.plays_json) + self.plays_json = self.plays_json.to_dicts() pbp_json = { - "gameId": self.gameId, - "plays": self.plays_json.to_dict(orient="records"), + "gameId": int(self.gameId), + "plays": self.plays_json, "season": pbp_txt["season"], "week": pbp_txt["header"]["week"], "gameInfo": pbp_txt["gameInfo"], @@ -4441,6 +4578,7 @@ def run_processing_pipeline(self): "playByPlaySource": pbp_txt["playByPlaySource"], "drives": pbp_txt["drives"], "boxscore": pbp_txt["boxscore"], + "advBoxScore": advBoxScore, "header": pbp_txt["header"], "standings": pbp_txt["standings"], "leaders": np.array(pbp_txt["leaders"]).tolist(), @@ -4457,17 +4595,16 @@ def run_processing_pipeline(self): } self.json = pbp_json self.ran_pipeline = True - return self.json + return self.json if self.return_keys is None else {k: self.json[k] for k in self.return_keys} def run_cleaning_pipeline(self): if self.ran_cleaning_pipeline == False: pbp_txt = self.__helper_nfl_pbp_drives(self.json) - pbp_txt["plays"]["week"] = pbp_txt["header"]["week"] self.plays_json = pbp_txt["plays"] pbp_json = { - "gameId": self.gameId, - "plays": np.array(self.plays_json).tolist(), + "gameId": int(self.gameId), + "plays": self.plays_json, "season": pbp_txt["season"], "week": pbp_txt["header"]["week"], "gameInfo": pbp_txt["gameInfo"], @@ -4490,23 +4627,25 @@ def run_cleaning_pipeline(self): "videos": np.array(pbp_txt["videos"]).tolist(), } self.json = pbp_json - self.plays_json = pd.DataFrame(pbp_txt["plays"].to_dict(orient="records")) + self.plays_json = pbp_txt["plays"] if pbp_json.get("header").get("competitions")[0].get("playByPlaySource") != "none": - self.plays_json = self.__add_downs_data(self.plays_json) - self.plays_json = self.__add_play_type_flags(self.plays_json) - self.plays_json = self.__add_rush_pass_flags(self.plays_json) - self.plays_json = self.__add_team_score_variables(self.plays_json) - self.plays_json = self.__add_new_play_types(self.plays_json) - self.plays_json = self.__setup_penalty_data(self.plays_json) - self.plays_json = self.__add_play_category_flags(self.plays_json) - self.plays_json = self.__add_yardage_cols(self.plays_json) - self.plays_json = self.__add_player_cols(self.plays_json) - self.plays_json = self.__after_cols(self.plays_json) - self.plays_json = self.__add_spread_time(self.plays_json) - self.plays_json = self.plays_json.replace({np.nan: None}) + self.plays_json = ( + self.plays_json.pipe(self.__add_downs_data) + .pipe(self.__add_play_type_flags) + .pipe(self.__add_rush_pass_flags) + .pipe(self.__add_team_score_variables) + .pipe(self.__add_new_play_types) + .pipe(self.__setup_penalty_data) + .pipe(self.__add_play_category_flags) + .pipe(self.__add_yardage_cols) + .pipe(self.__add_player_cols) + .pipe(self.__after_cols) + .pipe(self.__add_spread_time) + ) + self.plays_json = self.plays_json.to_dicts() pbp_json = { - "gameId": self.gameId, - "plays": self.plays_json.to_dict(orient="records"), + "gameId": int(self.gameId), + "plays": self.plays_json, "season": pbp_txt["season"], "week": pbp_txt["header"]["week"], "gameInfo": pbp_txt["gameInfo"], @@ -4530,4 +4669,4 @@ def run_cleaning_pipeline(self): } self.json = pbp_json self.ran_cleaning_pipeline = True - return self.json + return self.json From 4fe13998862de408b73a10b549b62e8599c2f4a3 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Mon, 31 Jul 2023 16:38:15 -0400 Subject: [PATCH 56/79] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 29fa79b..8738e30 100755 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ dist .ipynb_checkpoints ./.ipynb_checkpoints */.ipynb_checkpoints +*.egg-info From 3651508e8fa5acb717bd87e8d35268a11da310eb Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Mon, 31 Jul 2023 16:43:55 -0400 Subject: [PATCH 57/79] nfl_pbp was missing a few params --- .../_build/doctrees/environment.pickle | Bin 934634 -> 935061 bytes .../doctrees/sportsdataverse.nfl.doctree | Bin 207389 -> 210375 bytes .../_build/markdown/sportsdataverse.nfl.md | 8 +++++--- docs/docs/nfl/index.md | 8 +++++--- sportsdataverse/nfl/nfl_pbp.py | 9 +++++---- 5 files changed, 15 insertions(+), 10 deletions(-) diff --git a/Sphinx-docs/_build/doctrees/environment.pickle b/Sphinx-docs/_build/doctrees/environment.pickle index 43f1debe471defef86b7624baa52cd058f74178d..42c54880d84c1a8a5c24d6905b92834168b53097 100755 GIT binary patch delta 50279 zcmd6Q2Y6J)7O<1-CfQAP(|ddYHL_nCqc;(W_0X4aIJR5jLHs!OV>tFmXzn%-D%o?AY%p<}bxb=M)>w`D>E{E$Y>{SMsDzZC{dDYH#V4P6^V|6st5irI++;wpIE* zH9_i^9Uy&{o+;hgGfc`&F-f+ROzB^#HfckTMCt4HSZ9@%bnYVElaea^tw*wSJT+F@ zni}BU;CNly(=%4uljbL_PP7T9}?9ebA$gG%Qopm8bh!8XRwc zY-<}cq~`QAX=smd=}?BRRF-Q4>B6Lj3_q!TXCaZoF{7iXKap#de#sK})fru-j_FCj z?YPXXW6ua_d%HksR=WXG|13Y!N#SsFYM8V=(@%P`Ljbk7y`8U=pJS6MpmqG6kZA>O z$x?l0Hz})qoK)4mJ=i8}eTN7qHXz9Q4p#5V>6OyOj$Mf2IAU2%)g{GcQc#CP>BCI4 z%n7NmL#&k4%_KeCHC%cmBaD70<^)MUbjwz)4RAa7T1jc)!r}#0*_DeMrKENVWWg_8X?`5Ud zqI-l%L0$Z$oUTF8L?`$i2F-Ma(jPW9OLveL-I^xicwBV5Yv;VX^miMXPNu~)aY z&yi9)2|vlp4VG4R&HxWgAsplUZ@~iK5#Gg>bERKW@}Mun{0a-_md~v&ER=#f zM1v3^z(kl8J5lebI`_c-L^Q>^m1*H*-CO1M(Pw?Z5{mbIpW03u(j!wkozV?s_m>*F z`xr)}HWCBL8HC$`Q|FNzfgea4x_2VKK(^?-&;+=l^j%hh+=qT(xpeRai9FHRcUB_D z`ugrx)hh$KVSkvU<2|6yGR1h-TX|#mNnnU}(yh5YAYPav12ooKrgM$kL%*t1s78rYaD+;jNOFcIEJ!ap`oI_v2Cd#8^!UY%HkRG zif1d$G!YYMM%vmpFB|znyq{ZLTGCiE-SOI0Sa*CqT*~hkAf24x2WpEeo1Mge<4SyA zzXrZv&I^-yemO57P}t<`m6&2EzLFg}z8)d1zBQDX;xGSj!Sp{0Qz+5UV=KVW7vMrQ z^ml|o(fMi()aUt2WqHCH5mJ3#Kp0trnVdV*Z5wZ1MTuYXGC_e~^4iG42VN`V6IWr3 zo!4hjfRr#u>B>HX~zO4abW zgh6BON``-8KujC*@Bm6pl0?!YkE8xdD=2+?8aVZ$KQRNj65HDb##`=clt-_0+rWgG z)R#9X429T^;UuwRq$sWz#VApXp+X+7aT zKG=pNLVH<5qJ(}&FR#g|ytu85R zlp0+o4k=?;*FYyCU{pFR%B8MRl7@9kKs?7L{6jvw9R$+(5Kbmuj$INPo|{S`-?E@& zR>^{r@)^psu-Qmp)n*@Q&BUm&#CQYYIo3kqA}+KI^W1#^sP_f+s~41%7{~FLa-oQU^N@XLK`@Ri7VHin2j2JJ1$8%WWJU!-W0s`N9|LHZQr6tvcXi_;A z2x&|7b84+Kk*))Tsp@-BCt#A=^{!I|6j!75CxQkS!Mgptj81SPk0wYx|MXI@k=e9pH z+z#>I)$4zejE97?A6g8c4sZ

06kq7uL8^HySU7Mk;D|0?ZN#3a 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 Mwb=f03S%k{005>U>Hq)$ diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.doctree index 234a04ba781d90d2bf003d3998dc5bdae65285bd..555eb12d119be21dec391f646efe0ce108ca6f88 100755 GIT binary patch literal 57625 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 literal 57651 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 diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.mbb.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.mbb.doctree index 3ee2e6f799023e660cb8e15c5a3c6a2ac9a95833..0f5ab9f857cee928437cb9513a71457616997d22 100755 GIT binary patch delta 305 zcmcaVkM+|$)(z1TETt?A3{$5wYD{KPQrf&l)_`SlSug+O3lmf)v&-hO$^ zjagVqSRo>tS(NInr!HjFoGce_Ho30cYO+jR0hDh(`Ar-rh`m`Z{+~EhkYloM`E4Y& z;pDCgE@b}ZZxtmF#eEYsz$Sj3P%wQCBhZSziAK}ELl~Nyg(j_sD2rQd4ptVu3Sx2G zYKzG_t05N0t)3IYIW^Xw0SqQTj1ivR&&0^SRhDs@CCJk03f7GB(|=nsu7dFtroXdd a1Z&<7loRA+XG<sFTPi~aZ*leK`Yb}Iga6n>SO5)@ValVr$lv_>Kiz~n`9gbbtZ*zM5 zadB*N4&}eF3kOX88^F+HWPvDW0oBrR55r>sv&2rlVtr;06G_xX{f2 diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.mlb.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.mlb.doctree index 5985b2acec740056063c9b2bc6ab6a2676226690..23d7d0300fe269aae99625e33a73419e28667ccd 100755 GIT binary patch delta 903 zcmZvaUr19?9LM)NHoLh}^Iz9?XIWcWIaUk8$C(&GaS?in*vgwNz6_l*H_VaPdm_wp z>wr%_wLp-B#K$ZUtR9REp9(<;dkK0G1R*;2?#~_7%Xv8Wd_SMx?{|Njvo$}0)lb~d zzO}d(tDiIeo;>_%UgaxnNilF@!t6l1AdMPDUz8O7u(_TLvG1jJzFw#Bx#q57LYrwx zF%0mx{ℜ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 delta 612 zcmX@GfOYKxR+a|VsgpOdq)8~aurM$LrIwTyDV52m zCg3m~A`LR#U}7as(;uufo_xOFYO?dn0$jE{SZRw}%4BohD&sK5wym2OuUleQCNcfH zHKW|*q+-$O6J!}xr*m5~;;;*>QeGMw7oOPTLSj46H~~&J*3yzRU5o8}u8c<903}G% ArT_o{ diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.nfl.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.nfl.doctree index d532462e59050adaf4a0d5d77271325e31e466f6..3871334254a66eb19b3c334ad9e7b5e9e1d63645 100755 GIT binary patch delta 28551 zcmaJ~cVJY-)@Nn|*-f(Py=;0&CzVu4lOjC=B1j7mzy&E%REi{`B1jOpz&w>_h=QUh z4<1SoP(j59HbAi}g!j;AMQn(O`kiv`?%m+~_+xWt=Cql6esgBdoqJjTjo

    |RY> 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 U1j|S3)aGJK90K4#v1Qc%0bAFCvH$=8 delta 29970 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 Dgrf;u delta 13669 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 HKVF~9 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 U7XK+Wl7!l$r@Tx!9|6aI0f&|ZmjD0& diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.wnba.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.wnba.doctree index 69d8a68a55e0a78dc3be4699f427e4644a171dc0..6f04fd1551087e266ddd878ed148b5f4f8504def 100755 GIT binary patch delta 322 zcmaDkk@f#XR+a|VsrnmPW=pVmvoJ7B{m3Y{d4a4E%j7pQi&&h1BGUty7)60XrYtN@ ztWfUc1+u!EMU-Y)PyNlPIr%}H*<{|hRnr5Rz#In{!+di?{6TT35{}7r6^1}%R+I0R z7r<121g()dMw1)jxi+g+Cd0I`Oy-%S0@fxnv0(Z)Mz9SaLDk6{D%hv{Gcjrd?PuA{ zGwB>cOZ94Fu$GclaGO{rSFbh!X<}<&ehDN4F+gIB%+HnH_tZ8tz delta 608 zcmexAk@e+7R+a|VsZtwRW=lx9urM$LrIwTy>b=vMw7MUx$sFF zRB~^gAHQ83w;Gno|0@h9zn@?=`BQlTeq~0|8SJrBGQ@h4Q9Os@SeDKIE9T?0?(~Fn zxU4umL1pr*i8wt2k_LGuU=l9Ve_%J+Pi6|#)rr_$?FTmZ-0IvA#@$&8ttYX|VOFT-GbX^il5@U)}%5+mw%fNI&<>ZZQs;pBo zUi0gRRYw&dAS@$zU!`+I&qXm=OS8 C;zKV0 literal 11817 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 Axc~qF diff --git a/Sphinx-docs/_build/markdown/index.md b/Sphinx-docs/_build/markdown/index.md index 6538b9c..3c97368 100755 --- a/Sphinx-docs/_build/markdown/index.md +++ b/Sphinx-docs/_build/markdown/index.md @@ -67,39 +67,6 @@ contain the root `toctree` directive. --> * Module contents - * sportsdataverse.mlb package - - - * Submodules - - - * sportsdataverse.mlb.mlb_loaders module - - - * sportsdataverse.mlb.mlbam_games module - - - * sportsdataverse.mlb.mlbam_players module - - - * sportsdataverse.mlb.mlbam_reports module - - - * sportsdataverse.mlb.mlbam_stats module - - - * sportsdataverse.mlb.mlbam_teams module - - - * sportsdataverse.mlb.retrosheet module - - - * sportsdataverse.mlb.retrosplits module - - - * Module contents - - * sportsdataverse.nba package diff --git a/Sphinx-docs/_build/markdown/modules.md b/Sphinx-docs/_build/markdown/modules.md index addbbe0..747ddf3 100755 --- a/Sphinx-docs/_build/markdown/modules.md +++ b/Sphinx-docs/_build/markdown/modules.md @@ -58,39 +58,6 @@ * Module contents - * sportsdataverse.mlb package - - - * Submodules - - - * sportsdataverse.mlb.mlb_loaders module - - - * sportsdataverse.mlb.mlbam_games module - - - * sportsdataverse.mlb.mlbam_players module - - - * sportsdataverse.mlb.mlbam_reports module - - - * sportsdataverse.mlb.mlbam_stats module - - - * sportsdataverse.mlb.mlbam_teams module - - - * sportsdataverse.mlb.retrosheet module - - - * sportsdataverse.mlb.retrosplits module - - - * Module contents - - * sportsdataverse.nba package diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.cfb.md b/Sphinx-docs/_build/markdown/sportsdataverse.cfb.md index 25700e4..7d3dc1f 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.cfb.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.cfb.md @@ -15,7 +15,7 @@ Args: Returns: - pd.DataFrame: Data frame of game roster data with columns: + pl.DataFrame: Polars dataframe of game roster data with columns: ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, @@ -62,7 +62,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing teams available. + pl.DataFrame: Polars dataframe containing teams available. ### sportsdataverse.cfb.cfb_loaders.load_cfb_betting_lines(return_as_pandas=False) @@ -78,7 +78,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing betting lines available for the available seasons. + pl.DataFrame: Polars dataframe containing betting lines available for the available seasons. ### sportsdataverse.cfb.cfb_loaders.load_cfb_pbp(seasons: List[int], return_as_pandas=False) @@ -95,7 +95,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the play-by-plays available for the requested seasons. + pl.DataFrame: Polars dataframe containing the play-by-plays available for the requested seasons. Raises: @@ -116,7 +116,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing rosters available for the requested seasons. + pl.DataFrame: Polars dataframe containing rosters available for the requested seasons. Raises: @@ -137,7 +137,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the schedule for the requested seasons. + pl.DataFrame: Polars dataframe containing the schedule for the requested seasons. Raises: @@ -158,7 +158,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the team info available for the requested seasons. + pl.DataFrame: Polars dataframe containing the team info available for the requested seasons. Raises: @@ -270,7 +270,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. + pl.DataFrame: Polars dataframe containing schedule dates for the requested season. Example: diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.mbb.md b/Sphinx-docs/_build/markdown/sportsdataverse.mbb.md index 57fd208..7b4a861 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.mbb.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.mbb.md @@ -15,7 +15,7 @@ Args: Returns: - pd.DataFrame: Data frame of game roster data with columns: + pl.DataFrame: Polars dataframe of game roster data with columns: ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, @@ -63,7 +63,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the play-by-plays available for the requested seasons. Raises: @@ -85,7 +85,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the player boxscores available for the requested seasons. Raises: @@ -107,7 +107,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the schedule for the requested seasons. Raises: @@ -129,7 +129,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the team boxscores available for the requested seasons. Raises: @@ -218,7 +218,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing teams for the requested league. + pl.DataFrame: Polars dataframe containing teams for the requested league. Example: diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.md b/Sphinx-docs/_build/markdown/sportsdataverse.md index acbfe75..3f38581 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.md @@ -54,39 +54,6 @@ * Module contents -* sportsdataverse.mlb package - - - * Submodules - - - * sportsdataverse.mlb.mlb_loaders module - - - * sportsdataverse.mlb.mlbam_games module - - - * sportsdataverse.mlb.mlbam_players module - - - * sportsdataverse.mlb.mlbam_reports module - - - * sportsdataverse.mlb.mlbam_stats module - - - * sportsdataverse.mlb.mlbam_teams module - - - * sportsdataverse.mlb.retrosheet module - - - * sportsdataverse.mlb.retrosplits module - - - * Module contents - - * sportsdataverse.nba package diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.mlb.md b/Sphinx-docs/_build/markdown/sportsdataverse.mlb.md index 1daccd6..60c6ad6 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.mlb.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.mlb.md @@ -4,620 +4,18 @@ ## sportsdataverse.mlb.mlb_loaders module - -### sportsdataverse.mlb.mlb_loaders.mlbam_copyright_info(saveFile=False, returnFile=False, \*\*kwargs) -Displays the copyright info for the MLBAM API. - -Args: - - saveFile (boolean) = False - If saveFile is set to True, the copyright file generated is saved. - - returnFile (boolean) = False - If returnFile is set to True, the copyright file is returned. - ## sportsdataverse.mlb.mlbam_games module - -### sportsdataverse.mlb.mlbam_games.mlbam_schedule(season: int, gameType='R') -Retrieves the start and end date for games for every league, and the MLB,for a given season. -This function does not get individual games. - -Args: - - season (int): - - Required parameter. Indicates the season you are trying to find the games for. - - gameType (string) = “R”: - - Optional parameter. If there’s no input, this function will get the info for the regular season. - - Other parts of the season are indicated as follows in the MLBAM API: - - ‘S’ - Spring Training - ‘E’ - Exhibition - ‘A’ - All Star Game - ‘D’ - Division Series - ‘F’ - First Round (Wild Card) - ‘L’ - League Championship - ‘W’ - World Series - -Returns: - - A pandas dataframe containing MLB scheduled games. - ## sportsdataverse.mlb.mlbam_players module - -### sportsdataverse.mlb.mlbam_players.mlbam_player_info(playerID: int) -Retrieves the player info for an MLB player, given a proper MLBAM ID - -Args: - - playerID (int): - - Required parameter. If no playerID is provided, the function wil not work. - -Returns: - - A pandas dataframe cointaining player information for the specified MLBAM player ID. - - -### sportsdataverse.mlb.mlbam_players.mlbam_player_teams(playerID: int, season: int) -Retrieves the info regarding which teams that player played for in a given season, or in the player’s career. - -Args: - - playerID (int): - - Required parameter. If no playerID is provided, the function wil not work. - - season (int): - - Required parameter. If provided, the search will only look for teams - that player played for in that season. - -Returns: - - A pandas dataframe containing teams a player played for in that season. - - -### sportsdataverse.mlb.mlbam_players.mlbam_search_mlb_players(search: str, isActive='') -Searches for an MLB player in the MLBAM API. - -Args: - - search (string): - - Inputted string of the player(s) the user is intending to search. - If there is nothing inputted, nothing will be searched. - - isActive (string, optional): - - If called, it will specify if you want active players, or inactive players - in your search. - - If you want active players, set isActive to “Y” or “Yes”. - - If you want inactive players, set isActive to “N” or “No”. - -Returns: - - A pandas dataframe containing MLBAM players whose name(s) matches the input string. - ## sportsdataverse.mlb.mlbam_reports module - -### sportsdataverse.mlb.mlbam_reports.mlbam_broadcast_info(season: int, home_away='e') -Retrieves the broadcasters (radio and TV) involved with certain games. - -Args: - - season (int): - - Required parameter. If no season is provided, the function wil not work. - - home_away (string): - - Optional parameter. Used to get broadcasters from either the home OR the away side. - Leave blank if you want both home and away broadcasters. - - If you want home broadcasters only, set home_away=’H’ or home_away=’a’. - - If you want away broadcasters only, set home_away=’A’ or home_away=’a’. - - If you want both home and away broadcasters, set home_away=’E’ or home_away=’e’. - -Returns: - - A pandas dataframe containing TV and radio broadcast information for various MLB games. - - -### sportsdataverse.mlb.mlbam_reports.mlbam_transactions(startDate: str, endDate: str) -Retrieves all transactions in a given range of dates. -You MUST provide two dates for this function to work, and both dates must be in MM/DD/YYYY format. -For example, December 31st, 2021 would be represented as 12/31/2021. - -Args: - - startDate (int): - - Required parameter. If no startDate is provided, the function wil not work. - Additionally, startDate must be in MM/DD/YYYY format. - - endDate (int): - - Required parameter. If no endDate is provided, the function wil not work. - Additionally, endDate must be in MM/DD/YYYY format. - -Returns: - - A pandas dataframe containing MLB transactions between two dates. - ## sportsdataverse.mlb.mlbam_stats module - -### sportsdataverse.mlb.mlbam_stats.mlbam_player_career_hitting_stats(playerID: int, gameType='R') -Retrieves the career hitting stats for an MLB player, given a proper MLBAM ID - -Args: - - playerID (int): - - Required parameter. If no playerID is provided, the function wil not work. - - gameType (string) = “R”: - - Optional parameter. If there’s no input, this function will get the info for the regular season. - - Other parts of the season are indicated as follows in the MLBAM API: - - ‘S’ - Spring Training - ‘E’ - Exhibition - ‘A’ - All Star Game - ‘D’ - Division Series - ‘F’ - First Round (Wild Card) - ‘L’ - League Championship - ‘W’ - World Series - -Returns: - - A pandas dataframe containing hitting stats for an MLB player in a given season. - - -### sportsdataverse.mlb.mlbam_stats.mlbam_player_career_pitching_stats(playerID: int, gameType='R') -Retrieves the career pitching stats for an MLB player, given a proper MLBAM ID - -Args: - - playerID (int): - - Required parameter. If no playerID is provided, the function wil not work. - - gameType (string) = “R”: - - Optional parameter. If there’s no input, this function will get the info for the regular season. - - Other parts of the season are indicated as follows in the MLBAM API: - - ‘S’ - Spring Training - ‘E’ - Exhibition - ‘A’ - All Star Game - ‘D’ - Division Series - ‘F’ - First Round (Wild Card) - ‘L’ - League Championship - ‘W’ - World Series - -Returns: - - A pandas dataframe containing career pitching stats for an MLB player. - - -### sportsdataverse.mlb.mlbam_stats.mlbam_player_season_hitting_stats(playerID: int, season: int, gameType='R') -Retrieves the hitting stats for an MLB player in a given season, given a proper MLBAM ID. - -Args: - - playerID (int): - - Required parameter. If no playerID is provided, the function wil not work. - - season (int): - - Required parameter. Indicates the season you are trying to find the games for. - - gameType (string) = “R”: - - Optional parameter. If there’s no input, this function will get the info for the regular season. - - Other parts of the season are indicated as follows in the MLBAM API: - - ‘S’ - Spring Training - ‘E’ - Exhibition - ‘A’ - All Star Game - ‘D’ - Division Series - ‘F’ - First Round (Wild Card) - ‘L’ - League Championship - ‘W’ - World Series - -Returns: - - A pandas dataframe containing career hitting stats for an MLB player. - - -### sportsdataverse.mlb.mlbam_stats.mlbam_player_season_pitching_stats(playerID: int, season: int, gameType='R') -Retrieves the pitching stats for an MLB player in a given season, given a proper MLBAM ID - -Args: - - playerID (int): - - Required parameter. If no playerID is provided, the function wil not work. - - season (int): - - Required parameter. Indicates the season you are trying to find the games for. - - gameType (string) = “R”: - - Optional parameter. If there’s no input, this function will get the info for the regular season. - - Other parts of the season are indicated as follows in the MLBAM API: - - ‘S’ - Spring Training - ‘E’ - Exhibition - ‘A’ - All Star Game - ‘D’ - Division Series - ‘F’ - First Round (Wild Card) - ‘L’ - League Championship - ‘W’ - World Series - -Returns: - - A pandas dataframe containing pitching stats for an MLB player in a given season. - ## sportsdataverse.mlb.mlbam_teams module - -### sportsdataverse.mlb.mlbam_teams.mlbam_40_man_roster(teamID: int) -Retrieves the current 40-man roster for a team, given a proper MLBAM team ID. - -Args: - -teamID (int): -Required parameter. This should be the MLBAM team ID for the MLB team you want a 40-man roster from. - -Returns: - - A pandas dataframe containing the current 40-man roster for the given MLBAM team ID. - - -### sportsdataverse.mlb.mlbam_teams.mlbam_team_roster(teamID: int, startSeason: int, endSeason: int) -Retrieves the cumulative roster for a MLB team in a specified timeframe. - -Args: - - teamID (int): - - Required parameter. This should be the number MLBAM associates for an MLB team. - For example, the Cincinnati Reds have an MLBAM team ID of 113. - - startSeason (int): - - Required parameter. This value must be less than endSeason for this function to work. - - endSeason (int): - - Required parameter. This value must be greater than startSeason for this function to work. - -Returns: - - A pandas dataframe containg the roster(s) for the MLB team. - - -### sportsdataverse.mlb.mlbam_teams.mlbam_teams(season: int, retriveAllStarRosters=False) -Retrieves the player info for an MLB team, given an MLB season. - -Args: - - season (int): - - Required parameter. If no season is provided, the function wil not work. - - retriveAllStarRosters (boolean): - - Optional parameter. If set to ‘True’, MLB All-Star rosters will be returned when - running this function. - -Returns: - - A pandas dataframe containing information about MLB teams that played in that season. - ## sportsdataverse.mlb.retrosheet module -RETROSHEET NOTICE: - -> The information used here was obtained free of -> charge from and is copyrighted by Retrosheet. Interested -> parties may contact Retrosheet at “www.retrosheet.org”. - - -### sportsdataverse.mlb.retrosheet.retrosheet_ballparks() -Retrives the current TEAMABR.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. - -Args: - - None - -Returns: - - A pandas Dataframe with the biographical information of notable major league teams. - - -### sportsdataverse.mlb.retrosheet.retrosheet_ejections() -Retrives the current Ejecdata.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. - -Args: - - None - -Returns: - - A pandas Dataframe with the ejection data of known MLB ejections. - - -### sportsdataverse.mlb.retrosheet.retrosheet_franchises() -Retrives the current TEAMABR.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. - -Args: - - None - -Returns: - - A pandas Dataframe with the biographical information of notable major league teams. - - -### sportsdataverse.mlb.retrosheet.retrosheet_game_logs_team() -Retrives the team-level stats for MLB games in a season, or range of seasons. -THIS DOES NOT GET PLAYER STATS! -Use retrosplits_game_logs_player() for player-level game stats. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - - game_type (str): - - Optional parameter. By default, this is set to “regular”, or to put it in another way, this function call will return only regular season games. - - The full list of supported keywards for game_type are as follows. Case does not matter (you can set game_type to “rEgUlAr”, and the function call will still work): - - > - > * “regular”: Regular season games. - - - > * “asg”: All-Star games. - - - > * “playoffs”: Playoff games. - - filter_out_seasons (bool): - - If game_type is set to either “asg” or “playoffs”, and filter_out_seasons is set to true, this function will filter out seasons that do not match the inputted season and/or the range of seasons. By default, this is set to True. - -Returns: - - A pandas dataframe containing team-level stats for MLB games. - - -### sportsdataverse.mlb.retrosheet.retrosheet_people() -Retrives the current BioFile.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. - -Args: - - None - -Returns: - - A pandas Dataframe with the biographical information of various individuals who have played baseball. - - -### sportsdataverse.mlb.retrosheet.retrosheet_schedule() -Retrives the scheduled games of an MLB season, or MLB seasons. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - - original_2020_schedule (bool): - - Retrosheet keeps a record of the orignial 2020 MLB season, before the season was delayed due to the COVID-19 pandemic. - - - * If this is set to True, this function will return the original 2020 MLB season, before it was altered due to the COVID-19 pandemic, if the user wants this function to return the schedule for the 2020 MLB season. - - - * If this is set to False, this function will return the altered 2020 MLB season, after it was altered due to the COVID-19 pandemic, if the user wants this function to return the schedule for the 2020 MLB season. - -Returns: - - A pandas dataframe containg historical MLB schedules. - ## sportsdataverse.mlb.retrosplits module -RETROSHEET NOTICE: - -> The information used here was obtained free of -> charge from and is copyrighted by Retrosheet. Interested -> parties may contact Retrosheet at “www.retrosheet.org”. - - -### sportsdataverse.mlb.retrosplits.retrosplits_game_logs_player() -Retrives game-level player stats from the Retrosplits project. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - -Returns: - - A pandas dataframe containing game-level player stats from historical MLB games. - - -### sportsdataverse.mlb.retrosplits.retrosplits_game_logs_team() -Retrives game-level team stats from the Retrosplits project. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - -Returns: - - A pandas dataframe containing game-level team stats from historical MLB games. - - -### sportsdataverse.mlb.retrosplits.retrosplits_player_batting_by_platoon() -Retrives player-level, batting by platoon (left/right hitting vs. left/right pitching) split stats from the Retrosplits project. -The stats are batting stats, based off of the handedness of the batter vs the handedness of the pitcher. -The stats returned by this function are season-level stats, not game-level stats. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - -Returns: - - A pandas dataframe containing player-level, batting by platoon stats for batters. - - -### sportsdataverse.mlb.retrosplits.retrosplits_player_batting_by_position() -Retrives player-level, batting by position split stats from the Retrosplits project. -The stats returned by this function are season-level stats, not game-level stats. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - -Returns: - - A pandas dataframe containing batting by position split stats for MLB players. - - -### sportsdataverse.mlb.retrosplits.retrosplits_player_batting_by_runners() -Retrives player-level, batting by runners split stats from the Retrosplits project. -The stats are batting stats, based off of how many runners are on base at the time of the at bat. -The stats returned by this function are season-level stats, not game-level stats. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - -Returns: - - A pandas dataframe containing player-level, batting by runners split stats. - - -### sportsdataverse.mlb.retrosplits.retrosplits_player_head_to_head_stats() -Retrives batter vs. pitcher stats from the Retrosplits project. -The stats are batting stats, based off of the preformance of that specific batter agianst a specific pitcher for the durration of that specific season. -The stats returned by this function are season-level stats, not game-level stats. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - -Returns: - - A pandas dataframe containing batter vs. pitcher stats for a season, or for a range of seasons. - - -### sportsdataverse.mlb.retrosplits.retrosplits_player_pitching_by_platoon() -Retrives player-level, pitching by platoon (left/right pitching vs. left/right hitting) split stats from the Retrosplits project. -The stats are pitching stats, based off of the handedness of the pitcher vs the handedness of the batter. -The stats returned by this function are season-level stats, not game-level stats. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - -Returns: - - A pandas dataframe containing player-level, pitching by platoon stats for pitchers. - - -### sportsdataverse.mlb.retrosplits.retrosplits_player_pitching_by_runners() -Retrives player-level, pitching by runners split stats from the Retrosplits project. -The stats are pitching stats, based off of how many runners are on base at the time of the at bat. -The stats returned by this function are season-level stats, not game-level stats. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - -Returns: - - A pandas dataframe containing pitching by runners split stats for a season, or for a range of seasons. - ## Module contents diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.nba.md b/Sphinx-docs/_build/markdown/sportsdataverse.nba.md index 7d7d008..4aabaf1 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.nba.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.nba.md @@ -15,7 +15,7 @@ Args: Returns: - pd.DataFrame: Data frame of game roster data with columns: + pl.DataFrame: Polars dataframe of game roster data with columns: ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, @@ -63,7 +63,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the play-by-plays available for the requested seasons. Raises: @@ -85,7 +85,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the player boxscores available for the requested seasons. Raises: @@ -107,7 +107,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the schedule for the requested seasons. Raises: @@ -129,7 +129,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the team boxscores available for the requested seasons. Raises: @@ -220,7 +220,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing teams for the requested league. + pl.DataFrame: Polars dataframe containing teams for the requested league. Example: diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.nfl.md b/Sphinx-docs/_build/markdown/sportsdataverse.nfl.md index 666adf9..7d33b23 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.nfl.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.nfl.md @@ -17,7 +17,7 @@ Args: Returns: - pd.DataFrame: Data frame of game roster data with columns: + pl.DataFrame: Polars dataframe of game roster data with columns: ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, @@ -108,9 +108,11 @@ Example: Args: + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Returns: - pd.DataFrame: Pandas dataframe containing NFL combine data available. + pl.DataFrame: Polars dataframe containing NFL combine data available. ### sportsdataverse.nfl.nfl_loaders.load_nfl_contracts(return_as_pandas=False) @@ -122,9 +124,11 @@ Example: Args: + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Returns: - pd.DataFrame: Pandas dataframe containing historical contracts available. + pl.DataFrame: Polars dataframe containing historical contracts available. ### sportsdataverse.nfl.nfl_loaders.load_nfl_depth_charts(seasons: List[int], return_as_pandas=False) @@ -137,10 +141,11 @@ Example: Args: seasons (list): Used to define different seasons. 2001 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing depth chart data available for the requested seasons. + pl.DataFrame: Polars dataframe containing depth chart data available for the requested seasons. ### sportsdataverse.nfl.nfl_loaders.load_nfl_draft_picks(return_as_pandas=False) @@ -152,9 +157,11 @@ Example: Args: + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Returns: - pd.DataFrame: Pandas dataframe containing NFL Draft picks data available. + pl.DataFrame: Polars dataframe containing NFL Draft picks data available. ### sportsdataverse.nfl.nfl_loaders.load_nfl_injuries(seasons: List[int], return_as_pandas=False) @@ -167,46 +174,59 @@ Example: Args: seasons (list): Used to define different seasons. 2009 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing injuries data available for the requested seasons. + pl.DataFrame: Polars dataframe containing injuries data available for the requested seasons. ### sportsdataverse.nfl.nfl_loaders.load_nfl_ngs_passing(return_as_pandas=False) Load NFL NextGen Stats Passing data going back to 2016 +Args: + + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Example: nfl_df = sportsdataverse.nfl.load_nfl_ngs_passing() Returns: - pd.DataFrame: Pandas dataframe containing the NextGen Stats Passing data available. + pl.DataFrame: Polars dataframe containing the NextGen Stats Passing data available. ### sportsdataverse.nfl.nfl_loaders.load_nfl_ngs_receiving(return_as_pandas=False) Load NFL NextGen Stats Receiving data going back to 2016 +Args: + + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Example: nfl_df = sportsdataverse.nfl.load_nfl_ngs_receiving() Returns: - pd.DataFrame: Pandas dataframe containing the NextGen Stats Receiving data available. + pl.DataFrame: Polars dataframe containing the NextGen Stats Receiving data available. ### sportsdataverse.nfl.nfl_loaders.load_nfl_ngs_rushing(return_as_pandas=False) Load NFL NextGen Stats Rushing data going back to 2016 +Args: + + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Example: nfl_df = sportsdataverse.nfl.load_nfl_ngs_rushing() Returns: - pd.DataFrame: Pandas dataframe containing the NextGen Stats Rushing data available. + pl.DataFrame: Polars dataframe containing the NextGen Stats Rushing data available. ### sportsdataverse.nfl.nfl_loaders.load_nfl_officials(return_as_pandas=False) @@ -218,9 +238,11 @@ Example: Args: + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Returns: - pd.DataFrame: Pandas dataframe containing officials available. + pl.DataFrame: Polars dataframe containing officials available. ### sportsdataverse.nfl.nfl_loaders.load_nfl_pbp(seasons: List[int], return_as_pandas=False) @@ -237,7 +259,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the play-by-plays available for the requested seasons. + pl.DataFrame: Polars dataframe containing the play-by-plays available for the requested seasons. Raises: @@ -254,22 +276,27 @@ Example: Args: seasons (list): Used to define different seasons. 2016 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing play-by-play participation data available for the requested seasons. + pl.DataFrame: Polars dataframe containing play-by-play participation data available for the requested seasons. ### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_def(return_as_pandas=False) Load NFL Pro-Football Reference Advanced Defensive data going back to 2018 +Args: + + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Example: nfl_df = sportsdataverse.nfl.load_nfl_pfr_def() Returns: - pd.DataFrame: Pandas dataframe containing Pro-Football Reference + pl.DataFrame: Polars dataframe containing Pro-Football Reference advanced defensive stats data available. @@ -277,13 +304,17 @@ Returns: ### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_pass(return_as_pandas=False) Load NFL Pro-Football Reference Advanced Passing data going back to 2018 +Args: + + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Example: nfl_df = sportsdataverse.nfl.load_nfl_pfr_pass() Returns: - pd.DataFrame: Pandas dataframe containing Pro-Football Reference + pl.DataFrame: Polars dataframe containing Pro-Football Reference advanced passing stats data available. @@ -291,13 +322,17 @@ Returns: ### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_rec(return_as_pandas=False) Load NFL Pro-Football Reference Advanced Receiving data going back to 2018 +Args: + + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Example: nfl_df = sportsdataverse.nfl.load_nfl_pfr_rec() Returns: - pd.DataFrame: Pandas dataframe containing Pro-Football Reference + pl.DataFrame: Polars dataframe containing Pro-Football Reference advanced receiving stats data available. @@ -305,13 +340,17 @@ Returns: ### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_rush(return_as_pandas=False) Load NFL Pro-Football Reference Advanced Rushing data going back to 2018 +Args: + + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Example: nfl_df = sportsdataverse.nfl.load_nfl_pfr_rush() Returns: - pd.DataFrame: Pandas dataframe containing Pro-Football Reference + pl.DataFrame: Polars dataframe containing Pro-Football Reference advanced rushing stats data available. @@ -326,10 +365,11 @@ Example: Args: seasons (list): Used to define different seasons. 2018 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing Pro-Football Reference + pl.DataFrame: Polars dataframe containing Pro-Football Reference advanced defensive stats data available for the requested seasons. @@ -344,10 +384,11 @@ Example: Args: seasons (list): Used to define different seasons. 2018 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing Pro-Football Reference + pl.DataFrame: Polars dataframe containing Pro-Football Reference advanced passing stats data available for the requested seasons. @@ -362,10 +403,11 @@ Example: Args: seasons (list): Used to define different seasons. 2018 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing Pro-Football Reference + pl.DataFrame: Polars dataframe containing Pro-Football Reference advanced receiving stats data available for the requested seasons. @@ -380,10 +422,11 @@ Example: Args: seasons (list): Used to define different seasons. 2018 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing Pro-Football Reference + pl.DataFrame: Polars dataframe containing Pro-Football Reference advanced rushing stats data available for the requested seasons. @@ -398,10 +441,11 @@ Example: Args: kicking (bool): If True, load kicking stats. If False, load all other stats. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing player stats. + pl.DataFrame: Polars dataframe containing player stats. ### sportsdataverse.nfl.nfl_loaders.load_nfl_players(return_as_pandas=False) @@ -413,9 +457,11 @@ Example: Args: + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Returns: - pd.DataFrame: Pandas dataframe containing players available. + pl.DataFrame: Polars dataframe containing players available. ### sportsdataverse.nfl.nfl_loaders.load_nfl_rosters(seasons: List[int], return_as_pandas=False) @@ -428,10 +474,11 @@ Example: Args: seasons (list): Used to define different seasons. 1920 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing rosters available for the requested seasons. + pl.DataFrame: Polars dataframe containing rosters available for the requested seasons. ### sportsdataverse.nfl.nfl_loaders.load_nfl_schedule(seasons: List[int], return_as_pandas=False) @@ -444,10 +491,11 @@ Example: Args: seasons (list): Used to define different seasons. 1999 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing the schedule for the requested seasons. + pl.DataFrame: Polars dataframe containing the schedule for the requested seasons. Raises: @@ -464,10 +512,11 @@ Example: Args: seasons (list): Used to define different seasons. 2012 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing snap counts available for the requested seasons. + pl.DataFrame: Polars dataframe containing snap counts available for the requested seasons. ### sportsdataverse.nfl.nfl_loaders.load_nfl_teams(return_as_pandas=False) @@ -479,9 +528,11 @@ Example: Args: + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Returns: - pd.DataFrame: Pandas dataframe containing teams available. + pl.DataFrame: Polars dataframe containing teams available. ### sportsdataverse.nfl.nfl_loaders.load_nfl_weekly_rosters(seasons: List[int], return_as_pandas=False) @@ -494,10 +545,11 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing weekly rosters available for the requested seasons. + pl.DataFrame: Polars dataframe containing weekly rosters available for the requested seasons. ## sportsdataverse.nfl.nfl_pbp module @@ -601,7 +653,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing teams for the requested league. + pl.DataFrame: Polars dataframe containing teams for the requested league. Example: diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.nhl.md b/Sphinx-docs/_build/markdown/sportsdataverse.nhl.md index d093c6a..778f4bf 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.nhl.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.nhl.md @@ -24,16 +24,18 @@ Example: nhl_df = sportsdataverse.nhl.nhl_api_pbp(game_id=2021020079) -### sportsdataverse.nhl.nhl_api.nhl_api_schedule(start_date: str, end_date: str, return_as_pandas=False) -nhl_api_schedule() - Pull the game by id. Data from API endpoints - nhl/schedule +### sportsdataverse.nhl.nhl_api.nhl_api_schedule(start_date: str, end_date: str, return_as_pandas=False, \*\*kwargs) +nhl_api_schedule() - Pull the schedule by start and end date. Data from API endpoints - nhl/schedule Args: - game_id (int): Unique game_id, can be obtained from nhl_schedule(). + start_date (str): Start date to pull the NHL API schedule. + end_date (str): End date to pull the NHL API schedule. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing the schedule for the requested seasons. + pl.DataFrame: Polars dataframe containing the schedule for the requested seasons. Example: @@ -52,7 +54,7 @@ Args: Returns: - pd.DataFrame: Data frame of game roster data with columns: + pl.DataFrame: Polars dataframe of game roster data with columns: ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, @@ -100,7 +102,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the play-by-plays available for the requested seasons. + pl.DataFrame: Polars dataframe containing the play-by-plays available for the requested seasons. Raises: @@ -121,7 +123,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the player boxscores available for the requested seasons. Raises: @@ -143,7 +145,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the schedule for the requested seasons. + pl.DataFrame: Polars dataframe containing the schedule for the requested seasons. Raises: @@ -164,7 +166,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the team boxscores available for the requested seasons. Raises: @@ -185,7 +187,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing teams available for the requested seasons. + pl.DataFrame: Polars dataframe containing teams available for the requested seasons. ## sportsdataverse.nhl.nhl_pbp module @@ -269,7 +271,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing teams for the requested league. + pl.DataFrame: Polars dataframe containing teams for the requested league. Example: diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.wbb.md b/Sphinx-docs/_build/markdown/sportsdataverse.wbb.md index 32743c0..e6622ff 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.wbb.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.wbb.md @@ -15,7 +15,7 @@ Args: Returns: - pd.DataFrame: Data frame of game roster data with columns: + pl.DataFrame: Polars dataframe of game roster data with columns: ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, @@ -63,7 +63,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the play-by-plays available for the requested seasons. Raises: @@ -85,7 +85,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the player boxscores available for the requested seasons. Raises: @@ -107,7 +107,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the schedule for the requested seasons. Raises: @@ -129,7 +129,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the team boxscores available for the requested seasons. Raises: @@ -146,8 +146,7 @@ womens-college-basketball/summary Args: game_id (int): Unique game_id, can be obtained from wbb_schedule(). - -raw (bool): If True, returns the raw json from the API endpoint. If False, returns a cleaned dictionary of datasets. + raw (bool): If True, returns the raw json from the API endpoint. If False, returns a cleaned dictionary of datasets. Returns: @@ -168,7 +167,7 @@ Example: ### sportsdataverse.wbb.wbb_pbp.helper_wbb_pickcenter(pbp_txt) -### sportsdataverse.wbb.wbb_pbp.mbb_pbp_disk(game_id, path_to_json) +### sportsdataverse.wbb.wbb_pbp.wbb_pbp_disk(game_id, path_to_json) ## sportsdataverse.wbb.wbb_schedule module @@ -220,7 +219,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing teams for the requested league. + pl.DataFrame: Polars dataframe containing teams for the requested league. Example: diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.wnba.md b/Sphinx-docs/_build/markdown/sportsdataverse.wnba.md index c701f7a..fcae786 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.wnba.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.wnba.md @@ -15,7 +15,7 @@ Args: Returns: - pd.DataFrame: Data frame of game roster data with columns: + pl.DataFrame: Polars Data frame of game roster data with columns: ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, @@ -63,7 +63,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the play-by-plays available for the requested seasons. Raises: @@ -85,7 +85,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the player boxscores available for the requested seasons. Raises: @@ -107,7 +107,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the schedule for the requested seasons. Raises: @@ -129,7 +129,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the team boxscores available for the requested seasons. Raises: @@ -214,7 +214,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing teams for the requested league. + pl.DataFrame: Polars dataframe containing teams for the requested league. Example: diff --git a/Sphinx-docs/_build/markdown/tests.md b/Sphinx-docs/_build/markdown/tests.md index 9017c83..4b1b429 100755 --- a/Sphinx-docs/_build/markdown/tests.md +++ b/Sphinx-docs/_build/markdown/tests.md @@ -28,18 +28,3 @@ ## Module contents - - -### class tests.SpecTestSuite() -Bases: `object` - - -#### assert_case(text, html) - -#### classmethod ignore_case(n) - -#### classmethod load_spec(spec_name) - -#### test_mixed_tab_space_in_list_item() - -### tests.parse_examples(text) diff --git a/Sphinx-docs/sportsdataverse.rst b/Sphinx-docs/sportsdataverse.rst index 36efdfc..6552b12 100755 --- a/Sphinx-docs/sportsdataverse.rst +++ b/Sphinx-docs/sportsdataverse.rst @@ -9,7 +9,6 @@ Subpackages sportsdataverse.cfb sportsdataverse.mbb - sportsdataverse.mlb sportsdataverse.nba sportsdataverse.nfl sportsdataverse.nhl diff --git a/sportsdataverse/mlb/__init__.py b/archive/mlb/__init__.py old mode 100755 new mode 100644 similarity index 100% rename from sportsdataverse/mlb/__init__.py rename to archive/mlb/__init__.py diff --git a/sportsdataverse/mlb/mlb_loaders.py b/archive/mlb/mlb_loaders.py old mode 100755 new mode 100644 similarity index 100% rename from sportsdataverse/mlb/mlb_loaders.py rename to archive/mlb/mlb_loaders.py diff --git a/sportsdataverse/mlb/mlbam_games.py b/archive/mlb/mlbam_games.py old mode 100755 new mode 100644 similarity index 100% rename from sportsdataverse/mlb/mlbam_games.py rename to archive/mlb/mlbam_games.py diff --git a/sportsdataverse/mlb/mlbam_players.py b/archive/mlb/mlbam_players.py old mode 100755 new mode 100644 similarity index 100% rename from sportsdataverse/mlb/mlbam_players.py rename to archive/mlb/mlbam_players.py diff --git a/sportsdataverse/mlb/mlbam_reports.py b/archive/mlb/mlbam_reports.py old mode 100755 new mode 100644 similarity index 100% rename from sportsdataverse/mlb/mlbam_reports.py rename to archive/mlb/mlbam_reports.py diff --git a/sportsdataverse/mlb/mlbam_stats.py b/archive/mlb/mlbam_stats.py old mode 100755 new mode 100644 similarity index 100% rename from sportsdataverse/mlb/mlbam_stats.py rename to archive/mlb/mlbam_stats.py diff --git a/sportsdataverse/mlb/mlbam_teams.py b/archive/mlb/mlbam_teams.py old mode 100755 new mode 100644 similarity index 100% rename from sportsdataverse/mlb/mlbam_teams.py rename to archive/mlb/mlbam_teams.py diff --git a/sportsdataverse/mlb/retrosheet.py b/archive/mlb/retrosheet.py old mode 100755 new mode 100644 similarity index 100% rename from sportsdataverse/mlb/retrosheet.py rename to archive/mlb/retrosheet.py diff --git a/sportsdataverse/mlb/retrosplits.py b/archive/mlb/retrosplits.py old mode 100755 new mode 100644 similarity index 100% rename from sportsdataverse/mlb/retrosplits.py rename to archive/mlb/retrosplits.py diff --git a/create_docs.sh b/create_docs.sh index 544cbe1..4b1565a 100755 --- a/create_docs.sh +++ b/create_docs.sh @@ -5,7 +5,6 @@ cd Sphinx-docs make markdown cp _build/markdown/sportsdataverse.cfb.md ../docs/docs/cfb/index.md cp _build/markdown/sportsdataverse.mbb.md ../docs/docs/mbb/index.md -cp _build/markdown/sportsdataverse.mlb.md ../docs/docs/mlb/index.md cp _build/markdown/sportsdataverse.nba.md ../docs/docs/nba/index.md cp _build/markdown/sportsdataverse.nfl.md ../docs/docs/nfl/index.md cp _build/markdown/sportsdataverse.nhl.md ../docs/docs/nhl/index.md diff --git a/docs/docs/cfb/index.md b/docs/docs/cfb/index.md index 25700e4..7d3dc1f 100755 --- a/docs/docs/cfb/index.md +++ b/docs/docs/cfb/index.md @@ -15,7 +15,7 @@ Args: Returns: - pd.DataFrame: Data frame of game roster data with columns: + pl.DataFrame: Polars dataframe of game roster data with columns: ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, @@ -62,7 +62,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing teams available. + pl.DataFrame: Polars dataframe containing teams available. ### sportsdataverse.cfb.cfb_loaders.load_cfb_betting_lines(return_as_pandas=False) @@ -78,7 +78,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing betting lines available for the available seasons. + pl.DataFrame: Polars dataframe containing betting lines available for the available seasons. ### sportsdataverse.cfb.cfb_loaders.load_cfb_pbp(seasons: List[int], return_as_pandas=False) @@ -95,7 +95,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the play-by-plays available for the requested seasons. + pl.DataFrame: Polars dataframe containing the play-by-plays available for the requested seasons. Raises: @@ -116,7 +116,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing rosters available for the requested seasons. + pl.DataFrame: Polars dataframe containing rosters available for the requested seasons. Raises: @@ -137,7 +137,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the schedule for the requested seasons. + pl.DataFrame: Polars dataframe containing the schedule for the requested seasons. Raises: @@ -158,7 +158,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the team info available for the requested seasons. + pl.DataFrame: Polars dataframe containing the team info available for the requested seasons. Raises: @@ -270,7 +270,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. + pl.DataFrame: Polars dataframe containing schedule dates for the requested season. Example: diff --git a/docs/docs/mbb/index.md b/docs/docs/mbb/index.md index 57fd208..7b4a861 100755 --- a/docs/docs/mbb/index.md +++ b/docs/docs/mbb/index.md @@ -15,7 +15,7 @@ Args: Returns: - pd.DataFrame: Data frame of game roster data with columns: + pl.DataFrame: Polars dataframe of game roster data with columns: ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, @@ -63,7 +63,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the play-by-plays available for the requested seasons. Raises: @@ -85,7 +85,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the player boxscores available for the requested seasons. Raises: @@ -107,7 +107,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the schedule for the requested seasons. Raises: @@ -129,7 +129,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the team boxscores available for the requested seasons. Raises: @@ -218,7 +218,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing teams for the requested league. + pl.DataFrame: Polars dataframe containing teams for the requested league. Example: diff --git a/docs/docs/mlb/index.md b/docs/docs/mlb/index.md deleted file mode 100755 index 1daccd6..0000000 --- a/docs/docs/mlb/index.md +++ /dev/null @@ -1,623 +0,0 @@ -# sportsdataverse.mlb package - -## Submodules - -## sportsdataverse.mlb.mlb_loaders module - - -### sportsdataverse.mlb.mlb_loaders.mlbam_copyright_info(saveFile=False, returnFile=False, \*\*kwargs) -Displays the copyright info for the MLBAM API. - -Args: - - saveFile (boolean) = False - If saveFile is set to True, the copyright file generated is saved. - - returnFile (boolean) = False - If returnFile is set to True, the copyright file is returned. - -## sportsdataverse.mlb.mlbam_games module - - -### sportsdataverse.mlb.mlbam_games.mlbam_schedule(season: int, gameType='R') -Retrieves the start and end date for games for every league, and the MLB,for a given season. -This function does not get individual games. - -Args: - - season (int): - - Required parameter. Indicates the season you are trying to find the games for. - - gameType (string) = “R”: - - Optional parameter. If there’s no input, this function will get the info for the regular season. - - Other parts of the season are indicated as follows in the MLBAM API: - - ‘S’ - Spring Training - ‘E’ - Exhibition - ‘A’ - All Star Game - ‘D’ - Division Series - ‘F’ - First Round (Wild Card) - ‘L’ - League Championship - ‘W’ - World Series - -Returns: - - A pandas dataframe containing MLB scheduled games. - -## sportsdataverse.mlb.mlbam_players module - - -### sportsdataverse.mlb.mlbam_players.mlbam_player_info(playerID: int) -Retrieves the player info for an MLB player, given a proper MLBAM ID - -Args: - - playerID (int): - - Required parameter. If no playerID is provided, the function wil not work. - -Returns: - - A pandas dataframe cointaining player information for the specified MLBAM player ID. - - -### sportsdataverse.mlb.mlbam_players.mlbam_player_teams(playerID: int, season: int) -Retrieves the info regarding which teams that player played for in a given season, or in the player’s career. - -Args: - - playerID (int): - - Required parameter. If no playerID is provided, the function wil not work. - - season (int): - - Required parameter. If provided, the search will only look for teams - that player played for in that season. - -Returns: - - A pandas dataframe containing teams a player played for in that season. - - -### sportsdataverse.mlb.mlbam_players.mlbam_search_mlb_players(search: str, isActive='') -Searches for an MLB player in the MLBAM API. - -Args: - - search (string): - - Inputted string of the player(s) the user is intending to search. - If there is nothing inputted, nothing will be searched. - - isActive (string, optional): - - If called, it will specify if you want active players, or inactive players - in your search. - - If you want active players, set isActive to “Y” or “Yes”. - - If you want inactive players, set isActive to “N” or “No”. - -Returns: - - A pandas dataframe containing MLBAM players whose name(s) matches the input string. - -## sportsdataverse.mlb.mlbam_reports module - - -### sportsdataverse.mlb.mlbam_reports.mlbam_broadcast_info(season: int, home_away='e') -Retrieves the broadcasters (radio and TV) involved with certain games. - -Args: - - season (int): - - Required parameter. If no season is provided, the function wil not work. - - home_away (string): - - Optional parameter. Used to get broadcasters from either the home OR the away side. - Leave blank if you want both home and away broadcasters. - - If you want home broadcasters only, set home_away=’H’ or home_away=’a’. - - If you want away broadcasters only, set home_away=’A’ or home_away=’a’. - - If you want both home and away broadcasters, set home_away=’E’ or home_away=’e’. - -Returns: - - A pandas dataframe containing TV and radio broadcast information for various MLB games. - - -### sportsdataverse.mlb.mlbam_reports.mlbam_transactions(startDate: str, endDate: str) -Retrieves all transactions in a given range of dates. -You MUST provide two dates for this function to work, and both dates must be in MM/DD/YYYY format. -For example, December 31st, 2021 would be represented as 12/31/2021. - -Args: - - startDate (int): - - Required parameter. If no startDate is provided, the function wil not work. - Additionally, startDate must be in MM/DD/YYYY format. - - endDate (int): - - Required parameter. If no endDate is provided, the function wil not work. - Additionally, endDate must be in MM/DD/YYYY format. - -Returns: - - A pandas dataframe containing MLB transactions between two dates. - -## sportsdataverse.mlb.mlbam_stats module - - -### sportsdataverse.mlb.mlbam_stats.mlbam_player_career_hitting_stats(playerID: int, gameType='R') -Retrieves the career hitting stats for an MLB player, given a proper MLBAM ID - -Args: - - playerID (int): - - Required parameter. If no playerID is provided, the function wil not work. - - gameType (string) = “R”: - - Optional parameter. If there’s no input, this function will get the info for the regular season. - - Other parts of the season are indicated as follows in the MLBAM API: - - ‘S’ - Spring Training - ‘E’ - Exhibition - ‘A’ - All Star Game - ‘D’ - Division Series - ‘F’ - First Round (Wild Card) - ‘L’ - League Championship - ‘W’ - World Series - -Returns: - - A pandas dataframe containing hitting stats for an MLB player in a given season. - - -### sportsdataverse.mlb.mlbam_stats.mlbam_player_career_pitching_stats(playerID: int, gameType='R') -Retrieves the career pitching stats for an MLB player, given a proper MLBAM ID - -Args: - - playerID (int): - - Required parameter. If no playerID is provided, the function wil not work. - - gameType (string) = “R”: - - Optional parameter. If there’s no input, this function will get the info for the regular season. - - Other parts of the season are indicated as follows in the MLBAM API: - - ‘S’ - Spring Training - ‘E’ - Exhibition - ‘A’ - All Star Game - ‘D’ - Division Series - ‘F’ - First Round (Wild Card) - ‘L’ - League Championship - ‘W’ - World Series - -Returns: - - A pandas dataframe containing career pitching stats for an MLB player. - - -### sportsdataverse.mlb.mlbam_stats.mlbam_player_season_hitting_stats(playerID: int, season: int, gameType='R') -Retrieves the hitting stats for an MLB player in a given season, given a proper MLBAM ID. - -Args: - - playerID (int): - - Required parameter. If no playerID is provided, the function wil not work. - - season (int): - - Required parameter. Indicates the season you are trying to find the games for. - - gameType (string) = “R”: - - Optional parameter. If there’s no input, this function will get the info for the regular season. - - Other parts of the season are indicated as follows in the MLBAM API: - - ‘S’ - Spring Training - ‘E’ - Exhibition - ‘A’ - All Star Game - ‘D’ - Division Series - ‘F’ - First Round (Wild Card) - ‘L’ - League Championship - ‘W’ - World Series - -Returns: - - A pandas dataframe containing career hitting stats for an MLB player. - - -### sportsdataverse.mlb.mlbam_stats.mlbam_player_season_pitching_stats(playerID: int, season: int, gameType='R') -Retrieves the pitching stats for an MLB player in a given season, given a proper MLBAM ID - -Args: - - playerID (int): - - Required parameter. If no playerID is provided, the function wil not work. - - season (int): - - Required parameter. Indicates the season you are trying to find the games for. - - gameType (string) = “R”: - - Optional parameter. If there’s no input, this function will get the info for the regular season. - - Other parts of the season are indicated as follows in the MLBAM API: - - ‘S’ - Spring Training - ‘E’ - Exhibition - ‘A’ - All Star Game - ‘D’ - Division Series - ‘F’ - First Round (Wild Card) - ‘L’ - League Championship - ‘W’ - World Series - -Returns: - - A pandas dataframe containing pitching stats for an MLB player in a given season. - -## sportsdataverse.mlb.mlbam_teams module - - -### sportsdataverse.mlb.mlbam_teams.mlbam_40_man_roster(teamID: int) -Retrieves the current 40-man roster for a team, given a proper MLBAM team ID. - -Args: - -teamID (int): -Required parameter. This should be the MLBAM team ID for the MLB team you want a 40-man roster from. - -Returns: - - A pandas dataframe containing the current 40-man roster for the given MLBAM team ID. - - -### sportsdataverse.mlb.mlbam_teams.mlbam_team_roster(teamID: int, startSeason: int, endSeason: int) -Retrieves the cumulative roster for a MLB team in a specified timeframe. - -Args: - - teamID (int): - - Required parameter. This should be the number MLBAM associates for an MLB team. - For example, the Cincinnati Reds have an MLBAM team ID of 113. - - startSeason (int): - - Required parameter. This value must be less than endSeason for this function to work. - - endSeason (int): - - Required parameter. This value must be greater than startSeason for this function to work. - -Returns: - - A pandas dataframe containg the roster(s) for the MLB team. - - -### sportsdataverse.mlb.mlbam_teams.mlbam_teams(season: int, retriveAllStarRosters=False) -Retrieves the player info for an MLB team, given an MLB season. - -Args: - - season (int): - - Required parameter. If no season is provided, the function wil not work. - - retriveAllStarRosters (boolean): - - Optional parameter. If set to ‘True’, MLB All-Star rosters will be returned when - running this function. - -Returns: - - A pandas dataframe containing information about MLB teams that played in that season. - -## sportsdataverse.mlb.retrosheet module - -RETROSHEET NOTICE: - -> The information used here was obtained free of -> charge from and is copyrighted by Retrosheet. Interested -> parties may contact Retrosheet at “www.retrosheet.org”. - - -### sportsdataverse.mlb.retrosheet.retrosheet_ballparks() -Retrives the current TEAMABR.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. - -Args: - - None - -Returns: - - A pandas Dataframe with the biographical information of notable major league teams. - - -### sportsdataverse.mlb.retrosheet.retrosheet_ejections() -Retrives the current Ejecdata.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. - -Args: - - None - -Returns: - - A pandas Dataframe with the ejection data of known MLB ejections. - - -### sportsdataverse.mlb.retrosheet.retrosheet_franchises() -Retrives the current TEAMABR.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. - -Args: - - None - -Returns: - - A pandas Dataframe with the biographical information of notable major league teams. - - -### sportsdataverse.mlb.retrosheet.retrosheet_game_logs_team() -Retrives the team-level stats for MLB games in a season, or range of seasons. -THIS DOES NOT GET PLAYER STATS! -Use retrosplits_game_logs_player() for player-level game stats. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - - game_type (str): - - Optional parameter. By default, this is set to “regular”, or to put it in another way, this function call will return only regular season games. - - The full list of supported keywards for game_type are as follows. Case does not matter (you can set game_type to “rEgUlAr”, and the function call will still work): - - > - > * “regular”: Regular season games. - - - > * “asg”: All-Star games. - - - > * “playoffs”: Playoff games. - - filter_out_seasons (bool): - - If game_type is set to either “asg” or “playoffs”, and filter_out_seasons is set to true, this function will filter out seasons that do not match the inputted season and/or the range of seasons. By default, this is set to True. - -Returns: - - A pandas dataframe containing team-level stats for MLB games. - - -### sportsdataverse.mlb.retrosheet.retrosheet_people() -Retrives the current BioFile.txt file from the Retrosheet website, and then returns the current file as a pandas dataframe. - -Args: - - None - -Returns: - - A pandas Dataframe with the biographical information of various individuals who have played baseball. - - -### sportsdataverse.mlb.retrosheet.retrosheet_schedule() -Retrives the scheduled games of an MLB season, or MLB seasons. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - - original_2020_schedule (bool): - - Retrosheet keeps a record of the orignial 2020 MLB season, before the season was delayed due to the COVID-19 pandemic. - - - * If this is set to True, this function will return the original 2020 MLB season, before it was altered due to the COVID-19 pandemic, if the user wants this function to return the schedule for the 2020 MLB season. - - - * If this is set to False, this function will return the altered 2020 MLB season, after it was altered due to the COVID-19 pandemic, if the user wants this function to return the schedule for the 2020 MLB season. - -Returns: - - A pandas dataframe containg historical MLB schedules. - -## sportsdataverse.mlb.retrosplits module - -RETROSHEET NOTICE: - -> The information used here was obtained free of -> charge from and is copyrighted by Retrosheet. Interested -> parties may contact Retrosheet at “www.retrosheet.org”. - - -### sportsdataverse.mlb.retrosplits.retrosplits_game_logs_player() -Retrives game-level player stats from the Retrosplits project. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - -Returns: - - A pandas dataframe containing game-level player stats from historical MLB games. - - -### sportsdataverse.mlb.retrosplits.retrosplits_game_logs_team() -Retrives game-level team stats from the Retrosplits project. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - -Returns: - - A pandas dataframe containing game-level team stats from historical MLB games. - - -### sportsdataverse.mlb.retrosplits.retrosplits_player_batting_by_platoon() -Retrives player-level, batting by platoon (left/right hitting vs. left/right pitching) split stats from the Retrosplits project. -The stats are batting stats, based off of the handedness of the batter vs the handedness of the pitcher. -The stats returned by this function are season-level stats, not game-level stats. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - -Returns: - - A pandas dataframe containing player-level, batting by platoon stats for batters. - - -### sportsdataverse.mlb.retrosplits.retrosplits_player_batting_by_position() -Retrives player-level, batting by position split stats from the Retrosplits project. -The stats returned by this function are season-level stats, not game-level stats. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - -Returns: - - A pandas dataframe containing batting by position split stats for MLB players. - - -### sportsdataverse.mlb.retrosplits.retrosplits_player_batting_by_runners() -Retrives player-level, batting by runners split stats from the Retrosplits project. -The stats are batting stats, based off of how many runners are on base at the time of the at bat. -The stats returned by this function are season-level stats, not game-level stats. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - -Returns: - - A pandas dataframe containing player-level, batting by runners split stats. - - -### sportsdataverse.mlb.retrosplits.retrosplits_player_head_to_head_stats() -Retrives batter vs. pitcher stats from the Retrosplits project. -The stats are batting stats, based off of the preformance of that specific batter agianst a specific pitcher for the durration of that specific season. -The stats returned by this function are season-level stats, not game-level stats. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - -Returns: - - A pandas dataframe containing batter vs. pitcher stats for a season, or for a range of seasons. - - -### sportsdataverse.mlb.retrosplits.retrosplits_player_pitching_by_platoon() -Retrives player-level, pitching by platoon (left/right pitching vs. left/right hitting) split stats from the Retrosplits project. -The stats are pitching stats, based off of the handedness of the pitcher vs the handedness of the batter. -The stats returned by this function are season-level stats, not game-level stats. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - -Returns: - - A pandas dataframe containing player-level, pitching by platoon stats for pitchers. - - -### sportsdataverse.mlb.retrosplits.retrosplits_player_pitching_by_runners() -Retrives player-level, pitching by runners split stats from the Retrosplits project. -The stats are pitching stats, based off of how many runners are on base at the time of the at bat. -The stats returned by this function are season-level stats, not game-level stats. - -Args: - - first_season (int): - - Required parameter. Indicates the season you are trying to find the games for, or the first season you are trying to find games for, if you want games from a range of seasons. - - last_season (int): - - Optional parameter. If you want to get games from a range of seasons, set this variable to the last season you want games from. - -Returns: - - A pandas dataframe containing pitching by runners split stats for a season, or for a range of seasons. - -## Module contents diff --git a/docs/docs/nba/index.md b/docs/docs/nba/index.md index 7d7d008..4aabaf1 100755 --- a/docs/docs/nba/index.md +++ b/docs/docs/nba/index.md @@ -15,7 +15,7 @@ Args: Returns: - pd.DataFrame: Data frame of game roster data with columns: + pl.DataFrame: Polars dataframe of game roster data with columns: ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, @@ -63,7 +63,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the play-by-plays available for the requested seasons. Raises: @@ -85,7 +85,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the player boxscores available for the requested seasons. Raises: @@ -107,7 +107,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the schedule for the requested seasons. Raises: @@ -129,7 +129,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the team boxscores available for the requested seasons. Raises: @@ -220,7 +220,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing teams for the requested league. + pl.DataFrame: Polars dataframe containing teams for the requested league. Example: diff --git a/docs/docs/nfl/index.md b/docs/docs/nfl/index.md index 666adf9..7d33b23 100755 --- a/docs/docs/nfl/index.md +++ b/docs/docs/nfl/index.md @@ -17,7 +17,7 @@ Args: Returns: - pd.DataFrame: Data frame of game roster data with columns: + pl.DataFrame: Polars dataframe of game roster data with columns: ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, @@ -108,9 +108,11 @@ Example: Args: + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Returns: - pd.DataFrame: Pandas dataframe containing NFL combine data available. + pl.DataFrame: Polars dataframe containing NFL combine data available. ### sportsdataverse.nfl.nfl_loaders.load_nfl_contracts(return_as_pandas=False) @@ -122,9 +124,11 @@ Example: Args: + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Returns: - pd.DataFrame: Pandas dataframe containing historical contracts available. + pl.DataFrame: Polars dataframe containing historical contracts available. ### sportsdataverse.nfl.nfl_loaders.load_nfl_depth_charts(seasons: List[int], return_as_pandas=False) @@ -137,10 +141,11 @@ Example: Args: seasons (list): Used to define different seasons. 2001 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing depth chart data available for the requested seasons. + pl.DataFrame: Polars dataframe containing depth chart data available for the requested seasons. ### sportsdataverse.nfl.nfl_loaders.load_nfl_draft_picks(return_as_pandas=False) @@ -152,9 +157,11 @@ Example: Args: + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Returns: - pd.DataFrame: Pandas dataframe containing NFL Draft picks data available. + pl.DataFrame: Polars dataframe containing NFL Draft picks data available. ### sportsdataverse.nfl.nfl_loaders.load_nfl_injuries(seasons: List[int], return_as_pandas=False) @@ -167,46 +174,59 @@ Example: Args: seasons (list): Used to define different seasons. 2009 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing injuries data available for the requested seasons. + pl.DataFrame: Polars dataframe containing injuries data available for the requested seasons. ### sportsdataverse.nfl.nfl_loaders.load_nfl_ngs_passing(return_as_pandas=False) Load NFL NextGen Stats Passing data going back to 2016 +Args: + + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Example: nfl_df = sportsdataverse.nfl.load_nfl_ngs_passing() Returns: - pd.DataFrame: Pandas dataframe containing the NextGen Stats Passing data available. + pl.DataFrame: Polars dataframe containing the NextGen Stats Passing data available. ### sportsdataverse.nfl.nfl_loaders.load_nfl_ngs_receiving(return_as_pandas=False) Load NFL NextGen Stats Receiving data going back to 2016 +Args: + + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Example: nfl_df = sportsdataverse.nfl.load_nfl_ngs_receiving() Returns: - pd.DataFrame: Pandas dataframe containing the NextGen Stats Receiving data available. + pl.DataFrame: Polars dataframe containing the NextGen Stats Receiving data available. ### sportsdataverse.nfl.nfl_loaders.load_nfl_ngs_rushing(return_as_pandas=False) Load NFL NextGen Stats Rushing data going back to 2016 +Args: + + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Example: nfl_df = sportsdataverse.nfl.load_nfl_ngs_rushing() Returns: - pd.DataFrame: Pandas dataframe containing the NextGen Stats Rushing data available. + pl.DataFrame: Polars dataframe containing the NextGen Stats Rushing data available. ### sportsdataverse.nfl.nfl_loaders.load_nfl_officials(return_as_pandas=False) @@ -218,9 +238,11 @@ Example: Args: + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Returns: - pd.DataFrame: Pandas dataframe containing officials available. + pl.DataFrame: Polars dataframe containing officials available. ### sportsdataverse.nfl.nfl_loaders.load_nfl_pbp(seasons: List[int], return_as_pandas=False) @@ -237,7 +259,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the play-by-plays available for the requested seasons. + pl.DataFrame: Polars dataframe containing the play-by-plays available for the requested seasons. Raises: @@ -254,22 +276,27 @@ Example: Args: seasons (list): Used to define different seasons. 2016 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing play-by-play participation data available for the requested seasons. + pl.DataFrame: Polars dataframe containing play-by-play participation data available for the requested seasons. ### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_def(return_as_pandas=False) Load NFL Pro-Football Reference Advanced Defensive data going back to 2018 +Args: + + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Example: nfl_df = sportsdataverse.nfl.load_nfl_pfr_def() Returns: - pd.DataFrame: Pandas dataframe containing Pro-Football Reference + pl.DataFrame: Polars dataframe containing Pro-Football Reference advanced defensive stats data available. @@ -277,13 +304,17 @@ Returns: ### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_pass(return_as_pandas=False) Load NFL Pro-Football Reference Advanced Passing data going back to 2018 +Args: + + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Example: nfl_df = sportsdataverse.nfl.load_nfl_pfr_pass() Returns: - pd.DataFrame: Pandas dataframe containing Pro-Football Reference + pl.DataFrame: Polars dataframe containing Pro-Football Reference advanced passing stats data available. @@ -291,13 +322,17 @@ Returns: ### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_rec(return_as_pandas=False) Load NFL Pro-Football Reference Advanced Receiving data going back to 2018 +Args: + + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Example: nfl_df = sportsdataverse.nfl.load_nfl_pfr_rec() Returns: - pd.DataFrame: Pandas dataframe containing Pro-Football Reference + pl.DataFrame: Polars dataframe containing Pro-Football Reference advanced receiving stats data available. @@ -305,13 +340,17 @@ Returns: ### sportsdataverse.nfl.nfl_loaders.load_nfl_pfr_rush(return_as_pandas=False) Load NFL Pro-Football Reference Advanced Rushing data going back to 2018 +Args: + + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Example: nfl_df = sportsdataverse.nfl.load_nfl_pfr_rush() Returns: - pd.DataFrame: Pandas dataframe containing Pro-Football Reference + pl.DataFrame: Polars dataframe containing Pro-Football Reference advanced rushing stats data available. @@ -326,10 +365,11 @@ Example: Args: seasons (list): Used to define different seasons. 2018 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing Pro-Football Reference + pl.DataFrame: Polars dataframe containing Pro-Football Reference advanced defensive stats data available for the requested seasons. @@ -344,10 +384,11 @@ Example: Args: seasons (list): Used to define different seasons. 2018 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing Pro-Football Reference + pl.DataFrame: Polars dataframe containing Pro-Football Reference advanced passing stats data available for the requested seasons. @@ -362,10 +403,11 @@ Example: Args: seasons (list): Used to define different seasons. 2018 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing Pro-Football Reference + pl.DataFrame: Polars dataframe containing Pro-Football Reference advanced receiving stats data available for the requested seasons. @@ -380,10 +422,11 @@ Example: Args: seasons (list): Used to define different seasons. 2018 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing Pro-Football Reference + pl.DataFrame: Polars dataframe containing Pro-Football Reference advanced rushing stats data available for the requested seasons. @@ -398,10 +441,11 @@ Example: Args: kicking (bool): If True, load kicking stats. If False, load all other stats. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing player stats. + pl.DataFrame: Polars dataframe containing player stats. ### sportsdataverse.nfl.nfl_loaders.load_nfl_players(return_as_pandas=False) @@ -413,9 +457,11 @@ Example: Args: + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Returns: - pd.DataFrame: Pandas dataframe containing players available. + pl.DataFrame: Polars dataframe containing players available. ### sportsdataverse.nfl.nfl_loaders.load_nfl_rosters(seasons: List[int], return_as_pandas=False) @@ -428,10 +474,11 @@ Example: Args: seasons (list): Used to define different seasons. 1920 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing rosters available for the requested seasons. + pl.DataFrame: Polars dataframe containing rosters available for the requested seasons. ### sportsdataverse.nfl.nfl_loaders.load_nfl_schedule(seasons: List[int], return_as_pandas=False) @@ -444,10 +491,11 @@ Example: Args: seasons (list): Used to define different seasons. 1999 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing the schedule for the requested seasons. + pl.DataFrame: Polars dataframe containing the schedule for the requested seasons. Raises: @@ -464,10 +512,11 @@ Example: Args: seasons (list): Used to define different seasons. 2012 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing snap counts available for the requested seasons. + pl.DataFrame: Polars dataframe containing snap counts available for the requested seasons. ### sportsdataverse.nfl.nfl_loaders.load_nfl_teams(return_as_pandas=False) @@ -479,9 +528,11 @@ Example: Args: + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Returns: - pd.DataFrame: Pandas dataframe containing teams available. + pl.DataFrame: Polars dataframe containing teams available. ### sportsdataverse.nfl.nfl_loaders.load_nfl_weekly_rosters(seasons: List[int], return_as_pandas=False) @@ -494,10 +545,11 @@ Example: Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing weekly rosters available for the requested seasons. + pl.DataFrame: Polars dataframe containing weekly rosters available for the requested seasons. ## sportsdataverse.nfl.nfl_pbp module @@ -601,7 +653,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing teams for the requested league. + pl.DataFrame: Polars dataframe containing teams for the requested league. Example: diff --git a/docs/docs/nhl/index.md b/docs/docs/nhl/index.md index d093c6a..778f4bf 100755 --- a/docs/docs/nhl/index.md +++ b/docs/docs/nhl/index.md @@ -24,16 +24,18 @@ Example: nhl_df = sportsdataverse.nhl.nhl_api_pbp(game_id=2021020079) -### sportsdataverse.nhl.nhl_api.nhl_api_schedule(start_date: str, end_date: str, return_as_pandas=False) -nhl_api_schedule() - Pull the game by id. Data from API endpoints - nhl/schedule +### sportsdataverse.nhl.nhl_api.nhl_api_schedule(start_date: str, end_date: str, return_as_pandas=False, \*\*kwargs) +nhl_api_schedule() - Pull the schedule by start and end date. Data from API endpoints - nhl/schedule Args: - game_id (int): Unique game_id, can be obtained from nhl_schedule(). + start_date (str): Start date to pull the NHL API schedule. + end_date (str): End date to pull the NHL API schedule. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing the schedule for the requested seasons. + pl.DataFrame: Polars dataframe containing the schedule for the requested seasons. Example: @@ -52,7 +54,7 @@ Args: Returns: - pd.DataFrame: Data frame of game roster data with columns: + pl.DataFrame: Polars dataframe of game roster data with columns: ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, @@ -100,7 +102,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the play-by-plays available for the requested seasons. + pl.DataFrame: Polars dataframe containing the play-by-plays available for the requested seasons. Raises: @@ -121,7 +123,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the player boxscores available for the requested seasons. Raises: @@ -143,7 +145,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the schedule for the requested seasons. + pl.DataFrame: Polars dataframe containing the schedule for the requested seasons. Raises: @@ -164,7 +166,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the team boxscores available for the requested seasons. Raises: @@ -185,7 +187,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing teams available for the requested seasons. + pl.DataFrame: Polars dataframe containing teams available for the requested seasons. ## sportsdataverse.nhl.nhl_pbp module @@ -269,7 +271,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing teams for the requested league. + pl.DataFrame: Polars dataframe containing teams for the requested league. Example: diff --git a/docs/docs/wbb/index.md b/docs/docs/wbb/index.md index 32743c0..e6622ff 100755 --- a/docs/docs/wbb/index.md +++ b/docs/docs/wbb/index.md @@ -15,7 +15,7 @@ Args: Returns: - pd.DataFrame: Data frame of game roster data with columns: + pl.DataFrame: Polars dataframe of game roster data with columns: ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, @@ -63,7 +63,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the play-by-plays available for the requested seasons. Raises: @@ -85,7 +85,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the player boxscores available for the requested seasons. Raises: @@ -107,7 +107,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the schedule for the requested seasons. Raises: @@ -129,7 +129,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the team boxscores available for the requested seasons. Raises: @@ -146,8 +146,7 @@ womens-college-basketball/summary Args: game_id (int): Unique game_id, can be obtained from wbb_schedule(). - -raw (bool): If True, returns the raw json from the API endpoint. If False, returns a cleaned dictionary of datasets. + raw (bool): If True, returns the raw json from the API endpoint. If False, returns a cleaned dictionary of datasets. Returns: @@ -168,7 +167,7 @@ Example: ### sportsdataverse.wbb.wbb_pbp.helper_wbb_pickcenter(pbp_txt) -### sportsdataverse.wbb.wbb_pbp.mbb_pbp_disk(game_id, path_to_json) +### sportsdataverse.wbb.wbb_pbp.wbb_pbp_disk(game_id, path_to_json) ## sportsdataverse.wbb.wbb_schedule module @@ -220,7 +219,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing teams for the requested league. + pl.DataFrame: Polars dataframe containing teams for the requested league. Example: diff --git a/docs/docs/wnba/index.md b/docs/docs/wnba/index.md index c701f7a..fcae786 100755 --- a/docs/docs/wnba/index.md +++ b/docs/docs/wnba/index.md @@ -15,7 +15,7 @@ Args: Returns: - pd.DataFrame: Data frame of game roster data with columns: + pl.DataFrame: Polars Data frame of game roster data with columns: ‘athlete_id’, ‘athlete_uid’, ‘athlete_guid’, ‘athlete_type’, ‘first_name’, ‘last_name’, ‘full_name’, ‘athlete_display_name’, ‘short_name’, ‘weight’, ‘display_weight’, ‘height’, ‘display_height’, @@ -63,7 +63,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the play-by-plays available for the requested seasons. Raises: @@ -85,7 +85,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the player boxscores available for the requested seasons. Raises: @@ -107,7 +107,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the schedule for the requested seasons. Raises: @@ -129,7 +129,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the team boxscores available for the requested seasons. Raises: @@ -214,7 +214,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing teams for the requested league. + pl.DataFrame: Polars dataframe containing teams for the requested league. Example: diff --git a/examples/test_mlbam.py b/examples/test_mlbam.py deleted file mode 100755 index 6275b8d..0000000 --- a/examples/test_mlbam.py +++ /dev/null @@ -1,390 +0,0 @@ -import sportsdataverse as sdv - -""" -mlbam_copyright_info(saveFile=False,returnFile=False): - -Displays the copyright info for the MLBAM API. - -Args: -saveFile (boolean) = False - If saveFile is set to True, the copyright file generated is saved. - -returnFile (boolean) = False - If returnFile is set to True, the copyright file is returned. - -Example: - -import sportsdataverse as sdv -MLBAM_copyright_info = sdv.mlb.mlbam_copyright_info() -print(MLBAM_copyright_info) -""" -print("mlbam_copyright_info") -print(sdv.mlb.mlbam_copyright_info()) -print("") -""" -mlbam_schedule(season:int,gameType="R"): - -Retrieves the start and end date for games for every league, and the MLB, -for a given season. - -This function does not get individual games. - -Args: - -season (int): - Required parameter. Indicates the season you are trying to find the games for. - -gameType (string) = "R": - Optional parameter. If there's no input, this function will get the info for the regular season. - - Other parts of the season are indicated as follows in the MLBAM API: - - 'S' - Spring Training - 'E' - Exhibition - 'A' - All Star Game - 'D' - Division Series - 'F' - First Round (Wild Card) - 'L' - League Championship - 'W' - World Series - -Example: - -import sportsdataverse as sdv -df = sdv.mlb.mlbam_schedule(2020) -print(df) -""" -print("mlbam_schedule") -print(sdv.mlb.mlbam_schedule(2017)) -print("") -""" -mlbam_search_mlb_players(search:str,isActive=""): - -Searches for an MLB player in the MLBAM API. - -Args: -search (string): - Inputted string of the player(s) the user is intending to search. - If there is nothing inputted, nothing will be searched. - -isActive (string, optional): - If called, it will specify if you want active players, or innactive players - in your search. - - If you want active players, set isActive to "Y" or "Yes". - - If you want inactive players, set isActive to "N" or "No". - -Example: - -import sportsdataverse as sdv -df = sdv.mlb.mlbam_search_mlb_players(search="Votto",isActive="y") -print(df) -""" -print("mlbam_search_mlb_players") -print(sdv.mlb.mlbam_search_mlb_players(search="Votto", isActive="y")) -print(sdv.mlb.mlbam_search_mlb_players(search="Joe", isActive="y")) -print("") -""" -mlbam_player_info(playerID:int): - -Retrieves the player info for an MLB player, given a proper MLBAM ID - -Args: - -playerID (int): - Required parameter. If no playerID is provided, the function wil not work. - -Example: - -import sportsdataverse as sdv -df = sdv.mlb.mlbam_player_info(playerID=458015) -print(df) -""" -print("getPlayerInfo") -print(sdv.mlb.mlbam_player_info(playerID=458015)) -print("") -""" -def mlbam_player_teams(playerID:int,season:int): - -Retrieves the info regarding which teams that player played for in a given -season, or in the player's career - -Args: - -playerID (int): - Required parameter. If no playerID is provided, the function wil not work. - -season (int): - Optional parameter. If provided, the search will only look for teams - that player played for in that season. - -Example: - -import sportsdataverse as sdv -df = sdv.mlb.mlbam_player_teams(playerID=523260,season=2014) -print(df) -""" -print("getPlayerTeams") -print(sdv.mlb.mlbam_player_teams(playerID=523260, season=2014)) -print("") -""" -def mlbam_player_season_hitting_stats(playerID:int,season:int,gameType="R"): - -Retrieves the hitting stats for an MLB player in a given season, given a proper MLBAM ID - -Args: - -playerID (int): - Required parameter. If no playerID is provided, the function wil not work. - -season (int): - Required parameter. Indicates the season you are trying to find the games for. - -gameType (string) = "R": - Optional parameter. If there's no input, this function will get the info for the regular season. - - Other parts of the season are indicated as follows in the MLBAM API: - - 'S' - Spring Training - 'E' - Exhibition - 'A' - All Star Game - 'D' - Division Series - 'F' - First Round (Wild Card) - 'L' - League Championship - 'W' - World Series - -Example: - -import sportsdataverse as sdv -df = sdv.mlb.mlbam_player_season_hitting_stats(playerID=458015,season=2021,gameType="R") -print(df) -""" -print("mlbam_player_season_hitting_stats") -print(sdv.mlb.mlbam_player_season_hitting_stats(playerID=458015, season=2021, gameType="R")) -print("") -""" -def mlbam_player_season_pitching_stats(playerID:int,season:int,gameType="R"): - -Retrieves the pitching stats for an MLB player in a given season, given a proper MLBAM ID - -Args: - -playerID (int): - Required parameter. If no playerID is provided, the function wil not work. - -season (int): - Required parameter. Indicates the season you are trying to find the games for. - -gameType (string) = "R": - Optional parameter. If there's no input, this function will get the info for the regular season. - - Other parts of the season are indicated as follows in the MLBAM API: - - 'S' - Spring Training - 'E' - Exhibition - 'A' - All Star Game - 'D' - Division Series - 'F' - First Round (Wild Card) - 'L' - League Championship - 'W' - World Series - -Example: - -import sportsdataverse as sdv -df = sdv.mlb.mlbam_player_season_pitching_stats(playerID=642840,season=2019,gameType="R") -print(df) -""" -print("mlbam_player_season_pitching_stats") -print(sdv.mlb.mlbam_player_season_pitching_stats(playerID=642840, season=2019, gameType="R")) -print("") -""" -def mlbam_player_career_hitting_stats(playerID:int,gameType="R"): - -Retrieves the career hitting stats for an MLB player, given a proper MLBAM ID - -Args: - -playerID (int): - Required parameter. If no playerID is provided, the function wil not work. - -season (int): - Required parameter. Indicates the season you are trying to find the games for. - -gameType (string) = "R": - Optional parameter. If there's no input, this function will get the info for the regular season. - - Other parts of the season are indicated as follows in the MLBAM API: - - 'S' - Spring Training - 'E' - Exhibition - 'A' - All Star Game - 'D' - Division Series - 'F' - First Round (Wild Card) - 'L' - League Championship - 'W' - World Series - -Example: - -import sportsdataverse as sdv -df = sdv.mlb.mlbam_player_career_hitting_stats(playerID=458015,gameType="R") -print(df) -""" -print("mlbam_player_career_hitting_stats") -print(sdv.mlb.mlbam_player_career_hitting_stats(playerID=458015, gameType="R")) -print("") -""" -mlbam_player_career_pitching_stats(playerID:int,gameType="R"): - -Retrieves the career pitching stats for an MLB player, given a proper MLBAM ID - -Args: - -playerID (int): - Required parameter. If no playerID is provided, the function wil not work. - -gameType (string) = "R": - Optional parameter. If there's no input, this function will get the info for the regular season. - - Other parts of the season are indicated as follows in the MLBAM API: - - 'S' - Spring Training - 'E' - Exhibition - 'A' - All Star Game - 'D' - Division Series - 'F' - First Round (Wild Card) - 'L' - League Championship - 'W' - World Series - -Example: - -import sportsdataverse as sdv -df = sdv.mlb.mlbam_player_career_pitching_stats(playerID=642840,gameType="R") -print(df) -""" -print("mlbam_player_career_pitching_stats") -print(sdv.mlb.mlbam_player_career_pitching_stats(playerID=642840, gameType="R")) -print("") -""" -mlbam_transactions(startDate=0,endDate=0): - -Retrieves all transactions in a given range of dates. -You MUST provide two dates for this function to work, and both dates must -be in YYYYMMDD format. For example, December 31st, 2021 would be represented -as 20211231 - -Args: - -startDate (int): - Required parameter. If no startDate is provided, the function wil not work. - Additionally, startDate must be in YYYYMMDD format. - -endDate (int): - Required parameter. If no endDate is provided, the function wil not work. - Additionally, endDate must be in YYYYMMDD format. - -Example: - -import sportsdataverse as sdv -df =sdv.mlb.mlbam_transactions(startDate=20200901,endDate=20200914) -print(df) -""" -print("mlbam_transactions") -print(sdv.mlb.mlbam_transactions(startDate="09/01/2020", endDate="09/01/2020")) -print("") -""" -mlbam_broadcast_info(season:int,home_away="e"): - -Retrieves the broadcasters (radio and TV) involved with certian games. - -Args: - -season (int): - Required parameter. If no season is provided, the function wil not work. - -home_away (string): - Optional parameter. Used to get broadcasters from either the home OR the away side. - Leave blank if you want both home and away broadcasters. - - If you want home broadcasters only, set home_away='H' or home_away='a'. - - If you want away broadcasters only, set home_away='A' or home_away='a'. - - If you want both home and away broadcasters, set home_away='E' or home_away='e'. - -Example: - -import sportsdataverse as sdv -df = sdv.mlb.mlbam_broadcast_info(season=2020,home_away="e") -print(df) -""" -print("mlbam_broadcast_info") -print(sdv.mlb.mlbam_broadcast_info(season=2020, home_away="e")) -print("") -""" -mlbam_teams(season:int,retriveAllStarRosters=False): - -Retrieves the player info for an MLB player, given a proper MLBAM ID - -Args: - -season (int): - Required parameter. If no season is provided, the function wil not work. - -retriveAllStarRosters (boolean): - Optional parameter. If set to 'True', MLB All-Star rosters will be returned when - running this function. - -Example: - -import sportsdataverse as sdv -df = sdv.mlb.mlbam_teams(season=2020) -print(df) -""" -print("mlbam_teams") -print(sdv.mlb.mlbam_teams(season=2020)) -print("") -""" -mlbam_40_man_roster(teamID=113): - -Retrieves the current 40-man roster for a team, given a proper MLBAM Team ID - -Args: - -teamID (int): - Required parameter. If no MLBAM Team ID is provided, the current 40-man roster for the Cincinnati Reds will be returned. - -Example: - -import sportsdataverse as sdv -df = sdv.mlb.mlbam_40_man_roster(teamID=113) -print(df) -""" -print("mlbam_40_man_roster") -print(sdv.mlb.mlbam_40_man_roster(teamID=113)) -print("") -""" -mlbam_team_roster(teamID=113,startSeason=2020,endSeason=2021): - -Retrieves the cumulative roster for a MLB team in a specified timeframe. - -Args: - -teamID (int): - Required parameter. If no MLBAM Team ID is provided, the cumulative roster for the Cincinnati Reds will be returned. - -startSeason (int): - Required parameter. This value must be less than endSeason for this function to work. - -endSeason (int): - Required parameter. This value must be greater than startSeason for this function to work. - -Example: - -import sportsdataverse as sdv -df = sdv.mlb.mlbam_team_roster(teamID=113,startSeason=2020,endSeason=2021) -print(df) -""" -print("mlbam_team_roster") -df = sdv.mlb.mlbam_team_roster(teamID=113, startSeason=2020, endSeason=2021) -print(df) -print("") diff --git a/sportsdataverse/__init__.py b/sportsdataverse/__init__.py index c49b3fe..dd01778 100755 --- a/sportsdataverse/__init__.py +++ b/sportsdataverse/__init__.py @@ -1,6 +1,7 @@ from sportsdataverse.cfb import * from sportsdataverse.mbb import * -from sportsdataverse.mlb import * + +# from sportsdataverse.mlb import * from sportsdataverse.nba import * from sportsdataverse.nfl import * from sportsdataverse.nhl import * diff --git a/sportsdataverse/cfb/cfb_game_rosters.py b/sportsdataverse/cfb/cfb_game_rosters.py index 8ba3927..b651ca1 100644 --- a/sportsdataverse/cfb/cfb_game_rosters.py +++ b/sportsdataverse/cfb/cfb_game_rosters.py @@ -12,7 +12,7 @@ def espn_cfb_game_rosters(game_id: int, raw=False, return_as_pandas=False, **kwa return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Data frame of game roster data with columns: + pl.DataFrame: Polars dataframe of game roster data with columns: 'athlete_id', 'athlete_uid', 'athlete_guid', 'athlete_type', 'first_name', 'last_name', 'full_name', 'athlete_display_name', 'short_name', 'weight', 'display_weight', 'height', 'display_height', diff --git a/sportsdataverse/cfb/cfb_loaders.py b/sportsdataverse/cfb/cfb_loaders.py index b0c8b45..0b2565f 100755 --- a/sportsdataverse/cfb/cfb_loaders.py +++ b/sportsdataverse/cfb/cfb_loaders.py @@ -25,7 +25,7 @@ def load_cfb_pbp(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing the play-by-plays available for the requested seasons. + pl.DataFrame: Polars dataframe containing the play-by-plays available for the requested seasons. Raises: ValueError: If `season` is less than 2003. @@ -52,7 +52,7 @@ def load_cfb_schedule(seasons: List[int], return_as_pandas=False) -> pl.DataFram return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing the schedule for the requested seasons. + pl.DataFrame: Polars dataframe containing the schedule for the requested seasons. Raises: ValueError: If `season` is less than 2002. @@ -80,7 +80,7 @@ def load_cfb_rosters(seasons: List[int], return_as_pandas=False) -> pl.DataFrame return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing rosters available for the requested seasons. + pl.DataFrame: Polars dataframe containing rosters available for the requested seasons. Raises: ValueError: If `season` is less than 2014. @@ -107,7 +107,7 @@ def load_cfb_team_info(seasons: List[int], return_as_pandas=False) -> pl.DataFra return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing the team info available for the requested seasons. + pl.DataFrame: Polars dataframe containing the team info available for the requested seasons. Raises: ValueError: If `season` is less than 2002. @@ -136,7 +136,7 @@ def load_cfb_betting_lines(return_as_pandas=False) -> pl.DataFrame: return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing betting lines available for the available seasons. + pl.DataFrame: Polars dataframe containing betting lines available for the available seasons. """ return ( @@ -158,7 +158,7 @@ def get_cfb_teams(return_as_pandas=False) -> pl.DataFrame: return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing teams available. + pl.DataFrame: Polars dataframe containing teams available. """ return ( diff --git a/sportsdataverse/cfb/cfb_teams.py b/sportsdataverse/cfb/cfb_teams.py index bc0dfdd..68b772c 100755 --- a/sportsdataverse/cfb/cfb_teams.py +++ b/sportsdataverse/cfb/cfb_teams.py @@ -15,7 +15,7 @@ def espn_cfb_teams(groups=None, return_as_pandas=False, **kwargs) -> pl.DataFram return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. + pl.DataFrame: Polars dataframe containing schedule dates for the requested season. Example: `cfb_df = sportsdataverse.cfb.espn_cfb_teams()` diff --git a/sportsdataverse/mbb/mbb_game_rosters.py b/sportsdataverse/mbb/mbb_game_rosters.py index 3533d49..5b69227 100755 --- a/sportsdataverse/mbb/mbb_game_rosters.py +++ b/sportsdataverse/mbb/mbb_game_rosters.py @@ -12,7 +12,7 @@ def espn_mbb_game_rosters(game_id: int, raw=False, return_as_pandas=False, **kwa return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Data frame of game roster data with columns: + pl.DataFrame: Polars dataframe of game roster data with columns: 'athlete_id', 'athlete_uid', 'athlete_guid', 'athlete_type', 'first_name', 'last_name', 'full_name', 'athlete_display_name', 'short_name', 'weight', 'display_weight', 'height', 'display_height', diff --git a/sportsdataverse/mbb/mbb_loaders.py b/sportsdataverse/mbb/mbb_loaders.py index 407141c..9997d8a 100755 --- a/sportsdataverse/mbb/mbb_loaders.py +++ b/sportsdataverse/mbb/mbb_loaders.py @@ -23,7 +23,7 @@ def load_mbb_pbp(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the play-by-plays available for the requested seasons. Raises: @@ -51,7 +51,7 @@ def load_mbb_team_boxscore(seasons: List[int], return_as_pandas=False) -> pl.Dat return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the team boxscores available for the requested seasons. Raises: @@ -79,7 +79,7 @@ def load_mbb_player_boxscore(seasons: List[int], return_as_pandas=False) -> pl.D return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the player boxscores available for the requested seasons. Raises: @@ -107,7 +107,7 @@ def load_mbb_schedule(seasons: List[int], return_as_pandas=False) -> pl.DataFram return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the schedule for the requested seasons. Raises: diff --git a/sportsdataverse/mbb/mbb_teams.py b/sportsdataverse/mbb/mbb_teams.py index a831423..7aedc61 100755 --- a/sportsdataverse/mbb/mbb_teams.py +++ b/sportsdataverse/mbb/mbb_teams.py @@ -15,7 +15,7 @@ def espn_mbb_teams(groups=None, return_as_pandas=False, **kwargs) -> pl.DataFram return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing teams for the requested league. + pl.DataFrame: Polars dataframe containing teams for the requested league. Example: `mbb_df = sportsdataverse.mbb.espn_mbb_teams()` diff --git a/sportsdataverse/nba/nba_game_rosters.py b/sportsdataverse/nba/nba_game_rosters.py index 915eb23..f10d8d2 100644 --- a/sportsdataverse/nba/nba_game_rosters.py +++ b/sportsdataverse/nba/nba_game_rosters.py @@ -12,7 +12,7 @@ def espn_nba_game_rosters(game_id: int, raw=False, return_as_pandas=False, **kwa return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Data frame of game roster data with columns: + pl.DataFrame: Polars dataframe of game roster data with columns: 'athlete_id', 'athlete_uid', 'athlete_guid', 'athlete_type', 'first_name', 'last_name', 'full_name', 'athlete_display_name', 'short_name', 'weight', 'display_weight', 'height', 'display_height', diff --git a/sportsdataverse/nba/nba_loaders.py b/sportsdataverse/nba/nba_loaders.py index 2812066..983f6d1 100755 --- a/sportsdataverse/nba/nba_loaders.py +++ b/sportsdataverse/nba/nba_loaders.py @@ -23,7 +23,7 @@ def load_nba_pbp(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the play-by-plays available for the requested seasons. Raises: @@ -51,7 +51,7 @@ def load_nba_team_boxscore(seasons: List[int], return_as_pandas=False) -> pl.Dat return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the team boxscores available for the requested seasons. Raises: @@ -79,7 +79,7 @@ def load_nba_player_boxscore(seasons: List[int], return_as_pandas=False) -> pl.D return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the player boxscores available for the requested seasons. Raises: @@ -107,7 +107,7 @@ def load_nba_schedule(seasons: List[int], return_as_pandas=False) -> pl.DataFram return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the schedule for the requested seasons. Raises: diff --git a/sportsdataverse/nba/nba_teams.py b/sportsdataverse/nba/nba_teams.py index 043da8e..2af1e3b 100755 --- a/sportsdataverse/nba/nba_teams.py +++ b/sportsdataverse/nba/nba_teams.py @@ -14,7 +14,7 @@ def espn_nba_teams(return_as_pandas=False, **kwargs) -> pl.DataFrame: return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing teams for the requested league. + pl.DataFrame: Polars dataframe containing teams for the requested league. Example: `nba_df = sportsdataverse.nba.espn_nba_teams()` diff --git a/sportsdataverse/nfl/nfl_game_rosters.py b/sportsdataverse/nfl/nfl_game_rosters.py index 3b7706c..ab6e61f 100644 --- a/sportsdataverse/nfl/nfl_game_rosters.py +++ b/sportsdataverse/nfl/nfl_game_rosters.py @@ -12,7 +12,7 @@ def espn_nfl_game_rosters(game_id: int, raw=False, return_as_pandas=False, **kwa return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Data frame of game roster data with columns: + pl.DataFrame: Polars dataframe of game roster data with columns: 'athlete_id', 'athlete_uid', 'athlete_guid', 'athlete_type', 'first_name', 'last_name', 'full_name', 'athlete_display_name', 'short_name', 'weight', 'display_weight', 'height', 'display_height', diff --git a/sportsdataverse/nfl/nfl_loaders.py b/sportsdataverse/nfl/nfl_loaders.py index c0c6090..d40de83 100755 --- a/sportsdataverse/nfl/nfl_loaders.py +++ b/sportsdataverse/nfl/nfl_loaders.py @@ -48,7 +48,7 @@ def load_nfl_pbp(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing the play-by-plays available for the requested seasons. + pl.DataFrame: Polars dataframe containing the play-by-plays available for the requested seasons. Raises: ValueError: If `season` is less than 1999. @@ -71,9 +71,10 @@ def load_nfl_schedule(seasons: List[int], return_as_pandas=False) -> pl.DataFram Args: seasons (list): Used to define different seasons. 1999 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing the schedule for the requested seasons. + pl.DataFrame: Polars dataframe containing the schedule for the requested seasons. Raises: ValueError: If `season` is less than 1999. @@ -100,9 +101,10 @@ def load_nfl_player_stats(kicking=False, return_as_pandas=False) -> pl.DataFrame Args: kicking (bool): If True, load kicking stats. If False, load all other stats. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing player stats. + pl.DataFrame: Polars dataframe containing player stats. """ data = pl.DataFrame() if kicking is False: @@ -116,11 +118,14 @@ def load_nfl_player_stats(kicking=False, return_as_pandas=False) -> pl.DataFrame def load_nfl_ngs_passing(return_as_pandas=False) -> pl.DataFrame: """Load NFL NextGen Stats Passing data going back to 2016 + Args: + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Example: `nfl_df = sportsdataverse.nfl.load_nfl_ngs_passing()` Returns: - pd.DataFrame: Pandas dataframe containing the NextGen Stats Passing data available. + pl.DataFrame: Polars dataframe containing the NextGen Stats Passing data available. """ return ( @@ -135,11 +140,14 @@ def load_nfl_ngs_passing(return_as_pandas=False) -> pl.DataFrame: def load_nfl_ngs_rushing(return_as_pandas=False) -> pl.DataFrame: """Load NFL NextGen Stats Rushing data going back to 2016 + Args: + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Example: `nfl_df = sportsdataverse.nfl.load_nfl_ngs_rushing()` Returns: - pd.DataFrame: Pandas dataframe containing the NextGen Stats Rushing data available. + pl.DataFrame: Polars dataframe containing the NextGen Stats Rushing data available. """ return ( @@ -154,11 +162,14 @@ def load_nfl_ngs_rushing(return_as_pandas=False) -> pl.DataFrame: def load_nfl_ngs_receiving(return_as_pandas=False) -> pl.DataFrame: """Load NFL NextGen Stats Receiving data going back to 2016 + Args: + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Example: `nfl_df = sportsdataverse.nfl.load_nfl_ngs_receiving()` Returns: - pd.DataFrame: Pandas dataframe containing the NextGen Stats Receiving data available. + pl.DataFrame: Polars dataframe containing the NextGen Stats Receiving data available. """ return ( @@ -173,11 +184,14 @@ def load_nfl_ngs_receiving(return_as_pandas=False) -> pl.DataFrame: def load_nfl_pfr_pass(return_as_pandas=False) -> pl.DataFrame: """Load NFL Pro-Football Reference Advanced Passing data going back to 2018 + Args: + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Example: `nfl_df = sportsdataverse.nfl.load_nfl_pfr_pass()` Returns: - pd.DataFrame: Pandas dataframe containing Pro-Football Reference + pl.DataFrame: Polars dataframe containing Pro-Football Reference advanced passing stats data available. """ @@ -198,9 +212,10 @@ def load_nfl_pfr_weekly_pass(seasons: List[int], return_as_pandas=False) -> pl.D Args: seasons (list): Used to define different seasons. 2018 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing Pro-Football Reference + pl.DataFrame: Polars dataframe containing Pro-Football Reference advanced passing stats data available for the requested seasons. """ @@ -217,11 +232,14 @@ def load_nfl_pfr_weekly_pass(seasons: List[int], return_as_pandas=False) -> pl.D def load_nfl_pfr_rush(return_as_pandas=False) -> pl.DataFrame: """Load NFL Pro-Football Reference Advanced Rushing data going back to 2018 + Args: + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Example: `nfl_df = sportsdataverse.nfl.load_nfl_pfr_rush()` Returns: - pd.DataFrame: Pandas dataframe containing Pro-Football Reference + pl.DataFrame: Polars dataframe containing Pro-Football Reference advanced rushing stats data available. """ @@ -242,9 +260,10 @@ def load_nfl_pfr_weekly_rush(seasons: List[int], return_as_pandas=False) -> pl.D Args: seasons (list): Used to define different seasons. 2018 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing Pro-Football Reference + pl.DataFrame: Polars dataframe containing Pro-Football Reference advanced rushing stats data available for the requested seasons. """ @@ -261,11 +280,14 @@ def load_nfl_pfr_weekly_rush(seasons: List[int], return_as_pandas=False) -> pl.D def load_nfl_pfr_rec(return_as_pandas=False) -> pl.DataFrame: """Load NFL Pro-Football Reference Advanced Receiving data going back to 2018 + Args: + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Example: `nfl_df = sportsdataverse.nfl.load_nfl_pfr_rec()` Returns: - pd.DataFrame: Pandas dataframe containing Pro-Football Reference + pl.DataFrame: Polars dataframe containing Pro-Football Reference advanced receiving stats data available. """ @@ -286,9 +308,10 @@ def load_nfl_pfr_weekly_rec(seasons: List[int], return_as_pandas=False) -> pl.Da Args: seasons (list): Used to define different seasons. 2018 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing Pro-Football Reference + pl.DataFrame: Polars dataframe containing Pro-Football Reference advanced receiving stats data available for the requested seasons. """ @@ -305,11 +328,14 @@ def load_nfl_pfr_weekly_rec(seasons: List[int], return_as_pandas=False) -> pl.Da def load_nfl_pfr_def(return_as_pandas=False) -> pl.DataFrame: """Load NFL Pro-Football Reference Advanced Defensive data going back to 2018 + Args: + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Example: `nfl_df = sportsdataverse.nfl.load_nfl_pfr_def()` Returns: - pd.DataFrame: Pandas dataframe containing Pro-Football Reference + pl.DataFrame: Polars dataframe containing Pro-Football Reference advanced defensive stats data available. """ @@ -330,9 +356,10 @@ def load_nfl_pfr_weekly_def(seasons: List[int], return_as_pandas=False) -> pl.Da Args: seasons (list): Used to define different seasons. 2018 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing Pro-Football Reference + pl.DataFrame: Polars dataframe containing Pro-Football Reference advanced defensive stats data available for the requested seasons. """ @@ -354,9 +381,10 @@ def load_nfl_rosters(seasons: List[int], return_as_pandas=False) -> pl.DataFrame Args: seasons (list): Used to define different seasons. 1920 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing rosters available for the requested seasons. + pl.DataFrame: Polars dataframe containing rosters available for the requested seasons. """ data = pl.DataFrame() @@ -377,9 +405,10 @@ def load_nfl_weekly_rosters(seasons: List[int], return_as_pandas=False) -> pl.Da Args: seasons (list): Used to define different seasons. 2002 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing weekly rosters available for the requested seasons. + pl.DataFrame: Polars dataframe containing weekly rosters available for the requested seasons. """ data = pl.DataFrame() @@ -399,9 +428,10 @@ def load_nfl_teams(return_as_pandas=False) -> pl.DataFrame: `nfl_df = sportsdataverse.nfl.load_nfl_teams()` Args: + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing teams available. + pl.DataFrame: Polars dataframe containing teams available. """ return ( pl.read_csv(NFL_TEAM_LOGO_URL).to_pandas(use_pyarrow_extension_array=True) @@ -417,9 +447,10 @@ def load_nfl_players(return_as_pandas=False) -> pl.DataFrame: `nfl_df = sportsdataverse.nfl.load_nfl_players()` Args: + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing players available. + pl.DataFrame: Polars dataframe containing players available. """ return ( pl.read_parquet(NFL_PLAYER_URL, use_pyarrow=True, columns=None).to_pandas(use_pyarrow_extension_array=True) @@ -436,9 +467,10 @@ def load_nfl_snap_counts(seasons: List[int], return_as_pandas=False) -> pl.DataF Args: seasons (list): Used to define different seasons. 2012 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing snap counts available for the requested seasons. + pl.DataFrame: Polars dataframe containing snap counts available for the requested seasons. """ data = pl.DataFrame() @@ -459,9 +491,10 @@ def load_nfl_pbp_participation(seasons: List[int], return_as_pandas=False) -> pl Args: seasons (list): Used to define different seasons. 2016 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing play-by-play participation data available for the requested seasons. + pl.DataFrame: Polars dataframe containing play-by-play participation data available for the requested seasons. """ data = pl.DataFrame() @@ -482,9 +515,10 @@ def load_nfl_injuries(seasons: List[int], return_as_pandas=False) -> pl.DataFram Args: seasons (list): Used to define different seasons. 2009 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing injuries data available for the requested seasons. + pl.DataFrame: Polars dataframe containing injuries data available for the requested seasons. """ data = pl.DataFrame() @@ -505,9 +539,10 @@ def load_nfl_depth_charts(seasons: List[int], return_as_pandas=False) -> pl.Data Args: seasons (list): Used to define different seasons. 2001 is the earliest available season. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing depth chart data available for the requested seasons. + pl.DataFrame: Polars dataframe containing depth chart data available for the requested seasons. """ data = pl.DataFrame() @@ -527,9 +562,10 @@ def load_nfl_contracts(return_as_pandas=False) -> pl.DataFrame: `nfl_df = sportsdataverse.nfl.load_nfl_contracts()` Args: + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing historical contracts available. + pl.DataFrame: Polars dataframe containing historical contracts available. """ return ( pl.read_parquet(NFL_CONTRACTS_URL, use_pyarrow=True, columns=None).to_pandas(use_pyarrow_extension_array=True) @@ -545,9 +581,10 @@ def load_nfl_combine(return_as_pandas=False) -> pl.DataFrame: `nfl_df = sportsdataverse.nfl.load_nfl_combine()` Args: + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing NFL combine data available. + pl.DataFrame: Polars dataframe containing NFL combine data available. """ return ( pl.read_parquet(NFL_COMBINE_URL, use_pyarrow=True, columns=None).to_pandas(use_pyarrow_extension_array=True) @@ -563,9 +600,10 @@ def load_nfl_draft_picks(return_as_pandas=False) -> pl.DataFrame: `nfl_df = sportsdataverse.nfl.load_nfl_draft_picks()` Args: + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing NFL Draft picks data available. + pl.DataFrame: Polars dataframe containing NFL Draft picks data available. """ return ( pl.read_parquet(NFL_DRAFT_PICKS_URL, use_pyarrow=True, columns=None).to_pandas( @@ -583,9 +621,10 @@ def load_nfl_officials(return_as_pandas=False) -> pl.DataFrame: `nfl_df = sportsdataverse.nfl.load_nfl_officials()` Args: + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing officials available. + pl.DataFrame: Polars dataframe containing officials available. """ return ( pl.read_parquet(NFL_OFFICIALS_URL, use_pyarrow=True, columns=None).to_pandas(use_pyarrow_extension_array=True) @@ -606,7 +645,7 @@ def load_nfl_officials(return_as_pandas=False) -> pl.DataFrame: # Args: # Returns: -# pd.DataFrame: Pandas dataframe containing player contracts detail data available. +# pl.DataFrame: Polars dataframe containing player contracts detail data available. # """ # data = pd.DataFrame() # with tempfile.TemporaryDirectory() as tempdirname: diff --git a/sportsdataverse/nfl/nfl_teams.py b/sportsdataverse/nfl/nfl_teams.py index e14314c..a3fc5dd 100755 --- a/sportsdataverse/nfl/nfl_teams.py +++ b/sportsdataverse/nfl/nfl_teams.py @@ -14,7 +14,7 @@ def espn_nfl_teams(return_as_pandas=False, **kwargs) -> pl.DataFrame: return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing teams for the requested league. + pl.DataFrame: Polars dataframe containing teams for the requested league. Example: `nfl_df = sportsdataverse.nfl.espn_nfl_teams()` diff --git a/sportsdataverse/nhl/nhl_api.py b/sportsdataverse/nhl/nhl_api.py index edfc9e0..d85f164 100755 --- a/sportsdataverse/nhl/nhl_api.py +++ b/sportsdataverse/nhl/nhl_api.py @@ -35,14 +35,16 @@ def nhl_api_pbp(game_id: int, **kwargs) -> Dict: return pbp_txt -def nhl_api_schedule(start_date: str, end_date: str, return_as_pandas=False) -> pl.DataFrame: - """nhl_api_schedule() - Pull the game by id. Data from API endpoints - `nhl/schedule` +def nhl_api_schedule(start_date: str, end_date: str, return_as_pandas=False, **kwargs) -> pl.DataFrame: + """nhl_api_schedule() - Pull the schedule by start and end date. Data from API endpoints - `nhl/schedule` Args: - game_id (int): Unique game_id, can be obtained from nhl_schedule(). + start_date (str): Start date to pull the NHL API schedule. + end_date (str): End date to pull the NHL API schedule. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing the schedule for the requested seasons. + pl.DataFrame: Polars dataframe containing the schedule for the requested seasons. Example: `nhl_sched_df = sportsdataverse.nhl.nhl_api_schedule(start_date=2021-10-23, end_date=2021-10-28)` diff --git a/sportsdataverse/nhl/nhl_game_rosters.py b/sportsdataverse/nhl/nhl_game_rosters.py index 97d6ef6..7b92b05 100644 --- a/sportsdataverse/nhl/nhl_game_rosters.py +++ b/sportsdataverse/nhl/nhl_game_rosters.py @@ -12,7 +12,7 @@ def espn_nhl_game_rosters(game_id: int, raw=False, return_as_pandas=False, **kwa return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Data frame of game roster data with columns: + pl.DataFrame: Polars dataframe of game roster data with columns: 'athlete_id', 'athlete_uid', 'athlete_guid', 'athlete_type', 'first_name', 'last_name', 'full_name', 'athlete_display_name', 'short_name', 'weight', 'display_weight', 'height', 'display_height', diff --git a/sportsdataverse/nhl/nhl_loaders.py b/sportsdataverse/nhl/nhl_loaders.py index 5a39196..557fb6a 100755 --- a/sportsdataverse/nhl/nhl_loaders.py +++ b/sportsdataverse/nhl/nhl_loaders.py @@ -24,7 +24,7 @@ def load_nhl_pbp(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing the play-by-plays available for the requested seasons. + pl.DataFrame: Polars dataframe containing the play-by-plays available for the requested seasons. Raises: ValueError: If `season` is less than 2011. @@ -51,7 +51,7 @@ def load_nhl_schedule(seasons: List[int], return_as_pandas=False) -> pl.DataFram return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing the schedule for the requested seasons. + pl.DataFrame: Polars dataframe containing the schedule for the requested seasons. Raises: ValueError: If `season` is less than 2002. @@ -78,7 +78,7 @@ def load_nhl_team_boxscore(seasons: List[int], return_as_pandas=False) -> pl.Dat return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the team boxscores available for the requested seasons. Raises: @@ -106,7 +106,7 @@ def load_nhl_player_boxscore(seasons: List[int], return_as_pandas=False) -> pl.D return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the player boxscores available for the requested seasons. Raises: @@ -133,6 +133,6 @@ def nhl_teams(return_as_pandas=False) -> pl.DataFrame: return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing teams available for the requested seasons. + pl.DataFrame: Polars dataframe containing teams available for the requested seasons. """ return pl.read_csv(NHL_TEAM_LOGO_URL).to_pandas if return_as_pandas else pl.read_csv(NHL_TEAM_LOGO_URL) diff --git a/sportsdataverse/nhl/nhl_pbp.py b/sportsdataverse/nhl/nhl_pbp.py index e5070f9..88c40e5 100755 --- a/sportsdataverse/nhl/nhl_pbp.py +++ b/sportsdataverse/nhl/nhl_pbp.py @@ -184,8 +184,8 @@ def helper_nhl_pickcenter(pbp_txt): gameSpreadAvailable = True # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") else: - gameSpread = 2.5 - overUnder = 190.5 + gameSpread = 1.5 + overUnder = 5.5 homeFavorite = True gameSpreadAvailable = False diff --git a/sportsdataverse/nhl/nhl_teams.py b/sportsdataverse/nhl/nhl_teams.py index cdc625f..245a7bd 100755 --- a/sportsdataverse/nhl/nhl_teams.py +++ b/sportsdataverse/nhl/nhl_teams.py @@ -14,7 +14,7 @@ def espn_nhl_teams(return_as_pandas=False, **kwargs) -> pl.DataFrame: return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing teams for the requested league. + pl.DataFrame: Polars dataframe containing teams for the requested league. Example: `nhl_df = sportsdataverse.nhl.espn_nhl_teams()` diff --git a/sportsdataverse/wbb/wbb_game_rosters.py b/sportsdataverse/wbb/wbb_game_rosters.py index 8bd4ea9..2b9c290 100644 --- a/sportsdataverse/wbb/wbb_game_rosters.py +++ b/sportsdataverse/wbb/wbb_game_rosters.py @@ -12,7 +12,7 @@ def espn_wbb_game_rosters(game_id: int, raw=False, return_as_pandas=False, **kwa return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Data frame of game roster data with columns: + pl.DataFrame: Polars dataframe of game roster data with columns: 'athlete_id', 'athlete_uid', 'athlete_guid', 'athlete_type', 'first_name', 'last_name', 'full_name', 'athlete_display_name', 'short_name', 'weight', 'display_weight', 'height', 'display_height', diff --git a/sportsdataverse/wbb/wbb_loaders.py b/sportsdataverse/wbb/wbb_loaders.py index df2c40b..32ed440 100755 --- a/sportsdataverse/wbb/wbb_loaders.py +++ b/sportsdataverse/wbb/wbb_loaders.py @@ -23,7 +23,7 @@ def load_wbb_pbp(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the play-by-plays available for the requested seasons. Raises: @@ -51,7 +51,7 @@ def load_wbb_team_boxscore(seasons: List[int], return_as_pandas=False) -> pl.Dat return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the team boxscores available for the requested seasons. Raises: @@ -79,7 +79,7 @@ def load_wbb_player_boxscore(seasons: List[int], return_as_pandas=False) -> pl.D return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the player boxscores available for the requested seasons. Raises: @@ -107,7 +107,7 @@ def load_wbb_schedule(seasons: List[int], return_as_pandas=False) -> pl.DataFram return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the schedule for the requested seasons. Raises: diff --git a/sportsdataverse/wbb/wbb_pbp.py b/sportsdataverse/wbb/wbb_pbp.py index 641c79f..2183176 100755 --- a/sportsdataverse/wbb/wbb_pbp.py +++ b/sportsdataverse/wbb/wbb_pbp.py @@ -16,7 +16,7 @@ def espn_wbb_pbp(game_id: int, raw=False, **kwargs) -> Dict: Args: game_id (int): Unique game_id, can be obtained from wbb_schedule(). - raw (bool): If True, returns the raw json from the API endpoint. If False, returns a cleaned dictionary of datasets. + raw (bool): If True, returns the raw json from the API endpoint. If False, returns a cleaned dictionary of datasets. Returns: Dict: Dictionary of game data with keys - "gameId", "plays", "winprobability", "boxscore", "header", @@ -90,7 +90,7 @@ def espn_wbb_pbp(game_id: int, raw=False, **kwargs) -> Dict: return helper_wbb_pbp(game_id, pbp_txt) -def mbb_pbp_disk(game_id, path_to_json): +def wbb_pbp_disk(game_id, path_to_json): with open(os.path.join(path_to_json, f"{game_id}.json")) as json_file: pbp_txt = json.load(json_file) return pbp_txt diff --git a/sportsdataverse/wbb/wbb_teams.py b/sportsdataverse/wbb/wbb_teams.py index f225e27..ddaee48 100755 --- a/sportsdataverse/wbb/wbb_teams.py +++ b/sportsdataverse/wbb/wbb_teams.py @@ -15,7 +15,7 @@ def espn_wbb_teams(groups=None, return_as_pandas=False, **kwargs) -> pl.DataFram return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing teams for the requested league. + pl.DataFrame: Polars dataframe containing teams for the requested league. Example: `wbb_df = sportsdataverse.wbb.espn_wbb_teams()` diff --git a/sportsdataverse/wnba/wnba_game_rosters.py b/sportsdataverse/wnba/wnba_game_rosters.py index 3e1a921..3c5433c 100644 --- a/sportsdataverse/wnba/wnba_game_rosters.py +++ b/sportsdataverse/wnba/wnba_game_rosters.py @@ -12,7 +12,7 @@ def espn_wnba_game_rosters(game_id: int, raw=False, return_as_pandas=False, **kw return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Data frame of game roster data with columns: + pl.DataFrame: Polars Data frame of game roster data with columns: 'athlete_id', 'athlete_uid', 'athlete_guid', 'athlete_type', 'first_name', 'last_name', 'full_name', 'athlete_display_name', 'short_name', 'weight', 'display_weight', 'height', 'display_height', diff --git a/sportsdataverse/wnba/wnba_loaders.py b/sportsdataverse/wnba/wnba_loaders.py index 3e2aa39..8967841 100755 --- a/sportsdataverse/wnba/wnba_loaders.py +++ b/sportsdataverse/wnba/wnba_loaders.py @@ -23,7 +23,7 @@ def load_wnba_pbp(seasons: List[int], return_as_pandas=False) -> pl.DataFrame: return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the play-by-plays available for the requested seasons. Raises: @@ -51,7 +51,7 @@ def load_wnba_team_boxscore(seasons: List[int], return_as_pandas=False) -> pl.Da return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the team boxscores available for the requested seasons. Raises: @@ -79,7 +79,7 @@ def load_wnba_player_boxscore(seasons: List[int], return_as_pandas=False) -> pl. return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the player boxscores available for the requested seasons. Raises: @@ -107,7 +107,7 @@ def load_wnba_schedule(seasons: List[int], return_as_pandas=False) -> pl.DataFra return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing the + pl.DataFrame: Polars dataframe containing the schedule for the requested seasons. Raises: diff --git a/sportsdataverse/wnba/wnba_teams.py b/sportsdataverse/wnba/wnba_teams.py index 1397e7e..b0c0c04 100755 --- a/sportsdataverse/wnba/wnba_teams.py +++ b/sportsdataverse/wnba/wnba_teams.py @@ -14,7 +14,7 @@ def espn_wnba_teams(return_as_pandas=False, **kwargs) -> pl.DataFrame: return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing teams for the requested league. + pl.DataFrame: Polars dataframe containing teams for the requested league. Example: `wnba_df = sportsdataverse.wnba.espn_wnba_teams()` From 444b4c11382a67f56809e75a4df1387c96ac2cf2 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Mon, 31 Jul 2023 22:45:45 -0400 Subject: [PATCH 63/79] mlb submodule moved to archive --- docs/sidebars.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/sidebars.js b/docs/sidebars.js index 815cb4a..2f27f90 100755 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -24,11 +24,6 @@ module.exports = { label: "MBB", items: ["mbb/index"], }, - { - type: "category", - label: "MLB", - items: ["mlb/index"], - }, { type: "category", label: "NBA", From 1df7a7c3fdc161b083a340a5514e393bcbeabbc5 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Mon, 31 Jul 2023 22:52:23 -0400 Subject: [PATCH 64/79] sidebars --- docs/package.json | 7 +- docs/yarn.lock | 3081 ++++++++++++++++++++++++++++++++------------- 2 files changed, 2207 insertions(+), 881 deletions(-) diff --git a/docs/package.json b/docs/package.json index 6e956ce..70113c4 100755 --- a/docs/package.json +++ b/docs/package.json @@ -14,15 +14,18 @@ "write-heading-ids": "docusaurus write-heading-ids" }, "dependencies": { - "@docusaurus/core": "^2.0.0-beta.3", - "@docusaurus/preset-classic": "^2.0.0-beta.3", + "@docusaurus/core": "^2.0.0-alpha.70", + "@docusaurus/preset-classic": "^2.0.0-alpha.70", "@mdx-js/react": "^1.6.21", "@svgr/webpack": "^5.5.0", + "autocomplete.js": "^0.38.0", + "clsx": "^1.1.1", "file-loader": "^6.2.0", "prism-react-renderer": "^1.2.1", "react": "^17.0.1", "react-dom": "^17.0.1", + "react-error-boundary": "^3.1.0", "url-loader": "^4.1.1" }, "browserslist": { diff --git a/docs/yarn.lock b/docs/yarn.lock index 6f0bf0f..9f44fc0 100755 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -2,24 +2,32 @@ # yarn lockfile v1 -"@algolia/autocomplete-core@1.5.2": - version "1.5.2" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-core/-/autocomplete-core-1.5.2.tgz#ec0178e07b44fd74a057728ac157291b26cecf37" - integrity sha512-DY0bhyczFSS1b/CqJlTE/nQRtnTAHl6IemIkBy0nEWnhDzRDdtdx4p5Uuk3vwAFxwEEgi1WqKwgSSMx6DpNL4A== +"@algolia/autocomplete-core@1.9.3": + version "1.9.3" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-core/-/autocomplete-core-1.9.3.tgz#1d56482a768c33aae0868c8533049e02e8961be7" + integrity sha512-009HdfugtGCdC4JdXUbVJClA0q0zh24yyePn+KUGk3rP7j8FEe/m5Yo/z65gn6nP/cM39PxpzqKrL7A6fP6PPw== dependencies: - "@algolia/autocomplete-shared" "1.5.2" + "@algolia/autocomplete-plugin-algolia-insights" "1.9.3" + "@algolia/autocomplete-shared" "1.9.3" -"@algolia/autocomplete-preset-algolia@1.5.2": - version "1.5.2" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.5.2.tgz#36c5638cc6dba6ea46a86e5a0314637ca40a77ca" - integrity sha512-3MRYnYQFJyovANzSX2CToS6/5cfVjbLLqFsZTKcvF3abhQzxbqwwaMBlJtt620uBUOeMzhdfasKhCc40+RHiZw== +"@algolia/autocomplete-plugin-algolia-insights@1.9.3": + version "1.9.3" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.9.3.tgz#9b7f8641052c8ead6d66c1623d444cbe19dde587" + integrity sha512-a/yTUkcO/Vyy+JffmAnTWbr4/90cLzw+CC3bRbhnULr/EM0fGNvM13oQQ14f2moLMcVDyAx/leczLlAOovhSZg== dependencies: - "@algolia/autocomplete-shared" "1.5.2" + "@algolia/autocomplete-shared" "1.9.3" -"@algolia/autocomplete-shared@1.5.2": - version "1.5.2" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.5.2.tgz#e157f9ad624ab8fd940ff28bd2094cdf199cdd79" - integrity sha512-ylQAYv5H0YKMfHgVWX0j0NmL8XBcAeeeVQUmppnnMtzDbDnca6CzhKj3Q8eF9cHCgcdTDdb5K+3aKyGWA0obug== +"@algolia/autocomplete-preset-algolia@1.9.3": + version "1.9.3" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.9.3.tgz#64cca4a4304cfcad2cf730e83067e0c1b2f485da" + integrity sha512-d4qlt6YmrLMYy95n5TB52wtNDr6EgAIPH81dvvvW8UmuWRgxEtY0NJiPwl/h95JtG2vmRM804M0DSwMCNZlzRA== + dependencies: + "@algolia/autocomplete-shared" "1.9.3" + +"@algolia/autocomplete-shared@1.9.3": + version "1.9.3" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.9.3.tgz#2e22e830d36f0a9cf2c0ccd3c7f6d59435b77dfa" + integrity sha512-Wnm9E4Ye6Rl6sTTqjoymD+l8DjSTHsHboVRYrKgEt8Q7UHm9nYbqhN/i0fhUYA3OAEH7WA8x3jfpnmJm3rKvaQ== "@algolia/cache-browser-local-storage@4.13.0": version "4.13.0" @@ -28,11 +36,23 @@ dependencies: "@algolia/cache-common" "4.13.0" +"@algolia/cache-browser-local-storage@4.19.1": + version "4.19.1" + resolved "https://registry.yarnpkg.com/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.19.1.tgz#d29f42775ed4d117182897ac164519c593faf399" + integrity sha512-FYAZWcGsFTTaSAwj9Std8UML3Bu8dyWDncM7Ls8g+58UOe4XYdlgzXWbrIgjaguP63pCCbMoExKr61B+ztK3tw== + dependencies: + "@algolia/cache-common" "4.19.1" + "@algolia/cache-common@4.13.0": version "4.13.0" resolved "https://registry.yarnpkg.com/@algolia/cache-common/-/cache-common-4.13.0.tgz#27b83fd3939d08d72261b36a07eeafc4cb4d2113" integrity sha512-f9mdZjskCui/dA/fA/5a+6hZ7xnHaaZI5tM/Rw9X8rRB39SUlF/+o3P47onZ33n/AwkpSbi5QOyhs16wHd55kA== +"@algolia/cache-common@4.19.1": + version "4.19.1" + resolved "https://registry.yarnpkg.com/@algolia/cache-common/-/cache-common-4.19.1.tgz#faa5eeacaffd6023c2cf26e9866bdb06193f9b26" + integrity sha512-XGghi3l0qA38HiqdoUY+wvGyBsGvKZ6U3vTiMBT4hArhP3fOGLXpIINgMiiGjTe4FVlTa5a/7Zf2bwlIHfRqqg== + "@algolia/cache-in-memory@4.13.0": version "4.13.0" resolved "https://registry.yarnpkg.com/@algolia/cache-in-memory/-/cache-in-memory-4.13.0.tgz#10801a74550cbabb64b59ff08c56bce9c278ff2d" @@ -40,6 +60,13 @@ dependencies: "@algolia/cache-common" "4.13.0" +"@algolia/cache-in-memory@4.19.1": + version "4.19.1" + resolved "https://registry.yarnpkg.com/@algolia/cache-in-memory/-/cache-in-memory-4.19.1.tgz#afe4f0f21149800358379871089e0141fb72415b" + integrity sha512-+PDWL+XALGvIginigzu8oU6eWw+o76Z8zHbBovWYcrtWOEtinbl7a7UTt3x3lthv+wNuFr/YD1Gf+B+A9V8n5w== + dependencies: + "@algolia/cache-common" "4.19.1" + "@algolia/client-account@4.13.0": version "4.13.0" resolved "https://registry.yarnpkg.com/@algolia/client-account/-/client-account-4.13.0.tgz#f8646dd40d1e9e3353e10abbd5d6c293ea92a8e2" @@ -49,6 +76,15 @@ "@algolia/client-search" "4.13.0" "@algolia/transporter" "4.13.0" +"@algolia/client-account@4.19.1": + version "4.19.1" + resolved "https://registry.yarnpkg.com/@algolia/client-account/-/client-account-4.19.1.tgz#1fa65881baab79ad35af6bcf44646a13b8d5edc9" + integrity sha512-Oy0ritA2k7AMxQ2JwNpfaEcgXEDgeyKu0V7E7xt/ZJRdXfEpZcwp9TOg4TJHC7Ia62gIeT2Y/ynzsxccPw92GA== + dependencies: + "@algolia/client-common" "4.19.1" + "@algolia/client-search" "4.19.1" + "@algolia/transporter" "4.19.1" + "@algolia/client-analytics@4.13.0": version "4.13.0" resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-4.13.0.tgz#a00bd02df45d71becb9dd4c5c993d805f2e1786d" @@ -59,6 +95,16 @@ "@algolia/requester-common" "4.13.0" "@algolia/transporter" "4.13.0" +"@algolia/client-analytics@4.19.1": + version "4.19.1" + resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-4.19.1.tgz#e6ed79acd4de5a0284c9696bf4e1c25278ba34db" + integrity sha512-5QCq2zmgdZLIQhHqwl55ZvKVpLM3DNWjFI4T+bHr3rGu23ew2bLO4YtyxaZeChmDb85jUdPDouDlCumGfk6wOg== + dependencies: + "@algolia/client-common" "4.19.1" + "@algolia/client-search" "4.19.1" + "@algolia/requester-common" "4.19.1" + "@algolia/transporter" "4.19.1" + "@algolia/client-common@4.13.0": version "4.13.0" resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-4.13.0.tgz#8bc373d164dbdcce38b4586912bbe162492bcb86" @@ -67,6 +113,14 @@ "@algolia/requester-common" "4.13.0" "@algolia/transporter" "4.13.0" +"@algolia/client-common@4.19.1": + version "4.19.1" + resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-4.19.1.tgz#40a8387316fa61d62ad1091beb3a8e227f008e75" + integrity sha512-3kAIVqTcPrjfS389KQvKzliC559x+BDRxtWamVJt8IVp7LGnjq+aVAXg4Xogkur1MUrScTZ59/AaUd5EdpyXgA== + dependencies: + "@algolia/requester-common" "4.19.1" + "@algolia/transporter" "4.19.1" + "@algolia/client-personalization@4.13.0": version "4.13.0" resolved "https://registry.yarnpkg.com/@algolia/client-personalization/-/client-personalization-4.13.0.tgz#10fb7af356422551f11a67222b39c52306f1512c" @@ -76,6 +130,15 @@ "@algolia/requester-common" "4.13.0" "@algolia/transporter" "4.13.0" +"@algolia/client-personalization@4.19.1": + version "4.19.1" + resolved "https://registry.yarnpkg.com/@algolia/client-personalization/-/client-personalization-4.19.1.tgz#fe362e0684dc74c3504c3641c5a7488c6ae02e07" + integrity sha512-8CWz4/H5FA+krm9HMw2HUQenizC/DxUtsI5oYC0Jxxyce1vsr8cb1aEiSJArQT6IzMynrERif1RVWLac1m36xw== + dependencies: + "@algolia/client-common" "4.19.1" + "@algolia/requester-common" "4.19.1" + "@algolia/transporter" "4.19.1" + "@algolia/client-search@4.13.0": version "4.13.0" resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-4.13.0.tgz#2d8ff8e755c4a37ec89968f3f9b358eed005c7f0" @@ -85,6 +148,15 @@ "@algolia/requester-common" "4.13.0" "@algolia/transporter" "4.13.0" +"@algolia/client-search@4.19.1": + version "4.19.1" + resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-4.19.1.tgz#5e54601aa5f5cea790cec3f2cde4af9d6403871e" + integrity sha512-mBecfMFS4N+yK/p0ZbK53vrZbL6OtWMk8YmnOv1i0LXx4pelY8TFhqKoTit3NPVPwoSNN0vdSN9dTu1xr1XOVw== + dependencies: + "@algolia/client-common" "4.19.1" + "@algolia/requester-common" "4.19.1" + "@algolia/transporter" "4.19.1" + "@algolia/events@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@algolia/events/-/events-4.0.1.tgz#fd39e7477e7bc703d7f893b556f676c032af3950" @@ -95,6 +167,11 @@ resolved "https://registry.yarnpkg.com/@algolia/logger-common/-/logger-common-4.13.0.tgz#be2606e71aae618a1ff1ea9a1b5f5a74284b35a8" integrity sha512-8yqXk7rMtmQJ9wZiHOt/6d4/JDEg5VCk83gJ39I+X/pwUPzIsbKy9QiK4uJ3aJELKyoIiDT1hpYVt+5ia+94IA== +"@algolia/logger-common@4.19.1": + version "4.19.1" + resolved "https://registry.yarnpkg.com/@algolia/logger-common/-/logger-common-4.19.1.tgz#0e46a11510f3e94e1afc0ac780ae52e9597be78f" + integrity sha512-i6pLPZW/+/YXKis8gpmSiNk1lOmYCmRI6+x6d2Qk1OdfvX051nRVdalRbEcVTpSQX6FQAoyeaui0cUfLYW5Elw== + "@algolia/logger-console@4.13.0": version "4.13.0" resolved "https://registry.yarnpkg.com/@algolia/logger-console/-/logger-console-4.13.0.tgz#f28028a760e3d9191e28a10b12925e48f6c9afde" @@ -102,6 +179,13 @@ dependencies: "@algolia/logger-common" "4.13.0" +"@algolia/logger-console@4.19.1": + version "4.19.1" + resolved "https://registry.yarnpkg.com/@algolia/logger-console/-/logger-console-4.19.1.tgz#656a6f4ebb5de39af6ef7095c398d9ab3cceb87d" + integrity sha512-jj72k9GKb9W0c7TyC3cuZtTr0CngLBLmc8trzZlXdfvQiigpUdvTi1KoWIb2ZMcRBG7Tl8hSb81zEY3zI2RlXg== + dependencies: + "@algolia/logger-common" "4.19.1" + "@algolia/requester-browser-xhr@4.13.0": version "4.13.0" resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.13.0.tgz#e2483f4e8d7f09e27cd0daf6c77711d15c5a919f" @@ -109,11 +193,23 @@ dependencies: "@algolia/requester-common" "4.13.0" +"@algolia/requester-browser-xhr@4.19.1": + version "4.19.1" + resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.19.1.tgz#7341ea2f980b8980a2976110142026721e452187" + integrity sha512-09K/+t7lptsweRTueHnSnmPqIxbHMowejAkn9XIcJMLdseS3zl8ObnS5GWea86mu3vy4+8H+ZBKkUN82Zsq/zg== + dependencies: + "@algolia/requester-common" "4.19.1" + "@algolia/requester-common@4.13.0": version "4.13.0" resolved "https://registry.yarnpkg.com/@algolia/requester-common/-/requester-common-4.13.0.tgz#47fb3464cfb26b55ba43676d13f295d812830596" integrity sha512-BRTDj53ecK+gn7ugukDWOOcBRul59C4NblCHqj4Zm5msd5UnHFjd/sGX+RLOEoFMhetILAnmg6wMrRrQVac9vw== +"@algolia/requester-common@4.19.1": + version "4.19.1" + resolved "https://registry.yarnpkg.com/@algolia/requester-common/-/requester-common-4.19.1.tgz#f3396c77631b9d36e8d4d6f819a2c27f9ddbf7a1" + integrity sha512-BisRkcWVxrDzF1YPhAckmi2CFYK+jdMT60q10d7z3PX+w6fPPukxHRnZwooiTUrzFe50UBmLItGizWHP5bDzVQ== + "@algolia/requester-node-http@4.13.0": version "4.13.0" resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-4.13.0.tgz#7d981bbd31492f51dd11820a665f9d8906793c37" @@ -121,6 +217,13 @@ dependencies: "@algolia/requester-common" "4.13.0" +"@algolia/requester-node-http@4.19.1": + version "4.19.1" + resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-4.19.1.tgz#ea210de9642628b3bdda1dd7fcd1fcb686da442e" + integrity sha512-6DK52DHviBHTG2BK/Vv2GIlEw7i+vxm7ypZW0Z7vybGCNDeWzADx+/TmxjkES2h15+FZOqVf/Ja677gePsVItA== + dependencies: + "@algolia/requester-common" "4.19.1" + "@algolia/transporter@4.13.0": version "4.13.0" resolved "https://registry.yarnpkg.com/@algolia/transporter/-/transporter-4.13.0.tgz#f6379e5329efa2127da68c914d1141f5f21dbd07" @@ -130,6 +233,15 @@ "@algolia/logger-common" "4.13.0" "@algolia/requester-common" "4.13.0" +"@algolia/transporter@4.19.1": + version "4.19.1" + resolved "https://registry.yarnpkg.com/@algolia/transporter/-/transporter-4.19.1.tgz#b5787299740c4bec9ba05502d98c14b5999860c8" + integrity sha512-nkpvPWbpuzxo1flEYqNIbGz7xhfhGOKGAZS7tzC+TELgEmi7z99qRyTfNSUlW7LZmB3ACdnqAo+9A9KFBENviQ== + dependencies: + "@algolia/cache-common" "4.19.1" + "@algolia/logger-common" "4.19.1" + "@algolia/requester-common" "4.19.1" + "@ampproject/remapping@^2.1.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" @@ -138,6 +250,14 @@ "@jridgewell/gen-mapping" "^0.1.0" "@jridgewell/trace-mapping" "^0.3.9" +"@ampproject/remapping@^2.2.0": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" + integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.8.3": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789" @@ -145,11 +265,23 @@ dependencies: "@babel/highlight" "^7.16.7" +"@babel/code-frame@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.5.tgz#234d98e1551960604f1246e6475891a570ad5658" + integrity sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ== + dependencies: + "@babel/highlight" "^7.22.5" + "@babel/compat-data@^7.13.11", "@babel/compat-data@^7.17.0", "@babel/compat-data@^7.17.10": version "7.17.10" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.17.10.tgz#711dc726a492dfc8be8220028b1b92482362baab" integrity sha512-GZt/TCsG70Ms19gfZO1tM4CVnXsPgEPBCpJu+Qz3L0LUDsY5nZqFZglIoPC1kIYOtNBZlrnFT+klg12vFGZXrw== +"@babel/compat-data@^7.22.5", "@babel/compat-data@^7.22.6", "@babel/compat-data@^7.22.9": + version "7.22.9" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.22.9.tgz#71cdb00a1ce3a329ce4cbec3a44f9fef35669730" + integrity sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ== + "@babel/core@7.12.9": version "7.12.9" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.9.tgz#fd450c4ec10cdbb980e2928b7aa7a28484593fc8" @@ -172,7 +304,7 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/core@^7.12.3", "@babel/core@^7.15.5", "@babel/core@^7.17.10": +"@babel/core@^7.12.3", "@babel/core@^7.15.5": version "7.17.10" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.17.10.tgz#74ef0fbf56b7dfc3f198fc2d927f4f03e12f4b05" integrity sha512-liKoppandF3ZcBnIYFjfSDHZLKdLHGJRkoWtG8zQyGJBQfIYobpnVGI5+pLBNtS6psFLDzyq8+h5HiVljW9PNA== @@ -193,6 +325,27 @@ json5 "^2.2.1" semver "^6.3.0" +"@babel/core@^7.18.6": + version "7.22.9" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.22.9.tgz#bd96492c68822198f33e8a256061da3cf391f58f" + integrity sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.22.5" + "@babel/generator" "^7.22.9" + "@babel/helper-compilation-targets" "^7.22.9" + "@babel/helper-module-transforms" "^7.22.9" + "@babel/helpers" "^7.22.6" + "@babel/parser" "^7.22.7" + "@babel/template" "^7.22.5" + "@babel/traverse" "^7.22.8" + "@babel/types" "^7.22.5" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.2" + semver "^6.3.1" + "@babel/generator@^7.12.5", "@babel/generator@^7.17.10": version "7.17.10" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.17.10.tgz#c281fa35b0c349bbe9d02916f4ae08fc85ed7189" @@ -202,6 +355,16 @@ "@jridgewell/gen-mapping" "^0.1.0" jsesc "^2.5.1" +"@babel/generator@^7.18.7", "@babel/generator@^7.22.7", "@babel/generator@^7.22.9": + version "7.22.9" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.22.9.tgz#572ecfa7a31002fa1de2a9d91621fd895da8493d" + integrity sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw== + dependencies: + "@babel/types" "^7.22.5" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + "@babel/helper-annotate-as-pure@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz#bb2339a7534a9c128e3102024c60760a3a7f3862" @@ -209,6 +372,13 @@ dependencies: "@babel/types" "^7.16.7" +"@babel/helper-annotate-as-pure@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882" + integrity sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-builder-binary-assignment-operator-visitor@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.16.7.tgz#38d138561ea207f0f69eb1626a418e4f7e6a580b" @@ -217,6 +387,13 @@ "@babel/helper-explode-assignable-expression" "^7.16.7" "@babel/types" "^7.16.7" +"@babel/helper-builder-binary-assignment-operator-visitor@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.5.tgz#a3f4758efdd0190d8927fcffd261755937c71878" + integrity sha512-m1EP3lVOPptR+2DwD125gziZNcmoNSHGmJROKoy87loWUQyJaVXDgpmruWqDARZSmtYQ+Dl25okU8+qhVzuykw== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.16.7", "@babel/helper-compilation-targets@^7.17.10": version "7.17.10" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.10.tgz#09c63106d47af93cf31803db6bc49fef354e2ebe" @@ -227,6 +404,17 @@ browserslist "^4.20.2" semver "^6.3.0" +"@babel/helper-compilation-targets@^7.22.5", "@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.22.9": + version "7.22.9" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.9.tgz#f9d0a7aaaa7cd32a3f31c9316a69f5a9bcacb892" + integrity sha512-7qYrNM6HjpnPHJbopxmb8hSPoZ0gsX8IvUS32JGVoy+pU9e5N0nLr1VjJoR6kA4d9dmGLxNYOjeB8sUDal2WMw== + dependencies: + "@babel/compat-data" "^7.22.9" + "@babel/helper-validator-option" "^7.22.5" + browserslist "^4.21.9" + lru-cache "^5.1.1" + semver "^6.3.1" + "@babel/helper-create-class-features-plugin@^7.16.10", "@babel/helper-create-class-features-plugin@^7.16.7", "@babel/helper-create-class-features-plugin@^7.17.6": version "7.17.9" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.17.9.tgz#71835d7fb9f38bd9f1378e40a4c0902fdc2ea49d" @@ -240,6 +428,21 @@ "@babel/helper-replace-supers" "^7.16.7" "@babel/helper-split-export-declaration" "^7.16.7" +"@babel/helper-create-class-features-plugin@^7.22.5", "@babel/helper-create-class-features-plugin@^7.22.9": + version "7.22.9" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.9.tgz#c36ea240bb3348f942f08b0fbe28d6d979fab236" + integrity sha512-Pwyi89uO4YrGKxL/eNJ8lfEH55DnRloGPOseaA8NFNL6jAUnn+KccaISiFazCj5IolPPDjGSdzQzXVzODVRqUQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-member-expression-to-functions" "^7.22.5" + "@babel/helper-optimise-call-expression" "^7.22.5" + "@babel/helper-replace-supers" "^7.22.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + semver "^6.3.1" + "@babel/helper-create-regexp-features-plugin@^7.16.7", "@babel/helper-create-regexp-features-plugin@^7.17.0": version "7.17.0" resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.17.0.tgz#1dcc7d40ba0c6b6b25618997c5dbfd310f186fe1" @@ -248,6 +451,15 @@ "@babel/helper-annotate-as-pure" "^7.16.7" regexpu-core "^5.0.1" +"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.22.5": + version "7.22.9" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.9.tgz#9d8e61a8d9366fe66198f57c40565663de0825f6" + integrity sha512-+svjVa/tFwsNSG4NEy1h85+HQ5imbT92Q5/bgtS7P0GTQlP8WuFdqsiABmQouhiFGyV66oGxZFpeYHza1rNsKw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + regexpu-core "^5.3.1" + semver "^6.3.1" + "@babel/helper-define-polyfill-provider@^0.3.1": version "0.3.1" resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.1.tgz#52411b445bdb2e676869e5a74960d2d3826d2665" @@ -262,6 +474,17 @@ resolve "^1.14.2" semver "^6.1.2" +"@babel/helper-define-polyfill-provider@^0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.2.tgz#82c825cadeeeee7aad237618ebbe8fa1710015d7" + integrity sha512-k0qnnOqHn5dK9pZpfD5XXZ9SojAITdCKRn2Lp6rnDGzIbaP0rHyMPk/4wsSxVBVz4RfN0q6VpXWP2pDGIoQ7hw== + dependencies: + "@babel/helper-compilation-targets" "^7.22.6" + "@babel/helper-plugin-utils" "^7.22.5" + debug "^4.1.1" + lodash.debounce "^4.0.8" + resolve "^1.14.2" + "@babel/helper-environment-visitor@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz#ff484094a839bde9d89cd63cba017d7aae80ecd7" @@ -269,6 +492,11 @@ dependencies: "@babel/types" "^7.16.7" +"@babel/helper-environment-visitor@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz#f06dd41b7c1f44e1f8da6c4055b41ab3a09a7e98" + integrity sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q== + "@babel/helper-explode-assignable-expression@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.16.7.tgz#12a6d8522fdd834f194e868af6354e8650242b7a" @@ -284,6 +512,14 @@ "@babel/template" "^7.16.7" "@babel/types" "^7.17.0" +"@babel/helper-function-name@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz#ede300828905bb15e582c037162f99d5183af1be" + integrity sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ== + dependencies: + "@babel/template" "^7.22.5" + "@babel/types" "^7.22.5" + "@babel/helper-hoist-variables@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz#86bcb19a77a509c7b77d0e22323ef588fa58c246" @@ -291,6 +527,13 @@ dependencies: "@babel/types" "^7.16.7" +"@babel/helper-hoist-variables@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" + integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-member-expression-to-functions@^7.16.7", "@babel/helper-member-expression-to-functions@^7.17.7": version "7.17.7" resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.17.7.tgz#a34013b57d8542a8c4ff8ba3f747c02452a4d8c4" @@ -298,6 +541,13 @@ dependencies: "@babel/types" "^7.17.0" +"@babel/helper-member-expression-to-functions@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.5.tgz#0a7c56117cad3372fbf8d2fb4bf8f8d64a1e76b2" + integrity sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437" @@ -305,6 +555,13 @@ dependencies: "@babel/types" "^7.16.7" +"@babel/helper-module-imports@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz#1a8f4c9f4027d23f520bd76b364d44434a72660c" + integrity sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-module-transforms@^7.12.1", "@babel/helper-module-transforms@^7.16.7", "@babel/helper-module-transforms@^7.17.7": version "7.17.7" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz#3943c7f777139e7954a5355c815263741a9c1cbd" @@ -319,6 +576,17 @@ "@babel/traverse" "^7.17.3" "@babel/types" "^7.17.0" +"@babel/helper-module-transforms@^7.22.5", "@babel/helper-module-transforms@^7.22.9": + version "7.22.9" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz#92dfcb1fbbb2bc62529024f72d942a8c97142129" + integrity sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ== + dependencies: + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-module-imports" "^7.22.5" + "@babel/helper-simple-access" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/helper-validator-identifier" "^7.22.5" + "@babel/helper-optimise-call-expression@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.7.tgz#a34e3560605abbd31a18546bd2aad3e6d9a174f2" @@ -326,6 +594,13 @@ dependencies: "@babel/types" "^7.16.7" +"@babel/helper-optimise-call-expression@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz#f21531a9ccbff644fdd156b4077c16ff0c3f609e" + integrity sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-plugin-utils@7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375" @@ -336,6 +611,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz#aa3a8ab4c3cceff8e65eb9e73d87dc4ff320b2f5" integrity sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA== +"@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295" + integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg== + "@babel/helper-remap-async-to-generator@^7.16.8": version "7.16.8" resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.8.tgz#29ffaade68a367e2ed09c90901986918d25e57e3" @@ -345,6 +625,15 @@ "@babel/helper-wrap-function" "^7.16.8" "@babel/types" "^7.16.8" +"@babel/helper-remap-async-to-generator@^7.22.5": + version "7.22.9" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.9.tgz#53a25b7484e722d7efb9c350c75c032d4628de82" + integrity sha512-8WWC4oR4Px+tr+Fp0X3RHDVfINGpF3ad1HIbrc8A77epiR6eMMc6jsgozkzT2uDiOOdoS9cLIQ+XD2XvI2WSmQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-wrap-function" "^7.22.9" + "@babel/helper-replace-supers@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.16.7.tgz#e9f5f5f32ac90429c1a4bdec0f231ef0c2838ab1" @@ -356,6 +645,15 @@ "@babel/traverse" "^7.16.7" "@babel/types" "^7.16.7" +"@babel/helper-replace-supers@^7.22.5", "@babel/helper-replace-supers@^7.22.9": + version "7.22.9" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.22.9.tgz#cbdc27d6d8d18cd22c81ae4293765a5d9afd0779" + integrity sha512-LJIKvvpgPOPUThdYqcX6IXRuIcTkcAub0IaDRGCZH0p5GPUp7PhRU9QVgFcDDd51BaPkk77ZjqFwh6DZTAEmGg== + dependencies: + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-member-expression-to-functions" "^7.22.5" + "@babel/helper-optimise-call-expression" "^7.22.5" + "@babel/helper-simple-access@^7.17.7": version "7.17.7" resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz#aaa473de92b7987c6dfa7ce9a7d9674724823367" @@ -363,6 +661,13 @@ dependencies: "@babel/types" "^7.17.0" +"@babel/helper-simple-access@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz#4938357dc7d782b80ed6dbb03a0fba3d22b1d5de" + integrity sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers@^7.16.0": version "7.16.0" resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.16.0.tgz#0ee3388070147c3ae051e487eca3ebb0e2e8bb09" @@ -370,6 +675,13 @@ dependencies: "@babel/types" "^7.16.0" +"@babel/helper-skip-transparent-expression-wrappers@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz#007f15240b5751c537c40e77abb4e89eeaaa8847" + integrity sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-split-export-declaration@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz#0b648c0c42da9d3920d85ad585f2778620b8726b" @@ -377,16 +689,38 @@ dependencies: "@babel/types" "^7.16.7" +"@babel/helper-split-export-declaration@^7.22.6": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" + integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-string-parser@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" + integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== + "@babel/helper-validator-identifier@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad" integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw== +"@babel/helper-validator-identifier@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz#9544ef6a33999343c8740fa51350f30eeaaaf193" + integrity sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ== + "@babel/helper-validator-option@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz#b203ce62ce5fe153899b617c08957de860de4d23" integrity sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ== +"@babel/helper-validator-option@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz#de52000a15a177413c8234fa3a8af4ee8102d0ac" + integrity sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw== + "@babel/helper-wrap-function@^7.16.8": version "7.16.8" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.16.8.tgz#58afda087c4cd235de92f7ceedebca2c41274200" @@ -397,6 +731,15 @@ "@babel/traverse" "^7.16.8" "@babel/types" "^7.16.8" +"@babel/helper-wrap-function@^7.22.9": + version "7.22.9" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.22.9.tgz#189937248c45b0182c1dcf32f3444ca153944cb9" + integrity sha512-sZ+QzfauuUEfxSEjKFmi3qDSHgLsTPK/pEpoD/qonZKOtTPTLbf59oabPQ4rKekt9lFcj/hTZaOhWwFYrgjk+Q== + dependencies: + "@babel/helper-function-name" "^7.22.5" + "@babel/template" "^7.22.5" + "@babel/types" "^7.22.5" + "@babel/helpers@^7.12.5", "@babel/helpers@^7.17.9": version "7.17.9" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.17.9.tgz#b2af120821bfbe44f9907b1826e168e819375a1a" @@ -406,6 +749,15 @@ "@babel/traverse" "^7.17.9" "@babel/types" "^7.17.0" +"@babel/helpers@^7.22.6": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.22.6.tgz#8e61d3395a4f0c5a8060f309fb008200969b5ecd" + integrity sha512-YjDs6y/fVOYFV8hAf1rxd1QvR9wJe1pDBZ2AREKq/SDayfPzgk0PBnVuTCE5X1acEpMMNOVUqoe+OwiZGJ+OaA== + dependencies: + "@babel/template" "^7.22.5" + "@babel/traverse" "^7.22.6" + "@babel/types" "^7.22.5" + "@babel/highlight@^7.16.7": version "7.17.9" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.17.9.tgz#61b2ee7f32ea0454612def4fccdae0de232b73e3" @@ -415,11 +767,25 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@babel/highlight@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.5.tgz#aa6c05c5407a67ebce408162b7ede789b4d22031" + integrity sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw== + dependencies: + "@babel/helper-validator-identifier" "^7.22.5" + chalk "^2.0.0" + js-tokens "^4.0.0" + "@babel/parser@^7.12.7", "@babel/parser@^7.16.7", "@babel/parser@^7.17.10": version "7.17.10" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.10.tgz#873b16db82a8909e0fbd7f115772f4b739f6ce78" integrity sha512-n2Q6i+fnJqzOaq2VkdXxy2TCPCWQZHiCo0XqmrCvDWcZQKRyZzYi4Z0yxlBuN0w+r2ZHmre+Q087DSrw3pbJDQ== +"@babel/parser@^7.18.8", "@babel/parser@^7.22.5", "@babel/parser@^7.22.7": + version "7.22.7" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.7.tgz#df8cf085ce92ddbdbf668a7f186ce848c9036cae" + integrity sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q== + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.16.7.tgz#4eda6d6c2a0aa79c70fa7b6da67763dfe2141050" @@ -427,6 +793,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.16.7" +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz#87245a21cd69a73b0b81bcda98d443d6df08f05e" + integrity sha512-NP1M5Rf+u2Gw9qfSO4ihjcTGW5zXTi36ITLd4/EoAcEhIZ0yjMqmftDNl3QC19CX7olhrjpyU454g/2W7X0jvQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.16.7.tgz#cc001234dfc139ac45f6bcf801866198c8c72ff9" @@ -436,6 +809,15 @@ "@babel/helper-skip-transparent-expression-wrappers" "^7.16.0" "@babel/plugin-proposal-optional-chaining" "^7.16.7" +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.5.tgz#fef09f9499b1f1c930da8a0c419db42167d792ca" + integrity sha512-31Bb65aZaUwqCbWMnZPduIZxCBngHFlzyN6Dq6KAJjtx+lx6ohKHubc61OomYi7XwVD4Ol0XCVz4h+pYFR048g== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/plugin-transform-optional-chaining" "^7.22.5" + "@babel/plugin-proposal-async-generator-functions@^7.16.8": version "7.16.8" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.16.8.tgz#3bdd1ebbe620804ea9416706cd67d60787504bc8" @@ -555,6 +937,11 @@ "@babel/helper-create-class-features-plugin" "^7.16.10" "@babel/helper-plugin-utils" "^7.16.7" +"@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": + version "7.21.0-placeholder-for-preset-env.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" + integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w== + "@babel/plugin-proposal-private-property-in-object@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.16.7.tgz#b0b8cef543c2c3d57e59e2c611994861d46a3fce" @@ -608,6 +995,27 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" +"@babel/plugin-syntax-import-assertions@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz#07d252e2aa0bc6125567f742cd58619cb14dce98" + integrity sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-syntax-import-attributes@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz#ab840248d834410b829f569f5262b9e517555ecb" + integrity sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-syntax-import-meta@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" + integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-json-strings@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" @@ -629,6 +1037,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.16.7" +"@babel/plugin-syntax-jsx@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz#a6b68e84fb76e759fc3b93e901876ffabbe1d918" + integrity sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-logical-assignment-operators@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" @@ -692,6 +1107,21 @@ dependencies: "@babel/helper-plugin-utils" "^7.16.7" +"@babel/plugin-syntax-typescript@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz#aac8d383b062c5072c647a31ef990c1d0af90272" + integrity sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-syntax-unicode-sets-regex@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357" + integrity sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-transform-arrow-functions@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.16.7.tgz#44125e653d94b98db76369de9c396dc14bef4154" @@ -699,6 +1129,23 @@ dependencies: "@babel/helper-plugin-utils" "^7.16.7" +"@babel/plugin-transform-arrow-functions@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz#e5ba566d0c58a5b2ba2a8b795450641950b71958" + integrity sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-async-generator-functions@^7.22.7": + version "7.22.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.7.tgz#053e76c0a903b72b573cb1ab7d6882174d460a1b" + integrity sha512-7HmE7pk/Fmke45TODvxvkxRMV9RazV+ZZzhOL9AG8G29TLrr3jkjwF7uJfxZ30EoXpO+LJkq4oA8NjO2DTnEDg== + dependencies: + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-remap-async-to-generator" "^7.22.5" + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-transform-async-to-generator@^7.16.8": version "7.16.8" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.16.8.tgz#b83dff4b970cf41f1b819f8b49cc0cfbaa53a808" @@ -708,6 +1155,15 @@ "@babel/helper-plugin-utils" "^7.16.7" "@babel/helper-remap-async-to-generator" "^7.16.8" +"@babel/plugin-transform-async-to-generator@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz#c7a85f44e46f8952f6d27fe57c2ed3cc084c3775" + integrity sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ== + dependencies: + "@babel/helper-module-imports" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-remap-async-to-generator" "^7.22.5" + "@babel/plugin-transform-block-scoped-functions@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.16.7.tgz#4d0d57d9632ef6062cdf354bb717102ee042a620" @@ -715,6 +1171,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.16.7" +"@babel/plugin-transform-block-scoped-functions@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz#27978075bfaeb9fa586d3cb63a3d30c1de580024" + integrity sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-transform-block-scoping@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.16.7.tgz#f50664ab99ddeaee5bc681b8f3a6ea9d72ab4f87" @@ -722,6 +1185,30 @@ dependencies: "@babel/helper-plugin-utils" "^7.16.7" +"@babel/plugin-transform-block-scoping@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.5.tgz#8bfc793b3a4b2742c0983fadc1480d843ecea31b" + integrity sha512-EcACl1i5fSQ6bt+YGuU/XGCeZKStLmyVGytWkpyhCLeQVA0eu6Wtiw92V+I1T/hnezUv7j74dA/Ro69gWcU+hg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-class-properties@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz#97a56e31ad8c9dc06a0b3710ce7803d5a48cca77" + integrity sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-class-static-block@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.5.tgz#3e40c46f048403472d6f4183116d5e46b1bff5ba" + integrity sha512-SPToJ5eYZLxlnp1UzdARpOGeC2GbHvr9d/UV0EukuVx8atktg194oe+C5BqQ8jRTkgLRVOPYeXRSBg1IlMoVRA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/plugin-transform-classes@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.16.7.tgz#8f4b9562850cd973de3b498f1218796eb181ce00" @@ -736,6 +1223,21 @@ "@babel/helper-split-export-declaration" "^7.16.7" globals "^11.1.0" +"@babel/plugin-transform-classes@^7.22.6": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.6.tgz#e04d7d804ed5b8501311293d1a0e6d43e94c3363" + integrity sha512-58EgM6nuPNG6Py4Z3zSuu0xWu2VfodiMi72Jt5Kj2FECmaYk1RrTXA45z6KBFsu9tRgwQDwIiY4FXTt+YsSFAQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-compilation-targets" "^7.22.6" + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-optimise-call-expression" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-replace-supers" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + globals "^11.1.0" + "@babel/plugin-transform-computed-properties@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.16.7.tgz#66dee12e46f61d2aae7a73710f591eb3df616470" @@ -743,6 +1245,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.16.7" +"@babel/plugin-transform-computed-properties@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz#cd1e994bf9f316bd1c2dafcd02063ec261bb3869" + integrity sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/template" "^7.22.5" + "@babel/plugin-transform-destructuring@^7.17.7": version "7.17.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.17.7.tgz#49dc2675a7afa9a5e4c6bdee636061136c3408d1" @@ -750,6 +1260,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.16.7" +"@babel/plugin-transform-destructuring@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.5.tgz#d3aca7438f6c26c78cdd0b0ba920a336001b27cc" + integrity sha512-GfqcFuGW8vnEqTUBM7UtPd5A4q797LTvvwKxXTgRsFjoqaJiEg9deBG6kWeQYkVEL569NpnmpC0Pkr/8BLKGnQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-transform-dotall-regex@^7.16.7", "@babel/plugin-transform-dotall-regex@^7.4.4": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.16.7.tgz#6b2d67686fab15fb6a7fd4bd895d5982cfc81241" @@ -758,6 +1275,14 @@ "@babel/helper-create-regexp-features-plugin" "^7.16.7" "@babel/helper-plugin-utils" "^7.16.7" +"@babel/plugin-transform-dotall-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz#dbb4f0e45766eb544e193fb00e65a1dd3b2a4165" + integrity sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-transform-duplicate-keys@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.16.7.tgz#2207e9ca8f82a0d36a5a67b6536e7ef8b08823c9" @@ -765,6 +1290,21 @@ dependencies: "@babel/helper-plugin-utils" "^7.16.7" +"@babel/plugin-transform-duplicate-keys@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz#b6e6428d9416f5f0bba19c70d1e6e7e0b88ab285" + integrity sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-dynamic-import@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.5.tgz#d6908a8916a810468c4edff73b5b75bda6ad393e" + integrity sha512-0MC3ppTB1AMxd8fXjSrbPa7LT9hrImt+/fcj+Pg5YMD7UQyWp/02+JWpdnCymmsXwIx5Z+sYn1bwCn4ZJNvhqQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/plugin-transform-exponentiation-operator@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.16.7.tgz#efa9862ef97e9e9e5f653f6ddc7b665e8536fe9b" @@ -773,6 +1313,22 @@ "@babel/helper-builder-binary-assignment-operator-visitor" "^7.16.7" "@babel/helper-plugin-utils" "^7.16.7" +"@babel/plugin-transform-exponentiation-operator@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz#402432ad544a1f9a480da865fda26be653e48f6a" + integrity sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g== + dependencies: + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-export-namespace-from@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.5.tgz#57c41cb1d0613d22f548fddd8b288eedb9973a5b" + integrity sha512-X4hhm7FRnPgd4nDA4b/5V280xCx6oL7Oob5+9qVS5C13Zq4bh1qq7LU0GgRU6b5dBWBvhGaXYVB4AcN6+ol6vg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/plugin-transform-for-of@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.16.7.tgz#649d639d4617dff502a9a158c479b3b556728d8c" @@ -780,6 +1336,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.16.7" +"@babel/plugin-transform-for-of@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.5.tgz#ab1b8a200a8f990137aff9a084f8de4099ab173f" + integrity sha512-3kxQjX1dU9uudwSshyLeEipvrLjBCVthCgeTp6CzE/9JYrlAIaeekVxRpCWsDDfYTfRZRoCeZatCQvwo+wvK8A== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-transform-function-name@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.16.7.tgz#5ab34375c64d61d083d7d2f05c38d90b97ec65cf" @@ -789,6 +1352,23 @@ "@babel/helper-function-name" "^7.16.7" "@babel/helper-plugin-utils" "^7.16.7" +"@babel/plugin-transform-function-name@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz#935189af68b01898e0d6d99658db6b164205c143" + integrity sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg== + dependencies: + "@babel/helper-compilation-targets" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-json-strings@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.5.tgz#14b64352fdf7e1f737eed68de1a1468bd2a77ec0" + integrity sha512-DuCRB7fu8MyTLbEQd1ew3R85nx/88yMoqo2uPSjevMj3yoN7CDM8jkgrY0wmVxfJZyJ/B9fE1iq7EQppWQmR5A== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-transform-literals@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.16.7.tgz#254c9618c5ff749e87cb0c0cef1a0a050c0bdab1" @@ -796,6 +1376,21 @@ dependencies: "@babel/helper-plugin-utils" "^7.16.7" +"@babel/plugin-transform-literals@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz#e9341f4b5a167952576e23db8d435849b1dd7920" + integrity sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-logical-assignment-operators@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.5.tgz#66ae5f068fd5a9a5dc570df16f56c2a8462a9d6c" + integrity sha512-MQQOUW1KL8X0cDWfbwYP+TbVbZm16QmQXJQ+vndPtH/BoO0lOKpVoEDMI7+PskYxH+IiE0tS8xZye0qr1lGzSA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-transform-member-expression-literals@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.16.7.tgz#6e5dcf906ef8a098e630149d14c867dd28f92384" @@ -803,6 +1398,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.16.7" +"@babel/plugin-transform-member-expression-literals@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz#4fcc9050eded981a468347dd374539ed3e058def" + integrity sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-transform-modules-amd@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.16.7.tgz#b28d323016a7daaae8609781d1f8c9da42b13186" @@ -812,6 +1414,14 @@ "@babel/helper-plugin-utils" "^7.16.7" babel-plugin-dynamic-import-node "^2.3.3" +"@babel/plugin-transform-modules-amd@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz#4e045f55dcf98afd00f85691a68fc0780704f526" + integrity sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ== + dependencies: + "@babel/helper-module-transforms" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-transform-modules-commonjs@^7.17.9": version "7.17.9" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.17.9.tgz#274be1a2087beec0254d4abd4d86e52442e1e5b6" @@ -822,6 +1432,15 @@ "@babel/helper-simple-access" "^7.17.7" babel-plugin-dynamic-import-node "^2.3.3" +"@babel/plugin-transform-modules-commonjs@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.5.tgz#7d9875908d19b8c0536085af7b053fd5bd651bfa" + integrity sha512-B4pzOXj+ONRmuaQTg05b3y/4DuFz3WcCNAXPLb2Q0GT0TrGKGxNKV4jwsXts+StaM0LQczZbOpj8o1DLPDJIiA== + dependencies: + "@babel/helper-module-transforms" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-simple-access" "^7.22.5" + "@babel/plugin-transform-modules-systemjs@^7.17.8": version "7.17.8" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.17.8.tgz#81fd834024fae14ea78fbe34168b042f38703859" @@ -833,6 +1452,16 @@ "@babel/helper-validator-identifier" "^7.16.7" babel-plugin-dynamic-import-node "^2.3.3" +"@babel/plugin-transform-modules-systemjs@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.5.tgz#18c31410b5e579a0092638f95c896c2a98a5d496" + integrity sha512-emtEpoaTMsOs6Tzz+nbmcePl6AKVtS1yC4YNAeMun9U8YCsgadPNxnOPQ8GhHFB2qdx+LZu9LgoC0Lthuu05DQ== + dependencies: + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-module-transforms" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.5" + "@babel/plugin-transform-modules-umd@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.16.7.tgz#23dad479fa585283dbd22215bff12719171e7618" @@ -841,6 +1470,14 @@ "@babel/helper-module-transforms" "^7.16.7" "@babel/helper-plugin-utils" "^7.16.7" +"@babel/plugin-transform-modules-umd@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz#4694ae40a87b1745e3775b6a7fe96400315d4f98" + integrity sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ== + dependencies: + "@babel/helper-module-transforms" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-transform-named-capturing-groups-regex@^7.17.10": version "7.17.10" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.17.10.tgz#715dbcfafdb54ce8bccd3d12e8917296a4ba66a4" @@ -848,6 +1485,14 @@ dependencies: "@babel/helper-create-regexp-features-plugin" "^7.17.0" +"@babel/plugin-transform-named-capturing-groups-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz#67fe18ee8ce02d57c855185e27e3dc959b2e991f" + integrity sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-transform-new-target@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.16.7.tgz#9967d89a5c243818e0800fdad89db22c5f514244" @@ -855,6 +1500,40 @@ dependencies: "@babel/helper-plugin-utils" "^7.16.7" +"@babel/plugin-transform-new-target@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz#1b248acea54ce44ea06dfd37247ba089fcf9758d" + integrity sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-nullish-coalescing-operator@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.5.tgz#f8872c65776e0b552e0849d7596cddd416c3e381" + integrity sha512-6CF8g6z1dNYZ/VXok5uYkkBBICHZPiGEl7oDnAx2Mt1hlHVHOSIKWJaXHjQJA5VB43KZnXZDIexMchY4y2PGdA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + +"@babel/plugin-transform-numeric-separator@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.5.tgz#57226a2ed9e512b9b446517ab6fa2d17abb83f58" + integrity sha512-NbslED1/6M+sXiwwtcAB/nieypGw02Ejf4KtDeMkCEpP6gWFMX1wI9WKYua+4oBneCCEmulOkRpwywypVZzs/g== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + +"@babel/plugin-transform-object-rest-spread@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.5.tgz#9686dc3447df4753b0b2a2fae7e8bc33cdc1f2e1" + integrity sha512-Kk3lyDmEslH9DnvCDA1s1kkd3YWQITiBOHngOtDL9Pt6BZjzqb6hiOlb8VfjiiQJ2unmegBqZu0rx5RxJb5vmQ== + dependencies: + "@babel/compat-data" "^7.22.5" + "@babel/helper-compilation-targets" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-transform-parameters" "^7.22.5" + "@babel/plugin-transform-object-super@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.16.7.tgz#ac359cf8d32cf4354d27a46867999490b6c32a94" @@ -863,6 +1542,31 @@ "@babel/helper-plugin-utils" "^7.16.7" "@babel/helper-replace-supers" "^7.16.7" +"@babel/plugin-transform-object-super@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz#794a8d2fcb5d0835af722173c1a9d704f44e218c" + integrity sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-replace-supers" "^7.22.5" + +"@babel/plugin-transform-optional-catch-binding@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.5.tgz#842080be3076703be0eaf32ead6ac8174edee333" + integrity sha512-pH8orJahy+hzZje5b8e2QIlBWQvGpelS76C63Z+jhZKsmzfNaPQ+LaW6dcJ9bxTpo1mtXbgHwy765Ro3jftmUg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + +"@babel/plugin-transform-optional-chaining@^7.22.5", "@babel/plugin-transform-optional-chaining@^7.22.6": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.6.tgz#4bacfe37001fe1901117672875e931d439811564" + integrity sha512-Vd5HiWml0mDVtcLHIoEU5sw6HOUW/Zk0acLs/SAeuLzkGNOPc9DB4nkUajemhCmTIz3eiaKREZn2hQQqF79YTg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-transform-parameters@^7.12.1", "@babel/plugin-transform-parameters@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.16.7.tgz#a1721f55b99b736511cb7e0152f61f17688f331f" @@ -870,6 +1574,31 @@ dependencies: "@babel/helper-plugin-utils" "^7.16.7" +"@babel/plugin-transform-parameters@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.5.tgz#c3542dd3c39b42c8069936e48717a8d179d63a18" + integrity sha512-AVkFUBurORBREOmHRKo06FjHYgjrabpdqRSwq6+C7R5iTCZOsM4QbcB27St0a4U6fffyAOqh3s/qEfybAhfivg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-private-methods@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz#21c8af791f76674420a147ae62e9935d790f8722" + integrity sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-private-property-in-object@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.5.tgz#07a77f28cbb251546a43d175a1dda4cf3ef83e32" + integrity sha512-/9xnaTTJcVoBtSSmrVyhtSvO3kbqS2ODoh2juEU72c3aYonNF0OMGiaz2gjukyKM2wBBYJP38S4JiE0Wfb5VMQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-create-class-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/plugin-transform-property-literals@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.16.7.tgz#2dadac85155436f22c696c4827730e0fe1057a55" @@ -877,6 +1606,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.16.7" +"@babel/plugin-transform-property-literals@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz#b5ddabd73a4f7f26cd0e20f5db48290b88732766" + integrity sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-transform-react-constant-elements@^7.12.1", "@babel/plugin-transform-react-constant-elements@^7.14.5": version "7.17.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.17.6.tgz#6cc273c2f612a6a50cb657e63ee1303e5e68d10a" @@ -891,6 +1627,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.16.7" +"@babel/plugin-transform-react-display-name@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.22.5.tgz#3c4326f9fce31c7968d6cb9debcaf32d9e279a2b" + integrity sha512-PVk3WPYudRF5z4GKMEYUrLjPl38fJSKNaEOkFuoprioowGuWN6w2RKznuFNSlJx7pzzXXStPUnNSOEO0jL5EVw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-transform-react-jsx-development@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.16.7.tgz#43a00724a3ed2557ed3f276a01a929e6686ac7b8" @@ -898,6 +1641,13 @@ dependencies: "@babel/plugin-transform-react-jsx" "^7.16.7" +"@babel/plugin-transform-react-jsx-development@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.22.5.tgz#e716b6edbef972a92165cd69d92f1255f7e73e87" + integrity sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A== + dependencies: + "@babel/plugin-transform-react-jsx" "^7.22.5" + "@babel/plugin-transform-react-jsx@^7.16.7": version "7.17.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.17.3.tgz#eac1565da176ccb1a715dae0b4609858808008c1" @@ -909,6 +1659,17 @@ "@babel/plugin-syntax-jsx" "^7.16.7" "@babel/types" "^7.17.0" +"@babel/plugin-transform-react-jsx@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.22.5.tgz#932c291eb6dd1153359e2a90cb5e557dcf068416" + integrity sha512-rog5gZaVbUip5iWDMTYbVM15XQq+RkUKhET/IHR6oizR+JEoN6CAfTTuHcK4vwUyzca30qqHqEpzBOnaRMWYMA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-module-imports" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-jsx" "^7.22.5" + "@babel/types" "^7.22.5" + "@babel/plugin-transform-react-pure-annotations@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.16.7.tgz#232bfd2f12eb551d6d7d01d13fe3f86b45eb9c67" @@ -917,6 +1678,14 @@ "@babel/helper-annotate-as-pure" "^7.16.7" "@babel/helper-plugin-utils" "^7.16.7" +"@babel/plugin-transform-react-pure-annotations@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.22.5.tgz#1f58363eef6626d6fa517b95ac66fe94685e32c0" + integrity sha512-gP4k85wx09q+brArVinTXhWiyzLl9UpmGva0+mWyKxk6JZequ05x3eUcIUE+FyttPKJFRRVtAvQaJ6YF9h1ZpA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-transform-regenerator@^7.17.9": version "7.17.9" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.17.9.tgz#0a33c3a61cf47f45ed3232903683a0afd2d3460c" @@ -924,6 +1693,14 @@ dependencies: regenerator-transform "^0.15.0" +"@babel/plugin-transform-regenerator@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.5.tgz#cd8a68b228a5f75fa01420e8cc2fc400f0fc32aa" + integrity sha512-rR7KePOE7gfEtNTh9Qw+iO3Q/e4DEsoQ+hdvM6QUDH7JRJ5qxq5AA52ZzBWbI5i9lfNuvySgOGP8ZN7LAmaiPw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + regenerator-transform "^0.15.1" + "@babel/plugin-transform-reserved-words@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.16.7.tgz#1d798e078f7c5958eec952059c460b220a63f586" @@ -931,17 +1708,24 @@ dependencies: "@babel/helper-plugin-utils" "^7.16.7" -"@babel/plugin-transform-runtime@^7.17.10": - version "7.17.10" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.17.10.tgz#b89d821c55d61b5e3d3c3d1d636d8d5a81040ae1" - integrity sha512-6jrMilUAJhktTr56kACL8LnWC5hx3Lf27BS0R0DSyW/OoJfb/iTHeE96V3b1dgKG3FSFdd/0culnYWMkjcKCig== +"@babel/plugin-transform-reserved-words@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz#832cd35b81c287c4bcd09ce03e22199641f964fb" + integrity sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA== dependencies: - "@babel/helper-module-imports" "^7.16.7" - "@babel/helper-plugin-utils" "^7.16.7" - babel-plugin-polyfill-corejs2 "^0.3.0" - babel-plugin-polyfill-corejs3 "^0.5.0" - babel-plugin-polyfill-regenerator "^0.3.0" - semver "^6.3.0" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-runtime@^7.18.6": + version "7.22.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.22.9.tgz#a87b11e170cbbfb018e6a2bf91f5c6e533b9e027" + integrity sha512-9KjBH61AGJetCPYp/IEyLEp47SyybZb0nDRpBvmtEkm+rUIwxdlKpyNHI1TmsGkeuLclJdleQHRZ8XLBnnh8CQ== + dependencies: + "@babel/helper-module-imports" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + babel-plugin-polyfill-corejs2 "^0.4.4" + babel-plugin-polyfill-corejs3 "^0.8.2" + babel-plugin-polyfill-regenerator "^0.5.1" + semver "^6.3.1" "@babel/plugin-transform-shorthand-properties@^7.16.7": version "7.16.7" @@ -950,6 +1734,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.16.7" +"@babel/plugin-transform-shorthand-properties@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz#6e277654be82b5559fc4b9f58088507c24f0c624" + integrity sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-transform-spread@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.16.7.tgz#a303e2122f9f12e0105daeedd0f30fb197d8ff44" @@ -958,6 +1749,14 @@ "@babel/helper-plugin-utils" "^7.16.7" "@babel/helper-skip-transparent-expression-wrappers" "^7.16.0" +"@babel/plugin-transform-spread@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz#6487fd29f229c95e284ba6c98d65eafb893fea6b" + integrity sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/plugin-transform-sticky-regex@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.16.7.tgz#c84741d4f4a38072b9a1e2e3fd56d359552e8660" @@ -965,6 +1764,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.16.7" +"@babel/plugin-transform-sticky-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz#295aba1595bfc8197abd02eae5fc288c0deb26aa" + integrity sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-transform-template-literals@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.16.7.tgz#f3d1c45d28967c8e80f53666fc9c3e50618217ab" @@ -972,6 +1778,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.16.7" +"@babel/plugin-transform-template-literals@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz#8f38cf291e5f7a8e60e9f733193f0bcc10909bff" + integrity sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-transform-typeof-symbol@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.16.7.tgz#9cdbe622582c21368bd482b660ba87d5545d4f7e" @@ -979,6 +1792,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.16.7" +"@babel/plugin-transform-typeof-symbol@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz#5e2ba478da4b603af8673ff7c54f75a97b716b34" + integrity sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-transform-typescript@^7.16.7": version "7.16.8" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.16.8.tgz#591ce9b6b83504903fa9dd3652c357c2ba7a1ee0" @@ -988,6 +1808,16 @@ "@babel/helper-plugin-utils" "^7.16.7" "@babel/plugin-syntax-typescript" "^7.16.7" +"@babel/plugin-transform-typescript@^7.22.5": + version "7.22.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.22.9.tgz#91e08ad1eb1028ecc62662a842e93ecfbf3c7234" + integrity sha512-BnVR1CpKiuD0iobHPaM1iLvcwPYN2uVFAqoLVSpEDKWuOikoCv5HbKLxclhKYUXlWkX86DoZGtqI4XhbOsyrMg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-create-class-features-plugin" "^7.22.9" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-typescript" "^7.22.5" + "@babel/plugin-transform-unicode-escapes@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.16.7.tgz#da8717de7b3287a2c6d659750c964f302b31ece3" @@ -995,6 +1825,21 @@ dependencies: "@babel/helper-plugin-utils" "^7.16.7" +"@babel/plugin-transform-unicode-escapes@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.5.tgz#ce0c248522b1cb22c7c992d88301a5ead70e806c" + integrity sha512-biEmVg1IYB/raUO5wT1tgfacCef15Fbzhkx493D3urBI++6hpJ+RFG4SrWMn0NEZLfvilqKf3QDrRVZHo08FYg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-unicode-property-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz#098898f74d5c1e86660dc112057b2d11227f1c81" + integrity sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-transform-unicode-regex@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.16.7.tgz#0f7aa4a501198976e25e82702574c34cfebe9ef2" @@ -1003,7 +1848,23 @@ "@babel/helper-create-regexp-features-plugin" "^7.16.7" "@babel/helper-plugin-utils" "^7.16.7" -"@babel/preset-env@^7.12.1", "@babel/preset-env@^7.15.6", "@babel/preset-env@^7.17.10": +"@babel/plugin-transform-unicode-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz#ce7e7bb3ef208c4ff67e02a22816656256d7a183" + integrity sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-unicode-sets-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz#77788060e511b708ffc7d42fdfbc5b37c3004e91" + integrity sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/preset-env@^7.12.1", "@babel/preset-env@^7.15.6": version "7.17.10" resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.17.10.tgz#a81b093669e3eb6541bb81a23173c5963c5de69c" integrity sha512-YNgyBHZQpeoBSRBg0xixsZzfT58Ze1iZrajvv0lJc70qDDGuGfonEnMGfWeSY0mQ3JTuCWFbMkzFRVafOyJx4g== @@ -1083,6 +1944,92 @@ core-js-compat "^3.22.1" semver "^6.3.0" +"@babel/preset-env@^7.18.6": + version "7.22.9" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.22.9.tgz#57f17108eb5dfd4c5c25a44c1977eba1df310ac7" + integrity sha512-wNi5H/Emkhll/bqPjsjQorSykrlfY5OWakd6AulLvMEytpKasMVUpVy8RL4qBIBs5Ac6/5i0/Rv0b/Fg6Eag/g== + dependencies: + "@babel/compat-data" "^7.22.9" + "@babel/helper-compilation-targets" "^7.22.9" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-validator-option" "^7.22.5" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.22.5" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.22.5" + "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-class-properties" "^7.12.13" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/plugin-syntax-import-assertions" "^7.22.5" + "@babel/plugin-syntax-import-attributes" "^7.22.5" + "@babel/plugin-syntax-import-meta" "^7.10.4" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/plugin-syntax-top-level-await" "^7.14.5" + "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" + "@babel/plugin-transform-arrow-functions" "^7.22.5" + "@babel/plugin-transform-async-generator-functions" "^7.22.7" + "@babel/plugin-transform-async-to-generator" "^7.22.5" + "@babel/plugin-transform-block-scoped-functions" "^7.22.5" + "@babel/plugin-transform-block-scoping" "^7.22.5" + "@babel/plugin-transform-class-properties" "^7.22.5" + "@babel/plugin-transform-class-static-block" "^7.22.5" + "@babel/plugin-transform-classes" "^7.22.6" + "@babel/plugin-transform-computed-properties" "^7.22.5" + "@babel/plugin-transform-destructuring" "^7.22.5" + "@babel/plugin-transform-dotall-regex" "^7.22.5" + "@babel/plugin-transform-duplicate-keys" "^7.22.5" + "@babel/plugin-transform-dynamic-import" "^7.22.5" + "@babel/plugin-transform-exponentiation-operator" "^7.22.5" + "@babel/plugin-transform-export-namespace-from" "^7.22.5" + "@babel/plugin-transform-for-of" "^7.22.5" + "@babel/plugin-transform-function-name" "^7.22.5" + "@babel/plugin-transform-json-strings" "^7.22.5" + "@babel/plugin-transform-literals" "^7.22.5" + "@babel/plugin-transform-logical-assignment-operators" "^7.22.5" + "@babel/plugin-transform-member-expression-literals" "^7.22.5" + "@babel/plugin-transform-modules-amd" "^7.22.5" + "@babel/plugin-transform-modules-commonjs" "^7.22.5" + "@babel/plugin-transform-modules-systemjs" "^7.22.5" + "@babel/plugin-transform-modules-umd" "^7.22.5" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.22.5" + "@babel/plugin-transform-new-target" "^7.22.5" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.22.5" + "@babel/plugin-transform-numeric-separator" "^7.22.5" + "@babel/plugin-transform-object-rest-spread" "^7.22.5" + "@babel/plugin-transform-object-super" "^7.22.5" + "@babel/plugin-transform-optional-catch-binding" "^7.22.5" + "@babel/plugin-transform-optional-chaining" "^7.22.6" + "@babel/plugin-transform-parameters" "^7.22.5" + "@babel/plugin-transform-private-methods" "^7.22.5" + "@babel/plugin-transform-private-property-in-object" "^7.22.5" + "@babel/plugin-transform-property-literals" "^7.22.5" + "@babel/plugin-transform-regenerator" "^7.22.5" + "@babel/plugin-transform-reserved-words" "^7.22.5" + "@babel/plugin-transform-shorthand-properties" "^7.22.5" + "@babel/plugin-transform-spread" "^7.22.5" + "@babel/plugin-transform-sticky-regex" "^7.22.5" + "@babel/plugin-transform-template-literals" "^7.22.5" + "@babel/plugin-transform-typeof-symbol" "^7.22.5" + "@babel/plugin-transform-unicode-escapes" "^7.22.5" + "@babel/plugin-transform-unicode-property-regex" "^7.22.5" + "@babel/plugin-transform-unicode-regex" "^7.22.5" + "@babel/plugin-transform-unicode-sets-regex" "^7.22.5" + "@babel/preset-modules" "^0.1.5" + "@babel/types" "^7.22.5" + babel-plugin-polyfill-corejs2 "^0.4.4" + babel-plugin-polyfill-corejs3 "^0.8.2" + babel-plugin-polyfill-regenerator "^0.5.1" + core-js-compat "^3.31.0" + semver "^6.3.1" + "@babel/preset-modules@^0.1.5": version "0.1.5" resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.5.tgz#ef939d6e7f268827e1841638dc6ff95515e115d9" @@ -1094,7 +2041,7 @@ "@babel/types" "^7.4.4" esutils "^2.0.2" -"@babel/preset-react@^7.12.5", "@babel/preset-react@^7.14.5", "@babel/preset-react@^7.16.7": +"@babel/preset-react@^7.12.5", "@babel/preset-react@^7.14.5": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.16.7.tgz#4c18150491edc69c183ff818f9f2aecbe5d93852" integrity sha512-fWpyI8UM/HE6DfPBzD8LnhQ/OcH8AgTaqcqP2nGOXEUV+VKBR5JRN9hCk9ai+zQQ57vtm9oWeXguBCPNUjytgA== @@ -1106,7 +2053,19 @@ "@babel/plugin-transform-react-jsx-development" "^7.16.7" "@babel/plugin-transform-react-pure-annotations" "^7.16.7" -"@babel/preset-typescript@^7.15.0", "@babel/preset-typescript@^7.16.7": +"@babel/preset-react@^7.18.6": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.22.5.tgz#c4d6058fbf80bccad02dd8c313a9aaa67e3c3dd6" + integrity sha512-M+Is3WikOpEJHgR385HbuCITPTaPRaNkibTEa9oiofmJvIsrceb4yp9RL9Kb+TE8LznmeyZqpP+Lopwcx59xPQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-validator-option" "^7.22.5" + "@babel/plugin-transform-react-display-name" "^7.22.5" + "@babel/plugin-transform-react-jsx" "^7.22.5" + "@babel/plugin-transform-react-jsx-development" "^7.22.5" + "@babel/plugin-transform-react-pure-annotations" "^7.22.5" + +"@babel/preset-typescript@^7.15.0": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.16.7.tgz#ab114d68bb2020afc069cd51b37ff98a046a70b9" integrity sha512-WbVEmgXdIyvzB77AQjGBEyYPZx+8tTsO50XtfozQrkW8QB2rLJpH2lgx0TRw5EJrBxOZQ+wCcyPVQvS8tjEHpQ== @@ -1115,21 +2074,44 @@ "@babel/helper-validator-option" "^7.16.7" "@babel/plugin-transform-typescript" "^7.16.7" -"@babel/runtime-corejs3@^7.17.9": - version "7.17.9" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.17.9.tgz#3d02d0161f0fbf3ada8e88159375af97690f4055" - integrity sha512-WxYHHUWF2uZ7Hp1K+D1xQgbgkGUfA+5UPOegEXGt2Y5SMog/rYCVaifLZDbw8UkNXozEqqrZTy6bglL7xTaCOw== +"@babel/preset-typescript@^7.18.6": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.22.5.tgz#16367d8b01d640e9a507577ed4ee54e0101e51c8" + integrity sha512-YbPaal9LxztSGhmndR46FmAbkJ/1fAsw293tSU+I5E5h+cnJ3d4GTwyUgGYmOXJYdGA+uNePle4qbaRzj2NISQ== dependencies: - core-js-pure "^3.20.2" - regenerator-runtime "^0.13.4" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-validator-option" "^7.22.5" + "@babel/plugin-syntax-jsx" "^7.22.5" + "@babel/plugin-transform-modules-commonjs" "^7.22.5" + "@babel/plugin-transform-typescript" "^7.22.5" + +"@babel/regjsgen@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" + integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== + +"@babel/runtime-corejs3@^7.18.6": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.22.6.tgz#e8e625eb3db29491e0326b3aeb9929c43b270ae4" + integrity sha512-M+37LLIRBTEVjktoJjbw4KVhupF0U/3PYUCbBwgAd9k17hoKhRu1n935QiG7Tuxv0LJOMrb2vuKEeYUlv0iyiw== + dependencies: + core-js-pure "^3.30.2" + regenerator-runtime "^0.13.11" -"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.17.9", "@babel/runtime@^7.8.4": +"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.8.4": version "7.17.9" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.9.tgz#d19fbf802d01a8cb6cf053a64e472d42c434ba72" integrity sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg== dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.18.6": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.6.tgz#57d64b9ae3cff1d67eb067ae117dac087f5bd438" + integrity sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ== + dependencies: + regenerator-runtime "^0.13.11" + "@babel/template@^7.12.7", "@babel/template@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155" @@ -1139,6 +2121,15 @@ "@babel/parser" "^7.16.7" "@babel/types" "^7.16.7" +"@babel/template@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec" + integrity sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw== + dependencies: + "@babel/code-frame" "^7.22.5" + "@babel/parser" "^7.22.5" + "@babel/types" "^7.22.5" + "@babel/traverse@^7.12.9", "@babel/traverse@^7.13.0", "@babel/traverse@^7.16.7", "@babel/traverse@^7.16.8", "@babel/traverse@^7.17.10", "@babel/traverse@^7.17.3", "@babel/traverse@^7.17.9": version "7.17.10" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.17.10.tgz#1ee1a5ac39f4eac844e6cf855b35520e5eb6f8b5" @@ -1155,6 +2146,22 @@ debug "^4.1.0" globals "^11.1.0" +"@babel/traverse@^7.18.8", "@babel/traverse@^7.22.6", "@babel/traverse@^7.22.8": + version "7.22.8" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.22.8.tgz#4d4451d31bc34efeae01eac222b514a77aa4000e" + integrity sha512-y6LPR+wpM2I3qJrsheCTwhIinzkETbplIgPBbwvqPKc+uljeA5gP+3nP8irdYt1mjQaDnlIcG+dw8OjAco4GXw== + dependencies: + "@babel/code-frame" "^7.22.5" + "@babel/generator" "^7.22.7" + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/parser" "^7.22.7" + "@babel/types" "^7.22.5" + debug "^4.1.0" + globals "^11.1.0" + "@babel/types@^7.12.6", "@babel/types@^7.12.7", "@babel/types@^7.15.6", "@babel/types@^7.16.0", "@babel/types@^7.16.7", "@babel/types@^7.16.8", "@babel/types@^7.17.0", "@babel/types@^7.17.10", "@babel/types@^7.4.4": version "7.17.10" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.17.10.tgz#d35d7b4467e439fcf06d195f8100e0fea7fc82c4" @@ -1163,68 +2170,78 @@ "@babel/helper-validator-identifier" "^7.16.7" to-fast-properties "^2.0.0" +"@babel/types@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.5.tgz#cd93eeaab025880a3a47ec881f4b096a5b786fbe" + integrity sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA== + dependencies: + "@babel/helper-string-parser" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.5" + to-fast-properties "^2.0.0" + "@colors/colors@1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== -"@docsearch/css@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-3.0.0.tgz#fe57b474802ffd706d3246eab25d52fac8aa3698" - integrity sha512-1kkV7tkAsiuEd0shunYRByKJe3xQDG2q7wYg24SOw1nV9/2lwEd4WrUYRJC/ukGTl2/kHeFxsaUvtiOy0y6fFA== +"@docsearch/css@3.5.1": + version "3.5.1" + resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-3.5.1.tgz#4adf9884735bbfea621c3716e80ea97baa419b73" + integrity sha512-2Pu9HDg/uP/IT10rbQ+4OrTQuxIWdKVUEdcw9/w7kZJv9NeHS6skJx1xuRiFyoGKwAzcHXnLp7csE99sj+O1YA== -"@docsearch/react@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@docsearch/react/-/react-3.0.0.tgz#d02ebdc67573412185a6a4df13bc254c7c0da491" - integrity sha512-yhMacqS6TVQYoBh/o603zszIb5Bl8MIXuOc6Vy617I74pirisDzzcNh0NEaYQt50fVVR3khUbeEhUEWEWipESg== +"@docsearch/react@^3.1.1": + version "3.5.1" + resolved "https://registry.yarnpkg.com/@docsearch/react/-/react-3.5.1.tgz#35f4a75f948211d8bb6830d2147c575f96a85274" + integrity sha512-t5mEODdLzZq4PTFAm/dvqcvZFdPDMdfPE5rJS5SC8OUq9mPzxEy6b+9THIqNM9P0ocCb4UC5jqBrxKclnuIbzQ== dependencies: - "@algolia/autocomplete-core" "1.5.2" - "@algolia/autocomplete-preset-algolia" "1.5.2" - "@docsearch/css" "3.0.0" + "@algolia/autocomplete-core" "1.9.3" + "@algolia/autocomplete-preset-algolia" "1.9.3" + "@docsearch/css" "3.5.1" algoliasearch "^4.0.0" -"@docusaurus/core@2.0.0-beta.20", "@docusaurus/core@^2.0.0-beta.3": - version "2.0.0-beta.20" - resolved "https://registry.yarnpkg.com/@docusaurus/core/-/core-2.0.0-beta.20.tgz#cf4aeeccecacb547a6fb42340c83bed0cccb57c0" - integrity sha512-a3UgZ4lIcIOoZd4j9INqVkWSXEDxR7EicJXt8eq2whg4N5hKGqLHoDSnWfrVSPQn4NoG5T7jhPypphSoysImfQ== +"@docusaurus/core@2.4.1", "@docusaurus/core@^2.0.0-alpha.70": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/core/-/core-2.4.1.tgz#4b8ff5766131ce3fbccaad0b1daf2ad4dc76f62d" + integrity sha512-SNsY7PshK3Ri7vtsLXVeAJGS50nJN3RgF836zkyUfAD01Fq+sAk5EwWgLw+nnm5KVNGDu7PRR2kRGDsWvqpo0g== dependencies: - "@babel/core" "^7.17.10" - "@babel/generator" "^7.17.10" + "@babel/core" "^7.18.6" + "@babel/generator" "^7.18.7" "@babel/plugin-syntax-dynamic-import" "^7.8.3" - "@babel/plugin-transform-runtime" "^7.17.10" - "@babel/preset-env" "^7.17.10" - "@babel/preset-react" "^7.16.7" - "@babel/preset-typescript" "^7.16.7" - "@babel/runtime" "^7.17.9" - "@babel/runtime-corejs3" "^7.17.9" - "@babel/traverse" "^7.17.10" - "@docusaurus/cssnano-preset" "2.0.0-beta.20" - "@docusaurus/logger" "2.0.0-beta.20" - "@docusaurus/mdx-loader" "2.0.0-beta.20" + "@babel/plugin-transform-runtime" "^7.18.6" + "@babel/preset-env" "^7.18.6" + "@babel/preset-react" "^7.18.6" + "@babel/preset-typescript" "^7.18.6" + "@babel/runtime" "^7.18.6" + "@babel/runtime-corejs3" "^7.18.6" + "@babel/traverse" "^7.18.8" + "@docusaurus/cssnano-preset" "2.4.1" + "@docusaurus/logger" "2.4.1" + "@docusaurus/mdx-loader" "2.4.1" "@docusaurus/react-loadable" "5.5.2" - "@docusaurus/utils" "2.0.0-beta.20" - "@docusaurus/utils-common" "2.0.0-beta.20" - "@docusaurus/utils-validation" "2.0.0-beta.20" - "@slorber/static-site-generator-webpack-plugin" "^4.0.4" + "@docusaurus/utils" "2.4.1" + "@docusaurus/utils-common" "2.4.1" + "@docusaurus/utils-validation" "2.4.1" + "@slorber/static-site-generator-webpack-plugin" "^4.0.7" "@svgr/webpack" "^6.2.1" - autoprefixer "^10.4.5" + autoprefixer "^10.4.7" babel-loader "^8.2.5" - babel-plugin-dynamic-import-node "2.3.0" + babel-plugin-dynamic-import-node "^2.3.3" boxen "^6.2.1" + chalk "^4.1.2" chokidar "^3.5.3" clean-css "^5.3.0" cli-table3 "^0.6.2" combine-promises "^1.1.0" commander "^5.1.0" - copy-webpack-plugin "^10.2.4" - core-js "^3.22.3" + copy-webpack-plugin "^11.0.0" + core-js "^3.23.3" css-loader "^6.7.1" - css-minimizer-webpack-plugin "^3.4.1" - cssnano "^5.1.7" - del "^6.0.0" + css-minimizer-webpack-plugin "^4.0.0" + cssnano "^5.1.12" + del "^6.1.1" detect-port "^1.3.0" escape-html "^1.0.3" - eta "^1.12.3" + eta "^2.0.0" file-loader "^6.2.0" fs-extra "^10.1.0" html-minifier-terser "^6.1.0" @@ -1233,59 +2250,59 @@ import-fresh "^3.3.0" leven "^3.1.0" lodash "^4.17.21" - mini-css-extract-plugin "^2.6.0" - postcss "^8.4.13" - postcss-loader "^6.2.1" + mini-css-extract-plugin "^2.6.1" + postcss "^8.4.14" + postcss-loader "^7.0.0" prompts "^2.4.2" react-dev-utils "^12.0.1" react-helmet-async "^1.3.0" react-loadable "npm:@docusaurus/react-loadable@5.5.2" react-loadable-ssr-addon-v5-slorber "^1.0.1" - react-router "^5.2.0" + react-router "^5.3.3" react-router-config "^5.1.1" - react-router-dom "^5.2.0" - remark-admonitions "^1.2.1" + react-router-dom "^5.3.3" rtl-detect "^1.0.4" semver "^7.3.7" serve-handler "^6.1.3" shelljs "^0.8.5" - terser-webpack-plugin "^5.3.1" + terser-webpack-plugin "^5.3.3" tslib "^2.4.0" update-notifier "^5.1.0" url-loader "^4.1.1" wait-on "^6.0.1" - webpack "^5.72.0" + webpack "^5.73.0" webpack-bundle-analyzer "^4.5.0" - webpack-dev-server "^4.8.1" + webpack-dev-server "^4.9.3" webpack-merge "^5.8.0" webpackbar "^5.0.2" -"@docusaurus/cssnano-preset@2.0.0-beta.20": - version "2.0.0-beta.20" - resolved "https://registry.yarnpkg.com/@docusaurus/cssnano-preset/-/cssnano-preset-2.0.0-beta.20.tgz#c47722e347fd044f2372e924199eabf61733232f" - integrity sha512-7pfrYuahHl3YYS+gYhbb1YHsq5s5+hk+1KIU7QqNNn4YjrIqAHlOznCQ9XfQfspe9boZmaNFGMZQ1tawNOVLqQ== +"@docusaurus/cssnano-preset@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/cssnano-preset/-/cssnano-preset-2.4.1.tgz#eacadefb1e2e0f59df3467a0fe83e4ff79eed163" + integrity sha512-ka+vqXwtcW1NbXxWsh6yA1Ckii1klY9E53cJ4O9J09nkMBgrNX3iEFED1fWdv8wf4mJjvGi5RLZ2p9hJNjsLyQ== dependencies: - cssnano-preset-advanced "^5.3.3" - postcss "^8.4.13" + cssnano-preset-advanced "^5.3.8" + postcss "^8.4.14" postcss-sort-media-queries "^4.2.1" + tslib "^2.4.0" -"@docusaurus/logger@2.0.0-beta.20": - version "2.0.0-beta.20" - resolved "https://registry.yarnpkg.com/@docusaurus/logger/-/logger-2.0.0-beta.20.tgz#bb49e8516e48082ab96bca11569a18541476d5a6" - integrity sha512-7Rt7c8m3ZM81o5jsm6ENgdbjq/hUICv8Om2i7grynI4GT2aQyFoHcusaNbRji4FZt0DaKT2CQxiAWP8BbD4xzQ== +"@docusaurus/logger@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/logger/-/logger-2.4.1.tgz#4d2c0626b40752641f9fdd93ad9b5a7a0792f767" + integrity sha512-5h5ysIIWYIDHyTVd8BjheZmQZmEgWDR54aQ1BX9pjFfpyzFo5puKXKYrYJXbjEHGyVhEzmB9UXwbxGfaZhOjcg== dependencies: chalk "^4.1.2" tslib "^2.4.0" -"@docusaurus/mdx-loader@2.0.0-beta.20": - version "2.0.0-beta.20" - resolved "https://registry.yarnpkg.com/@docusaurus/mdx-loader/-/mdx-loader-2.0.0-beta.20.tgz#7016e8ce7e4a11c9ea458e2236d3c93af71f393f" - integrity sha512-BBuf77sji3JxbCEW7Qsv3CXlgpm+iSLTQn6JUK7x8vJ1JYZ3KJbNgpo9TmxIIltpcvNQ/QOy6dvqrpSStaWmKQ== +"@docusaurus/mdx-loader@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/mdx-loader/-/mdx-loader-2.4.1.tgz#6425075d7fc136dbfdc121349060cedd64118393" + integrity sha512-4KhUhEavteIAmbBj7LVFnrVYDiU51H5YWW1zY6SmBSte/YLhDutztLTBE0PQl1Grux1jzUJeaSvAzHpTn6JJDQ== dependencies: - "@babel/parser" "^7.17.10" - "@babel/traverse" "^7.17.10" - "@docusaurus/logger" "2.0.0-beta.20" - "@docusaurus/utils" "2.0.0-beta.20" + "@babel/parser" "^7.18.8" + "@babel/traverse" "^7.18.8" + "@docusaurus/logger" "2.4.1" + "@docusaurus/utils" "2.4.1" "@mdx-js/mdx" "^1.6.22" escape-html "^1.0.3" file-loader "^6.2.0" @@ -1295,135 +2312,158 @@ remark-emoji "^2.2.0" stringify-object "^3.3.0" tslib "^2.4.0" + unified "^9.2.2" unist-util-visit "^2.0.3" url-loader "^4.1.1" - webpack "^5.72.0" + webpack "^5.73.0" -"@docusaurus/module-type-aliases@2.0.0-beta.20": - version "2.0.0-beta.20" - resolved "https://registry.yarnpkg.com/@docusaurus/module-type-aliases/-/module-type-aliases-2.0.0-beta.20.tgz#669605a64b04226c391e0284c44743e137eb2595" - integrity sha512-lUIXLwQEOyYwcb3iCNibPUL6O9ijvYF5xQwehGeVraTEBts/Ch8ZwELFk+XbaGHKh52PiVxuWL2CP4Gdjy5QKw== +"@docusaurus/module-type-aliases@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/module-type-aliases/-/module-type-aliases-2.4.1.tgz#38b3c2d2ae44bea6d57506eccd84280216f0171c" + integrity sha512-gLBuIFM8Dp2XOCWffUDSjtxY7jQgKvYujt7Mx5s4FCTfoL5dN1EVbnrn+O2Wvh8b0a77D57qoIDY7ghgmatR1A== dependencies: - "@docusaurus/types" "2.0.0-beta.20" + "@docusaurus/react-loadable" "5.5.2" + "@docusaurus/types" "2.4.1" + "@types/history" "^4.7.11" "@types/react" "*" "@types/react-router-config" "*" "@types/react-router-dom" "*" react-helmet-async "*" + react-loadable "npm:@docusaurus/react-loadable@5.5.2" -"@docusaurus/plugin-content-blog@2.0.0-beta.20": - version "2.0.0-beta.20" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-blog/-/plugin-content-blog-2.0.0-beta.20.tgz#7c0d413ac8df9a422a0b3ddd90b77ec491bbda15" - integrity sha512-6aby36Gmny5h2oo/eEZ2iwVsIlBWbRnNNeqT0BYnJO5aj53iCU/ctFPpJVYcw0l2l8+8ITS70FyePIWEsaZ0jA== - dependencies: - "@docusaurus/core" "2.0.0-beta.20" - "@docusaurus/logger" "2.0.0-beta.20" - "@docusaurus/mdx-loader" "2.0.0-beta.20" - "@docusaurus/utils" "2.0.0-beta.20" - "@docusaurus/utils-common" "2.0.0-beta.20" - "@docusaurus/utils-validation" "2.0.0-beta.20" - cheerio "^1.0.0-rc.10" +"@docusaurus/plugin-content-blog@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-blog/-/plugin-content-blog-2.4.1.tgz#c705a8b1a36a34f181dcf43b7770532e4dcdc4a3" + integrity sha512-E2i7Knz5YIbE1XELI6RlTnZnGgS52cUO4BlCiCUCvQHbR+s1xeIWz4C6BtaVnlug0Ccz7nFSksfwDpVlkujg5Q== + dependencies: + "@docusaurus/core" "2.4.1" + "@docusaurus/logger" "2.4.1" + "@docusaurus/mdx-loader" "2.4.1" + "@docusaurus/types" "2.4.1" + "@docusaurus/utils" "2.4.1" + "@docusaurus/utils-common" "2.4.1" + "@docusaurus/utils-validation" "2.4.1" + cheerio "^1.0.0-rc.12" feed "^4.2.2" fs-extra "^10.1.0" lodash "^4.17.21" reading-time "^1.5.0" - remark-admonitions "^1.2.1" tslib "^2.4.0" unist-util-visit "^2.0.3" utility-types "^3.10.0" - webpack "^5.72.0" - -"@docusaurus/plugin-content-docs@2.0.0-beta.20": - version "2.0.0-beta.20" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-docs/-/plugin-content-docs-2.0.0-beta.20.tgz#2a53b9fc355f45bf7c6917f19be7bfa0698b8b9e" - integrity sha512-XOgwUqXtr/DStpB3azdN6wgkKtQkOXOx1XetORzhHnjihrSMn6daxg+spmcJh1ki/mpT3n7yBbKJxVNo+VB38Q== - dependencies: - "@docusaurus/core" "2.0.0-beta.20" - "@docusaurus/logger" "2.0.0-beta.20" - "@docusaurus/mdx-loader" "2.0.0-beta.20" - "@docusaurus/utils" "2.0.0-beta.20" - "@docusaurus/utils-validation" "2.0.0-beta.20" + webpack "^5.73.0" + +"@docusaurus/plugin-content-docs@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-docs/-/plugin-content-docs-2.4.1.tgz#ed94d9721b5ce7a956fb01cc06c40d8eee8dfca7" + integrity sha512-Lo7lSIcpswa2Kv4HEeUcGYqaasMUQNpjTXpV0N8G6jXgZaQurqp7E8NGYeGbDXnb48czmHWbzDL4S3+BbK0VzA== + dependencies: + "@docusaurus/core" "2.4.1" + "@docusaurus/logger" "2.4.1" + "@docusaurus/mdx-loader" "2.4.1" + "@docusaurus/module-type-aliases" "2.4.1" + "@docusaurus/types" "2.4.1" + "@docusaurus/utils" "2.4.1" + "@docusaurus/utils-validation" "2.4.1" + "@types/react-router-config" "^5.0.6" combine-promises "^1.1.0" fs-extra "^10.1.0" import-fresh "^3.3.0" js-yaml "^4.1.0" lodash "^4.17.21" - remark-admonitions "^1.2.1" tslib "^2.4.0" utility-types "^3.10.0" - webpack "^5.72.0" + webpack "^5.73.0" -"@docusaurus/plugin-content-pages@2.0.0-beta.20": - version "2.0.0-beta.20" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-pages/-/plugin-content-pages-2.0.0-beta.20.tgz#6a76c7fa049983d2d94d8c4d738802acf01611fe" - integrity sha512-ubY6DG4F0skFKjfNGCbfO34Qf+MZy6C05OtpIYsoA2YU8ADx0nRH7qPgdEkwR3ma860DbY612rleRT13ogSlhg== - dependencies: - "@docusaurus/core" "2.0.0-beta.20" - "@docusaurus/mdx-loader" "2.0.0-beta.20" - "@docusaurus/utils" "2.0.0-beta.20" - "@docusaurus/utils-validation" "2.0.0-beta.20" +"@docusaurus/plugin-content-pages@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-pages/-/plugin-content-pages-2.4.1.tgz#c534f7e49967699a45bbe67050d1605ebbf3d285" + integrity sha512-/UjuH/76KLaUlL+o1OvyORynv6FURzjurSjvn2lbWTFc4tpYY2qLYTlKpTCBVPhlLUQsfyFnshEJDLmPneq2oA== + dependencies: + "@docusaurus/core" "2.4.1" + "@docusaurus/mdx-loader" "2.4.1" + "@docusaurus/types" "2.4.1" + "@docusaurus/utils" "2.4.1" + "@docusaurus/utils-validation" "2.4.1" fs-extra "^10.1.0" - remark-admonitions "^1.2.1" tslib "^2.4.0" - webpack "^5.72.0" + webpack "^5.73.0" -"@docusaurus/plugin-debug@2.0.0-beta.20": - version "2.0.0-beta.20" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-debug/-/plugin-debug-2.0.0-beta.20.tgz#7c026e81c45fd4f00801a785c928b9e8727a99de" - integrity sha512-acGZmpncPA1XDczpV1ji1ajBCRBY/H2lXN8alSjOB1vh0c/2Qz+KKD05p17lsUbhIyvsnZBa/BaOwtek91Lu7Q== +"@docusaurus/plugin-debug@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-debug/-/plugin-debug-2.4.1.tgz#461a2c77b0c5a91b2c05257c8f9585412aaa59dc" + integrity sha512-7Yu9UPzRShlrH/G8btOpR0e6INFZr0EegWplMjOqelIwAcx3PKyR8mgPTxGTxcqiYj6hxSCRN0D8R7YrzImwNA== dependencies: - "@docusaurus/core" "2.0.0-beta.20" - "@docusaurus/utils" "2.0.0-beta.20" + "@docusaurus/core" "2.4.1" + "@docusaurus/types" "2.4.1" + "@docusaurus/utils" "2.4.1" fs-extra "^10.1.0" react-json-view "^1.21.3" tslib "^2.4.0" -"@docusaurus/plugin-google-analytics@2.0.0-beta.20": - version "2.0.0-beta.20" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-2.0.0-beta.20.tgz#c1bdbc1239f987f9716fa30c2fd86962c190a765" - integrity sha512-4C5nY25j0R1lntFmpSEalhL7jYA7tWvk0VZObiIxGilLagT/f9gWPQtIjNBe4yzdQvkhiaXpa8xcMcJUAKRJyw== +"@docusaurus/plugin-google-analytics@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-2.4.1.tgz#30de1c35773bf9d52bb2d79b201b23eb98022613" + integrity sha512-dyZJdJiCoL+rcfnm0RPkLt/o732HvLiEwmtoNzOoz9MSZz117UH2J6U2vUDtzUzwtFLIf32KkeyzisbwUCgcaQ== dependencies: - "@docusaurus/core" "2.0.0-beta.20" - "@docusaurus/utils-validation" "2.0.0-beta.20" + "@docusaurus/core" "2.4.1" + "@docusaurus/types" "2.4.1" + "@docusaurus/utils-validation" "2.4.1" tslib "^2.4.0" -"@docusaurus/plugin-google-gtag@2.0.0-beta.20": - version "2.0.0-beta.20" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-2.0.0-beta.20.tgz#7284bcfad9cb4e5d63605e95f6da73ca8ac8f053" - integrity sha512-EMZdiMTNg4NwE60xwjbetcqMDqAOazMTwQAQ4OuNAclv7oh8+VPCvqRF8s8AxCoI2Uqc7vh8yzNUuM307Ne9JA== +"@docusaurus/plugin-google-gtag@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-2.4.1.tgz#6a3eb91022714735e625c7ca70ef5188fa7bd0dc" + integrity sha512-mKIefK+2kGTQBYvloNEKtDmnRD7bxHLsBcxgnbt4oZwzi2nxCGjPX6+9SQO2KCN5HZbNrYmGo5GJfMgoRvy6uA== dependencies: - "@docusaurus/core" "2.0.0-beta.20" - "@docusaurus/utils-validation" "2.0.0-beta.20" + "@docusaurus/core" "2.4.1" + "@docusaurus/types" "2.4.1" + "@docusaurus/utils-validation" "2.4.1" tslib "^2.4.0" -"@docusaurus/plugin-sitemap@2.0.0-beta.20": - version "2.0.0-beta.20" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-2.0.0-beta.20.tgz#08eada1260cafb273abea33c9c890ab667c7cbd3" - integrity sha512-Rf5a2vOBWjbe7PJJEBDeLZzDA7lsDi+16bqzKN8OKSXlcZLhxjmIpL5NrjANNbpGpL5vbl9z+iqvjbQmZ3QSmA== +"@docusaurus/plugin-google-tag-manager@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-2.4.1.tgz#b99f71aec00b112bbf509ef2416e404a95eb607e" + integrity sha512-Zg4Ii9CMOLfpeV2nG74lVTWNtisFaH9QNtEw48R5QE1KIwDBdTVaiSA18G1EujZjrzJJzXN79VhINSbOJO/r3g== dependencies: - "@docusaurus/core" "2.0.0-beta.20" - "@docusaurus/utils" "2.0.0-beta.20" - "@docusaurus/utils-common" "2.0.0-beta.20" - "@docusaurus/utils-validation" "2.0.0-beta.20" + "@docusaurus/core" "2.4.1" + "@docusaurus/types" "2.4.1" + "@docusaurus/utils-validation" "2.4.1" + tslib "^2.4.0" + +"@docusaurus/plugin-sitemap@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-2.4.1.tgz#8a7a76ed69dc3e6b4474b6abb10bb03336a9de6d" + integrity sha512-lZx+ijt/+atQ3FVE8FOHV/+X3kuok688OydDXrqKRJyXBJZKgGjA2Qa8RjQ4f27V2woaXhtnyrdPop/+OjVMRg== + dependencies: + "@docusaurus/core" "2.4.1" + "@docusaurus/logger" "2.4.1" + "@docusaurus/types" "2.4.1" + "@docusaurus/utils" "2.4.1" + "@docusaurus/utils-common" "2.4.1" + "@docusaurus/utils-validation" "2.4.1" fs-extra "^10.1.0" sitemap "^7.1.1" tslib "^2.4.0" -"@docusaurus/preset-classic@^2.0.0-beta.3": - version "2.0.0-beta.20" - resolved "https://registry.yarnpkg.com/@docusaurus/preset-classic/-/preset-classic-2.0.0-beta.20.tgz#19566be713ce0297834cd999392ea3605bc6f574" - integrity sha512-artUDjiYFIlGd2fxk0iqqcJ5xSCrgormOAoind1c0pn8TRXY1WSCQWYI6p4X24jjhSCzLv0s6Z9PMDyxZdivhg== - dependencies: - "@docusaurus/core" "2.0.0-beta.20" - "@docusaurus/plugin-content-blog" "2.0.0-beta.20" - "@docusaurus/plugin-content-docs" "2.0.0-beta.20" - "@docusaurus/plugin-content-pages" "2.0.0-beta.20" - "@docusaurus/plugin-debug" "2.0.0-beta.20" - "@docusaurus/plugin-google-analytics" "2.0.0-beta.20" - "@docusaurus/plugin-google-gtag" "2.0.0-beta.20" - "@docusaurus/plugin-sitemap" "2.0.0-beta.20" - "@docusaurus/theme-classic" "2.0.0-beta.20" - "@docusaurus/theme-common" "2.0.0-beta.20" - "@docusaurus/theme-search-algolia" "2.0.0-beta.20" +"@docusaurus/preset-classic@^2.0.0-alpha.70": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/preset-classic/-/preset-classic-2.4.1.tgz#072f22d0332588e9c5f512d4bded8d7c99f91497" + integrity sha512-P4//+I4zDqQJ+UDgoFrjIFaQ1MeS9UD1cvxVQaI6O7iBmiHQm0MGROP1TbE7HlxlDPXFJjZUK3x3cAoK63smGQ== + dependencies: + "@docusaurus/core" "2.4.1" + "@docusaurus/plugin-content-blog" "2.4.1" + "@docusaurus/plugin-content-docs" "2.4.1" + "@docusaurus/plugin-content-pages" "2.4.1" + "@docusaurus/plugin-debug" "2.4.1" + "@docusaurus/plugin-google-analytics" "2.4.1" + "@docusaurus/plugin-google-gtag" "2.4.1" + "@docusaurus/plugin-google-tag-manager" "2.4.1" + "@docusaurus/plugin-sitemap" "2.4.1" + "@docusaurus/theme-classic" "2.4.1" + "@docusaurus/theme-common" "2.4.1" + "@docusaurus/theme-search-algolia" "2.4.1" + "@docusaurus/types" "2.4.1" "@docusaurus/react-loadable@5.5.2", "react-loadable@npm:@docusaurus/react-loadable@5.5.2": version "5.5.2" @@ -1433,115 +2473,129 @@ "@types/react" "*" prop-types "^15.6.2" -"@docusaurus/theme-classic@2.0.0-beta.20": - version "2.0.0-beta.20" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-classic/-/theme-classic-2.0.0-beta.20.tgz#c4c7712c2b35c5654433228729f92ec73e6c598e" - integrity sha512-rs4U68x8Xk6rPsZC/7eaPxCKqzXX1S45FICKmq/IZuaDaQyQIijCvv2ssxYnUyVZUNayZfJK7ZtNu+A0kzYgSQ== - dependencies: - "@docusaurus/core" "2.0.0-beta.20" - "@docusaurus/plugin-content-blog" "2.0.0-beta.20" - "@docusaurus/plugin-content-docs" "2.0.0-beta.20" - "@docusaurus/plugin-content-pages" "2.0.0-beta.20" - "@docusaurus/theme-common" "2.0.0-beta.20" - "@docusaurus/theme-translations" "2.0.0-beta.20" - "@docusaurus/utils" "2.0.0-beta.20" - "@docusaurus/utils-common" "2.0.0-beta.20" - "@docusaurus/utils-validation" "2.0.0-beta.20" +"@docusaurus/theme-classic@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-classic/-/theme-classic-2.4.1.tgz#0060cb263c1a73a33ac33f79bb6bc2a12a56ad9e" + integrity sha512-Rz0wKUa+LTW1PLXmwnf8mn85EBzaGSt6qamqtmnh9Hflkc+EqiYMhtUJeLdV+wsgYq4aG0ANc+bpUDpsUhdnwg== + dependencies: + "@docusaurus/core" "2.4.1" + "@docusaurus/mdx-loader" "2.4.1" + "@docusaurus/module-type-aliases" "2.4.1" + "@docusaurus/plugin-content-blog" "2.4.1" + "@docusaurus/plugin-content-docs" "2.4.1" + "@docusaurus/plugin-content-pages" "2.4.1" + "@docusaurus/theme-common" "2.4.1" + "@docusaurus/theme-translations" "2.4.1" + "@docusaurus/types" "2.4.1" + "@docusaurus/utils" "2.4.1" + "@docusaurus/utils-common" "2.4.1" + "@docusaurus/utils-validation" "2.4.1" "@mdx-js/react" "^1.6.22" - clsx "^1.1.1" + clsx "^1.2.1" copy-text-to-clipboard "^3.0.1" - infima "0.2.0-alpha.39" + infima "0.2.0-alpha.43" lodash "^4.17.21" nprogress "^0.2.0" - postcss "^8.4.13" - prism-react-renderer "^1.3.1" + postcss "^8.4.14" + prism-react-renderer "^1.3.5" prismjs "^1.28.0" - react-router-dom "^5.2.0" + react-router-dom "^5.3.3" rtlcss "^3.5.0" + tslib "^2.4.0" + utility-types "^3.10.0" -"@docusaurus/theme-common@2.0.0-beta.20": - version "2.0.0-beta.20" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-common/-/theme-common-2.0.0-beta.20.tgz#4ec7d77ecd2ade9dad33b8689e3cd51b07e594b0" - integrity sha512-lmdGB3/GQM5z0GH0iHGRXUco4Wfqc6sR5eRKuW4j0sx3+UFVvtbVTTIGt0Cie4Dh6omnFxjPbNDlPDgWr/agVQ== - dependencies: - "@docusaurus/module-type-aliases" "2.0.0-beta.20" - "@docusaurus/plugin-content-blog" "2.0.0-beta.20" - "@docusaurus/plugin-content-docs" "2.0.0-beta.20" - "@docusaurus/plugin-content-pages" "2.0.0-beta.20" - clsx "^1.1.1" +"@docusaurus/theme-common@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-common/-/theme-common-2.4.1.tgz#03e16f7aa96455e952f3243ac99757b01a3c83d4" + integrity sha512-G7Zau1W5rQTaFFB3x3soQoZpkgMbl/SYNG8PfMFIjKa3M3q8n0m/GRf5/H/e5BqOvt8c+ZWIXGCiz+kUCSHovA== + dependencies: + "@docusaurus/mdx-loader" "2.4.1" + "@docusaurus/module-type-aliases" "2.4.1" + "@docusaurus/plugin-content-blog" "2.4.1" + "@docusaurus/plugin-content-docs" "2.4.1" + "@docusaurus/plugin-content-pages" "2.4.1" + "@docusaurus/utils" "2.4.1" + "@docusaurus/utils-common" "2.4.1" + "@types/history" "^4.7.11" + "@types/react" "*" + "@types/react-router-config" "*" + clsx "^1.2.1" parse-numeric-range "^1.3.0" - prism-react-renderer "^1.3.1" + prism-react-renderer "^1.3.5" tslib "^2.4.0" + use-sync-external-store "^1.2.0" utility-types "^3.10.0" -"@docusaurus/theme-search-algolia@2.0.0-beta.20": - version "2.0.0-beta.20" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-2.0.0-beta.20.tgz#14f2ea376a87d7cfa4840d8c917d1ec62d3a07f7" - integrity sha512-9XAyiXXHgyhDmKXg9RUtnC4WBkYAZUqKT9Ntuk0OaOb4mBwiYUGL74tyP0LLL6T+oa9uEdXiUMlIL1onU8xhvA== - dependencies: - "@docsearch/react" "^3.0.0" - "@docusaurus/core" "2.0.0-beta.20" - "@docusaurus/logger" "2.0.0-beta.20" - "@docusaurus/plugin-content-docs" "2.0.0-beta.20" - "@docusaurus/theme-common" "2.0.0-beta.20" - "@docusaurus/theme-translations" "2.0.0-beta.20" - "@docusaurus/utils" "2.0.0-beta.20" - "@docusaurus/utils-validation" "2.0.0-beta.20" - algoliasearch "^4.13.0" - algoliasearch-helper "^3.8.2" - clsx "^1.1.1" - eta "^1.12.3" +"@docusaurus/theme-search-algolia@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-2.4.1.tgz#906bd2cca3fced0241985ef502c892f58ff380fc" + integrity sha512-6BcqW2lnLhZCXuMAvPRezFs1DpmEKzXFKlYjruuas+Xy3AQeFzDJKTJFIm49N77WFCTyxff8d3E4Q9pi/+5McQ== + dependencies: + "@docsearch/react" "^3.1.1" + "@docusaurus/core" "2.4.1" + "@docusaurus/logger" "2.4.1" + "@docusaurus/plugin-content-docs" "2.4.1" + "@docusaurus/theme-common" "2.4.1" + "@docusaurus/theme-translations" "2.4.1" + "@docusaurus/utils" "2.4.1" + "@docusaurus/utils-validation" "2.4.1" + algoliasearch "^4.13.1" + algoliasearch-helper "^3.10.0" + clsx "^1.2.1" + eta "^2.0.0" fs-extra "^10.1.0" lodash "^4.17.21" tslib "^2.4.0" utility-types "^3.10.0" -"@docusaurus/theme-translations@2.0.0-beta.20": - version "2.0.0-beta.20" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-translations/-/theme-translations-2.0.0-beta.20.tgz#dcc7efb43ff110c19736764c287f6c5128a5dbba" - integrity sha512-O7J/4dHcg7Yr+r3ylgtqmtMEz6d5ScpUxBg8nsNTWOCRoGEXNZVmXSd5l6v72KCyxPZpllPrgjmqkL+I19qWiw== +"@docusaurus/theme-translations@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-translations/-/theme-translations-2.4.1.tgz#4d49df5865dae9ef4b98a19284ede62ae6f98726" + integrity sha512-T1RAGP+f86CA1kfE8ejZ3T3pUU3XcyvrGMfC/zxCtc2BsnoexuNI9Vk2CmuKCb+Tacvhxjv5unhxXce0+NKyvA== dependencies: fs-extra "^10.1.0" tslib "^2.4.0" -"@docusaurus/types@2.0.0-beta.20": - version "2.0.0-beta.20" - resolved "https://registry.yarnpkg.com/@docusaurus/types/-/types-2.0.0-beta.20.tgz#069d40cc225141d5c9a85605a1c61a460814bf5d" - integrity sha512-d4ZIpcrzGsUUcZJL3iz8/iSaewobPPiYfn2Lmmv7GTT5ZPtPkOAtR5mE6+LAf/KpjjgqrC7mpwDKADnOL/ic4Q== +"@docusaurus/types@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/types/-/types-2.4.1.tgz#d8e82f9e0f704984f98df1f93d6b4554d5458705" + integrity sha512-0R+cbhpMkhbRXX138UOc/2XZFF8hiZa6ooZAEEJFp5scytzCw4tC1gChMFXrpa3d2tYE6AX8IrOEpSonLmfQuQ== dependencies: + "@types/history" "^4.7.11" + "@types/react" "*" commander "^5.1.0" - history "^4.9.0" joi "^17.6.0" react-helmet-async "^1.3.0" utility-types "^3.10.0" - webpack "^5.72.0" + webpack "^5.73.0" webpack-merge "^5.8.0" -"@docusaurus/utils-common@2.0.0-beta.20": - version "2.0.0-beta.20" - resolved "https://registry.yarnpkg.com/@docusaurus/utils-common/-/utils-common-2.0.0-beta.20.tgz#adb914c331d711a3c0ef2ba3f64139acdf4cd781" - integrity sha512-HabHh23vOQn6ygs0PjuCSF/oZaNsYTFsxB2R6EwHNyw01nWgBC3QAcGVmyIWQhlb9p8V3byKgbzVS68hZX5t9A== +"@docusaurus/utils-common@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/utils-common/-/utils-common-2.4.1.tgz#7f72e873e49bd5179588869cc3ab7449a56aae63" + integrity sha512-bCVGdZU+z/qVcIiEQdyx0K13OC5mYwxhSuDUR95oFbKVuXYRrTVrwZIqQljuo1fyJvFTKHiL9L9skQOPokuFNQ== dependencies: tslib "^2.4.0" -"@docusaurus/utils-validation@2.0.0-beta.20": - version "2.0.0-beta.20" - resolved "https://registry.yarnpkg.com/@docusaurus/utils-validation/-/utils-validation-2.0.0-beta.20.tgz#ebf475a4388066bd450877fe920f405c53ff5ad2" - integrity sha512-7MxMoaF4VNAt5vUwvITa6nbkw1tb4WE6hp1VlfIoLCY4D7Wk5cMf1ZFhppCP1UzmPwvFb9zw8fPuvDfB3Tb5nQ== +"@docusaurus/utils-validation@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/utils-validation/-/utils-validation-2.4.1.tgz#19959856d4a886af0c5cfb357f4ef68b51151244" + integrity sha512-unII3hlJlDwZ3w8U+pMO3Lx3RhI4YEbY3YNsQj4yzrkZzlpqZOLuAiZK2JyULnD+TKbceKU0WyWkQXtYbLNDFA== dependencies: - "@docusaurus/logger" "2.0.0-beta.20" - "@docusaurus/utils" "2.0.0-beta.20" + "@docusaurus/logger" "2.4.1" + "@docusaurus/utils" "2.4.1" joi "^17.6.0" js-yaml "^4.1.0" tslib "^2.4.0" -"@docusaurus/utils@2.0.0-beta.20": - version "2.0.0-beta.20" - resolved "https://registry.yarnpkg.com/@docusaurus/utils/-/utils-2.0.0-beta.20.tgz#d5a9816a328b2ca5e4e1a3fbf267e3674abacd48" - integrity sha512-eUQquakhrbnvhsmx8jRPLgoyjyzMuOhmQC99m7rotar7XOzROpgEpm7+xVaquG5Ha47WkybE3djHJhKNih7GZQ== +"@docusaurus/utils@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@docusaurus/utils/-/utils-2.4.1.tgz#9c5f76eae37b71f3819c1c1f0e26e6807c99a4fc" + integrity sha512-1lvEZdAQhKNht9aPXPoh69eeKnV0/62ROhQeFKKxmzd0zkcuE/Oc5Gpnt00y/f5bIsmOsYMY7Pqfm/5rteT5GA== dependencies: - "@docusaurus/logger" "2.0.0-beta.20" + "@docusaurus/logger" "2.4.1" "@svgr/webpack" "^6.2.1" + escape-string-regexp "^4.0.0" file-loader "^6.2.0" fs-extra "^10.1.0" github-slugger "^1.4.0" @@ -1554,7 +2608,7 @@ shelljs "^0.8.5" tslib "^2.4.0" url-loader "^4.1.1" - webpack "^5.72.0" + webpack "^5.73.0" "@hapi/hoek@^9.0.0": version "9.3.0" @@ -1568,6 +2622,25 @@ dependencies: "@hapi/hoek" "^9.0.0" +"@jest/schemas@^29.6.0": + version "29.6.0" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.0.tgz#0f4cb2c8e3dca80c135507ba5635a4fd755b0040" + integrity sha512-rxLjXyJBTL4LQeJW3aKo0M/+GkCOXsO+8i9Iu7eDb6KwtP65ayoDsitrdPBtujxQ88k4wI2FNYfa6TOGwSn6cQ== + dependencies: + "@sinclair/typebox" "^0.27.8" + +"@jest/types@^29.6.1": + version "29.6.1" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.1.tgz#ae79080278acff0a6af5eb49d063385aaa897bf2" + integrity sha512-tPKQNMPuXgvdOn2/Lg9HNfUvjYVGolt04Hp03f5hAk878uwOLikN+JzeLY0HcVgKgFl9Hs3EIqpu3WX27XNhnw== + dependencies: + "@jest/schemas" "^29.6.0" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + "@jridgewell/gen-mapping@^0.1.0": version "0.1.1" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996" @@ -1576,6 +2649,20 @@ "@jridgewell/set-array" "^1.0.0" "@jridgewell/sourcemap-codec" "^1.4.10" +"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" + integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + "@jridgewell/resolve-uri@^3.0.3": version "3.0.7" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.7.tgz#30cd49820a962aff48c8fffc5cd760151fca61fe" @@ -1586,11 +2673,37 @@ resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.1.tgz#36a6acc93987adcf0ba50c66908bd0b70de8afea" integrity sha512-Ct5MqZkLGEXTVmQYbGtx9SVqD2fqwvdubdps5D3djjAkgkKwT918VNOz65pEHFaYTeWcukmJmH5SwsA9Tn2ObQ== +"@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/source-map@^0.3.3": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.5.tgz#a3bb4d5c6825aab0d281268f47f6ad5853431e91" + integrity sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/sourcemap-codec@1.4.14": + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + "@jridgewell/sourcemap-codec@^1.4.10": version "1.4.13" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz#b6461fb0c2964356c469e115f504c95ad97ab88c" integrity sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w== +"@jridgewell/trace-mapping@^0.3.17": + version "0.3.18" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz#25783b2086daf6ff1dcb53c9249ae480e4dd4cd6" + integrity sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA== + dependencies: + "@jridgewell/resolve-uri" "3.1.0" + "@jridgewell/sourcemap-codec" "1.4.14" + "@jridgewell/trace-mapping@^0.3.9": version "0.3.10" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.10.tgz#db436f0917d655393851bc258918c00226c9b183" @@ -1682,20 +2795,24 @@ resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== +"@sinclair/typebox@^0.27.8": + version "0.27.8" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" + integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== + "@sindresorhus/is@^0.14.0": version "0.14.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== -"@slorber/static-site-generator-webpack-plugin@^4.0.4": - version "4.0.4" - resolved "https://registry.yarnpkg.com/@slorber/static-site-generator-webpack-plugin/-/static-site-generator-webpack-plugin-4.0.4.tgz#2bf4a2545e027830d2aa5eb950437c26a289b0f1" - integrity sha512-FvMavoWEIePps6/JwGCOLYKCRhuwIHhMtmbKpBFgzNkxwpa/569LfTkrbRk1m1I3n+ezJK4on9E1A6cjuZmD9g== +"@slorber/static-site-generator-webpack-plugin@^4.0.7": + version "4.0.7" + resolved "https://registry.yarnpkg.com/@slorber/static-site-generator-webpack-plugin/-/static-site-generator-webpack-plugin-4.0.7.tgz#fc1678bddefab014e2145cbe25b3ce4e1cfc36f3" + integrity sha512-Ug7x6z5lwrz0WqdnNFOMYrDQNTPAprvHLSh6+/fmml3qUiz6l5eq+2MzLKWtn/q5K5NpSiFsZTP/fck/3vjSxA== dependencies: - bluebird "^3.7.1" - cheerio "^0.22.0" eval "^0.1.8" - webpack-sources "^1.4.3" + p-map "^4.0.0" + webpack-sources "^3.2.2" "@svgr/babel-plugin-add-jsx-attribute@^5.4.0": version "5.4.0" @@ -1962,11 +3079,16 @@ "@types/estree" "*" "@types/json-schema" "*" -"@types/estree@*", "@types/estree@^0.0.51": +"@types/estree@*": version "0.0.51" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== +"@types/estree@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194" + integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA== + "@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.18": version "4.17.28" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz#c47def9f34ec81dc6328d0b1b5303d1ec98d86b8" @@ -2003,6 +3125,11 @@ resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#4fc33a00c1d0c16987b1a20cf92d20614c55ac35" integrity sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg== +"@types/http-errors@*": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.1.tgz#20172f9578b225f6c7da63446f56d4ce108d5a65" + integrity sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ== + "@types/http-proxy@^1.17.8": version "1.17.9" resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.9.tgz#7f0e7931343761efde1e2bf48c40f02f3f75705a" @@ -2010,6 +3137,25 @@ dependencies: "@types/node" "*" +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" + integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g== + +"@types/istanbul-lib-report@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686" + integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^3.0.0": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz#9153fe98bba2bd565a63add9436d6f0d7f8468ff" + integrity sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw== + dependencies: + "@types/istanbul-lib-report" "*" + "@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.11" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" @@ -2022,6 +3168,11 @@ dependencies: "@types/unist" "*" +"@types/mime@*": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" + integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA== + "@types/mime@^1": version "1.3.2" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" @@ -2071,6 +3222,15 @@ "@types/react" "*" "@types/react-router" "*" +"@types/react-router-config@^5.0.6": + version "5.0.7" + resolved "https://registry.yarnpkg.com/@types/react-router-config/-/react-router-config-5.0.7.tgz#36207a3fe08b271abee62b26993ee932d13cbb02" + integrity sha512-pFFVXUIydHlcJP6wJm7sDii5mD/bCmmAY0wQzq+M+uX7bqS95AQqHZWP1iNMKrWVQSuHIzj5qi9BvrtLX2/T4w== + dependencies: + "@types/history" "^4.7.11" + "@types/react" "*" + "@types/react-router" "^5.1.0" + "@types/react-router-dom@*": version "5.3.3" resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83" @@ -2088,6 +3248,14 @@ "@types/history" "^4.7.11" "@types/react" "*" +"@types/react-router@^5.1.0": + version "5.1.20" + resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.20.tgz#88eccaa122a82405ef3efbcaaa5dcdd9f021387c" + integrity sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q== + dependencies: + "@types/history" "^4.7.11" + "@types/react" "*" + "@types/react@*": version "18.0.9" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.9.tgz#d6712a38bd6cd83469603e7359511126f122e878" @@ -2129,6 +3297,15 @@ "@types/mime" "^1" "@types/node" "*" +"@types/serve-static@^1.13.10": + version "1.15.2" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.2.tgz#3e5419ecd1e40e7405d34093f10befb43f63381a" + integrity sha512-J2LqtvFYCzaj8pVYKw8klQXrLLk7TBZmQ4ShlcdkELFKGwGMfevMLneMMRkMgZxotOD9wg497LpC7O8PcvAmfw== + dependencies: + "@types/http-errors" "*" + "@types/mime" "*" + "@types/node" "*" + "@types/sockjs@^0.3.33": version "0.3.33" resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.33.tgz#570d3a0b99ac995360e3136fd6045113b1bd236f" @@ -2141,132 +3318,144 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== -"@types/ws@^8.5.1": - version "8.5.3" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.3.tgz#7d25a1ffbecd3c4f2d35068d0b283c037003274d" - integrity sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w== +"@types/ws@^8.5.5": + version "8.5.5" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.5.tgz#af587964aa06682702ee6dcbc7be41a80e4b28eb" + integrity sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg== dependencies: "@types/node" "*" -"@webassemblyjs/ast@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" - integrity sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw== +"@types/yargs-parser@*": + version "21.0.0" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" + integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== + +"@types/yargs@^17.0.8": + version "17.0.24" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.24.tgz#b3ef8d50ad4aa6aecf6ddc97c580a00f5aa11902" + integrity sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw== dependencies: - "@webassemblyjs/helper-numbers" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@types/yargs-parser" "*" -"@webassemblyjs/floating-point-hex-parser@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz#f6c61a705f0fd7a6aecaa4e8198f23d9dc179e4f" - integrity sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ== +"@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24" + integrity sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q== + dependencies: + "@webassemblyjs/helper-numbers" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + +"@webassemblyjs/floating-point-hex-parser@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431" + integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw== -"@webassemblyjs/helper-api-error@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz#1a63192d8788e5c012800ba6a7a46c705288fd16" - integrity sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg== +"@webassemblyjs/helper-api-error@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" + integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== -"@webassemblyjs/helper-buffer@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz#832a900eb444884cde9a7cad467f81500f5e5ab5" - integrity sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA== +"@webassemblyjs/helper-buffer@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz#b66d73c43e296fd5e88006f18524feb0f2c7c093" + integrity sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA== -"@webassemblyjs/helper-numbers@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz#64d81da219fbbba1e3bd1bfc74f6e8c4e10a62ae" - integrity sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ== +"@webassemblyjs/helper-numbers@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5" + integrity sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g== dependencies: - "@webassemblyjs/floating-point-hex-parser" "1.11.1" - "@webassemblyjs/helper-api-error" "1.11.1" + "@webassemblyjs/floating-point-hex-parser" "1.11.6" + "@webassemblyjs/helper-api-error" "1.11.6" "@xtuc/long" "4.2.2" -"@webassemblyjs/helper-wasm-bytecode@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz#f328241e41e7b199d0b20c18e88429c4433295e1" - integrity sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q== +"@webassemblyjs/helper-wasm-bytecode@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" + integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== -"@webassemblyjs/helper-wasm-section@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz#21ee065a7b635f319e738f0dd73bfbda281c097a" - integrity sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg== +"@webassemblyjs/helper-wasm-section@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz#ff97f3863c55ee7f580fd5c41a381e9def4aa577" + integrity sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g== dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-buffer" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/wasm-gen" "1.11.1" + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" -"@webassemblyjs/ieee754@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz#963929e9bbd05709e7e12243a099180812992614" - integrity sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ== +"@webassemblyjs/ieee754@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a" + integrity sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg== dependencies: "@xtuc/ieee754" "^1.2.0" -"@webassemblyjs/leb128@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.1.tgz#ce814b45574e93d76bae1fb2644ab9cdd9527aa5" - integrity sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw== +"@webassemblyjs/leb128@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7" + integrity sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ== dependencies: "@xtuc/long" "4.2.2" -"@webassemblyjs/utf8@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.1.tgz#d1f8b764369e7c6e6bae350e854dec9a59f0a3ff" - integrity sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ== - -"@webassemblyjs/wasm-edit@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz#ad206ebf4bf95a058ce9880a8c092c5dec8193d6" - integrity sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-buffer" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/helper-wasm-section" "1.11.1" - "@webassemblyjs/wasm-gen" "1.11.1" - "@webassemblyjs/wasm-opt" "1.11.1" - "@webassemblyjs/wasm-parser" "1.11.1" - "@webassemblyjs/wast-printer" "1.11.1" - -"@webassemblyjs/wasm-gen@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz#86c5ea304849759b7d88c47a32f4f039ae3c8f76" - integrity sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/ieee754" "1.11.1" - "@webassemblyjs/leb128" "1.11.1" - "@webassemblyjs/utf8" "1.11.1" - -"@webassemblyjs/wasm-opt@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz#657b4c2202f4cf3b345f8a4c6461c8c2418985f2" - integrity sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-buffer" "1.11.1" - "@webassemblyjs/wasm-gen" "1.11.1" - "@webassemblyjs/wasm-parser" "1.11.1" - -"@webassemblyjs/wasm-parser@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz#86ca734534f417e9bd3c67c7a1c75d8be41fb199" - integrity sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-api-error" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/ieee754" "1.11.1" - "@webassemblyjs/leb128" "1.11.1" - "@webassemblyjs/utf8" "1.11.1" - -"@webassemblyjs/wast-printer@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz#d0c73beda8eec5426f10ae8ef55cee5e7084c2f0" - integrity sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg== - dependencies: - "@webassemblyjs/ast" "1.11.1" +"@webassemblyjs/utf8@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" + integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== + +"@webassemblyjs/wasm-edit@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz#c72fa8220524c9b416249f3d94c2958dfe70ceab" + integrity sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/helper-wasm-section" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" + "@webassemblyjs/wasm-opt" "1.11.6" + "@webassemblyjs/wasm-parser" "1.11.6" + "@webassemblyjs/wast-printer" "1.11.6" + +"@webassemblyjs/wasm-gen@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz#fb5283e0e8b4551cc4e9c3c0d7184a65faf7c268" + integrity sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wasm-opt@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz#d9a22d651248422ca498b09aa3232a81041487c2" + integrity sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" + "@webassemblyjs/wasm-parser" "1.11.6" + +"@webassemblyjs/wasm-parser@1.11.6", "@webassemblyjs/wasm-parser@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz#bb85378c527df824004812bbdb784eea539174a1" + integrity sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-api-error" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wast-printer@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz#a7bf8dd7e362aeb1668ff43f35cb849f188eff20" + integrity sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A== + dependencies: + "@webassemblyjs/ast" "1.11.6" "@xtuc/long" "4.2.2" "@xtuc/ieee754@^1.2.0": @@ -2287,21 +3476,26 @@ accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: mime-types "~2.1.34" negotiator "0.6.3" -acorn-import-assertions@^1.7.6: - version "1.8.0" - resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9" - integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw== +acorn-import-assertions@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" + integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== acorn-walk@^8.0.0: version "8.2.0" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== -acorn@^8.0.4, acorn@^8.4.1, acorn@^8.5.0: +acorn@^8.0.4, acorn@^8.5.0: version "8.7.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30" integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== +acorn@^8.7.1, acorn@^8.8.2: + version "8.10.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" + integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== + address@^1.0.1, address@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/address/-/address-1.2.0.tgz#d352a62c92fee90f89a693eccd2a8b2139ab02d9" @@ -2354,14 +3548,14 @@ ajv@^8.0.0, ajv@^8.8.0: require-from-string "^2.0.2" uri-js "^4.2.2" -algoliasearch-helper@^3.8.2: - version "3.8.2" - resolved "https://registry.yarnpkg.com/algoliasearch-helper/-/algoliasearch-helper-3.8.2.tgz#35726dc6d211f49dbab0bf6d37b4658165539523" - integrity sha512-AXxiF0zT9oYwl8ZBgU/eRXvfYhz7cBA5YrLPlw9inZHdaYF0QEya/f1Zp1mPYMXc1v6VkHwBq4pk6/vayBLICg== +algoliasearch-helper@^3.10.0: + version "3.14.0" + resolved "https://registry.yarnpkg.com/algoliasearch-helper/-/algoliasearch-helper-3.14.0.tgz#2409c2591952719ab6fba1de77b3bbe5094ab85e" + integrity sha512-gXDXzsSS0YANn5dHr71CUXOo84cN4azhHKUbg71vAWnH+1JBiR4jf7to3t3JHXknXkbV0F7f055vUSBKrltHLQ== dependencies: "@algolia/events" "^4.0.1" -algoliasearch@^4.0.0, algoliasearch@^4.13.0: +algoliasearch@^4.0.0: version "4.13.0" resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-4.13.0.tgz#e36611fda82b1fc548c156ae7929a7f486e4b663" integrity sha512-oHv4faI1Vl2s+YC0YquwkK/TsaJs79g2JFg5FDm2rKN12VItPTAeQ7hyJMHarOPPYuCnNC5kixbtcqvb21wchw== @@ -2381,6 +3575,26 @@ algoliasearch@^4.0.0, algoliasearch@^4.13.0: "@algolia/requester-node-http" "4.13.0" "@algolia/transporter" "4.13.0" +algoliasearch@^4.13.1: + version "4.19.1" + resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-4.19.1.tgz#18111fb422eaf841737adb92d5ab12133d244218" + integrity sha512-IJF5b93b2MgAzcE/tuzW0yOPnuUyRgGAtaPv5UUywXM8kzqfdwZTO4sPJBzoGz1eOy6H9uEchsJsBFTELZSu+g== + dependencies: + "@algolia/cache-browser-local-storage" "4.19.1" + "@algolia/cache-common" "4.19.1" + "@algolia/cache-in-memory" "4.19.1" + "@algolia/client-account" "4.19.1" + "@algolia/client-analytics" "4.19.1" + "@algolia/client-common" "4.19.1" + "@algolia/client-personalization" "4.19.1" + "@algolia/client-search" "4.19.1" + "@algolia/logger-common" "4.19.1" + "@algolia/logger-console" "4.19.1" + "@algolia/requester-browser-xhr" "4.19.1" + "@algolia/requester-common" "4.19.1" + "@algolia/requester-node-http" "4.19.1" + "@algolia/transporter" "4.19.1" + ansi-align@^3.0.0, ansi-align@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59" @@ -2462,11 +3676,6 @@ array-union@^2.1.0: resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== -array-union@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-3.0.1.tgz#da52630d327f8b88cfbfb57728e2af5cd9b6b975" - integrity sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw== - asap@~2.0.3: version "2.0.6" resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" @@ -2477,13 +3686,20 @@ at-least-node@^1.0.0: resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== -autoprefixer@^10.3.7, autoprefixer@^10.4.5: - version "10.4.7" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.7.tgz#1db8d195f41a52ca5069b7593be167618edbbedf" - integrity sha512-ypHju4Y2Oav95SipEcCcI5J7CGPuvz8oat7sUtYj3ClK44bldfvtvcxK6IEK++7rqB7YchDGzweZIBG+SD0ZAA== +autocomplete.js@^0.38.0: + version "0.38.1" + resolved "https://registry.yarnpkg.com/autocomplete.js/-/autocomplete.js-0.38.1.tgz#9b006c985d996165ebbc62af33f5b4c32d209cc2" + integrity sha512-6pSJzuRMY3pqpozt+SXThl2DmJfma8Bi3SVFbZHS0PW/N72bOUv+Db0jAh2cWOhTsA4X+GNmKvIl8wExJTnN9w== dependencies: - browserslist "^4.20.3" - caniuse-lite "^1.0.30001335" + immediate "^3.2.3" + +autoprefixer@^10.4.12, autoprefixer@^10.4.7: + version "10.4.14" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.14.tgz#e28d49902f8e759dd25b153264e862df2705f79d" + integrity sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ== + dependencies: + browserslist "^4.21.5" + caniuse-lite "^1.0.30001464" fraction.js "^4.2.0" normalize-range "^0.1.2" picocolors "^1.0.0" @@ -2514,13 +3730,6 @@ babel-plugin-apply-mdx-type-prop@1.6.22: "@babel/helper-plugin-utils" "7.10.4" "@mdx-js/util" "1.6.22" -babel-plugin-dynamic-import-node@2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz#f00f507bdaa3c3e3ff6e7e5e98d90a7acab96f7f" - integrity sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ== - dependencies: - object.assign "^4.1.0" - babel-plugin-dynamic-import-node@^2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3" @@ -2544,6 +3753,15 @@ babel-plugin-polyfill-corejs2@^0.3.0: "@babel/helper-define-polyfill-provider" "^0.3.1" semver "^6.1.1" +babel-plugin-polyfill-corejs2@^0.4.4: + version "0.4.5" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.5.tgz#8097b4cb4af5b64a1d11332b6fb72ef5e64a054c" + integrity sha512-19hwUH5FKl49JEsvyTcoHakh6BE0wgXLLptIyKZ3PijHc/Ci521wygORCUCCred+E/twuqRyAkE02BAWPmsHOg== + dependencies: + "@babel/compat-data" "^7.22.6" + "@babel/helper-define-polyfill-provider" "^0.4.2" + semver "^6.3.1" + babel-plugin-polyfill-corejs3@^0.5.0: version "0.5.2" resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.2.tgz#aabe4b2fa04a6e038b688c5e55d44e78cd3a5f72" @@ -2552,6 +3770,14 @@ babel-plugin-polyfill-corejs3@^0.5.0: "@babel/helper-define-polyfill-provider" "^0.3.1" core-js-compat "^3.21.0" +babel-plugin-polyfill-corejs3@^0.8.2: + version "0.8.3" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.3.tgz#b4f719d0ad9bb8e0c23e3e630c0c8ec6dd7a1c52" + integrity sha512-z41XaniZL26WLrvjy7soabMXrfPWARN25PZoriDEiLMxAp50AUW3t35BGQUMg5xK3UrpVTtagIDklxYa+MhiNA== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.4.2" + core-js-compat "^3.31.0" + babel-plugin-polyfill-regenerator@^0.3.0: version "0.3.1" resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.1.tgz#2c0678ea47c75c8cc2fbb1852278d8fb68233990" @@ -2559,6 +3785,13 @@ babel-plugin-polyfill-regenerator@^0.3.0: dependencies: "@babel/helper-define-polyfill-provider" "^0.3.1" +babel-plugin-polyfill-regenerator@^0.5.1: + version "0.5.2" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.2.tgz#80d0f3e1098c080c8b5a65f41e9427af692dc326" + integrity sha512-tAlOptU0Xj34V1Y2PNTL4Y0FOJMDB6bZmoW39FeCQIhigGLkqu3Fj6uiXpxIf6Ij274ENdYx64y6Au+ZKlb1IA== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.4.2" + bail@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776" @@ -2589,11 +3822,6 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== -bluebird@^3.7.1: - version "3.7.2" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" - integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== - body-parser@1.20.0: version "1.20.0" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.0.tgz#3de69bd89011c11573d7bfee6a64f11b6bd27cc5" @@ -2670,7 +3898,7 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" -browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.16.6, browserslist@^4.18.1, browserslist@^4.20.2, browserslist@^4.20.3: +browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.18.1, browserslist@^4.20.2, browserslist@^4.20.3: version "4.20.3" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.20.3.tgz#eb7572f49ec430e054f56d52ff0ebe9be915f8bf" integrity sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg== @@ -2681,6 +3909,16 @@ browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.16.6, browserslist@^4 node-releases "^2.0.3" picocolors "^1.0.0" +browserslist@^4.21.4, browserslist@^4.21.5, browserslist@^4.21.9: + version "4.21.10" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.10.tgz#dbbac576628c13d3b2231332cb2ec5a46e015bb0" + integrity sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ== + dependencies: + caniuse-lite "^1.0.30001517" + electron-to-chromium "^1.4.477" + node-releases "^2.0.13" + update-browserslist-db "^1.0.11" + buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" @@ -2750,12 +3988,17 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001332, caniuse-lite@^1.0.30001335: +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001332: version "1.0.30001338" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001338.tgz#b5dd7a7941a51a16480bdf6ff82bded1628eec0d" integrity sha512-1gLHWyfVoRDsHieO+CaeYe7jSo/MT7D7lhaXUiwwbuR5BwQxORs0f1tAwUSQr3YbxRXJvxHM/PA5FfPQRnsPeQ== -ccount@^1.0.0, ccount@^1.0.3: +caniuse-lite@^1.0.30001464, caniuse-lite@^1.0.30001517: + version "1.0.30001518" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001518.tgz#b3ca93904cb4699c01218246c4d77a71dbe97150" + integrity sha512-rup09/e3I0BKjncL+FesTayKtPrdwKhUufQFd3riFw1hHg8JmIFoInYfB102cFcY/pPgGmdyl/iy+jgiDi2vdA== + +ccount@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.1.0.tgz#246687debb6014735131be8abab2d93898f8d043" integrity sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg== @@ -2769,7 +4012,7 @@ chalk@^2.0.0, chalk@^2.4.1: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.1.0, chalk@^4.1.2: +chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -2792,51 +4035,30 @@ character-reference-invalid@^1.0.0: resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560" integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg== -cheerio-select@^1.5.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-1.6.0.tgz#489f36604112c722afa147dedd0d4609c09e1696" - integrity sha512-eq0GdBvxVFbqWgmCm7M3XGs1I8oLy/nExUnh6oLqmBditPO9AqQJrkslDpMun/hZ0yyTs8L0m85OHp4ho6Qm9g== +cheerio-select@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-2.1.0.tgz#4d8673286b8126ca2a8e42740d5e3c4884ae21b4" + integrity sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g== dependencies: - css-select "^4.3.0" - css-what "^6.0.1" - domelementtype "^2.2.0" - domhandler "^4.3.1" - domutils "^2.8.0" - -cheerio@^0.22.0: - version "0.22.0" - resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-0.22.0.tgz#a9baa860a3f9b595a6b81b1a86873121ed3a269e" - integrity sha1-qbqoYKP5tZWmuBsahocxIe06Jp4= - dependencies: - css-select "~1.2.0" - dom-serializer "~0.1.0" - entities "~1.1.1" - htmlparser2 "^3.9.1" - lodash.assignin "^4.0.9" - lodash.bind "^4.1.4" - lodash.defaults "^4.0.1" - lodash.filter "^4.4.0" - lodash.flatten "^4.2.0" - lodash.foreach "^4.3.0" - lodash.map "^4.4.0" - lodash.merge "^4.4.0" - lodash.pick "^4.2.1" - lodash.reduce "^4.4.0" - lodash.reject "^4.4.0" - lodash.some "^4.4.0" - -cheerio@^1.0.0-rc.10: - version "1.0.0-rc.10" - resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.10.tgz#2ba3dcdfcc26e7956fc1f440e61d51c643379f3e" - integrity sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw== - dependencies: - cheerio-select "^1.5.0" - dom-serializer "^1.3.2" - domhandler "^4.2.0" - htmlparser2 "^6.1.0" - parse5 "^6.0.1" - parse5-htmlparser2-tree-adapter "^6.0.1" - tslib "^2.2.0" + boolbase "^1.0.0" + css-select "^5.1.0" + css-what "^6.1.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + domutils "^3.0.1" + +cheerio@^1.0.0-rc.12: + version "1.0.0-rc.12" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.12.tgz#788bf7466506b1c6bf5fae51d24a2c4d62e47683" + integrity sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q== + dependencies: + cheerio-select "^2.1.0" + dom-serializer "^2.0.0" + domhandler "^5.0.3" + domutils "^3.0.1" + htmlparser2 "^8.0.1" + parse5 "^7.0.0" + parse5-htmlparser2-tree-adapter "^7.0.0" chokidar@^3.4.2, chokidar@^3.5.3: version "3.5.3" @@ -2863,6 +4085,11 @@ ci-info@^2.0.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== +ci-info@^3.2.0: + version "3.8.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91" + integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw== + clean-css@^5.2.2, clean-css@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.0.tgz#ad3d8238d5f3549e83d5f87205189494bc7cbb59" @@ -2915,6 +4142,11 @@ clsx@^1.1.1: resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== +clsx@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" + integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== + coa@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3" @@ -3035,10 +4267,10 @@ configstore@^5.0.1: write-file-atomic "^3.0.0" xdg-basedir "^4.0.0" -connect-history-api-fallback@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" - integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== +connect-history-api-fallback@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" + integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== consola@^2.15.3: version "2.15.3" @@ -3084,14 +4316,14 @@ copy-text-to-clipboard@^3.0.1: resolved "https://registry.yarnpkg.com/copy-text-to-clipboard/-/copy-text-to-clipboard-3.0.1.tgz#8cbf8f90e0a47f12e4a24743736265d157bce69c" integrity sha512-rvVsHrpFcL4F2P8ihsoLdFHmd404+CMg71S756oRSeQgqk51U3kicGdnvfkrxva0xXH92SjGS62B0XIJsbh+9Q== -copy-webpack-plugin@^10.2.4: - version "10.2.4" - resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-10.2.4.tgz#6c854be3fdaae22025da34b9112ccf81c63308fe" - integrity sha512-xFVltahqlsRcyyJqQbDY6EYTtyQZF9rf+JPjwHObLdPFMEISqkFkr7mFoVOC6BfYS/dNThyoQKvziugm+OnwBg== +copy-webpack-plugin@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz#96d4dbdb5f73d02dd72d0528d1958721ab72e04a" + integrity sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ== dependencies: - fast-glob "^3.2.7" + fast-glob "^3.2.11" glob-parent "^6.0.1" - globby "^12.0.2" + globby "^13.1.1" normalize-path "^3.0.0" schema-utils "^4.0.0" serialize-javascript "^6.0.0" @@ -3104,15 +4336,22 @@ core-js-compat@^3.21.0, core-js-compat@^3.22.1: browserslist "^4.20.3" semver "7.0.0" -core-js-pure@^3.20.2: - version "3.22.4" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.22.4.tgz#a992210f4cad8b32786b8654563776c56b0e0d0a" - integrity sha512-4iF+QZkpzIz0prAFuepmxwJ2h5t4agvE8WPYqs2mjLJMNNwJOnpch76w2Q7bUfCPEv/V7wpvOfog0w273M+ZSw== +core-js-compat@^3.31.0: + version "3.32.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.32.0.tgz#f41574b6893ab15ddb0ac1693681bd56c8550a90" + integrity sha512-7a9a3D1k4UCVKnLhrgALyFcP7YCsLOQIxPd0dKjf/6GuPcgyiGP70ewWdCGrSK7evyhymi0qO4EqCmSJofDeYw== + dependencies: + browserslist "^4.21.9" -core-js@^3.22.3: - version "3.22.4" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.22.4.tgz#f4b3f108d45736935aa028444a69397e40d8c531" - integrity sha512-1uLykR+iOfYja+6Jn/57743gc9n73EWiOnSJJ4ba3B4fOEYDBv25MagmEZBxTp5cWq4b/KPx/l77zgsp28ju4w== +core-js-pure@^3.30.2: + version "3.32.0" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.32.0.tgz#5d79f85da7a4373e9a06494ccbef995a4c639f8b" + integrity sha512-qsev1H+dTNYpDUEURRuOXMvpdtAnNEvQWS/FMJ2Vb5AY8ZP4rAPQldkE27joykZPJTe0+IVgHZYh1P5Xu1/i1g== + +core-js@^3.23.3: + version "3.32.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.32.0.tgz#7643d353d899747ab1f8b03d2803b0312a0fb3b6" + integrity sha512-rd4rYZNlF3WuoYuRIDEmbR/ga9CeuWX9U05umAvgrrZoHY4Z++cp/xwPQMvUpBB4Ag6J8KfD80G0zwCyaSxDww== core-util-is@~1.0.0: version "1.0.3" @@ -3141,6 +4380,16 @@ cosmiconfig@^7.0.0, cosmiconfig@^7.0.1: path-type "^4.0.0" yaml "^1.10.0" +cosmiconfig@^8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.2.0.tgz#f7d17c56a590856cd1e7cee98734dca272b0d8fd" + integrity sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ== + dependencies: + import-fresh "^3.2.1" + js-yaml "^4.1.0" + parse-json "^5.0.0" + path-type "^4.0.0" + cross-fetch@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" @@ -3162,10 +4411,10 @@ crypto-random-string@^2.0.0: resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== -css-declaration-sorter@^6.2.2: - version "6.2.2" - resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.2.2.tgz#bfd2f6f50002d6a3ae779a87d3a0c5d5b10e0f02" - integrity sha512-Ufadglr88ZLsrvS11gjeu/40Lw74D9Am/Jpr3LlYm5Q4ZP5KdlUhG+6u2EjyXeZcxmZ2h1ebCKngDjolpeLHpg== +css-declaration-sorter@^6.3.1: + version "6.4.1" + resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz#28beac7c20bad7f1775be3a7129d7eae409a3a71" + integrity sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g== css-loader@^6.7.1: version "6.7.1" @@ -3181,14 +4430,14 @@ css-loader@^6.7.1: postcss-value-parser "^4.2.0" semver "^7.3.5" -css-minimizer-webpack-plugin@^3.4.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.4.1.tgz#ab78f781ced9181992fe7b6e4f3422e76429878f" - integrity sha512-1u6D71zeIfgngN2XNRJefc/hY7Ybsxd74Jm4qngIXyUEk7fss3VUzuHxLAq/R8NAba4QU9OUSaMZlbpRc7bM4Q== +css-minimizer-webpack-plugin@^4.0.0: + version "4.2.2" + resolved "https://registry.yarnpkg.com/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-4.2.2.tgz#79f6199eb5adf1ff7ba57f105e3752d15211eb35" + integrity sha512-s3Of/4jKfw1Hj9CxEO1E5oXhQAxlayuHO2y/ML+C6I9sQ7FdzfEV6QgMLN3vI+qFsjJGIAFLKtQK7t8BOXAIyA== dependencies: - cssnano "^5.0.6" - jest-worker "^27.0.2" - postcss "^8.3.5" + cssnano "^5.1.8" + jest-worker "^29.1.2" + postcss "^8.4.17" schema-utils "^4.0.0" serialize-javascript "^6.0.0" source-map "^0.6.1" @@ -3208,7 +4457,7 @@ css-select@^2.0.0: domutils "^1.7.0" nth-check "^1.0.2" -css-select@^4.1.3, css-select@^4.3.0: +css-select@^4.1.3: version "4.3.0" resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b" integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ== @@ -3219,15 +4468,16 @@ css-select@^4.1.3, css-select@^4.3.0: domutils "^2.8.0" nth-check "^2.0.1" -css-select@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858" - integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg= +css-select@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" + integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== dependencies: - boolbase "~1.0.0" - css-what "2.1" - domutils "1.5.1" - nth-check "~1.0.1" + boolbase "^1.0.0" + css-what "^6.1.0" + domhandler "^5.0.2" + domutils "^3.0.1" + nth-check "^2.0.1" css-tree@1.0.0-alpha.37: version "1.0.0-alpha.37" @@ -3245,17 +4495,12 @@ css-tree@^1.1.2, css-tree@^1.1.3: mdn-data "2.0.14" source-map "^0.6.1" -css-what@2.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2" - integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg== - css-what@^3.2.1: version "3.4.2" resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4" integrity sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ== -css-what@^6.0.1: +css-what@^6.0.1, css-what@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== @@ -3265,49 +4510,49 @@ cssesc@^3.0.0: resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== -cssnano-preset-advanced@^5.3.3: - version "5.3.3" - resolved "https://registry.yarnpkg.com/cssnano-preset-advanced/-/cssnano-preset-advanced-5.3.3.tgz#848422118d7a62b5b29a53edc160f58c7f7f7539" - integrity sha512-AB9SmTSC2Gd8T7PpKUsXFJ3eNsg7dc4CTZ0+XAJ29MNxyJsrCEk7N1lw31bpHrsQH2PVJr21bbWgGAfA9j0dIA== +cssnano-preset-advanced@^5.3.8: + version "5.3.10" + resolved "https://registry.yarnpkg.com/cssnano-preset-advanced/-/cssnano-preset-advanced-5.3.10.tgz#25558a1fbf3a871fb6429ce71e41be7f5aca6eef" + integrity sha512-fnYJyCS9jgMU+cmHO1rPSPf9axbQyD7iUhLO5Df6O4G+fKIOMps+ZbU0PdGFejFBBZ3Pftf18fn1eG7MAPUSWQ== dependencies: - autoprefixer "^10.3.7" - cssnano-preset-default "^5.2.7" + autoprefixer "^10.4.12" + cssnano-preset-default "^5.2.14" postcss-discard-unused "^5.1.0" postcss-merge-idents "^5.1.1" postcss-reduce-idents "^5.2.0" postcss-zindex "^5.1.0" -cssnano-preset-default@^5.2.7: - version "5.2.7" - resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-5.2.7.tgz#791e3603fb8f1b46717ac53b47e3c418e950f5f3" - integrity sha512-JiKP38ymZQK+zVKevphPzNSGHSlTI+AOwlasoSRtSVMUU285O7/6uZyd5NbW92ZHp41m0sSHe6JoZosakj63uA== +cssnano-preset-default@^5.2.14: + version "5.2.14" + resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-5.2.14.tgz#309def4f7b7e16d71ab2438052093330d9ab45d8" + integrity sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A== dependencies: - css-declaration-sorter "^6.2.2" + css-declaration-sorter "^6.3.1" cssnano-utils "^3.1.0" postcss-calc "^8.2.3" - postcss-colormin "^5.3.0" - postcss-convert-values "^5.1.0" - postcss-discard-comments "^5.1.1" + postcss-colormin "^5.3.1" + postcss-convert-values "^5.1.3" + postcss-discard-comments "^5.1.2" postcss-discard-duplicates "^5.1.0" postcss-discard-empty "^5.1.1" postcss-discard-overridden "^5.1.0" - postcss-merge-longhand "^5.1.4" - postcss-merge-rules "^5.1.1" + postcss-merge-longhand "^5.1.7" + postcss-merge-rules "^5.1.4" postcss-minify-font-values "^5.1.0" postcss-minify-gradients "^5.1.1" - postcss-minify-params "^5.1.2" - postcss-minify-selectors "^5.2.0" + postcss-minify-params "^5.1.4" + postcss-minify-selectors "^5.2.1" postcss-normalize-charset "^5.1.0" postcss-normalize-display-values "^5.1.0" - postcss-normalize-positions "^5.1.0" - postcss-normalize-repeat-style "^5.1.0" + postcss-normalize-positions "^5.1.1" + postcss-normalize-repeat-style "^5.1.1" postcss-normalize-string "^5.1.0" postcss-normalize-timing-functions "^5.1.0" - postcss-normalize-unicode "^5.1.0" + postcss-normalize-unicode "^5.1.1" postcss-normalize-url "^5.1.0" postcss-normalize-whitespace "^5.1.1" - postcss-ordered-values "^5.1.1" - postcss-reduce-initial "^5.1.0" + postcss-ordered-values "^5.1.3" + postcss-reduce-initial "^5.1.2" postcss-reduce-transforms "^5.1.0" postcss-svgo "^5.1.0" postcss-unique-selectors "^5.1.1" @@ -3317,12 +4562,12 @@ cssnano-utils@^3.1.0: resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-3.1.0.tgz#95684d08c91511edfc70d2636338ca37ef3a6861" integrity sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA== -cssnano@^5.0.6, cssnano@^5.1.7: - version "5.1.7" - resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-5.1.7.tgz#99858bef6c76c9240f0cdc9239570bc7db8368be" - integrity sha512-pVsUV6LcTXif7lvKKW9ZrmX+rGRzxkEdJuVJcp5ftUjWITgwam5LMZOgaTvUrWPkcORBey6he7JKb4XAJvrpKg== +cssnano@^5.1.12, cssnano@^5.1.8: + version "5.1.15" + resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-5.1.15.tgz#ded66b5480d5127fcb44dac12ea5a983755136bf" + integrity sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw== dependencies: - cssnano-preset-default "^5.2.7" + cssnano-preset-default "^5.2.14" lilconfig "^2.0.3" yaml "^1.10.2" @@ -3394,10 +4639,10 @@ define-properties@^1.1.3, define-properties@^1.1.4: has-property-descriptors "^1.0.0" object-keys "^1.1.1" -del@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/del/-/del-6.0.0.tgz#0b40d0332cea743f1614f818be4feb717714c952" - integrity sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ== +del@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/del/-/del-6.1.1.tgz#3b70314f1ec0aa325c6b14eb36b95786671edb7a" + integrity sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg== dependencies: globby "^11.0.1" graceful-fs "^4.2.4" @@ -3485,7 +4730,7 @@ dom-serializer@0: domelementtype "^2.0.1" entities "^2.0.0" -dom-serializer@^1.0.1, dom-serializer@^1.3.2: +dom-serializer@^1.0.1: version "1.4.1" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30" integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag== @@ -3494,31 +4739,25 @@ dom-serializer@^1.0.1, dom-serializer@^1.3.2: domhandler "^4.2.0" entities "^2.0.0" -dom-serializer@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0" - integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA== +dom-serializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" + integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== dependencies: - domelementtype "^1.3.0" - entities "^1.1.1" + domelementtype "^2.3.0" + domhandler "^5.0.2" + entities "^4.2.0" -domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1: +domelementtype@1: version "1.3.1" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== -domelementtype@^2.0.1, domelementtype@^2.2.0: +domelementtype@^2.0.1, domelementtype@^2.2.0, domelementtype@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== -domhandler@^2.3.0: - version "2.4.2" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" - integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA== - dependencies: - domelementtype "1" - domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" @@ -3526,15 +4765,14 @@ domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1: dependencies: domelementtype "^2.2.0" -domutils@1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" - integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8= +domhandler@^5.0.2, domhandler@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" + integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== dependencies: - dom-serializer "0" - domelementtype "1" + domelementtype "^2.3.0" -domutils@^1.5.1, domutils@^1.7.0: +domutils@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== @@ -3551,6 +4789,15 @@ domutils@^2.5.2, domutils@^2.8.0: domelementtype "^2.2.0" domhandler "^4.2.0" +domutils@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e" + integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== + dependencies: + dom-serializer "^2.0.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + dot-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" @@ -3591,6 +4838,11 @@ electron-to-chromium@^1.4.118: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.137.tgz#186180a45617283f1c012284458510cd99d6787f" integrity sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA== +electron-to-chromium@^1.4.477: + version "1.4.478" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.478.tgz#687198cfcef9b854a79a229feaa6cd8961206290" + integrity sha512-qjTA8djMXd+ruoODDFGnRCRBpID+AAfYWCyGtYTNhsuwxI19s8q19gbjKTwRS5z/LyVf5wICaIiPQGLekmbJbA== + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" @@ -3623,19 +4875,14 @@ end-of-stream@^1.1.0: dependencies: once "^1.4.0" -enhanced-resolve@^5.9.2: - version "5.9.3" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.9.3.tgz#44a342c012cbc473254af5cc6ae20ebd0aae5d88" - integrity sha512-Bq9VSor+kjvW3f9/MiiR4eE3XYgOl7/rS8lnSxbRbF3kS0B2r+Y9w5krBWxZgDxASVZbdYrn5wT4j/Wb0J9qow== +enhanced-resolve@^5.15.0: + version "5.15.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" + integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" -entities@^1.1.1, entities@~1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" - integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== - entities@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" @@ -3646,6 +4893,11 @@ entities@^3.0.1: resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4" integrity sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q== +entities@^4.2.0, entities@^4.4.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" @@ -3682,10 +4934,10 @@ es-abstract@^1.17.2, es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19 string.prototype.trimstart "^1.0.5" unbox-primitive "^1.0.2" -es-module-lexer@^0.9.0: - version "0.9.3" - resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" - integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== +es-module-lexer@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.3.0.tgz#6be9c9e0b4543a60cd166ff6f8b4e9dae0b0c16f" + integrity sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA== es-to-primitive@^1.2.1: version "1.2.1" @@ -3756,10 +5008,10 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -eta@^1.12.3: - version "1.12.3" - resolved "https://registry.yarnpkg.com/eta/-/eta-1.12.3.tgz#2982d08adfbef39f9fa50e2fbd42d7337e7338b1" - integrity sha512-qHixwbDLtekO/d51Yr4glcaUJCIjGVJyTzuqV4GPlgZo1YpgOKG+avQynErZIYrfM6JIJdtiG2Kox8tbb+DoGg== +eta@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/eta/-/eta-2.2.0.tgz#eb8b5f8c4e8b6306561a455e62cd7492fe3a9b8a" + integrity sha512-UVQ72Rqjy/ZKQalzV5dCCJP80GrmPrMxh6NlNf+erV6ObL0ZFkhCstWRawS85z3smdr3d2wXPsZEY7rDPfGd2g== etag@~1.8.1: version "1.8.1" @@ -3853,7 +5105,18 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.2.7, fast-glob@^3.2.9: +fast-glob@^3.2.11, fast-glob@^3.3.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" + integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-glob@^3.2.9: version "3.2.11" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew== @@ -4210,15 +5473,14 @@ globby@^11.0.1, globby@^11.0.4, globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" -globby@^12.0.2: - version "12.2.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-12.2.0.tgz#2ab8046b4fba4ff6eede835b29f678f90e3d3c22" - integrity sha512-wiSuFQLZ+urS9x2gGPl1H5drc5twabmm4m2gTR27XDFyjUHJUNsS8o/2aKyIF6IoBaR630atdher0XJ5g6OMmA== +globby@^13.1.1: + version "13.2.2" + resolved "https://registry.yarnpkg.com/globby/-/globby-13.2.2.tgz#63b90b1bf68619c2135475cbd4e71e66aa090592" + integrity sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w== dependencies: - array-union "^3.0.1" dir-glob "^3.0.1" - fast-glob "^3.2.7" - ignore "^5.1.9" + fast-glob "^3.3.0" + ignore "^5.2.4" merge2 "^1.4.1" slash "^4.0.0" @@ -4325,17 +5587,6 @@ hast-to-hyperscript@^9.0.0: unist-util-is "^4.0.0" web-namespaces "^1.0.0" -hast-util-from-parse5@^5.0.0: - version "5.0.3" - resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-5.0.3.tgz#3089dc0ee2ccf6ec8bc416919b51a54a589e097c" - integrity sha512-gOc8UB99F6eWVWFtM9jUikjN7QkWxB3nY0df5Z0Zq1/Nkwl5V4hAAsl0tmwlgWl/1shlTF8DnNYLO8X6wRV9pA== - dependencies: - ccount "^1.0.3" - hastscript "^5.0.0" - property-information "^5.0.0" - web-namespaces "^1.1.2" - xtend "^4.0.1" - hast-util-from-parse5@^6.0.0: version "6.0.1" resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-6.0.1.tgz#554e34abdeea25ac76f5bd950a1f0180e0b3bc2a" @@ -4380,16 +5631,6 @@ hast-util-to-parse5@^6.0.0: xtend "^4.0.0" zwitch "^1.0.0" -hastscript@^5.0.0: - version "5.1.2" - resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-5.1.2.tgz#bde2c2e56d04c62dd24e8c5df288d050a355fb8a" - integrity sha512-WlztFuK+Lrvi3EggsqOkQ52rKbxkXL3RwB6t5lwoa8QLMemoWfBuL43eDrwOamJyR7uKQKdmKYaBH1NZBiIRrQ== - dependencies: - comma-separated-tokens "^1.0.0" - hast-util-parse-selector "^2.0.0" - property-information "^5.0.0" - space-separated-tokens "^1.0.0" - hastscript@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-6.0.0.tgz#e8768d7eac56c3fdeac8a92830d58e811e5bf640" @@ -4474,18 +5715,6 @@ html-webpack-plugin@^5.5.0: pretty-error "^4.0.0" tapable "^2.0.0" -htmlparser2@^3.9.1: - version "3.10.1" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" - integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== - dependencies: - domelementtype "^1.3.1" - domhandler "^2.3.0" - domutils "^1.5.1" - entities "^1.1.1" - inherits "^2.0.1" - readable-stream "^3.1.1" - htmlparser2@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" @@ -4496,6 +5725,16 @@ htmlparser2@^6.1.0: domutils "^2.5.2" entities "^2.0.0" +htmlparser2@^8.0.1: + version "8.0.2" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21" + integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.3" + domutils "^3.0.1" + entities "^4.4.0" + http-cache-semantics@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" @@ -4569,11 +5808,16 @@ icss-utils@^5.0.0, icss-utils@^5.1.0: resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== -ignore@^5.1.9, ignore@^5.2.0: +ignore@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== +ignore@^5.2.4: + version "5.2.4" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" + integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== + image-size@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/image-size/-/image-size-1.0.1.tgz#86d6cfc2b1d19eab5d2b368d4b9194d9e48541c5" @@ -4581,6 +5825,11 @@ image-size@^1.0.1: dependencies: queue "6.0.2" +immediate@^3.2.3: + version "3.3.0" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.3.0.tgz#1aef225517836bcdf7f2a2de2600c79ff0269266" + integrity sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q== + immer@^9.0.7: version "9.0.12" resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.12.tgz#2d33ddf3ee1d247deab9d707ca472c8c942a0f20" @@ -4609,10 +5858,10 @@ indent-string@^4.0.0: resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== -infima@0.2.0-alpha.39: - version "0.2.0-alpha.39" - resolved "https://registry.yarnpkg.com/infima/-/infima-0.2.0-alpha.39.tgz#054b13ac44f3e9a42bc083988f1a1586add2f59c" - integrity sha512-UyYiwD3nwHakGhuOUfpe3baJ8gkiPpRVx4a4sE/Ag+932+Y6swtLsdPoRR8ezhwqGnduzxmFkjumV9roz6QoLw== +infima@0.2.0-alpha.43: + version "0.2.0-alpha.43" + resolved "https://registry.yarnpkg.com/infima/-/infima-0.2.0-alpha.43.tgz#f7aa1d7b30b6c08afef441c726bac6150228cbe0" + integrity sha512-2uw57LvUqW0rK/SWYnd/2rRfxNA5DDNOh33jxF7fy46VWoNhGxiUQyVZHbBMjQ33mQem0cjdDVwgWVAmlRfgyQ== inflight@^1.0.4: version "1.0.6" @@ -4951,7 +6200,19 @@ isobject@^3.0.1: resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= -jest-worker@^27.0.2, jest-worker@^27.4.5: +jest-util@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.6.2.tgz#8a052df8fff2eebe446769fd88814521a517664d" + integrity sha512-3eX1qb6L88lJNCFlEADKOkjpXJQyZRiavX1INZ4tRnrBVr2COd3RgcTLyUiEXMNBlDU/cgYq6taUS0fExrWW4w== + dependencies: + "@jest/types" "^29.6.1" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + +jest-worker@^27.4.5: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== @@ -4960,6 +6221,21 @@ jest-worker@^27.0.2, jest-worker@^27.4.5: merge-stream "^2.0.0" supports-color "^8.0.0" +jest-worker@^29.1.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.6.2.tgz#682fbc4b6856ad0aa122a5403c6d048b83f3fb44" + integrity sha512-l3ccBOabTdkng8I/ORCkADz4eSMKejTYv1vB/Z83UiubqhC1oQ5Li6dWCyqOIvSifGjUBxuvxvlm6KGK2DtuAQ== + dependencies: + "@types/node" "*" + jest-util "^29.6.2" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +jiti@^1.18.2: + version "1.19.1" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.19.1.tgz#fa99e4b76a23053e0e7cde098efe1704a14c16f1" + integrity sha512-oVhqoRDaBXf7sjkll95LHVS6Myyyb1zaunVwk4Z0+WPSW4gjS0pl01zYKHScTuyEhQsFxV5L4DR5r+YqSyqyyg== + joi@^17.6.0: version "17.6.0" resolved "https://registry.yarnpkg.com/joi/-/joi-17.6.0.tgz#0bb54f2f006c09a96e75ce687957bd04290054b2" @@ -5006,12 +6282,7 @@ json-buffer@3.0.0: resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg= -json-parse-better-errors@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" - integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== - -json-parse-even-better-errors@^2.3.0: +json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== @@ -5031,6 +6302,11 @@ json5@^2.1.2, json5@^2.2.1: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== +json5@^2.2.2: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + jsonfile@^6.0.1: version "6.1.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" @@ -5057,11 +6333,6 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== -klona@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.5.tgz#d166574d90076395d9963aa7a928fabb8d76afbc" - integrity sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ== - latest-version@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face" @@ -5069,6 +6340,14 @@ latest-version@^5.1.0: dependencies: package-json "^6.3.0" +launch-editor@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.6.0.tgz#4c0c1a6ac126c572bd9ff9a30da1d2cae66defd7" + integrity sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ== + dependencies: + picocolors "^1.0.0" + shell-quote "^1.7.3" + leven@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" @@ -5125,16 +6404,6 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" -lodash.assignin@^4.0.9: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.assignin/-/lodash.assignin-4.2.0.tgz#ba8df5fb841eb0a3e8044232b0e263a8dc6a28a2" - integrity sha1-uo31+4QesKPoBEIysOJjqNxqKKI= - -lodash.bind@^4.1.4: - version "4.2.1" - resolved "https://registry.yarnpkg.com/lodash.bind/-/lodash.bind-4.2.1.tgz#7ae3017e939622ac31b7d7d7dcb1b34db1690d35" - integrity sha1-euMBfpOWIqwxt9fX3LGzTbFpDTU= - lodash.curry@^4.0.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.curry/-/lodash.curry-4.1.1.tgz#248e36072ede906501d75966200a86dab8b23170" @@ -5145,66 +6414,16 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= -lodash.defaults@^4.0.1: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" - integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw= - -lodash.filter@^4.4.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/lodash.filter/-/lodash.filter-4.6.0.tgz#668b1d4981603ae1cc5a6fa760143e480b4c4ace" - integrity sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4= - -lodash.flatten@^4.2.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" - integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8= - lodash.flow@^3.3.0: version "3.5.0" resolved "https://registry.yarnpkg.com/lodash.flow/-/lodash.flow-3.5.0.tgz#87bf40292b8cf83e4e8ce1a3ae4209e20071675a" integrity sha1-h79AKSuM+D5OjOGjrkIJ4gBxZ1o= -lodash.foreach@^4.3.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53" - integrity sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM= - -lodash.map@^4.4.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3" - integrity sha1-dx7Hg540c9nEzeKLGTlMNWL09tM= - lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= -lodash.merge@^4.4.0: - version "4.6.2" - resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" - integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== - -lodash.pick@^4.2.1: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" - integrity sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM= - -lodash.reduce@^4.4.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/lodash.reduce/-/lodash.reduce-4.6.0.tgz#f1ab6b839299ad48f784abbf476596f03b914d3b" - integrity sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs= - -lodash.reject@^4.4.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/lodash.reject/-/lodash.reject-4.6.0.tgz#80d6492dc1470864bbf583533b651f42a9f52415" - integrity sha1-gNZJLcFHCGS79YNTO2UfQqn1JBU= - -lodash.some@^4.4.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d" - integrity sha1-G7nzFO9ri63tE7VJFpsqlF62jk0= - lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" @@ -5244,6 +6463,13 @@ lowercase-keys@^2.0.0: resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" @@ -5390,18 +6616,10 @@ mimic-response@^1.0.0, mimic-response@^1.0.1: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== -mini-create-react-context@^0.4.0: - version "0.4.1" - resolved "https://registry.yarnpkg.com/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz#072171561bfdc922da08a60c2197a497cc2d1d5e" - integrity sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ== - dependencies: - "@babel/runtime" "^7.12.1" - tiny-warning "^1.0.3" - -mini-css-extract-plugin@^2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.6.0.tgz#578aebc7fc14d32c0ad304c2c34f08af44673f5e" - integrity sha512-ndG8nxCEnAemsg4FSgS+yNyHKgkTB4nPKqCOgh65j3/30qqC5RaSQQXMm++Y6sb6E1zRSxPkztj9fqxhS1Eo6w== +mini-css-extract-plugin@^2.6.1: + version "2.7.6" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.6.tgz#282a3d38863fddcd2e0c220aaed5b90bc156564d" + integrity sha512-Qk7HcgaPkGG6eD77mLvZS1nmxlao3j+9PkrT9Uc7HAE1id3F41+DdBRYRYkbyfNRGzm8/YWtzhw7nVPmwhqTQw== dependencies: schema-utils "^4.0.0" @@ -5469,6 +6687,11 @@ nanoid@^3.3.3: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== +nanoid@^3.3.6: + version "3.3.6" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" + integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== + negotiator@0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" @@ -5506,6 +6729,11 @@ node-forge@^1: resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== +node-releases@^2.0.13: + version "2.0.13" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d" + integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ== + node-releases@^2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.4.tgz#f38252370c43854dc48aa431c766c6c398f40476" @@ -5543,7 +6771,7 @@ nprogress@^0.2.0: resolved "https://registry.yarnpkg.com/nprogress/-/nprogress-0.2.0.tgz#cb8f34c53213d895723fcbab907e9422adbcafb1" integrity sha1-y480xTIT2JVyP8urkH6UIq28r7E= -nth-check@^1.0.2, nth-check@~1.0.1: +nth-check@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== @@ -5757,23 +6985,26 @@ parse-numeric-range@^1.3.0: resolved "https://registry.yarnpkg.com/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz#7c63b61190d61e4d53a1197f0c83c47bb670ffa3" integrity sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ== -parse5-htmlparser2-tree-adapter@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6" - integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA== +parse5-htmlparser2-tree-adapter@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz#23c2cc233bcf09bb7beba8b8a69d46b08c62c2f1" + integrity sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g== dependencies: - parse5 "^6.0.1" - -parse5@^5.0.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178" - integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug== + domhandler "^5.0.2" + parse5 "^7.0.0" -parse5@^6.0.0, parse5@^6.0.1: +parse5@^6.0.0: version "6.0.1" resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== +parse5@^7.0.0: + version "7.1.2" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" + integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== + dependencies: + entities "^4.4.0" + parseurl@~1.3.2, parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" @@ -5844,7 +7075,7 @@ picocolors@^1.0.0: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== @@ -5871,27 +7102,28 @@ postcss-calc@^8.2.3: postcss-selector-parser "^6.0.9" postcss-value-parser "^4.2.0" -postcss-colormin@^5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-5.3.0.tgz#3cee9e5ca62b2c27e84fce63affc0cfb5901956a" - integrity sha512-WdDO4gOFG2Z8n4P8TWBpshnL3JpmNmJwdnfP2gbk2qBA8PWwOYcmjmI/t3CmMeL72a7Hkd+x/Mg9O2/0rD54Pg== +postcss-colormin@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-5.3.1.tgz#86c27c26ed6ba00d96c79e08f3ffb418d1d1988f" + integrity sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ== dependencies: - browserslist "^4.16.6" + browserslist "^4.21.4" caniuse-api "^3.0.0" colord "^2.9.1" postcss-value-parser "^4.2.0" -postcss-convert-values@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-5.1.0.tgz#f8d3abe40b4ce4b1470702a0706343eac17e7c10" - integrity sha512-GkyPbZEYJiWtQB0KZ0X6qusqFHUepguBCNFi9t5JJc7I2OTXG7C0twbTLvCfaKOLl3rSXmpAwV7W5txd91V84g== +postcss-convert-values@^5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz#04998bb9ba6b65aa31035d669a6af342c5f9d393" + integrity sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA== dependencies: + browserslist "^4.21.4" postcss-value-parser "^4.2.0" -postcss-discard-comments@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-5.1.1.tgz#e90019e1a0e5b99de05f63516ce640bd0df3d369" - integrity sha512-5JscyFmvkUxz/5/+TB3QTTT9Gi9jHkcn8dcmmuN68JQcv3aQg4y88yEHHhwFB52l/NkaJ43O0dbksGMAo49nfQ== +postcss-discard-comments@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz#8df5e81d2925af2780075840c1526f0660e53696" + integrity sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ== postcss-discard-duplicates@^5.1.0: version "5.1.0" @@ -5915,14 +7147,14 @@ postcss-discard-unused@^5.1.0: dependencies: postcss-selector-parser "^6.0.5" -postcss-loader@^6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-6.2.1.tgz#0895f7346b1702103d30fdc66e4d494a93c008ef" - integrity sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q== +postcss-loader@^7.0.0: + version "7.3.3" + resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-7.3.3.tgz#6da03e71a918ef49df1bb4be4c80401df8e249dd" + integrity sha512-YgO/yhtevGO/vJePCQmTxiaEwER94LABZN0ZMT4A0vsak9TpO+RvKRs7EmJ8peIlB9xfXCsS7M8LjqncsUZ5HA== dependencies: - cosmiconfig "^7.0.0" - klona "^2.0.5" - semver "^7.3.5" + cosmiconfig "^8.2.0" + jiti "^1.18.2" + semver "^7.3.8" postcss-merge-idents@^5.1.1: version "5.1.1" @@ -5932,20 +7164,20 @@ postcss-merge-idents@^5.1.1: cssnano-utils "^3.1.0" postcss-value-parser "^4.2.0" -postcss-merge-longhand@^5.1.4: - version "5.1.4" - resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-5.1.4.tgz#0f46f8753989a33260efc47de9a0cdc571f2ec5c" - integrity sha512-hbqRRqYfmXoGpzYKeW0/NCZhvNyQIlQeWVSao5iKWdyx7skLvCfQFGIUsP9NUs3dSbPac2IC4Go85/zG+7MlmA== +postcss-merge-longhand@^5.1.7: + version "5.1.7" + resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz#24a1bdf402d9ef0e70f568f39bdc0344d568fb16" + integrity sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ== dependencies: postcss-value-parser "^4.2.0" - stylehacks "^5.1.0" + stylehacks "^5.1.1" -postcss-merge-rules@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-5.1.1.tgz#d327b221cd07540bcc8d9ff84446d8b404d00162" - integrity sha512-8wv8q2cXjEuCcgpIB1Xx1pIy8/rhMPIQqYKNzEdyx37m6gpq83mQQdCxgIkFgliyEnKvdwJf/C61vN4tQDq4Ww== +postcss-merge-rules@^5.1.4: + version "5.1.4" + resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-5.1.4.tgz#2f26fa5cacb75b1402e213789f6766ae5e40313c" + integrity sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g== dependencies: - browserslist "^4.16.6" + browserslist "^4.21.4" caniuse-api "^3.0.0" cssnano-utils "^3.1.0" postcss-selector-parser "^6.0.5" @@ -5966,19 +7198,19 @@ postcss-minify-gradients@^5.1.1: cssnano-utils "^3.1.0" postcss-value-parser "^4.2.0" -postcss-minify-params@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-5.1.2.tgz#77e250780c64198289c954884ebe3ee4481c3b1c" - integrity sha512-aEP+p71S/urY48HWaRHasyx4WHQJyOYaKpQ6eXl8k0kxg66Wt/30VR6/woh8THgcpRbonJD5IeD+CzNhPi1L8g== +postcss-minify-params@^5.1.4: + version "5.1.4" + resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz#c06a6c787128b3208b38c9364cfc40c8aa5d7352" + integrity sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw== dependencies: - browserslist "^4.16.6" + browserslist "^4.21.4" cssnano-utils "^3.1.0" postcss-value-parser "^4.2.0" -postcss-minify-selectors@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-5.2.0.tgz#17c2be233e12b28ffa8a421a02fc8b839825536c" - integrity sha512-vYxvHkW+iULstA+ctVNx0VoRAR4THQQRkG77o0oa4/mBS0OzGvvzLIvHDv/nNEM0crzN2WIyFU5X7wZhaUK3RA== +postcss-minify-selectors@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz#d4e7e6b46147b8117ea9325a915a801d5fe656c6" + integrity sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg== dependencies: postcss-selector-parser "^6.0.5" @@ -6022,17 +7254,17 @@ postcss-normalize-display-values@^5.1.0: dependencies: postcss-value-parser "^4.2.0" -postcss-normalize-positions@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-5.1.0.tgz#902a7cb97cf0b9e8b1b654d4a43d451e48966458" - integrity sha512-8gmItgA4H5xiUxgN/3TVvXRoJxkAWLW6f/KKhdsH03atg0cB8ilXnrB5PpSshwVu/dD2ZsRFQcR1OEmSBDAgcQ== +postcss-normalize-positions@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz#ef97279d894087b59325b45c47f1e863daefbb92" + integrity sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg== dependencies: postcss-value-parser "^4.2.0" -postcss-normalize-repeat-style@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.0.tgz#f6d6fd5a54f51a741cc84a37f7459e60ef7a6398" - integrity sha512-IR3uBjc+7mcWGL6CtniKNQ4Rr5fTxwkaDHwMBDGGs1x9IVRkYIT/M4NelZWkAOBdV6v3Z9S46zqaKGlyzHSchw== +postcss-normalize-repeat-style@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz#e9eb96805204f4766df66fd09ed2e13545420fb2" + integrity sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g== dependencies: postcss-value-parser "^4.2.0" @@ -6050,12 +7282,12 @@ postcss-normalize-timing-functions@^5.1.0: dependencies: postcss-value-parser "^4.2.0" -postcss-normalize-unicode@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.0.tgz#3d23aede35e160089a285e27bf715de11dc9db75" - integrity sha512-J6M3MizAAZ2dOdSjy2caayJLQT8E8K9XjLce8AUQMwOrCvjCHv24aLC/Lps1R1ylOfol5VIDMaM/Lo9NGlk1SQ== +postcss-normalize-unicode@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz#f67297fca3fea7f17e0d2caa40769afc487aa030" + integrity sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA== dependencies: - browserslist "^4.16.6" + browserslist "^4.21.4" postcss-value-parser "^4.2.0" postcss-normalize-url@^5.1.0: @@ -6073,10 +7305,10 @@ postcss-normalize-whitespace@^5.1.1: dependencies: postcss-value-parser "^4.2.0" -postcss-ordered-values@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-5.1.1.tgz#0b41b610ba02906a3341e92cab01ff8ebc598adb" - integrity sha512-7lxgXF0NaoMIgyihL/2boNAEZKiW0+HkMhdKMTD93CjW8TdCy2hSdj8lsAo+uwm7EDG16Da2Jdmtqpedl0cMfw== +postcss-ordered-values@^5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz#b6fd2bd10f937b23d86bc829c69e7732ce76ea38" + integrity sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ== dependencies: cssnano-utils "^3.1.0" postcss-value-parser "^4.2.0" @@ -6088,12 +7320,12 @@ postcss-reduce-idents@^5.2.0: dependencies: postcss-value-parser "^4.2.0" -postcss-reduce-initial@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-5.1.0.tgz#fc31659ea6e85c492fb2a7b545370c215822c5d6" - integrity sha512-5OgTUviz0aeH6MtBjHfbr57tml13PuedK/Ecg8szzd4XRMbYxH4572JFG067z+FqBIf6Zp/d+0581glkvvWMFw== +postcss-reduce-initial@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz#798cd77b3e033eae7105c18c9d371d989e1382d6" + integrity sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg== dependencies: - browserslist "^4.16.6" + browserslist "^4.21.4" caniuse-api "^3.0.0" postcss-reduce-transforms@^5.1.0: @@ -6143,7 +7375,7 @@ postcss-zindex@^5.1.0: resolved "https://registry.yarnpkg.com/postcss-zindex/-/postcss-zindex-5.1.0.tgz#4a5c7e5ff1050bd4c01d95b1847dfdcc58a496ff" integrity sha512-fgFMf0OtVSBR1va1JNHYgMxYk73yhn/qb4uQDq1DLGYolz8gHCyr/sesEuGUaYs58E3ZJRcpoGuPVoB7Meiq9A== -postcss@^8.3.11, postcss@^8.3.5, postcss@^8.4.13, postcss@^8.4.7: +postcss@^8.3.11, postcss@^8.4.7: version "8.4.13" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.13.tgz#7c87bc268e79f7f86524235821dfdf9f73e5d575" integrity sha512-jtL6eTBrza5MPzy8oJLFuUscHDXTV5KcLlqAWHl5q5WYRfnNRGSmOZmOZ1T6Gy7A99mOZfqungmZMpMmCVJ8ZA== @@ -6152,6 +7384,15 @@ postcss@^8.3.11, postcss@^8.3.5, postcss@^8.4.13, postcss@^8.4.7: picocolors "^1.0.0" source-map-js "^1.0.2" +postcss@^8.4.14, postcss@^8.4.17: + version "8.4.27" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.27.tgz#234d7e4b72e34ba5a92c29636734349e0d9c3057" + integrity sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ== + dependencies: + nanoid "^3.3.6" + picocolors "^1.0.0" + source-map-js "^1.0.2" + prepend-http@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" @@ -6170,11 +7411,16 @@ pretty-time@^1.1.0: resolved "https://registry.yarnpkg.com/pretty-time/-/pretty-time-1.1.0.tgz#ffb7429afabb8535c346a34e41873adf3d74dd0e" integrity sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA== -prism-react-renderer@^1.2.1, prism-react-renderer@^1.3.1: +prism-react-renderer@^1.2.1: version "1.3.1" resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-1.3.1.tgz#88fc9d0df6bed06ca2b9097421349f8c2f24e30d" integrity sha512-xUeDMEz074d0zc5y6rxiMp/dlC7C+5IDDlaEUlcBOFE2wddz7hz5PNupb087mPwTt7T9BrFmewObfCBuf/LKwQ== +prism-react-renderer@^1.3.5: + version "1.3.5" + resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-1.3.5.tgz#786bb69aa6f73c32ba1ee813fbe17a0115435085" + integrity sha512-IJ+MSwBWKG+SM3b2SUfdrhC+gu01QkV2KmRQgREThBfSQRoufqRfxfHUxpG1WcaFjP+kojcFyO9Qqtpgt3qLCg== + prismjs@^1.28.0: version "1.28.0" resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.28.0.tgz#0d8f561fa0f7cf6ebca901747828b149147044b6" @@ -6364,6 +7610,13 @@ react-dom@^17.0.1: object-assign "^4.1.1" scheduler "^0.20.2" +react-error-boundary@^3.1.0: + version "3.1.4" + resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.4.tgz#255db92b23197108757a888b01e5b729919abde0" + integrity sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA== + dependencies: + "@babel/runtime" "^7.12.5" + react-error-overlay@^6.0.11: version "6.0.11" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb" @@ -6419,29 +7672,28 @@ react-router-config@^5.1.1: dependencies: "@babel/runtime" "^7.1.2" -react-router-dom@^5.2.0: - version "5.3.1" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.3.1.tgz#0151baf2365c5fcd8493f6ec9b9b31f34d0f8ae1" - integrity sha512-f0pj/gMAbv9e8gahTmCEY20oFhxhrmHwYeIwH5EO5xu0qme+wXtsdB8YfUOAZzUz4VaXmb58m3ceiLtjMhqYmQ== +react-router-dom@^5.3.3: + version "5.3.4" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.3.4.tgz#2ed62ffd88cae6db134445f4a0c0ae8b91d2e5e6" + integrity sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ== dependencies: "@babel/runtime" "^7.12.13" history "^4.9.0" loose-envify "^1.3.1" prop-types "^15.6.2" - react-router "5.3.1" + react-router "5.3.4" tiny-invariant "^1.0.2" tiny-warning "^1.0.0" -react-router@5.3.1, react-router@^5.2.0: - version "5.3.1" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.3.1.tgz#b13e84a016c79b9e80dde123ca4112c4f117e3cf" - integrity sha512-v+zwjqb7bakqgF+wMVKlAPTca/cEmPOvQ9zt7gpSNyPXau1+0qvuYZ5BWzzNDP1y6s15zDwgb9rPN63+SIniRQ== +react-router@5.3.4, react-router@^5.3.3: + version "5.3.4" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.3.4.tgz#8ca252d70fcc37841e31473c7a151cf777887bb5" + integrity sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA== dependencies: "@babel/runtime" "^7.12.13" history "^4.9.0" hoist-non-react-statics "^3.1.0" loose-envify "^1.3.1" - mini-create-react-context "^0.4.0" path-to-regexp "^1.7.0" prop-types "^15.6.2" react-is "^16.6.0" @@ -6478,7 +7730,7 @@ readable-stream@^2.0.1: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.0.6, readable-stream@^3.1.1: +readable-stream@^3.0.6: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -6520,11 +7772,23 @@ regenerate-unicode-properties@^10.0.1: dependencies: regenerate "^1.4.2" +regenerate-unicode-properties@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c" + integrity sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ== + dependencies: + regenerate "^1.4.2" + regenerate@^1.4.2: version "1.4.2" resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== +regenerator-runtime@^0.13.11: + version "0.13.11" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" + integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== + regenerator-runtime@^0.13.4: version "0.13.9" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" @@ -6537,6 +7801,13 @@ regenerator-transform@^0.15.0: dependencies: "@babel/runtime" "^7.8.4" +regenerator-transform@^0.15.1: + version "0.15.1" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.1.tgz#f6c4e99fc1b4591f780db2586328e4d9a9d8dc56" + integrity sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg== + dependencies: + "@babel/runtime" "^7.8.4" + regexp.prototype.flags@^1.4.1: version "1.4.3" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" @@ -6558,6 +7829,18 @@ regexpu-core@^5.0.1: unicode-match-property-ecmascript "^2.0.0" unicode-match-property-value-ecmascript "^2.0.0" +regexpu-core@^5.3.1: + version "5.3.2" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.2.tgz#11a2b06884f3527aec3e93dbbf4a3b958a95546b" + integrity sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ== + dependencies: + "@babel/regjsgen" "^0.8.0" + regenerate "^1.4.2" + regenerate-unicode-properties "^10.1.0" + regjsparser "^0.9.1" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.1.0" + registry-auth-token@^4.0.0: version "4.2.1" resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.2.1.tgz#6d7b4006441918972ccd5fedcd41dc322c79b250" @@ -6584,29 +7867,18 @@ regjsparser@^0.8.2: dependencies: jsesc "~0.5.0" -rehype-parse@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/rehype-parse/-/rehype-parse-6.0.2.tgz#aeb3fdd68085f9f796f1d3137ae2b85a98406964" - integrity sha512-0S3CpvpTAgGmnz8kiCyFLGuW5yA4OQhyNTm/nwPopZ7+PI11WnGl1TTWTGv/2hPEe/g2jRLlhVVSsoDH8waRug== +regjsparser@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.9.1.tgz#272d05aa10c7c1f67095b1ff0addae8442fc5709" + integrity sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ== dependencies: - hast-util-from-parse5 "^5.0.0" - parse5 "^5.0.0" - xtend "^4.0.0" + jsesc "~0.5.0" relateurl@^0.2.7: version "0.2.7" resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk= -remark-admonitions@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/remark-admonitions/-/remark-admonitions-1.2.1.tgz#87caa1a442aa7b4c0cafa04798ed58a342307870" - integrity sha512-Ji6p68VDvD+H1oS95Fdx9Ar5WA2wcDA4kwrrhVU7fGctC6+d3uiMICu7w7/2Xld+lnU7/gi+432+rRbup5S8ow== - dependencies: - rehype-parse "^6.0.2" - unified "^8.4.2" - unist-util-visit "^2.0.1" - remark-emoji@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/remark-emoji/-/remark-emoji-2.2.0.tgz#1c702090a1525da5b80e15a8f963ef2c8236cac7" @@ -6813,7 +8085,7 @@ schema-utils@^2.6.5: ajv "^6.12.4" ajv-keywords "^3.5.2" -schema-utils@^3.0.0, schema-utils@^3.1.0, schema-utils@^3.1.1: +schema-utils@^3.0.0, schema-utils@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281" integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== @@ -6822,6 +8094,15 @@ schema-utils@^3.0.0, schema-utils@^3.1.0, schema-utils@^3.1.1: ajv "^6.12.5" ajv-keywords "^3.5.2" +schema-utils@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" + integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + schema-utils@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.0.0.tgz#60331e9e3ae78ec5d16353c467c34b3a0a1d3df7" @@ -6845,10 +8126,10 @@ select-hose@^2.0.0: resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= -selfsigned@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.0.1.tgz#8b2df7fa56bf014d19b6007655fff209c0ef0a56" - integrity sha512-LmME957M1zOsUhG+67rAjKfiWFox3SBxE/yymatMZsAx+oMrJ0YQ8AToOnyCm7xbeg2ep37IHLxdu0o2MavQOQ== +selfsigned@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.1.1.tgz#18a7613d714c0cd3385c48af0075abf3f266af61" + integrity sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ== dependencies: node-forge "^1" @@ -6874,6 +8155,11 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7: version "7.3.7" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" @@ -6881,6 +8167,13 @@ semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7: dependencies: lru-cache "^6.0.0" +semver@^7.3.8: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== + dependencies: + lru-cache "^6.0.0" + send@0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" @@ -6907,6 +8200,13 @@ serialize-javascript@^6.0.0: dependencies: randombytes "^2.1.0" +serialize-javascript@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz#b206efb27c3da0b0ab6b52f48d170b7996458e5c" + integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w== + dependencies: + randombytes "^2.1.0" + serve-handler@^6.1.3: version "6.1.3" resolved "https://registry.yarnpkg.com/serve-handler/-/serve-handler-6.1.3.tgz#1bf8c5ae138712af55c758477533b9117f6435e8" @@ -7045,7 +8345,7 @@ slash@^4.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7" integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== -sockjs@^0.3.21: +sockjs@^0.3.24: version "0.3.24" resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" integrity sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ== @@ -7059,11 +8359,6 @@ sort-css-media-queries@2.0.4: resolved "https://registry.yarnpkg.com/sort-css-media-queries/-/sort-css-media-queries-2.0.4.tgz#b2badfa519cb4a938acbc6d3aaa913d4949dc908" integrity sha512-PAIsEK/XupCQwitjv7XxoMvYhT7EAfyzI3hsy/MyDgTvc+Ft55ctdkctJLOy6cQejaIC+zjpUL4djFVm2ivOOw== -source-list-map@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" - integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== - source-map-js@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" @@ -7082,7 +8377,7 @@ source-map@^0.5.0: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= -source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== @@ -7252,12 +8547,12 @@ style-to-object@0.3.0, style-to-object@^0.3.0: dependencies: inline-style-parser "0.1.1" -stylehacks@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-5.1.0.tgz#a40066490ca0caca04e96c6b02153ddc39913520" - integrity sha512-SzLmvHQTrIWfSgljkQCw2++C9+Ne91d/6Sp92I8c5uHTcy/PgeHamwITIbBW9wnFTY/3ZfSXR9HIL6Ikqmcu6Q== +stylehacks@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-5.1.1.tgz#7934a34eb59d7152149fa69d6e9e56f2fc34bcc9" + integrity sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw== dependencies: - browserslist "^4.16.6" + browserslist "^4.21.4" postcss-selector-parser "^6.0.4" supports-color@^5.3.0: @@ -7333,18 +8628,18 @@ tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0: resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== -terser-webpack-plugin@^5.1.3, terser-webpack-plugin@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.1.tgz#0320dcc270ad5372c1e8993fabbd927929773e54" - integrity sha512-GvlZdT6wPQKbDNW/GDQzZFg/j4vKU96yl2q6mcUkzKOgW4gwf1Z8cZToUCrz31XHlPWH8MVb1r2tFtdDtTGJ7g== +terser-webpack-plugin@^5.3.3, terser-webpack-plugin@^5.3.7: + version "5.3.9" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz#832536999c51b46d468067f9e37662a3b96adfe1" + integrity sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA== dependencies: + "@jridgewell/trace-mapping" "^0.3.17" jest-worker "^27.4.5" schema-utils "^3.1.1" - serialize-javascript "^6.0.0" - source-map "^0.6.1" - terser "^5.7.2" + serialize-javascript "^6.0.1" + terser "^5.16.8" -terser@^5.10.0, terser@^5.7.2: +terser@^5.10.0: version "5.13.1" resolved "https://registry.yarnpkg.com/terser/-/terser-5.13.1.tgz#66332cdc5a01b04a224c9fad449fc1a18eaa1799" integrity sha512-hn4WKOfwnwbYfe48NgrQjqNOH9jzLqRcIfbYytOXCOv46LBfWr9bDS17MQqOi+BWGD0sJK3Sj5NC/gJjiojaoA== @@ -7354,6 +8649,16 @@ terser@^5.10.0, terser@^5.7.2: source-map "~0.8.0-beta.0" source-map-support "~0.5.20" +terser@^5.16.8: + version "5.19.2" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.19.2.tgz#bdb8017a9a4a8de4663a7983f45c506534f9234e" + integrity sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" + commander "^2.20.0" + source-map-support "~0.5.20" + text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -7369,7 +8674,7 @@ tiny-invariant@^1.0.2: resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.2.0.tgz#a1141f86b672a9148c72e978a19a73b9b94a15a9" integrity sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg== -tiny-warning@^1.0.0, tiny-warning@^1.0.3: +tiny-warning@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== @@ -7428,7 +8733,7 @@ trough@^1.0.0: resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406" integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA== -tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.4.0: +tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== @@ -7499,6 +8804,11 @@ unicode-match-property-value-ecmascript@^2.0.0: resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.0.0.tgz#1a01aa57247c14c568b89775a54938788189a714" integrity sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw== +unicode-match-property-value-ecmascript@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz#cb5fffdcd16a05124f5a4b0bf7c3770208acbbe0" + integrity sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA== + unicode-property-aliases-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz#0a36cb9a585c4f6abd51ad1deddb285c165297c8" @@ -7516,13 +8826,14 @@ unified@9.2.0: trough "^1.0.0" vfile "^4.0.0" -unified@^8.4.2: - version "8.4.2" - resolved "https://registry.yarnpkg.com/unified/-/unified-8.4.2.tgz#13ad58b4a437faa2751a4a4c6a16f680c500fff1" - integrity sha512-JCrmN13jI4+h9UAyKEoGcDZV+i1E7BLFuG7OsaDvTXI5P0qhHX+vZO/kOhz9jn8HGENDKbwSeB0nVOg4gVStGA== +unified@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/unified/-/unified-9.2.2.tgz#67649a1abfc3ab85d2969502902775eb03146975" + integrity sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ== dependencies: bail "^1.0.0" extend "^3.0.0" + is-buffer "^2.0.0" is-plain-obj "^2.0.0" trough "^1.0.0" vfile "^4.0.0" @@ -7583,7 +8894,7 @@ unist-util-visit-parents@^3.0.0: "@types/unist" "^2.0.0" unist-util-is "^4.0.0" -unist-util-visit@2.0.3, unist-util-visit@^2.0.0, unist-util-visit@^2.0.1, unist-util-visit@^2.0.3: +unist-util-visit@2.0.3, unist-util-visit@^2.0.0, unist-util-visit@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-2.0.3.tgz#c3703893146df47203bb8a9795af47d7b971208c" integrity sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q== @@ -7607,6 +8918,14 @@ unquote@~1.1.1: resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544" integrity sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ= +update-browserslist-db@^1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940" + integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + update-notifier@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-5.1.0.tgz#4ab0d7c7f36a231dd7316cf7729313f0214d9ad9" @@ -7667,6 +8986,11 @@ use-latest@^1.0.0: dependencies: use-isomorphic-layout-effect "^1.1.1" +use-sync-external-store@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" + integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== + util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -7746,10 +9070,10 @@ wait-on@^6.0.1: minimist "^1.2.5" rxjs "^7.5.4" -watchpack@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.3.1.tgz#4200d9447b401156eeca7767ee610f8809bc9d25" - integrity sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA== +watchpack@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" + integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== dependencies: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" @@ -7761,7 +9085,7 @@ wbuf@^1.1.0, wbuf@^1.7.3: dependencies: minimalistic-assert "^1.0.0" -web-namespaces@^1.0.0, web-namespaces@^1.1.2: +web-namespaces@^1.0.0: version "1.1.4" resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-1.1.4.tgz#bc98a3de60dadd7faefc403d1076d529f5e030ec" integrity sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw== @@ -7802,39 +9126,41 @@ webpack-dev-middleware@^5.3.1: range-parser "^1.2.1" schema-utils "^4.0.0" -webpack-dev-server@^4.8.1: - version "4.9.0" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.9.0.tgz#737dbf44335bb8bde68f8f39127fc401c97a1557" - integrity sha512-+Nlb39iQSOSsFv0lWUuUTim3jDQO8nhK3E68f//J2r5rIcp4lULHXz2oZ0UVdEeWXEh5lSzYUlzarZhDAeAVQw== +webpack-dev-server@^4.9.3: + version "4.15.1" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz#8944b29c12760b3a45bdaa70799b17cb91b03df7" + integrity sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA== dependencies: "@types/bonjour" "^3.5.9" "@types/connect-history-api-fallback" "^1.3.5" "@types/express" "^4.17.13" "@types/serve-index" "^1.9.1" + "@types/serve-static" "^1.13.10" "@types/sockjs" "^0.3.33" - "@types/ws" "^8.5.1" + "@types/ws" "^8.5.5" ansi-html-community "^0.0.8" bonjour-service "^1.0.11" chokidar "^3.5.3" colorette "^2.0.10" compression "^1.7.4" - connect-history-api-fallback "^1.6.0" + connect-history-api-fallback "^2.0.0" default-gateway "^6.0.3" express "^4.17.3" graceful-fs "^4.2.6" html-entities "^2.3.2" http-proxy-middleware "^2.0.3" ipaddr.js "^2.0.1" + launch-editor "^2.6.0" open "^8.0.9" p-retry "^4.5.0" rimraf "^3.0.2" schema-utils "^4.0.0" - selfsigned "^2.0.1" + selfsigned "^2.1.1" serve-index "^1.9.1" - sockjs "^0.3.21" + sockjs "^0.3.24" spdy "^4.0.2" webpack-dev-middleware "^5.3.1" - ws "^8.4.2" + ws "^8.13.0" webpack-merge@^5.8.0: version "5.8.0" @@ -7844,47 +9170,39 @@ webpack-merge@^5.8.0: clone-deep "^4.0.1" wildcard "^2.0.0" -webpack-sources@^1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" - integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== - dependencies: - source-list-map "^2.0.0" - source-map "~0.6.1" - -webpack-sources@^3.2.3: +webpack-sources@^3.2.2, webpack-sources@^3.2.3: version "3.2.3" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== -webpack@^5.72.0: - version "5.72.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.72.0.tgz#f8bc40d9c6bb489a4b7a8a685101d6022b8b6e28" - integrity sha512-qmSmbspI0Qo5ld49htys8GY9XhS9CGqFoHTsOVAnjBdg0Zn79y135R+k4IR4rKK6+eKaabMhJwiVB7xw0SJu5w== +webpack@^5.73.0: + version "5.88.2" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.88.2.tgz#f62b4b842f1c6ff580f3fcb2ed4f0b579f4c210e" + integrity sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ== dependencies: "@types/eslint-scope" "^3.7.3" - "@types/estree" "^0.0.51" - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/wasm-edit" "1.11.1" - "@webassemblyjs/wasm-parser" "1.11.1" - acorn "^8.4.1" - acorn-import-assertions "^1.7.6" + "@types/estree" "^1.0.0" + "@webassemblyjs/ast" "^1.11.5" + "@webassemblyjs/wasm-edit" "^1.11.5" + "@webassemblyjs/wasm-parser" "^1.11.5" + acorn "^8.7.1" + acorn-import-assertions "^1.9.0" browserslist "^4.14.5" chrome-trace-event "^1.0.2" - enhanced-resolve "^5.9.2" - es-module-lexer "^0.9.0" + enhanced-resolve "^5.15.0" + es-module-lexer "^1.2.1" eslint-scope "5.1.1" events "^3.2.0" glob-to-regexp "^0.4.1" graceful-fs "^4.2.9" - json-parse-better-errors "^1.0.2" + json-parse-even-better-errors "^2.3.1" loader-runner "^4.2.0" mime-types "^2.1.27" neo-async "^2.6.2" - schema-utils "^3.1.0" + schema-utils "^3.2.0" tapable "^2.1.1" - terser-webpack-plugin "^5.1.3" - watchpack "^2.3.1" + terser-webpack-plugin "^5.3.7" + watchpack "^2.4.0" webpack-sources "^3.2.3" webpackbar@^5.0.2: @@ -8010,10 +9328,10 @@ ws@^7.3.1: resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.7.tgz#9e0ac77ee50af70d58326ecff7e85eb3fa375e67" integrity sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A== -ws@^8.4.2: - version "8.6.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.6.0.tgz#e5e9f1d9e7ff88083d0c0dd8281ea662a42c9c23" - integrity sha512-AzmM3aH3gk0aX7/rZLYvjdvZooofDu3fFOzGqcSnQ1tOcTWwhM/o+q++E8mAyVVIyUdajrkzWUGftaVSDLn1bw== +ws@^8.13.0: + version "8.13.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" + integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== xdg-basedir@^4.0.0: version "4.0.0" @@ -8032,6 +9350,11 @@ xtend@^4.0.0, xtend@^4.0.1: resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" From 2ae31fb5694d7f92e7c00777953f25c47df84151 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Tue, 1 Aug 2023 00:46:51 -0400 Subject: [PATCH 65/79] add cache documentation to teams functions --- .../_build/doctrees/environment.pickle | Bin 844847 -> 846498 bytes .../doctrees/sportsdataverse.cfb.doctree | Bin 106830 -> 107234 bytes .../_build/doctrees/sportsdataverse.doctree | Bin 57625 -> 57624 bytes .../doctrees/sportsdataverse.mbb.doctree | Bin 85874 -> 86278 bytes .../doctrees/sportsdataverse.nba.doctree | Bin 86080 -> 86484 bytes .../doctrees/sportsdataverse.nfl.doctree | Bin 221714 -> 222118 bytes .../doctrees/sportsdataverse.nhl.doctree | Bin 103489 -> 103893 bytes .../doctrees/sportsdataverse.wbb.doctree | Bin 85913 -> 86317 bytes .../doctrees/sportsdataverse.wnba.doctree | Bin 84223 -> 84633 bytes .../_build/markdown/sportsdataverse.cfb.md | 2 ++ .../_build/markdown/sportsdataverse.mbb.md | 2 ++ .../_build/markdown/sportsdataverse.nba.md | 2 ++ .../_build/markdown/sportsdataverse.nfl.md | 2 ++ .../_build/markdown/sportsdataverse.nhl.md | 2 ++ .../_build/markdown/sportsdataverse.wbb.md | 2 ++ .../_build/markdown/sportsdataverse.wnba.md | 2 ++ docs/docs/cfb/index.md | 2 ++ docs/docs/mbb/index.md | 2 ++ docs/docs/nba/index.md | 2 ++ docs/docs/nfl/index.md | 2 ++ docs/docs/nhl/index.md | 2 ++ docs/docs/wbb/index.md | 2 ++ docs/docs/wnba/index.md | 2 ++ sportsdataverse/cfb/cfb_pbp.py | 8 ++++++++ sportsdataverse/cfb/cfb_teams.py | 2 ++ sportsdataverse/dl_utils.py | 7 ++++++- sportsdataverse/mbb/mbb_teams.py | 2 ++ sportsdataverse/nba/nba_teams.py | 2 ++ sportsdataverse/nfl/nfl_teams.py | 2 ++ sportsdataverse/nhl/nhl_teams.py | 2 ++ sportsdataverse/wbb/wbb_teams.py | 2 ++ sportsdataverse/wnba/wnba_teams.py | 2 ++ 32 files changed, 56 insertions(+), 1 deletion(-) diff --git a/Sphinx-docs/_build/doctrees/environment.pickle b/Sphinx-docs/_build/doctrees/environment.pickle index 36f81940cfdfbcaab64b28ac00639eb8b7eccff2..2d05f6e1b2b111bf3d969ddacc53814c6345b8a1 100755 GIT binary patch delta 62465 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 delta 60916 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 Cc}(a4 diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.cfb.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.cfb.doctree index fecbff402a1b319fb7d73478c055cccdb771c826..e7028af7b76fae7218db295c1630b8e315784e28 100755 GIT binary patch delta 504 zcmX?ii0#o)wuUK;%1qlAZB=FLZ)bcw*>RKcbpJd?jWDi|jLc$%w9>rflFa-(h2+HK zjMQRclk0*tK*1(9 zJ->^woQ(%4^FnU2;ThHGg58Xt*m!~bUviT-)@x2T>R~hj%L8@B2QlhypWegh&B4Z6 PT9T$~vHj%~##9~vn7*$R delta 149 zcmaEKl%-Wi*-YpU0>%eVH$#)%1%UjLvKv8PZuhSpBI&fdpZz`@RzTwrKqYP7v}8e=LC051hHW&i*H diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.doctree index 555eb12d119be21dec391f646efe0ce108ca6f88..fbb420fdcc8eba735e52af4bfd3e5bd1e73f3b1a 100755 GIT binary patch delta 39 vcmbPvhQGlwUM?|22utBG*~C06F^)VE_OC diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.mbb.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.mbb.doctree index 0f5ab9f857cee928437cb9513a71457616997d22..3b5e0a2fde6c58e0ca0440dba7d44b7eff60f31b 100755 GIT binary patch delta 488 zcmew~kF{+PYr_;qUZ#%$TlpDRTQc679510U`NKq>>F+HWPQ5JJ%nV)|ZIM&0e=4vYs`+1Zi{42?{Uw%>DQ)ZhjH D@nJdz diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.nba.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.nba.doctree index a5db36c63700016de1e477ef526e8e321532dc93..2f341956a46e2b8b0758452e5660a4ffc7aa0076 100755 GIT binary patch delta 497 zcmX@Gfc45^)`lsJ8=0g7w{B)UZOM3Rdc756aA-(IX0bwAXtVY#BeX@&NhT(;Mv=l_wW;vrm6$$7lpr1Jv_it?qU=d&YyT RY^Djpb2_69qbKK-4EES58Dc#U?(|i*jO!WMr$^Z_USQ({DwdR-d@)vadZ<0)CpIo1 zUmM6@r8&LKfzfDsq#YyQ9--i8*&7N#l8F-%U$TjQ8{-5768Kd8rOGJSy?vwUbsMrN@>T4`Q#NoIbY zLULkqMryG_Ql&ylYFc7xPKk~}alS%knnGoMsX}>TUWr0UzCuxIT2X3ohC)e3szOR) zNurKIX>lr;mzqg%Ek>0 rj%sjlOkeBC{E3lg`T{TJ1V--djNZ(jS=m@iOVV^Lw*L-b78e8nk)fmL delta 170 zcmZ2>oOjX@-i8*&7N#l8F-+?cx5hE^x-kY%Kd8rOGJSy?v;6eMeoU6r>)n~1+1N9r zvwWN;Z`4+uynT=M^!Y&9$&7oA!HntCJ(<^oRY*BaE}SejJ;{rCD;pSZ-^p_<} z^3&IOGYd?YC}k3v-s%lh&IOe21uCDaIX&5j*=V|(7c<}F$Y-kCPxvq&WMyYdE-*AQ MHQL@E#4Iid08~miuK)l5 diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.nhl.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.nhl.doctree index 06beb2731c0feae364430ad3049e7633907d7aa0..0a20bbe865a373509f18cc3bd6f86ddd8095ecb4 100755 GIT binary patch delta 485 zcmX@Og6-;RwuUK;;!NA7Zk1%ru4TM6oiBmWc>13@M)@$Vkc`Y?g|yPV5@&1e3NIc;+y`g zo^dN9_x3prj53UDJiw6IFE=?*SabUGMn)qxZXn-BZt_DJ-R(Y2j0aiySW8RNbSKL8b*fXTF z>f|OXE>&gpoP6o8SJrBGQ@fiLen=iFs^50pPtajc!7~~yI~Wf3?n1g jbiZcCgz52(jC|YoG&4S9WoJt+Ff=kX+P}STf$4+#hQ)J<6I&H7&6;r$k4gIA0+%O`$TsRG~aEuSB6FU!f>9tthoPL!l%iRUsv@BvD79 zv^W*aOU}>DP0UN-n!M0UWO~0XBOjk$a!zVuQ9RgO4b91kf5j%ho1i*9&x+BRjVnVs zYoo+ug$Px~3zHYdY173;5~wcHo07pEJ0(M`hkh}STY7r?vFK@-fzt)KRNlY$n**=eF8J!t9r@uF5)MC6e zIk7@}`g9XUxiBu8x<(d@YxJgMu*Xix5bL3vE3k#h^erZg8>a6uXXKmwOr39fgDK+< wR_+YxtOe5(4H?y@Cz~<;VC0z|W6qes$i029Ipbf}={2^D9NQ1uGTLwh0Bx0}J^%m! delta 147 zcmbO^mG%Eb)`lsJ)=Yamw%Rg2G-3>%E^o{jJXtx;a=M8LqcbaehICfp^h85OHAc_L zi51$@&zUgFaZbr#kDZbs)`Jk59$?0}o{@c;IpbwkPN1FzU_I0SnKS-i;{x(GNlbQ( p)ts(s$!IkFzZoOn pl.DataFram Returns: pl.DataFrame: Polars dataframe containing schedule dates for the requested season. + This function caches by default, so if you want to refresh the data, use the command + sportsdataverse.cfb.espn_cfb_teams.clear_cache(). Example: `cfb_df = sportsdataverse.cfb.espn_cfb_teams()` diff --git a/sportsdataverse/dl_utils.py b/sportsdataverse/dl_utils.py index 5156606..35808ad 100755 --- a/sportsdataverse/dl_utils.py +++ b/sportsdataverse/dl_utils.py @@ -15,6 +15,7 @@ def download(url, params=None, headers=None, proxy=None, timeout=30, num_retries params = {} if logger is None: logger = logging.getLogger(__name__) + logger.addHandler(logging.NullHandler()) try: response = requests.get(url, params=params, proxies=proxy, headers=headers, timeout=timeout) # print(response.url) @@ -32,8 +33,12 @@ def download(url, params=None, headers=None, proxy=None, timeout=30, num_retries num_retries=num_retries - 1, logger=logger, ) + if num_retries > 0 and (hasattr(e, "code") and getattr(e, "code") == 404): + print(f"404: {url} \nparams: {params}") + logger.error(f"404: {url} \nparams: {params}") if num_retries == 0: - logger.error("Retry Limit Exceeded") + print(f"Retry Limit Exceeded: {url} \nparams: {params}") + logger.error(f"Retry Limit Exceeded: {url} \nparams: {params}") return response diff --git a/sportsdataverse/mbb/mbb_teams.py b/sportsdataverse/mbb/mbb_teams.py index 7aedc61..01e9224 100755 --- a/sportsdataverse/mbb/mbb_teams.py +++ b/sportsdataverse/mbb/mbb_teams.py @@ -16,6 +16,8 @@ def espn_mbb_teams(groups=None, return_as_pandas=False, **kwargs) -> pl.DataFram Returns: pl.DataFrame: Polars dataframe containing teams for the requested league. + This function caches by default, so if you want to refresh the data, use the command + sportsdataverse.mbb.espn_mbb_teams.clear_cache(). Example: `mbb_df = sportsdataverse.mbb.espn_mbb_teams()` diff --git a/sportsdataverse/nba/nba_teams.py b/sportsdataverse/nba/nba_teams.py index 2af1e3b..0bec19c 100755 --- a/sportsdataverse/nba/nba_teams.py +++ b/sportsdataverse/nba/nba_teams.py @@ -15,6 +15,8 @@ def espn_nba_teams(return_as_pandas=False, **kwargs) -> pl.DataFrame: Returns: pl.DataFrame: Polars dataframe containing teams for the requested league. + This function caches by default, so if you want to refresh the data, use the command + sportsdataverse.nba.espn_nba_teams.clear_cache(). Example: `nba_df = sportsdataverse.nba.espn_nba_teams()` diff --git a/sportsdataverse/nfl/nfl_teams.py b/sportsdataverse/nfl/nfl_teams.py index a3fc5dd..c4ddd50 100755 --- a/sportsdataverse/nfl/nfl_teams.py +++ b/sportsdataverse/nfl/nfl_teams.py @@ -15,6 +15,8 @@ def espn_nfl_teams(return_as_pandas=False, **kwargs) -> pl.DataFrame: Returns: pl.DataFrame: Polars dataframe containing teams for the requested league. + This function caches by default, so if you want to refresh the data, use the command + sportsdataverse.nfl.espn_nfl_teams.clear_cache(). Example: `nfl_df = sportsdataverse.nfl.espn_nfl_teams()` diff --git a/sportsdataverse/nhl/nhl_teams.py b/sportsdataverse/nhl/nhl_teams.py index 245a7bd..075a46f 100755 --- a/sportsdataverse/nhl/nhl_teams.py +++ b/sportsdataverse/nhl/nhl_teams.py @@ -15,6 +15,8 @@ def espn_nhl_teams(return_as_pandas=False, **kwargs) -> pl.DataFrame: Returns: pl.DataFrame: Polars dataframe containing teams for the requested league. + This function caches by default, so if you want to refresh the data, use the command + sportsdataverse.nhl.espn_nhl_teams.clear_cache(). Example: `nhl_df = sportsdataverse.nhl.espn_nhl_teams()` diff --git a/sportsdataverse/wbb/wbb_teams.py b/sportsdataverse/wbb/wbb_teams.py index ddaee48..40a50aa 100755 --- a/sportsdataverse/wbb/wbb_teams.py +++ b/sportsdataverse/wbb/wbb_teams.py @@ -16,6 +16,8 @@ def espn_wbb_teams(groups=None, return_as_pandas=False, **kwargs) -> pl.DataFram Returns: pl.DataFrame: Polars dataframe containing teams for the requested league. + This function caches by default, so if you want to refresh the data, use the command + sportsdataverse.wbb.espn_wbb_teams.clear_cache(). Example: `wbb_df = sportsdataverse.wbb.espn_wbb_teams()` diff --git a/sportsdataverse/wnba/wnba_teams.py b/sportsdataverse/wnba/wnba_teams.py index b0c0c04..fb40b2f 100755 --- a/sportsdataverse/wnba/wnba_teams.py +++ b/sportsdataverse/wnba/wnba_teams.py @@ -15,6 +15,8 @@ def espn_wnba_teams(return_as_pandas=False, **kwargs) -> pl.DataFrame: Returns: pl.DataFrame: Polars dataframe containing teams for the requested league. + This function caches by default, so if you want to refresh the data, use the command + sportsdataverse.wnba.espn_wnba_teams.clear_cache(). Example: `wnba_df = sportsdataverse.wnba.espn_wnba_teams()` From 935073c930bb2cbc8810a71c8f323031e214b4ce Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Tue, 1 Aug 2023 01:06:03 -0400 Subject: [PATCH 66/79] cast sequenceNumber to pl.Int32 --- sportsdataverse/cfb/cfb_pbp.py | 1 + sportsdataverse/mbb/mbb_pbp.py | 1 + sportsdataverse/nba/nba_pbp.py | 1 + sportsdataverse/nfl/nfl_pbp.py | 1 + sportsdataverse/nhl/nhl_pbp.py | 1 + sportsdataverse/wbb/wbb_pbp.py | 1 + sportsdataverse/wnba/wnba_pbp.py | 1 + 7 files changed, 7 insertions(+) diff --git a/sportsdataverse/cfb/cfb_pbp.py b/sportsdataverse/cfb/cfb_pbp.py index 920ff86..cbeead8 100755 --- a/sportsdataverse/cfb/cfb_pbp.py +++ b/sportsdataverse/cfb/cfb_pbp.py @@ -283,6 +283,7 @@ def __helper_cfb_pbp_features(self, pbp_txt, init): .otherwise(60 * pl.col("clock.minutes") + pl.col("clock.seconds")) .alias("start.adj_TimeSecsRem"), pl.col("id").cast(pl.Int64), + pl.col("sequenceNumber").cast(pl.Int32), ) ) pbp_txt["plays"] = pbp_txt["plays"].sort(by=["id", "start.adj_TimeSecsRem"]) diff --git a/sportsdataverse/mbb/mbb_pbp.py b/sportsdataverse/mbb/mbb_pbp.py index dfc3798..d45922d 100755 --- a/sportsdataverse/mbb/mbb_pbp.py +++ b/sportsdataverse/mbb/mbb_pbp.py @@ -224,6 +224,7 @@ def helper_mbb_pbp_features(game_id, pbp_txt, init): .with_columns( game_id=pl.lit(game_id).cast(pl.Int32), id=(pl.col("id").cast(pl.Int64)), + sequenceNumber=pl.col("sequenceNumber").cast(pl.Int32), season=pl.lit(pbp_txt["header"]["season"]["year"]), seasonType=pl.lit(pbp_txt["header"]["season"]["type"]), homeTeamId=pl.lit(init["homeTeamId"]), diff --git a/sportsdataverse/nba/nba_pbp.py b/sportsdataverse/nba/nba_pbp.py index fc3407f..2409bc9 100755 --- a/sportsdataverse/nba/nba_pbp.py +++ b/sportsdataverse/nba/nba_pbp.py @@ -198,6 +198,7 @@ def helper_nba_pbp_features(game_id, pbp_txt, init): .with_columns( game_id=pl.lit(game_id).cast(pl.Int32), id=(pl.col("id").cast(pl.Int64)), + sequenceNumber=pl.col("sequenceNumber").cast(pl.Int32), season=pl.lit(pbp_txt["header"]["season"]["year"]).cast(pl.Int32), seasonType=pl.lit(pbp_txt["header"]["season"]["type"]), homeTeamId=pl.lit(init["homeTeamId"]).cast(pl.Int32), diff --git a/sportsdataverse/nfl/nfl_pbp.py b/sportsdataverse/nfl/nfl_pbp.py index f433a9e..dd9a155 100755 --- a/sportsdataverse/nfl/nfl_pbp.py +++ b/sportsdataverse/nfl/nfl_pbp.py @@ -281,6 +281,7 @@ def __helper_nfl_pbp_features(self, pbp_txt, init): .otherwise(60 * pl.col("clock.minutes") + pl.col("clock.seconds")) .alias("start.adj_TimeSecsRem"), pl.col("id").cast(pl.Int64), + pl.col("sequenceNumber").cast(pl.Int32), ) ) pbp_txt["plays"] = pbp_txt["plays"].sort(by=["id", "start.adj_TimeSecsRem"]) diff --git a/sportsdataverse/nhl/nhl_pbp.py b/sportsdataverse/nhl/nhl_pbp.py index 88c40e5..9124ab3 100755 --- a/sportsdataverse/nhl/nhl_pbp.py +++ b/sportsdataverse/nhl/nhl_pbp.py @@ -208,6 +208,7 @@ def helper_nhl_pbp_features(game_id, pbp_txt, init): .with_columns( game_id=pl.lit(game_id).cast(pl.Int32), id=(pl.col("id").cast(pl.Int64)), + sequenceNumber=pl.col("sequenceNumber").cast(pl.Int32), season=pl.lit(pbp_txt["header"]["season"]["year"]).cast(pl.Int32), seasonType=pl.lit(pbp_txt["header"]["season"]["type"]), homeTeamId=pl.lit(init["homeTeamId"]).cast(pl.Int32), diff --git a/sportsdataverse/wbb/wbb_pbp.py b/sportsdataverse/wbb/wbb_pbp.py index 2183176..d45534e 100755 --- a/sportsdataverse/wbb/wbb_pbp.py +++ b/sportsdataverse/wbb/wbb_pbp.py @@ -224,6 +224,7 @@ def helper_wbb_pbp_features(game_id, pbp_txt, init): .with_columns( game_id=pl.lit(game_id).cast(pl.Int32), id=(pl.col("id").cast(pl.Int64)), + sequenceNumber=pl.col("sequenceNumber").cast(pl.Int32), season=pl.lit(pbp_txt["header"]["season"]["year"]), seasonType=pl.lit(pbp_txt["header"]["season"]["type"]), homeTeamId=pl.lit(init["homeTeamId"]), diff --git a/sportsdataverse/wnba/wnba_pbp.py b/sportsdataverse/wnba/wnba_pbp.py index 08a9636..5581635 100755 --- a/sportsdataverse/wnba/wnba_pbp.py +++ b/sportsdataverse/wnba/wnba_pbp.py @@ -197,6 +197,7 @@ def helper_wnba_pbp_features(game_id, pbp_txt, init): .with_columns( game_id=pl.lit(game_id).cast(pl.Int32), id=(pl.col("id").cast(pl.Int64)), + sequenceNumber=pl.col("sequenceNumber").cast(pl.Int32), season=pl.lit(pbp_txt["header"]["season"]["year"]).cast(pl.Int32), seasonType=pl.lit(pbp_txt["header"]["season"]["type"]), homeTeamId=pl.lit(init["homeTeamId"]).cast(pl.Int32), From 0a15b22c745363e82b268a7f603b58586de3162c Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Tue, 1 Aug 2023 10:34:55 -0400 Subject: [PATCH 67/79] None case for formatted (column) regex extraction, punt block return player --- .../_build/doctrees/environment.pickle | Bin 846498 -> 847119 bytes sportsdataverse/__init__.py | 4 ++-- sportsdataverse/cfb/cfb_pbp.py | 10 ++++++---- sportsdataverse/nfl/nfl_pbp.py | 6 ++++-- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Sphinx-docs/_build/doctrees/environment.pickle b/Sphinx-docs/_build/doctrees/environment.pickle index 2d05f6e1b2b111bf3d969ddacc53814c6345b8a1..3239be9fe07a437fd2784971b6d3982ca4f33a45 100755 GIT binary patch delta 12537 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 Date: Tue, 1 Aug 2023 12:43:09 -0400 Subject: [PATCH 68/79] slight modifications for no --- sportsdataverse/cfb/cfb_pbp.py | 19 ++++++++++++------- sportsdataverse/nfl/nfl_pbp.py | 27 +++++++++++++++++---------- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/sportsdataverse/cfb/cfb_pbp.py b/sportsdataverse/cfb/cfb_pbp.py index 462e43f..5a7fbe8 100755 --- a/sportsdataverse/cfb/cfb_pbp.py +++ b/sportsdataverse/cfb/cfb_pbp.py @@ -34,7 +34,7 @@ wp_start_columns, wp_start_touchback_columns, ) -from sportsdataverse.dl_utils import download, key_check +from sportsdataverse.dl_utils import download ep_model_file = resource_filename("sportsdataverse", "cfb/models/ep_model.model") wp_spread_file = resource_filename("sportsdataverse", "cfb/models/wp_spread.model") @@ -150,12 +150,14 @@ def espn_cfb_pbp(self, **kwargs): "odds", "predictor", "winprobability", - "espnWP", "gameInfo", - "season", "leaders", + "drives", ]: - pbp_txt[k] = key_check(obj=summary, key=k) + if k in summary.keys(): + pbp_txt[k] = summary[k] + else: + pbp_txt[k] = {} if k in dict_keys_expected else [] for k in ["news", "shop"]: pbp_txt.pop(f"{k}", None) self.json = pbp_txt @@ -219,8 +221,11 @@ def __helper_cfb_pbp_features(self, pbp_txt, init): ) pbp_txt["plays"] = pl.concat([pbp_txt["plays"], pl.from_pandas(prev_drives)], how="vertical") - # pbp_txt["plays"] = pbp_txt["plays"].to_dict(orient="records") - # pbp_txt["plays"] = pd.DataFrame(pbp_txt["plays"]) + pbp_txt["timeouts"] = { + init["homeTeamId"]: {"1": [], "2": []}, + init["awayTeamId"]: {"1": [], "2": []}, + } + logging.debug(f"{self.gameId}: plays_df length - {len(pbp_txt['plays'])}") pbp_txt["plays"] = ( pbp_txt["plays"] @@ -676,7 +681,7 @@ def __helper_cfb_pickcenter(self, pbp_txt): # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") else: gameSpread = 2.5 - overUnder = 140.5 + overUnder = 55.0 homeFavorite = True gameSpreadAvailable = False diff --git a/sportsdataverse/nfl/nfl_pbp.py b/sportsdataverse/nfl/nfl_pbp.py index 81a7321..be225f7 100755 --- a/sportsdataverse/nfl/nfl_pbp.py +++ b/sportsdataverse/nfl/nfl_pbp.py @@ -1,4 +1,5 @@ import json +import logging import os import re import time @@ -10,7 +11,7 @@ from pkg_resources import resource_filename from xgboost import Booster, DMatrix -from sportsdataverse.dl_utils import download, key_check +from sportsdataverse.dl_utils import download from sportsdataverse.nfl.model_vars import ( defense_score_vec, end_change_vec, @@ -55,6 +56,9 @@ qbr_model = Booster({"nthread": 4}) # init model qbr_model.load_model(qbr_model_file) +logger = logging.getLogger(__name__) +logger.addHandler(logging.NullHandler()) + class NFLPlayProcess(object): gameId = 0 @@ -126,6 +130,7 @@ def espn_nfl_pbp(self, **kwargs): # "videos", # ] if self.raw == True: + logging.debug(f"{self.gameId}: raw nfl_pbp data requested, returning keys: {summary.keys()}") # reorder keys in raw format, appending empty keys which are defined later to the end pbp_json = {} for k in incoming_keys_expected: @@ -135,6 +140,7 @@ def espn_nfl_pbp(self, **kwargs): pbp_json[k] = {} if k in dict_keys_expected else [] return pbp_json + logging.debug(f"{self.gameId}: full nfl_pbp data requested, returning keys: {summary.keys()}") for k in incoming_keys_expected: if k in summary.keys(): pbp_txt[k] = summary[k] @@ -150,12 +156,14 @@ def espn_nfl_pbp(self, **kwargs): "odds", "predictor", "winprobability", - "espnWP", "gameInfo", - "season", "leaders", + "drives", ]: - pbp_txt[k] = key_check(obj=summary, key=k) + if k in summary.keys(): + pbp_txt[k] = summary[k] + else: + pbp_txt[k] = {} if k in dict_keys_expected else [] for k in ["news", "shop"]: pbp_txt.pop(f"{k}", None) self.json = pbp_txt @@ -218,8 +226,11 @@ def __helper_nfl_pbp_features(self, pbp_txt, init): ) pbp_txt["plays"] = pl.concat([pbp_txt["plays"], pl.from_pandas(prev_drives)], how="vertical") - # pbp_txt["plays"] = pbp_txt["plays"].to_dict(orient="records") - # pbp_txt["plays"] = pd.DataFrame(pbp_txt["plays"]) + pbp_txt["timeouts"] = { + init["homeTeamId"]: {"1": [], "2": []}, + init["awayTeamId"]: {"1": [], "2": []}, + } + pbp_txt["plays"] = ( pbp_txt["plays"] .with_columns( @@ -413,10 +424,6 @@ def __helper_nfl_pbp_features(self, pbp_txt, init): ) ) - pbp_txt["timeouts"] = { - init["homeTeamId"]: {"1": [], "2": []}, - init["awayTeamId"]: {"1": [], "2": []}, - } pbp_txt["timeouts"][init["homeTeamId"]]["1"] = ( pbp_txt["plays"] .filter((pl.col("homeTimeoutCalled") == True).and_(pl.col("period.number") <= 2)) From bd5458767e4b2cf39cff2be6e4bf2038546efda3 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Tue, 1 Aug 2023 13:03:12 -0400 Subject: [PATCH 69/79] Update nfl_pbp.py --- sportsdataverse/nfl/nfl_pbp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sportsdataverse/nfl/nfl_pbp.py b/sportsdataverse/nfl/nfl_pbp.py index be225f7..6714ccd 100755 --- a/sportsdataverse/nfl/nfl_pbp.py +++ b/sportsdataverse/nfl/nfl_pbp.py @@ -681,7 +681,7 @@ def __helper_nfl_pickcenter(self, pbp_txt): # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") else: gameSpread = 2.5 - overUnder = 140.5 + overUnder = 46.5 homeFavorite = True gameSpreadAvailable = False From dd1e6c79665fd8a217505fb1a39a72e03076106e Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Tue, 1 Aug 2023 14:09:03 -0400 Subject: [PATCH 70/79] more filtering to stop pipeline if len(plays_df) == 0 --- sportsdataverse/cfb/cfb_pbp.py | 16 ++++++++++------ sportsdataverse/nfl/nfl_pbp.py | 9 ++++++++- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/sportsdataverse/cfb/cfb_pbp.py b/sportsdataverse/cfb/cfb_pbp.py index 5a7fbe8..1235259 100755 --- a/sportsdataverse/cfb/cfb_pbp.py +++ b/sportsdataverse/cfb/cfb_pbp.py @@ -227,6 +227,8 @@ def __helper_cfb_pbp_features(self, pbp_txt, init): } logging.debug(f"{self.gameId}: plays_df length - {len(pbp_txt['plays'])}") + if len(pbp_txt["plays"]) == 0: + return pbp_txt pbp_txt["plays"] = ( pbp_txt["plays"] .with_columns( @@ -420,10 +422,6 @@ def __helper_cfb_pbp_features(self, pbp_txt, init): ) ) - pbp_txt["timeouts"] = { - init["homeTeamId"]: {"1": [], "2": []}, - init["awayTeamId"]: {"1": [], "2": []}, - } pbp_txt["timeouts"][init["homeTeamId"]]["1"] = ( pbp_txt["plays"] .filter((pl.col("homeTimeoutCalled") == True).and_(pl.col("period.number") <= 2)) @@ -4515,7 +4513,10 @@ def run_processing_pipeline(self): } self.json = pbp_json self.plays_json = pbp_txt["plays"] - if pbp_json.get("header").get("competitions")[0].get("playByPlaySource") != "none": + if ( + pbp_json.get("header").get("competitions")[0].get("playByPlaySource") != "none" + and len(pbp_txt["plays"]) > 0 + ): self.plays_json = ( self.plays_json.pipe(self.__add_downs_data) .pipe(self.__add_play_type_flags) @@ -4596,7 +4597,10 @@ def run_cleaning_pipeline(self): } self.json = pbp_json self.plays_json = pbp_txt["plays"] - if pbp_json.get("header").get("competitions")[0].get("playByPlaySource") != "none": + if ( + pbp_json.get("header").get("competitions")[0].get("playByPlaySource") != "none" + and len(pbp_txt["plays"]) > 0 + ): self.plays_json = ( self.plays_json.pipe(self.__add_downs_data) .pipe(self.__add_play_type_flags) diff --git a/sportsdataverse/nfl/nfl_pbp.py b/sportsdataverse/nfl/nfl_pbp.py index 6714ccd..c52b327 100755 --- a/sportsdataverse/nfl/nfl_pbp.py +++ b/sportsdataverse/nfl/nfl_pbp.py @@ -231,6 +231,10 @@ def __helper_nfl_pbp_features(self, pbp_txt, init): init["awayTeamId"]: {"1": [], "2": []}, } + logging.debug(f"{self.gameId}: plays_df length - {len(pbp_txt['plays'])}") + if len(pbp_txt["plays"]) == 0: + return pbp_txt + pbp_txt["plays"] = ( pbp_txt["plays"] .with_columns( @@ -4515,7 +4519,10 @@ def run_processing_pipeline(self): } self.json = pbp_json self.plays_json = pbp_txt["plays"] - if pbp_json.get("header").get("competitions")[0].get("playByPlaySource") != "none": + if ( + pbp_json.get("header").get("competitions")[0].get("playByPlaySource") != "none" + and len(pbp_txt["plays"]) > 0 + ): self.plays_json = ( self.plays_json.pipe(self.__add_downs_data) .pipe(self.__add_play_type_flags) From aab7e940837ab158995438ad9561a9d6de88994e Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Tue, 1 Aug 2023 14:23:37 -0400 Subject: [PATCH 71/79] updating default overUnders basketball sports --- sportsdataverse/mbb/mbb_pbp.py | 2 +- sportsdataverse/nba/nba_pbp.py | 2 +- sportsdataverse/nfl/nfl_pbp.py | 2 +- sportsdataverse/wbb/wbb_pbp.py | 2 +- sportsdataverse/wnba/wnba_pbp.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/sportsdataverse/mbb/mbb_pbp.py b/sportsdataverse/mbb/mbb_pbp.py index d45922d..3f178b1 100755 --- a/sportsdataverse/mbb/mbb_pbp.py +++ b/sportsdataverse/mbb/mbb_pbp.py @@ -201,7 +201,7 @@ def helper_mbb_pickcenter(pbp_txt): # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") else: gameSpread = 2.5 - overUnder = 140.5 + overUnder = 142.0 homeFavorite = True gameSpreadAvailable = False diff --git a/sportsdataverse/nba/nba_pbp.py b/sportsdataverse/nba/nba_pbp.py index 2409bc9..a344909 100755 --- a/sportsdataverse/nba/nba_pbp.py +++ b/sportsdataverse/nba/nba_pbp.py @@ -393,7 +393,7 @@ def helper_nba_pickcenter(pbp_txt): # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") else: gameSpread = 2.5 - overUnder = 190.5 + overUnder = 215.5 homeFavorite = True gameSpreadAvailable = False diff --git a/sportsdataverse/nfl/nfl_pbp.py b/sportsdataverse/nfl/nfl_pbp.py index c52b327..e4a6c2c 100755 --- a/sportsdataverse/nfl/nfl_pbp.py +++ b/sportsdataverse/nfl/nfl_pbp.py @@ -195,7 +195,7 @@ def __helper_nfl_pbp_features(self, pbp_txt, init): pbp_txt["plays"] = pl.DataFrame() for key in pbp_txt.get("drives").keys(): prev_drives = pd.json_normalize( - data=pbp_txt.get("drives").get("{}".format(key)), + data=pbp_txt.get("drives").get(f"{key}"), record_path="plays", meta=[ "id", diff --git a/sportsdataverse/wbb/wbb_pbp.py b/sportsdataverse/wbb/wbb_pbp.py index d45534e..ceebaee 100755 --- a/sportsdataverse/wbb/wbb_pbp.py +++ b/sportsdataverse/wbb/wbb_pbp.py @@ -201,7 +201,7 @@ def helper_wbb_pickcenter(pbp_txt): # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") else: gameSpread = 2.5 - overUnder = 140.5 + overUnder = 130.5 homeFavorite = True gameSpreadAvailable = False diff --git a/sportsdataverse/wnba/wnba_pbp.py b/sportsdataverse/wnba/wnba_pbp.py index 5581635..16763ee 100755 --- a/sportsdataverse/wnba/wnba_pbp.py +++ b/sportsdataverse/wnba/wnba_pbp.py @@ -392,7 +392,7 @@ def helper_wnba_pickcenter(pbp_txt): # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") else: gameSpread = 2.5 - overUnder = 160.5 + overUnder = 165.5 homeFavorite = True gameSpreadAvailable = False From 89018c2d3de3bee3cc3d36b9ab2bc1163135e0db Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Tue, 1 Aug 2023 16:39:31 -0400 Subject: [PATCH 72/79] adding a to in case of early termination of run_processing_pipeline --- sportsdataverse/cfb/cfb_pbp.py | 2 +- sportsdataverse/nfl/nfl_pbp.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sportsdataverse/cfb/cfb_pbp.py b/sportsdataverse/cfb/cfb_pbp.py index 1235259..faa041f 100755 --- a/sportsdataverse/cfb/cfb_pbp.py +++ b/sportsdataverse/cfb/cfb_pbp.py @@ -4573,7 +4573,7 @@ def run_cleaning_pipeline(self): pbp_json = { "gameId": int(self.gameId), - "plays": self.plays_json, + "plays": self.plays_json.to_dicts(), "season": pbp_txt["season"], "week": pbp_txt["header"]["week"], "gameInfo": pbp_txt["gameInfo"], diff --git a/sportsdataverse/nfl/nfl_pbp.py b/sportsdataverse/nfl/nfl_pbp.py index e4a6c2c..20a4ac6 100755 --- a/sportsdataverse/nfl/nfl_pbp.py +++ b/sportsdataverse/nfl/nfl_pbp.py @@ -4579,7 +4579,7 @@ def run_cleaning_pipeline(self): pbp_json = { "gameId": int(self.gameId), - "plays": self.plays_json, + "plays": self.plays_json.to_dicts(), "season": pbp_txt["season"], "week": pbp_txt["header"]["week"], "gameInfo": pbp_txt["gameInfo"], From e5b8ecd99a11230077d2b3a5f98f19fb7cc39454 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Tue, 1 Aug 2023 16:40:10 -0400 Subject: [PATCH 73/79] adding a to in case of early termination of run_processing_pipeline --- sportsdataverse/cfb/cfb_pbp.py | 2 +- sportsdataverse/nfl/nfl_pbp.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sportsdataverse/cfb/cfb_pbp.py b/sportsdataverse/cfb/cfb_pbp.py index faa041f..4632624 100755 --- a/sportsdataverse/cfb/cfb_pbp.py +++ b/sportsdataverse/cfb/cfb_pbp.py @@ -4489,7 +4489,7 @@ def run_processing_pipeline(self): pbp_json = { "gameId": int(self.gameId), - "plays": self.plays_json, + "plays": self.plays_json.to_dicts(), "season": pbp_txt["season"], "week": pbp_txt["header"]["week"], "gameInfo": pbp_txt["gameInfo"], diff --git a/sportsdataverse/nfl/nfl_pbp.py b/sportsdataverse/nfl/nfl_pbp.py index 20a4ac6..0cfb527 100755 --- a/sportsdataverse/nfl/nfl_pbp.py +++ b/sportsdataverse/nfl/nfl_pbp.py @@ -4495,7 +4495,7 @@ def run_processing_pipeline(self): pbp_json = { "gameId": int(self.gameId), - "plays": self.plays_json, + "plays": self.plays_json.to_dicts(), "season": pbp_txt["season"], "week": pbp_txt["header"]["week"], "gameInfo": pbp_txt["gameInfo"], From 1ef7a15c470157fd614278bdf9e8f8ddba99bb24 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Wed, 2 Aug 2023 06:59:53 -0400 Subject: [PATCH 74/79] updating pickcenter methods to be a bit more robust --- sportsdataverse/cfb/cfb_pbp.py | 33 +++++++++++++++++++++++++------- sportsdataverse/mbb/mbb_pbp.py | 22 ++++++++++++++------- sportsdataverse/nba/nba_pbp.py | 22 ++++++++++++++------- sportsdataverse/nfl/nfl_pbp.py | 33 +++++++++++++++++++++++++------- sportsdataverse/nhl/nhl_pbp.py | 22 ++++++++++++++------- sportsdataverse/wbb/wbb_pbp.py | 22 ++++++++++++++------- sportsdataverse/wnba/wnba_pbp.py | 22 ++++++++++++++------- 7 files changed, 127 insertions(+), 49 deletions(-) diff --git a/sportsdataverse/cfb/cfb_pbp.py b/sportsdataverse/cfb/cfb_pbp.py index 4632624..f37a037 100755 --- a/sportsdataverse/cfb/cfb_pbp.py +++ b/sportsdataverse/cfb/cfb_pbp.py @@ -668,13 +668,23 @@ def __helper_cfb_pbp(self, pbp_txt): def __helper_cfb_pickcenter(self, pbp_txt): # Spread definition if len(pbp_txt.get("pickcenter", [])) > 1: - homeFavorite = pbp_txt.get("pickcenter", {})[0].get("homeTeamOdds", {}).get("favorite", "") - if "spread" in pbp_txt.get("pickcenter", {})[1].keys(): - gameSpread = pbp_txt.get("pickcenter", {})[1].get("spread", "") - overUnder = pbp_txt.get("pickcenter", {})[1].get("overUnder", "") - else: - gameSpread = pbp_txt.get("pickcenter", {})[0].get("spread", "") - overUnder = pbp_txt.get("pickcenter", {})[0].get("overUnder", "") + pickcenter = pd.json_normalize(data=pbp_txt, record_path="pickcenter") + pickcenter = pickcenter.sort_values(by=["provider.id"]) + homeFavorite = ( + pickcenter[pickcenter["homeTeamOdds.favorite"].notnull()][["homeTeamOdds.favorite"]].values[0] + if "homeTeamOdds.favorite" in pickcenter.columns + else True + ) + gameSpread = ( + pickcenter[pickcenter["spread"].notnull()][["spread"]].values[0] + if "spread" in pickcenter.columns + else 2.5 + ) + overUnder = ( + pickcenter[pickcenter["overUnder"].notnull()][["overUnder"]].values[0] + if "overUnder" in pickcenter.columns + else 55.0 + ) gameSpreadAvailable = True # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") else: @@ -3705,6 +3715,15 @@ def __process_wpa(self, play_df): def __add_drive_data(self, play_df): play_df = ( play_df.with_columns( + ( + pl.when(pl.col("drive.result").is_null()) + .then(pl.lit("Not provided")) + .otherwise(pl.col("drive.result")) + ) + .cast(pl.Utf8) + .alias("drive.result"), + ) + .with_columns( drive_start=pl.when(pl.col("start.pos_team.id") == pl.col("homeTeamId")) .then(100 - pl.col("drive.start.yardLine")) .otherwise(pl.col("drive.start.yardLine")), diff --git a/sportsdataverse/mbb/mbb_pbp.py b/sportsdataverse/mbb/mbb_pbp.py index 3f178b1..4b07e3a 100755 --- a/sportsdataverse/mbb/mbb_pbp.py +++ b/sportsdataverse/mbb/mbb_pbp.py @@ -190,13 +190,21 @@ def helper_mbb_game_data(pbp_txt, init): def helper_mbb_pickcenter(pbp_txt): # Spread definition if len(pbp_txt.get("pickcenter", [])) > 1: - homeFavorite = pbp_txt.get("pickcenter", {})[0].get("homeTeamOdds", {}).get("favorite", "") - if "spread" in pbp_txt.get("pickcenter", {})[1].keys(): - gameSpread = pbp_txt.get("pickcenter", {})[1].get("spread", "") - overUnder = pbp_txt.get("pickcenter", {})[1].get("overUnder", "") - else: - gameSpread = pbp_txt.get("pickcenter", {})[0].get("spread", "") - overUnder = pbp_txt.get("pickcenter", {})[0].get("overUnder", "") + pickcenter = pd.json_normalize(data=pbp_txt, record_path="pickcenter") + pickcenter = pickcenter.sort_values(by=["provider.id"]) + homeFavorite = ( + pickcenter[pickcenter["homeTeamOdds.favorite"].notnull()][["homeTeamOdds.favorite"]].values[0] + if "homeTeamOdds.favorite" in pickcenter.columns + else True + ) + gameSpread = ( + pickcenter[pickcenter["spread"].notnull()][["spread"]].values[0] if "spread" in pickcenter.columns else 2.5 + ) + overUnder = ( + pickcenter[pickcenter["overUnder"].notnull()][["overUnder"]].values[0] + if "overUnder" in pickcenter.columns + else 142.0 + ) gameSpreadAvailable = True # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") else: diff --git a/sportsdataverse/nba/nba_pbp.py b/sportsdataverse/nba/nba_pbp.py index a344909..b5df6cc 100755 --- a/sportsdataverse/nba/nba_pbp.py +++ b/sportsdataverse/nba/nba_pbp.py @@ -382,13 +382,21 @@ def helper_nba_pbp_features(game_id, pbp_txt, init): def helper_nba_pickcenter(pbp_txt): # Spread definition if len(pbp_txt.get("pickcenter", [])) > 1: - homeFavorite = pbp_txt.get("pickcenter", {})[0].get("homeTeamOdds", {}).get("favorite", "") - if "spread" in pbp_txt.get("pickcenter", {})[1].keys(): - gameSpread = pbp_txt.get("pickcenter", {})[1].get("spread", "") - overUnder = pbp_txt.get("pickcenter", {})[1].get("overUnder", "") - else: - gameSpread = pbp_txt.get("pickcenter", {})[0].get("spread", "") - overUnder = pbp_txt.get("pickcenter", {})[0].get("overUnder", "") + pickcenter = pd.json_normalize(data=pbp_txt, record_path="pickcenter") + pickcenter = pickcenter.sort_values(by=["provider.id"]) + homeFavorite = ( + pickcenter[pickcenter["homeTeamOdds.favorite"].notnull()][["homeTeamOdds.favorite"]].values[0] + if "homeTeamOdds.favorite" in pickcenter.columns + else True + ) + gameSpread = ( + pickcenter[pickcenter["spread"].notnull()][["spread"]].values[0] if "spread" in pickcenter.columns else 2.5 + ) + overUnder = ( + pickcenter[pickcenter["overUnder"].notnull()][["overUnder"]].values[0] + if "overUnder" in pickcenter.columns + else 215.5 + ) gameSpreadAvailable = True # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") else: diff --git a/sportsdataverse/nfl/nfl_pbp.py b/sportsdataverse/nfl/nfl_pbp.py index 0cfb527..88b585e 100755 --- a/sportsdataverse/nfl/nfl_pbp.py +++ b/sportsdataverse/nfl/nfl_pbp.py @@ -674,13 +674,23 @@ def __helper_nfl_pbp(self, pbp_txt): def __helper_nfl_pickcenter(self, pbp_txt): # Spread definition if len(pbp_txt.get("pickcenter", [])) > 1: - homeFavorite = pbp_txt.get("pickcenter", {})[0].get("homeTeamOdds", {}).get("favorite", "") - if "spread" in pbp_txt.get("pickcenter", {})[1].keys(): - gameSpread = pbp_txt.get("pickcenter", {})[1].get("spread", "") - overUnder = pbp_txt.get("pickcenter", {})[1].get("overUnder", "") - else: - gameSpread = pbp_txt.get("pickcenter", {})[0].get("spread", "") - overUnder = pbp_txt.get("pickcenter", {})[0].get("overUnder", "") + pickcenter = pd.json_normalize(data=pbp_txt, record_path="pickcenter") + pickcenter = pickcenter.sort_values(by=["provider.id"]) + homeFavorite = ( + pickcenter[pickcenter["homeTeamOdds.favorite"].notnull()][["homeTeamOdds.favorite"]].values[0] + if "homeTeamOdds.favorite" in pickcenter.columns + else True + ) + gameSpread = ( + pickcenter[pickcenter["spread"].notnull()][["spread"]].values[0] + if "spread" in pickcenter.columns + else 2.5 + ) + overUnder = ( + pickcenter[pickcenter["overUnder"].notnull()][["overUnder"]].values[0] + if "overUnder" in pickcenter.columns + else 46.5 + ) gameSpreadAvailable = True # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") else: @@ -3711,6 +3721,15 @@ def __process_wpa(self, play_df): def __add_drive_data(self, play_df): play_df = ( play_df.with_columns( + ( + pl.when(pl.col("drive.result").is_null()) + .then(pl.lit("Not provided")) + .otherwise(pl.col("drive.result")) + ) + .cast(pl.Utf8) + .alias("drive.result"), + ) + .with_columns( drive_start=pl.when(pl.col("start.pos_team.id") == pl.col("homeTeamId")) .then(100 - pl.col("drive.start.yardLine")) .otherwise(pl.col("drive.start.yardLine")), diff --git a/sportsdataverse/nhl/nhl_pbp.py b/sportsdataverse/nhl/nhl_pbp.py index 9124ab3..c5ab3bf 100755 --- a/sportsdataverse/nhl/nhl_pbp.py +++ b/sportsdataverse/nhl/nhl_pbp.py @@ -174,13 +174,21 @@ def helper_nhl_game_data(pbp_txt, init): def helper_nhl_pickcenter(pbp_txt): # Spread definition if len(pbp_txt.get("pickcenter", [])) > 1: - homeFavorite = pbp_txt.get("pickcenter", {})[0].get("homeTeamOdds", {}).get("favorite", "") - if "spread" in pbp_txt.get("pickcenter", {})[1].keys(): - gameSpread = pbp_txt.get("pickcenter", {})[1].get("spread", "") - overUnder = pbp_txt.get("pickcenter", {})[1].get("overUnder", "") - else: - gameSpread = pbp_txt.get("pickcenter", {})[0].get("spread", "") - overUnder = pbp_txt.get("pickcenter", {})[0].get("overUnder", "") + pickcenter = pd.json_normalize(data=pbp_txt, record_path="pickcenter") + pickcenter = pickcenter.sort_values(by=["provider.id"]) + homeFavorite = ( + pickcenter[pickcenter["homeTeamOdds.favorite"].notnull()][["homeTeamOdds.favorite"]].values[0] + if "homeTeamOdds.favorite" in pickcenter.columns + else True + ) + gameSpread = ( + pickcenter[pickcenter["spread"].notnull()][["spread"]].values[0] if "spread" in pickcenter.columns else 1.5 + ) + overUnder = ( + pickcenter[pickcenter["overUnder"].notnull()][["overUnder"]].values[0] + if "overUnder" in pickcenter.columns + else 5.5 + ) gameSpreadAvailable = True # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") else: diff --git a/sportsdataverse/wbb/wbb_pbp.py b/sportsdataverse/wbb/wbb_pbp.py index ceebaee..7f0215d 100755 --- a/sportsdataverse/wbb/wbb_pbp.py +++ b/sportsdataverse/wbb/wbb_pbp.py @@ -190,13 +190,21 @@ def helper_wbb_game_data(pbp_txt, init): def helper_wbb_pickcenter(pbp_txt): # Spread definition if len(pbp_txt.get("pickcenter", [])) > 1: - homeFavorite = pbp_txt.get("pickcenter", {})[0].get("homeTeamOdds", {}).get("favorite", "") - if "spread" in pbp_txt.get("pickcenter", {})[1].keys(): - gameSpread = pbp_txt.get("pickcenter", {})[1].get("spread", "") - overUnder = pbp_txt.get("pickcenter", {})[1].get("overUnder", "") - else: - gameSpread = pbp_txt.get("pickcenter", {})[0].get("spread", "") - overUnder = pbp_txt.get("pickcenter", {})[0].get("overUnder", "") + pickcenter = pd.json_normalize(data=pbp_txt, record_path="pickcenter") + pickcenter = pickcenter.sort_values(by=["provider.id"]) + homeFavorite = ( + pickcenter[pickcenter["homeTeamOdds.favorite"].notnull()][["homeTeamOdds.favorite"]].values[0] + if "homeTeamOdds.favorite" in pickcenter.columns + else True + ) + gameSpread = ( + pickcenter[pickcenter["spread"].notnull()][["spread"]].values[0] if "spread" in pickcenter.columns else 2.5 + ) + overUnder = ( + pickcenter[pickcenter["overUnder"].notnull()][["overUnder"]].values[0] + if "overUnder" in pickcenter.columns + else 130.5 + ) gameSpreadAvailable = True # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") else: diff --git a/sportsdataverse/wnba/wnba_pbp.py b/sportsdataverse/wnba/wnba_pbp.py index 16763ee..1f0e2e8 100755 --- a/sportsdataverse/wnba/wnba_pbp.py +++ b/sportsdataverse/wnba/wnba_pbp.py @@ -381,13 +381,21 @@ def helper_wnba_pbp_features(game_id, pbp_txt, init): def helper_wnba_pickcenter(pbp_txt): # Spread definition if len(pbp_txt.get("pickcenter", [])) > 1: - homeFavorite = pbp_txt.get("pickcenter", {})[0].get("homeTeamOdds", {}).get("favorite", "") - if "spread" in pbp_txt.get("pickcenter", {})[1].keys(): - gameSpread = pbp_txt.get("pickcenter", {})[1].get("spread", "") - overUnder = pbp_txt.get("pickcenter", {})[1].get("overUnder", "") - else: - gameSpread = pbp_txt.get("pickcenter", {})[0].get("spread", "") - overUnder = pbp_txt.get("pickcenter", {})[0].get("overUnder", "") + pickcenter = pd.json_normalize(data=pbp_txt, record_path="pickcenter") + pickcenter = pickcenter.sort_values(by=["provider.id"]) + homeFavorite = ( + pickcenter[pickcenter["homeTeamOdds.favorite"].notnull()][["homeTeamOdds.favorite"]].values[0] + if "homeTeamOdds.favorite" in pickcenter.columns + else True + ) + gameSpread = ( + pickcenter[pickcenter["spread"].notnull()][["spread"]].values[0] if "spread" in pickcenter.columns else 2.5 + ) + overUnder = ( + pickcenter[pickcenter["overUnder"].notnull()][["overUnder"]].values[0] + if "overUnder" in pickcenter.columns + else 165.5 + ) gameSpreadAvailable = True # self.logger.info(f"Spread: {gameSpread}, home Favorite: {homeFavorite}, ou: {overUnder}") else: From a2fcba0b82ca9ae7060e5d48026846efbe1a1783 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Tue, 8 Aug 2023 10:15:03 -0400 Subject: [PATCH 75/79] add janitor functions to polars dataframe namespace --- sportsdataverse/dl_utils.py | 164 ++++++++++++++++++++++++++++++------ 1 file changed, 138 insertions(+), 26 deletions(-) diff --git a/sportsdataverse/dl_utils.py b/sportsdataverse/dl_utils.py index 35808ad..2ca5594 100755 --- a/sportsdataverse/dl_utils.py +++ b/sportsdataverse/dl_utils.py @@ -1,75 +1,104 @@ -import http.client import json import logging import re import time from itertools import chain, starmap -from urllib.error import ContentTooShortError, HTTPError, URLError import numpy as np +import polars as pl import requests +from sportsdataverse.errors import no_espn_data -def download(url, params=None, headers=None, proxy=None, timeout=30, num_retries=15, logger=None): - if params is None: - params = {} - if logger is None: - logger = logging.getLogger(__name__) - logger.addHandler(logging.NullHandler()) +logger = logging.getLogger("sdv.dl_utils") +logger.addHandler(logging.NullHandler()) + + +def download(url, params=None, headers=None, proxy=None, timeout=30, num_retries=15, session=None, logger=None): + session, params, logger = init_request_settings(params, session, logger) try: - response = requests.get(url, params=params, proxies=proxy, headers=headers, timeout=timeout) - # print(response.url) - except (URLError, HTTPError, ContentTooShortError, http.client.HTTPException, http.client.IncompleteRead) as e: - logger.warn("Download error: %i - %s for url (%s)", response.status_code, response.reason, response.url) - response = None - if num_retries > 0 and (hasattr(e, "code") and 500 <= getattr(e, "code") < 600): + response = session.get(url, params=params, proxies=proxy, headers=headers, timeout=timeout) + response = no_espn_data(response) + + except Exception as e: + if num_retries == 0: + logger.error(f"Retry Limit Exceeded: {url} \nparams: {params}\n {e}") + + if hasattr(e, "code") and getattr(e, "code") == 404: + logger.error(f"404: {url} \nparams: {params}") + + if num_retries > 0: + logger.warn("%s: %i - %s for url (%s)", e, response.status_code, response.reason, response.url) time.sleep(2) return download( url, params=params, - proxies=proxy, + proxy=proxy, headers=headers, timeout=timeout, num_retries=num_retries - 1, + session=session, logger=logger, ) - if num_retries > 0 and (hasattr(e, "code") and getattr(e, "code") == 404): - print(f"404: {url} \nparams: {params}") - logger.error(f"404: {url} \nparams: {params}") - if num_retries == 0: - print(f"Retry Limit Exceeded: {url} \nparams: {params}") - logger.error(f"Retry Limit Exceeded: {url} \nparams: {params}") + else: + logger.error(f"Download Error: {url} \nparams: {params}\n {e}") + return response +def init_request_settings(params, session, logger): + if params is None: + params = {} + + if session is None: + session = requests.Session() + + if logger is None: + logger = logging.getLogger("sdv.dl_utils") + logger.addHandler(logging.NullHandler()) + return session, params, logger + + def flatten_json_iterative(dictionary, sep=".", ind_start=0): """Flattening a nested json file""" def unpack_one(parent_key, parent_value): """Unpack one level (only one) of nesting in json file""" + # Unpacking one level + if isinstance(parent_value, dict): for key, value in parent_value.items(): t1 = parent_key + sep + key + yield t1, value + elif isinstance(parent_value, list): i = ind_start + for value in parent_value: t2 = parent_key + sep + str(i) + i += 1 + yield t2, value else: yield parent_key, parent_value # Continue iterating the unpack_one function until the terminating condition is satisfied + while True: # Continue unpacking the json file until all values are atomic elements (aka neither a dictionary nor a list) + dictionary = dict(chain.from_iterable(starmap(unpack_one, dictionary.items()))) + # Terminating condition: none of the values in the json file are a dictionary or a list + if not any(isinstance(value, dict) for value in dictionary.values()) and not any( isinstance(value, list) for value in dictionary.values() ): break + return dictionary @@ -77,48 +106,122 @@ def key_check(obj, key, replacement=np.array([])): return obj[key] if key in obj.keys() else replacement +@pl.api.register_dataframe_namespace("janitor") +class ColumnJanitor: + def __init__(self, df: pl.DataFrame): + self._df = df + + def clean_names(self) -> pl.DataFrame: + return self._df.rename({c: underscore(c) for c in self._df.columns}) + + def to_pascal_case(self) -> pl.DataFrame: + return self._df.rename({c: camelize(c, True) for c in self._df.columns}) + + def to_camel_case(self) -> pl.DataFrame: + return self._df.rename({c: camelize(c, False) for c in self._df.columns}) + + def to_kebab_case(self) -> pl.DataFrame: + return self._df.rename({c: kebabize(c) for c in self._df.columns}) + + def underscore(word): """ + Make an underscored, lowercase form from the expression in the string. + Example:: + >>> underscore("DeviceType") + 'device_type' + As a rule of thumb you can think of :func:`underscore` as the inverse of + :func:`camelize`, though there are cases where that does not hold:: + >>> camelize(underscore("IOError")) + 'IoError' + """ + word = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", word) + word = re.sub(r"([a-z\d])([A-Z])", r"\1_\2", word) + word = word.replace("-", "_") + + return word.lower() + + +def kebabize(word): + """ + + Make a kebab-case, lowercase form from the expression in the string. + + + Example:: + + + >>> kebabize("DeviceType") + + 'device-type' + + + As a rule of thumb you can think of :func:`kebabize` as the sister function of + + :func:`underscore`, replacing underscores with dashes + + """ + + word = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", word) + + word = re.sub(r"([a-z\d])([A-Z])", r"\1_\2", word) + + word = word.replace("_", "-") + return word.lower() def camelize(string, uppercase_first_letter=True): """ + Convert strings to CamelCase. + Examples:: + >>> camelize("device_type") + 'DeviceType' + >>> camelize("device_type", False) + 'deviceType' + :func:`camelize` can be thought of as a inverse of :func:`underscore`, + although there are some cases where that does not hold:: + >>> camelize(underscore("IOError")) + 'IoError' + :param uppercase_first_letter: if set to `True` :func:`camelize` converts + strings to UpperCamelCase. If set to `False` :func:`camelize` produces + lowerCamelCase. Defaults to `True`. + """ if uppercase_first_letter: return re.sub(r"(?:^|_)(.)", lambda m: m.group(1).upper(), string) @@ -129,7 +232,9 @@ def camelize(string, uppercase_first_letter=True): class ESPNResponse: def __init__(self, response, status_code, url): self._response = response + self._status_code = status_code + self._url = url def get_response(self): @@ -144,8 +249,10 @@ def get_json(self): def valid_json(self): try: self.get_dict() + except ValueError: return False + return True def get_url(self): @@ -169,29 +276,34 @@ def send_api_request( ): if not self.base_url: raise Exception("Cannot use send_api_request from _HTTP class.") + base_url = self.base_url.format(endpoint=endpoint) + endpoint = endpoint.lower() - self.parameters = parameters - if headers is None: - request_headers = self.headers - else: - request_headers = headers + self.parameters = parameters + request_headers = self.headers if headers is None else headers if referer: request_headers["Referer"] = referer url = None + status_code = None + contents = None # Sort parameters by key... for some reason this matters for some requests... + parameters = sorted(parameters.items(), key=lambda kv: kv[0]) if not contents: response = requests.get(url=base_url, params=parameters, headers=request_headers, timeout=timeout) + url = response.url + status_code = response.status_code + contents = response.text contents = self.clean_contents(contents) From e0b110ddd7d205c5822950e4e9147d0985857e61 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Tue, 8 Aug 2023 10:15:23 -0400 Subject: [PATCH 76/79] no_espn_data error added --- sportsdataverse/errors.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/sportsdataverse/errors.py b/sportsdataverse/errors.py index b0bac33..547c0fb 100755 --- a/sportsdataverse/errors.py +++ b/sportsdataverse/errors.py @@ -12,3 +12,16 @@ def season_not_found_error(season, min_season): return else: raise SeasonNotFoundError(f"Season {season} not found, season cannot be less than {min_season}") + + +class NoESPNDataError(Exception): + pass + + +def no_espn_data(response): + if response.status_code == 404: + raise NoESPNDataError(f"NoESPNDataError: No response for {response.url}") + elif response.json().get("code", None) == 404: + raise NoESPNDataError(f"NoESPNDataError: No data found for {response.url}, response: {response.json()}") + else: + return response From 259ec39c4d0f3b1d4612d0e2efbe41a0c93804e5 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Tue, 8 Aug 2023 10:15:42 -0400 Subject: [PATCH 77/79] configs updated to use fstrings --- sportsdataverse/config.py | 48 ++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/sportsdataverse/config.py b/sportsdataverse/config.py index c2123b6..4f0a551 100755 --- a/sportsdataverse/config.py +++ b/sportsdataverse/config.py @@ -1,18 +1,24 @@ +import logging + +logger = logging.getLogger("sdv.dl_utils") +logger.addHandler(logging.NullHandler()) + + SGITHUB = "https://raw.githubusercontent.com/sportsdataverse/" SDVRELEASES = "https://github.com/sportsdataverse/sportsdataverse-data/releases/download/" CFB_BASE_URL = SGITHUB + "cfbfastR-data/main/pbp/parquet/play_by_play_{season}.parquet" CFB_ROSTER_URL = SGITHUB + "cfbfastR-data/main/rosters/parquet/cfb_rosters_{season}.parquet" -CFB_TEAM_LOGO_URL = SGITHUB + "cfbfastR-data/main/teams/teams_colors_logos.parquet" +CFB_TEAM_LOGO_URL = f"{SGITHUB}cfbfastR-data/main/teams/teams_colors_logos.parquet" CFB_TEAM_SCHEDULE_URL = SGITHUB + "cfbfastR-data/main/schedules/parquet/cfb_schedules_{season}.parquet" CFB_TEAM_INFO_URL = SGITHUB + "cfbfastR-data/main/team_info/parquet/cfb_team_info_{season}.parquet" -CFB_BETTING_LINES_URL = SGITHUB + "cfbfastR-data/main/betting/parquet/cfb_line_odds.parquet" +CFB_BETTING_LINES_URL = f"{SGITHUB}cfbfastR-data/main/betting/parquet/cfb_line_odds.parquet" NHL_BASE_URL = SGITHUB + "fastRhockey-data/main/nhl/pbp/parquet/play_by_play_{season}.parquet" NHL_PLAYER_BOX_URL = SGITHUB + "fastRhockey-data/main/nhl/player_box/parquet/player_box_{season}.parquet" NHL_TEAM_BOX_URL = SGITHUB + "fastRhockey-data/main/nhl/team_box/parquet/team_box_{season}.parquet" NHL_TEAM_SCHEDULE_URL = SGITHUB + "fastRhockey-data/main/nhl/schedules/parquet/nhl_schedule_{season}.parquet" -NHL_TEAM_LOGO_URL = SGITHUB + "fastRhockey-data/main/nhl/nhl_teams_colors_logos.csv" +NHL_TEAM_LOGO_URL = f"{SGITHUB}fastRhockey-data/main/nhl/nhl_teams_colors_logos.csv" PHF_BASE_URL = SGITHUB + "fastRhockey-data/main/phf/pbp/parquet/play_by_play_{season}.parquet" PHF_PLAYER_BOX_URL = SGITHUB + "fastRhockey-data/main/phf/player_box/parquet/player_box_{season}.parquet" @@ -22,7 +28,7 @@ MBB_BASE_URL = SDVRELEASES + "espn_mens_college_basketball_pbp/play_by_play_{season}.parquet" MBB_TEAM_BOX_URL = SDVRELEASES + "espn_mens_college_basketball_team_boxscores/team_box_{season}.parquet" MBB_PLAYER_BOX_URL = SDVRELEASES + "espn_mens_college_basketball_player_boxscores/player_box_{season}.parquet" -MBB_TEAM_LOGO_URL = SDVRELEASES + "hoopR-data/master/mbb/teams_colors_logos.csv" +MBB_TEAM_LOGO_URL = f"{SDVRELEASES}hoopR-data/master/mbb/teams_colors_logos.csv" MBB_TEAM_SCHEDULE_URL = SDVRELEASES + "espn_mens_college_basketball_schedules/mbb_schedule_{season}.parquet" NBA_BASE_URL = SDVRELEASES + "espn_nba_pbp/play_by_play_{season}.parquet" @@ -33,7 +39,7 @@ WBB_BASE_URL = SDVRELEASES + "espn_womens_college_basketball_pbp/play_by_play_{season}.parquet" WBB_TEAM_BOX_URL = SDVRELEASES + "espn_womens_college_basketball_team_boxscores/team_box_{season}.parquet" WBB_PLAYER_BOX_URL = SDVRELEASES + "espn_womens_college_basketball_player_boxscores/player_box_{season}.parquet" -WBB_TEAM_LOGO_URL = SDVRELEASES + "wehoop-data/master/wbb/teams_colors_logos.csv" +WBB_TEAM_LOGO_URL = f"{SDVRELEASES}wehoop-data/master/wbb/teams_colors_logos.csv" WBB_TEAM_SCHEDULE_URL = SDVRELEASES + "espn_womens_college_basketball_schedules/wbb_schedule_{season}.parquet" WNBA_BASE_URL = SDVRELEASES + "espn_wnba_pbp/play_by_play_{season}.parquet" @@ -45,30 +51,30 @@ NFLVERSEGITHUB = "https://github.com/nflverse/nflverse-data/releases/download/" NFLVERSEGITHUBPBP = "https://raw.githubusercontent.com/nflverse/" NFL_BASE_URL = NFLVERSEGITHUB + "pbp/play_by_play_{season}.parquet" # done -NFL_PLAYER_URL = NFLVERSEGITHUB + "players/players.parquet" # done -NFL_PLAYER_STATS_URL = NFLVERSEGITHUB + "player_stats/player_stats.parquet" # done -NFL_PLAYER_KICKING_STATS_URL = NFLVERSEGITHUB + "player_stats/player_stats_kicking.parquet" # done -NFL_PFR_SEASON_DEF_URL = NFLVERSEGITHUB + "pfr_advstats/advstats_season_def.parquet" +NFL_PLAYER_URL = f"{NFLVERSEGITHUB}players/players.parquet" +NFL_PLAYER_STATS_URL = f"{NFLVERSEGITHUB}player_stats/player_stats.parquet" +NFL_PLAYER_KICKING_STATS_URL = f"{NFLVERSEGITHUB}player_stats/player_stats_kicking.parquet" +NFL_PFR_SEASON_DEF_URL = f"{NFLVERSEGITHUB}pfr_advstats/advstats_season_def.parquet" NFL_PFR_WEEK_DEF_URL = NFLVERSEGITHUB + "pfr_advstats/advstats_week_def_{season}.parquet" -NFL_PFR_SEASON_PASS_URL = NFLVERSEGITHUB + "pfr_advstats/advstats_season_pass.parquet" +NFL_PFR_SEASON_PASS_URL = f"{NFLVERSEGITHUB}pfr_advstats/advstats_season_pass.parquet" NFL_PFR_WEEK_PASS_URL = NFLVERSEGITHUB + "pfr_advstats/advstats_week_pass_{season}.parquet" -NFL_PFR_SEASON_REC_URL = NFLVERSEGITHUB + "pfr_advstats/advstats_season_rec.parquet" +NFL_PFR_SEASON_REC_URL = f"{NFLVERSEGITHUB}pfr_advstats/advstats_season_rec.parquet" NFL_PFR_WEEK_REC_URL = NFLVERSEGITHUB + "pfr_advstats/advstats_week_rec_{season}.parquet" -NFL_PFR_SEASON_RUSH_URL = NFLVERSEGITHUB + "pfr_advstats/advstats_season_rush.parquet" +NFL_PFR_SEASON_RUSH_URL = f"{NFLVERSEGITHUB}pfr_advstats/advstats_season_rush.parquet" NFL_PFR_WEEK_RUSH_URL = NFLVERSEGITHUB + "pfr_advstats/advstats_week_rush_{season}.parquet" -NFL_NGS_RUSHING_URL = NFLVERSEGITHUB + "nextgen_stats/ngs_rushing.parquet" -NFL_NGS_PASSING_URL = NFLVERSEGITHUB + "nextgen_stats/ngs_passing.parquet" -NFL_NGS_RECEIVING_URL = NFLVERSEGITHUB + "nextgen_stats/ngs_receiving.parquet" +NFL_NGS_RUSHING_URL = f"{NFLVERSEGITHUB}nextgen_stats/ngs_rushing.parquet" +NFL_NGS_PASSING_URL = f"{NFLVERSEGITHUB}nextgen_stats/ngs_passing.parquet" +NFL_NGS_RECEIVING_URL = f"{NFLVERSEGITHUB}nextgen_stats/ngs_receiving.parquet" NFL_ROSTER_URL = NFLVERSEGITHUB + "rosters/roster_{season}.parquet" # done NFL_WEEKLY_ROSTER_URL = NFLVERSEGITHUB + "weekly_rosters/roster_weekly_{season}.parquet" # done NFL_SNAP_COUNTS_URL = NFLVERSEGITHUB + "snap_counts/snap_counts_{season}.parquet" NFL_PBP_PARTICIPATION_URL = NFLVERSEGITHUB + "pbp_participation/pbp_participation_{season}.parquet" -NFL_CONTRACTS_URL = NFLVERSEGITHUB + "contracts/historical_contracts.parquet" -NFL_OTC_PLAYER_DETAILS_URL = NFLVERSEGITHUB + "contracts/otc_player_details.rds" -NFL_DRAFT_PICKS_URL = NFLVERSEGITHUB + "draft_picks/draft_picks.parquet" -NFL_COMBINE_URL = NFLVERSEGITHUB + "combine/combine.parquet" +NFL_CONTRACTS_URL = f"{NFLVERSEGITHUB}contracts/historical_contracts.parquet" +NFL_OTC_PLAYER_DETAILS_URL = f"{NFLVERSEGITHUB}contracts/otc_player_details.rds" +NFL_DRAFT_PICKS_URL = f"{NFLVERSEGITHUB}draft_picks/draft_picks.parquet" +NFL_COMBINE_URL = f"{NFLVERSEGITHUB}combine/combine.parquet" NFL_INJURIES_URL = NFLVERSEGITHUB + "injuries/injuries_{season}.parquet" NFL_DEPTH_CHARTS_URL = NFLVERSEGITHUB + "depth_charts/depth_charts_{season}.parquet" -NFL_OFFICIALS_URL = NFLVERSEGITHUB + "officials/officials.parquet" -NFL_TEAM_LOGO_URL = NFLVERSEGITHUBPBP + "nflverse-pbp/master/teams_colors_logos.csv" +NFL_OFFICIALS_URL = f"{NFLVERSEGITHUB}officials/officials.parquet" +NFL_TEAM_LOGO_URL = f"{NFLVERSEGITHUBPBP}nflverse-pbp/master/teams_colors_logos.csv" NFL_TEAM_SCHEDULE_URL = NFLVERSEGITHUBPBP + "nflverse-pbp/master/schedules/sched_{season}.rds" From d1a0fa6eecb995c8f29453f50828ce951febbe94 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Tue, 8 Aug 2023 10:51:01 -0400 Subject: [PATCH 78/79] extract event parsing, janitor clean names --- sportsdataverse/cfb/cfb_schedule.py | 135 ++++++++++++++++++-------- sportsdataverse/mbb/mbb_schedule.py | 75 +++++++------- sportsdataverse/nba/nba_schedule.py | 74 +++++++------- sportsdataverse/nfl/nfl_schedule.py | 79 +++++++-------- sportsdataverse/nhl/nhl_schedule.py | 74 +++++++------- sportsdataverse/wbb/wbb_schedule.py | 75 +++++++------- sportsdataverse/wnba/wnba_schedule.py | 74 +++++++------- 7 files changed, 332 insertions(+), 254 deletions(-) diff --git a/sportsdataverse/cfb/cfb_schedule.py b/sportsdataverse/cfb/cfb_schedule.py index 2f5d38b..6553df7 100755 --- a/sportsdataverse/cfb/cfb_schedule.py +++ b/sportsdataverse/cfb/cfb_schedule.py @@ -3,7 +3,7 @@ import pandas as pd import polars as pl -from sportsdataverse.dl_utils import download, underscore +from sportsdataverse.dl_utils import download def espn_cfb_schedule( @@ -11,15 +11,24 @@ def espn_cfb_schedule( ) -> pl.DataFrame: """espn_cfb_schedule - look up the college football schedule for a given season + Args: + dates (int): Used to define different seasons. 2002 is the earliest available season. + week (int): Week of the schedule. + groups (int): Used to define different divisions. 80 is FBS, 81 is FCS. + season_type (int): 2 for regular season, 3 for post-season, 4 for off-season. + limit (int): number of records to return, default: 500. + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Returns: + pl.DataFrame: Polars dataframe containing schedule dates for the requested season. Returns None if no games """ @@ -32,51 +41,23 @@ def espn_cfb_schedule( } url = "http://site.api.espn.com/apis/site/v2/sports/football/college-football/scoreboard" + resp = download(url=url, params=params, **kwargs) ev = pd.DataFrame() + events_txt = resp.json() + events = events_txt.get("events") + if events is None: return None + if len(events) == 0: return None for event in events: - event.get("competitions")[0].get("competitors")[0].get("team").pop("links", None) - event.get("competitions")[0].get("competitors")[1].get("team").pop("links", None) - if event.get("competitions")[0].get("competitors")[0].get("homeAway") == "home": - event = _extract_home_away(event, 0, "home") - event = _extract_home_away(event, 1, "away") - else: - event = _extract_home_away(event, 0, "away") - event = _extract_home_away(event, 1, "home") - del_keys = ["geoBroadcasts", "headlines", "series", "situation", "tickets", "odds", "leaders"] - for k in del_keys: - event.get("competitions")[0].pop(k, None) - event.get("competitions")[0]["notes_type"] = ( - event.get("competitions")[0]["notes"][0].get("type") - if len(event.get("competitions")[0]["notes"]) > 0 - else "" - ) - event.get("competitions")[0]["notes_headline"] = ( - event.get("competitions")[0]["notes"][0].get("headline").replace('"', "") - if len(event.get("competitions")[0]["notes"]) > 0 - else "" - ) - event.get("competitions")[0]["broadcast_market"] = ( - event.get("competitions")[0].get("broadcasts", [])[0].get("market", "") - if len(event.get("competitions")[0].get("broadcasts")) > 0 - else "" - ) - event.get("competitions")[0]["broadcast_name"] = ( - event.get("competitions")[0].get("broadcasts", [])[0].get("names", [])[0] - if len(event.get("competitions")[0].get("broadcasts")) > 0 - else "" - ) - event.get("competitions")[0].pop("broadcasts", None) - event.get("competitions")[0].pop("notes", None) - event.get("competitions")[0].pop("competitors", None) + event = scoreboard_event_parsing(event) x = pl.from_pandas(pd.json_normalize(event.get("competitions")[0], sep="_")) x = x.with_columns( game_id=(pl.col("id").cast(pl.Int32)), @@ -94,37 +75,83 @@ def espn_cfb_schedule( season_type=pl.col("season_type").cast(pl.Int32), week=pl.col("week").cast(pl.Int32), ) + x = x[[s.name for s in x if s.null_count() != x.height]] + ev = pd.concat([ev, x.to_pandas()], axis=0, ignore_index=True) ev = pl.from_pandas(ev) - ev.columns = [underscore(c) for c in ev.columns] + ev = ev.janitor.clean_names() return ev.to_pandas() if return_as_pandas else ev -# TODO Rename this here and in `espn_cfb_schedule` -def _extract_home_away(event, arg1, arg2): +def scoreboard_event_parsing(event): + event.get("competitions")[0].get("competitors")[0].get("team").pop("links", None) + event.get("competitions")[0].get("competitors")[1].get("team").pop("links", None) + if event.get("competitions")[0].get("competitors")[0].get("homeAway") == "home": + event = __extract_home_away(event, 0, "home") + event = __extract_home_away(event, 1, "away") + else: + event = __extract_home_away(event, 0, "away") + event = __extract_home_away(event, 1, "home") + del_keys = ["geoBroadcasts", "headlines", "series", "situation", "tickets", "odds", "leaders"] + for k in del_keys: + event.get("competitions")[0].pop(k, None) + event.get("competitions")[0]["notes_type"] = ( + event.get("competitions")[0]["notes"][0].get("type") if len(event.get("competitions")[0]["notes"]) > 0 else "" + ) + event.get("competitions")[0]["notes_headline"] = ( + event.get("competitions")[0]["notes"][0].get("headline").replace('"', "") + if len(event.get("competitions")[0]["notes"]) > 0 + else "" + ) + event.get("competitions")[0]["broadcast_market"] = ( + event.get("competitions")[0].get("broadcasts", [])[0].get("market", "") + if len(event.get("competitions")[0].get("broadcasts")) > 0 + else "" + ) + event.get("competitions")[0]["broadcast_name"] = ( + event.get("competitions")[0].get("broadcasts", [])[0].get("names", [])[0] + if len(event.get("competitions")[0].get("broadcasts")) > 0 + else "" + ) + event.get("competitions")[0].pop("broadcasts", None) + event.get("competitions")[0].pop("notes", None) + event.get("competitions")[0].pop("competitors", None) + return event + + +def __extract_home_away(event, arg1, arg2): event["competitions"][0][arg2] = event.get("competitions")[0].get("competitors")[arg1].get("team") + event["competitions"][0][arg2]["score"] = event.get("competitions")[0].get("competitors")[arg1].get("score") + event["competitions"][0][arg2]["winner"] = event.get("competitions")[0].get("competitors")[arg1].get("winner") + # add winner back to main competitors if does not exist + event["competitions"][0]["competitors"][arg1]["winner"] = ( event.get("competitions")[0].get("competitors")[arg1].get("winner", False) ) + event["competitions"][0][arg2]["currentRank"] = ( event.get("competitions")[0].get("competitors")[arg1].get("curatedRank", {}).get("current", 99) ) + event["competitions"][0][arg2]["linescores"] = ( event.get("competitions")[0] .get("competitors")[arg1] .get("linescores", [{"value": 0}, {"value": 0}, {"value": 0}, {"value": 0}]) ) + # add linescores back to main competitors if does not exist + event["competitions"][0]["competitors"][arg1]["linescores"] = ( event.get("competitions")[0] .get("competitors")[arg1] .get("linescores", [{"value": 0}, {"value": 0}, {"value": 0}, {"value": 0}]) ) + event["competitions"][0][arg2]["records"] = ( event.get("competitions")[0] .get("competitors")[arg1] @@ -144,27 +171,44 @@ def _extract_home_away(event, arg1, arg2): def espn_cfb_calendar(season=None, groups=None, ondays=None, return_as_pandas=False, **kwargs) -> pl.DataFrame: """espn_cfb_calendar - look up the men's college football calendar for a given season + Args: + season (int): Used to define different seasons. 2002 is the earliest available season. + groups (int): Used to define different divisions. 80 is FBS, 81 is FCS. + ondays (boolean): Used to return dates for calendar ondays + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + Returns: + pl.DataFrame: Polars dataframe containing calendar dates for the requested season. + Raises: + ValueError: If `season` is less than 2002. """ + if ondays is not None: full_schedule = __ondays_cfb_calendar(season, **kwargs) + else: url = "http://site.api.espn.com/apis/site/v2/sports/football/college-football/scoreboard" + params = {"dates": season, "groups": groups if groups is not None else "80"} + resp = download(url=url, params=params, **kwargs) + txt = resp.json() + txt = txt.get("leagues")[0].get("calendar") + full_schedule = pl.DataFrame() + for i in range(len(txt)): if txt[i].get("entries", None) is not None: reg = pd.json_normalize( @@ -176,20 +220,30 @@ def espn_cfb_calendar(season=None, groups=None, ondays=None, return_as_pandas=Fa errors="ignore", sep="_", ) + full_schedule = pl.concat([full_schedule, pl.from_pandas(reg)], how="vertical") + full_schedule = full_schedule.with_columns(season=season) - full_schedule.columns = [underscore(c) for c in full_schedule.columns] + + full_schedule = full_schedule.janitor.clean_names() + full_schedule = full_schedule.rename({"week_value": "week", "season_type_value": "season_type"}) + return full_schedule.to_pandas() if return_as_pandas else full_schedule def __ondays_cfb_calendar(season, **kwargs): url = f"https://sports.core.api.espn.com/v2/sports/football/leagues/college-football/seasons/{season}/types/2/calendar/ondays" + resp = download(url=url, **kwargs) + if resp is not None: txt = resp.json().get("eventDate").get("dates") + result = pl.DataFrame(txt, schema=["dates"]) + result = result.with_columns(dateURL=pl.col("dates").str.slice(0, 10)) + result = result.with_columns( url="http://site.api.espn.com/apis/site/v2/sports/football/college-football/scoreboard?dates=" + pl.col("dateURL") @@ -200,9 +254,12 @@ def __ondays_cfb_calendar(season, **kwargs): def most_recent_cfb_season(): date = datetime.datetime.now() + if date.month >= 8 and date.day >= 15: return date.year + elif date.month >= 9: return date.year + else: return date.year - 1 diff --git a/sportsdataverse/mbb/mbb_schedule.py b/sportsdataverse/mbb/mbb_schedule.py index 641759a..b5add3e 100755 --- a/sportsdataverse/mbb/mbb_schedule.py +++ b/sportsdataverse/mbb/mbb_schedule.py @@ -3,7 +3,7 @@ import pandas as pd import polars as pl -from sportsdataverse.dl_utils import download, underscore +from sportsdataverse.dl_utils import download from sportsdataverse.errors import SeasonNotFoundError @@ -39,40 +39,7 @@ def espn_mbb_schedule( return pd.DataFrame() if return_as_pandas else pl.DataFrame() for event in events: - event.get("competitions")[0].get("competitors")[0].get("team").pop("links", None) - event.get("competitions")[0].get("competitors")[1].get("team").pop("links", None) - if event.get("competitions")[0].get("competitors")[0].get("homeAway") == "home": - event = __extract_home_away(event, 0, "home") - event = __extract_home_away(event, 1, "away") - else: - event = __extract_home_away(event, 0, "away") - event = __extract_home_away(event, 1, "home") - del_keys = ["geoBroadcasts", "headlines", "series", "situation", "tickets", "odds", "leaders"] - for k in del_keys: - event.get("competitions")[0].pop(k, None) - event.get("competitions")[0]["notes_type"] = ( - event.get("competitions")[0]["notes"][0].get("type") - if len(event.get("competitions")[0]["notes"]) > 0 - else "" - ) - event.get("competitions")[0]["notes_headline"] = ( - event.get("competitions")[0]["notes"][0].get("headline").replace('"', "") - if len(event.get("competitions")[0]["notes"]) > 0 - else "" - ) - event.get("competitions")[0]["broadcast_market"] = ( - event.get("competitions")[0].get("broadcasts", [])[0].get("market", "") - if len(event.get("competitions")[0].get("broadcasts")) > 0 - else "" - ) - event.get("competitions")[0]["broadcast_name"] = ( - event.get("competitions")[0].get("broadcasts", [])[0].get("names", [])[0] - if len(event.get("competitions")[0].get("broadcasts")) > 0 - else "" - ) - event.get("competitions")[0].pop("broadcasts", None) - event.get("competitions")[0].pop("notes", None) - event.get("competitions")[0].pop("competitors", None) + event = scoreboard_event_parsing(event) x = pl.from_pandas(pd.json_normalize(event.get("competitions")[0], sep="_")) x = x.with_columns( game_id=(pl.col("id").cast(pl.Int32)), @@ -91,11 +58,47 @@ def espn_mbb_schedule( x = x[[s.name for s in x if s.null_count() != x.height]] ev = pd.concat([ev, x.to_pandas()], axis=0, ignore_index=True) ev = pl.from_pandas(ev) - ev.columns = [underscore(c) for c in ev.columns] + ev = ev.janitor.clean_names() return ev.to_pandas() if return_as_pandas else ev +def scoreboard_event_parsing(event): + event.get("competitions")[0].get("competitors")[0].get("team").pop("links", None) + event.get("competitions")[0].get("competitors")[1].get("team").pop("links", None) + if event.get("competitions")[0].get("competitors")[0].get("homeAway") == "home": + event = __extract_home_away(event, 0, "home") + event = __extract_home_away(event, 1, "away") + else: + event = __extract_home_away(event, 0, "away") + event = __extract_home_away(event, 1, "home") + del_keys = ["geoBroadcasts", "headlines", "series", "situation", "tickets", "odds", "leaders"] + for k in del_keys: + event.get("competitions")[0].pop(k, None) + event.get("competitions")[0]["notes_type"] = ( + event.get("competitions")[0]["notes"][0].get("type") if len(event.get("competitions")[0]["notes"]) > 0 else "" + ) + event.get("competitions")[0]["notes_headline"] = ( + event.get("competitions")[0]["notes"][0].get("headline").replace('"', "") + if len(event.get("competitions")[0]["notes"]) > 0 + else "" + ) + event.get("competitions")[0]["broadcast_market"] = ( + event.get("competitions")[0].get("broadcasts", [])[0].get("market", "") + if len(event.get("competitions")[0].get("broadcasts")) > 0 + else "" + ) + event.get("competitions")[0]["broadcast_name"] = ( + event.get("competitions")[0].get("broadcasts", [])[0].get("names", [])[0] + if len(event.get("competitions")[0].get("broadcasts")) > 0 + else "" + ) + event.get("competitions")[0].pop("broadcasts", None) + event.get("competitions")[0].pop("notes", None) + event.get("competitions")[0].pop("competitors", None) + return event + + def __extract_home_away(event, arg1, arg2): event["competitions"][0][arg2] = event.get("competitions")[0].get("competitors")[arg1].get("team") event["competitions"][0][arg2]["score"] = event.get("competitions")[0].get("competitors")[arg1].get("score") diff --git a/sportsdataverse/nba/nba_schedule.py b/sportsdataverse/nba/nba_schedule.py index 778eb18..db7bc04 100755 --- a/sportsdataverse/nba/nba_schedule.py +++ b/sportsdataverse/nba/nba_schedule.py @@ -3,7 +3,7 @@ import pandas as pd import polars as pl -from sportsdataverse.dl_utils import download, underscore +from sportsdataverse.dl_utils import download def espn_nba_schedule(dates=None, season_type=None, limit=500, return_as_pandas=False, **kwargs) -> pl.DataFrame: @@ -32,39 +32,7 @@ def espn_nba_schedule(dates=None, season_type=None, limit=500, return_as_pandas= return pd.DataFrame() if return_as_pandas else pl.DataFrame() for event in events: - event.get("competitions")[0].get("competitors")[0].get("team").pop("links", None) - event.get("competitions")[0].get("competitors")[1].get("team").pop("links", None) - if event.get("competitions")[0].get("competitors")[0].get("homeAway") == "home": - event = __extract_home_away(event, 0, "home") - event = __extract_home_away(event, 1, "away") - else: - event = __extract_home_away(event, 0, "away") - event = __extract_home_away(event, 1, "home") - - event.get("competitions")[0]["notes_type"] = ( - event.get("competitions")[0]["notes"][0].get("type") - if len(event.get("competitions")[0]["notes"]) > 0 - else "" - ) - event.get("competitions")[0]["notes_headline"] = ( - event.get("competitions")[0]["notes"][0].get("headline").replace('"', "") - if len(event.get("competitions")[0]["notes"]) > 0 - else "" - ) - event.get("competitions")[0]["broadcast_market"] = ( - event.get("competitions")[0].get("broadcasts", [])[0].get("market", "") - if len(event.get("competitions")[0].get("broadcasts")) > 0 - else "" - ) - event.get("competitions")[0]["broadcast_name"] = ( - event.get("competitions")[0].get("broadcasts", [])[0].get("names", [])[0] - if len(event.get("competitions")[0].get("broadcasts")) > 0 - else "" - ) - del_keys = ["geoBroadcasts", "headlines", "series", "situation", "tickets", "odds", "leaders"] - for k in del_keys: - event.get("competitions")[0].pop(k, None) - + event = scoreboard_event_parsing(event) x = pl.from_pandas(pd.json_normalize(event.get("competitions")[0], sep="_")) x = x.with_columns( game_id=(pl.col("id").cast(pl.Int32)), @@ -83,11 +51,47 @@ def espn_nba_schedule(dates=None, season_type=None, limit=500, return_as_pandas= x = x[[s.name for s in x if s.null_count() != x.height]] ev = pd.concat([ev, x.to_pandas()], axis=0, ignore_index=True) ev = pl.from_pandas(ev) - ev.columns = [underscore(c) for c in ev.columns] + ev = ev.janitor.clean_names() return ev.to_pandas() if return_as_pandas else ev +def scoreboard_event_parsing(event): + event.get("competitions")[0].get("competitors")[0].get("team").pop("links", None) + event.get("competitions")[0].get("competitors")[1].get("team").pop("links", None) + if event.get("competitions")[0].get("competitors")[0].get("homeAway") == "home": + event = __extract_home_away(event, 0, "home") + event = __extract_home_away(event, 1, "away") + else: + event = __extract_home_away(event, 0, "away") + event = __extract_home_away(event, 1, "home") + del_keys = ["geoBroadcasts", "headlines", "series", "situation", "tickets", "odds", "leaders"] + for k in del_keys: + event.get("competitions")[0].pop(k, None) + event.get("competitions")[0]["notes_type"] = ( + event.get("competitions")[0]["notes"][0].get("type") if len(event.get("competitions")[0]["notes"]) > 0 else "" + ) + event.get("competitions")[0]["notes_headline"] = ( + event.get("competitions")[0]["notes"][0].get("headline").replace('"', "") + if len(event.get("competitions")[0]["notes"]) > 0 + else "" + ) + event.get("competitions")[0]["broadcast_market"] = ( + event.get("competitions")[0].get("broadcasts", [])[0].get("market", "") + if len(event.get("competitions")[0].get("broadcasts")) > 0 + else "" + ) + event.get("competitions")[0]["broadcast_name"] = ( + event.get("competitions")[0].get("broadcasts", [])[0].get("names", [])[0] + if len(event.get("competitions")[0].get("broadcasts")) > 0 + else "" + ) + event.get("competitions")[0].pop("broadcasts", None) + event.get("competitions")[0].pop("notes", None) + event.get("competitions")[0].pop("competitors", None) + return event + + def __extract_home_away(event, arg1, arg2): event["competitions"][0][arg2] = event.get("competitions")[0].get("competitors")[arg1].get("team") event["competitions"][0][arg2]["score"] = event.get("competitions")[0].get("competitors")[arg1].get("score") diff --git a/sportsdataverse/nfl/nfl_schedule.py b/sportsdataverse/nfl/nfl_schedule.py index c6d4103..55667ba 100755 --- a/sportsdataverse/nfl/nfl_schedule.py +++ b/sportsdataverse/nfl/nfl_schedule.py @@ -3,7 +3,7 @@ import pandas as pd import polars as pl -from sportsdataverse.dl_utils import download, underscore +from sportsdataverse.dl_utils import download def espn_nfl_schedule( @@ -36,40 +36,7 @@ def espn_nfl_schedule( return pd.DataFrame() if return_as_pandas else pl.DataFrame() for event in events: - event.get("competitions")[0].get("competitors")[0].get("team").pop("links", None) - event.get("competitions")[0].get("competitors")[1].get("team").pop("links", None) - if event.get("competitions")[0].get("competitors")[0].get("homeAway") == "home": - event = _extract_home_away(event, 0, "home") - event = _extract_home_away(event, 1, "away") - else: - event = _extract_home_away(event, 0, "away") - event = _extract_home_away(event, 1, "home") - del_keys = ["geoBroadcasts", "headlines", "series", "situation", "tickets", "odds", "leaders"] - for k in del_keys: - event.get("competitions")[0].pop(k, None) - event.get("competitions")[0]["notes_type"] = ( - event.get("competitions")[0]["notes"][0].get("type") - if len(event.get("competitions")[0]["notes"]) > 0 - else "" - ) - event.get("competitions")[0]["notes_headline"] = ( - event.get("competitions")[0]["notes"][0].get("headline").replace('"', "") - if len(event.get("competitions")[0]["notes"]) > 0 - else "" - ) - event.get("competitions")[0]["broadcast_market"] = ( - event.get("competitions")[0].get("broadcasts", [])[0].get("market", "") - if len(event.get("competitions")[0].get("broadcasts")) > 0 - else "" - ) - event.get("competitions")[0]["broadcast_name"] = ( - event.get("competitions")[0].get("broadcasts", [])[0].get("names", [])[0] - if len(event.get("competitions")[0].get("broadcasts")) > 0 - else "" - ) - event.get("competitions")[0].pop("broadcasts", None) - event.get("competitions")[0].pop("notes", None) - event.get("competitions")[0].pop("competitors", None) + event = scoreboard_event_parsing(event) x = pl.from_pandas(pd.json_normalize(event.get("competitions")[0], sep="_")) x = x.with_columns( game_id=(pl.col("id").cast(pl.Int32)), @@ -90,12 +57,48 @@ def espn_nfl_schedule( x = x[[s.name for s in x if s.null_count() != x.height]] ev = pd.concat([ev, x.to_pandas()], axis=0, ignore_index=True) ev = pl.from_pandas(ev) - ev.columns = [underscore(c) for c in ev.columns] + ev = ev.janitor.clean_names() return ev.to_pandas() if return_as_pandas else ev -def _extract_home_away(event, arg1, arg2): +def scoreboard_event_parsing(event): + event.get("competitions")[0].get("competitors")[0].get("team").pop("links", None) + event.get("competitions")[0].get("competitors")[1].get("team").pop("links", None) + if event.get("competitions")[0].get("competitors")[0].get("homeAway") == "home": + event = __extract_home_away(event, 0, "home") + event = __extract_home_away(event, 1, "away") + else: + event = __extract_home_away(event, 0, "away") + event = __extract_home_away(event, 1, "home") + del_keys = ["geoBroadcasts", "headlines", "series", "situation", "tickets", "odds", "leaders"] + for k in del_keys: + event.get("competitions")[0].pop(k, None) + event.get("competitions")[0]["notes_type"] = ( + event.get("competitions")[0]["notes"][0].get("type") if len(event.get("competitions")[0]["notes"]) > 0 else "" + ) + event.get("competitions")[0]["notes_headline"] = ( + event.get("competitions")[0]["notes"][0].get("headline").replace('"', "") + if len(event.get("competitions")[0]["notes"]) > 0 + else "" + ) + event.get("competitions")[0]["broadcast_market"] = ( + event.get("competitions")[0].get("broadcasts", [])[0].get("market", "") + if len(event.get("competitions")[0].get("broadcasts")) > 0 + else "" + ) + event.get("competitions")[0]["broadcast_name"] = ( + event.get("competitions")[0].get("broadcasts", [])[0].get("names", [])[0] + if len(event.get("competitions")[0].get("broadcasts")) > 0 + else "" + ) + event.get("competitions")[0].pop("broadcasts", None) + event.get("competitions")[0].pop("notes", None) + event.get("competitions")[0].pop("competitors", None) + return event + + +def __extract_home_away(event, arg1, arg2): event["competitions"][0][arg2] = event.get("competitions")[0].get("competitors")[arg1].get("team") event["competitions"][0][arg2]["score"] = event.get("competitions")[0].get("competitors")[arg1].get("score") event["competitions"][0][arg2]["winner"] = event.get("competitions")[0].get("competitors")[arg1].get("winner") @@ -168,7 +171,7 @@ def espn_nfl_calendar(season=None, ondays=None, return_as_pandas=False, **kwargs ) full_schedule = pl.concat([full_schedule, pl.from_pandas(reg)], how="vertical") full_schedule = full_schedule.with_columns(season=season) - full_schedule.columns = [underscore(c) for c in full_schedule.columns] + full_schedule = full_schedule.janitor.clean_names() full_schedule = full_schedule.rename({"week_value": "week", "season_type_value": "season_type"}) return full_schedule.to_pandas() if return_as_pandas else full_schedule diff --git a/sportsdataverse/nhl/nhl_schedule.py b/sportsdataverse/nhl/nhl_schedule.py index 9487b76..4a82c1a 100755 --- a/sportsdataverse/nhl/nhl_schedule.py +++ b/sportsdataverse/nhl/nhl_schedule.py @@ -3,7 +3,7 @@ import pandas as pd import polars as pl -from sportsdataverse.dl_utils import download, underscore +from sportsdataverse.dl_utils import download def espn_nhl_schedule(dates=None, season_type=None, limit=500, return_as_pandas=False, **kwargs) -> pl.DataFrame: @@ -33,39 +33,7 @@ def espn_nhl_schedule(dates=None, season_type=None, limit=500, return_as_pandas= return pd.DataFrame() if return_as_pandas else pl.DataFrame() for event in events: - event.get("competitions")[0].get("competitors")[0].get("team").pop("links", None) - event.get("competitions")[0].get("competitors")[1].get("team").pop("links", None) - if event.get("competitions")[0].get("competitors")[0].get("homeAway") == "home": - event = __extract_home_away(event, 0, "home") - event = __extract_home_away(event, 1, "away") - else: - event = __extract_home_away(event, 0, "away") - event = __extract_home_away(event, 1, "home") - - event.get("competitions")[0]["notes_type"] = ( - event.get("competitions")[0]["notes"][0].get("type") - if len(event.get("competitions")[0]["notes"]) > 0 - else "" - ) - event.get("competitions")[0]["notes_headline"] = ( - event.get("competitions")[0]["notes"][0].get("headline").replace('"', "") - if len(event.get("competitions")[0]["notes"]) > 0 - else "" - ) - event.get("competitions")[0]["broadcast_market"] = ( - event.get("competitions")[0].get("broadcasts", [])[0].get("market", "") - if len(event.get("competitions")[0].get("broadcasts")) > 0 - else "" - ) - event.get("competitions")[0]["broadcast_name"] = ( - event.get("competitions")[0].get("broadcasts", [])[0].get("names", [])[0] - if len(event.get("competitions")[0].get("broadcasts")) > 0 - else "" - ) - del_keys = ["geoBroadcasts", "headlines", "series", "situation", "tickets", "odds", "leaders"] - for k in del_keys: - event.get("competitions")[0].pop(k, None) - + event = scoreboard_event_parsing(event) x = pl.from_pandas(pd.json_normalize(event.get("competitions")[0], sep="_")) x = x.with_columns( game_id=(pl.col("id").cast(pl.Int32)), @@ -84,11 +52,47 @@ def espn_nhl_schedule(dates=None, season_type=None, limit=500, return_as_pandas= x = x[[s.name for s in x if s.null_count() != x.height]] ev = pd.concat([ev, x.to_pandas()], axis=0, ignore_index=True) ev = pl.from_pandas(ev) - ev.columns = [underscore(c) for c in ev.columns] + ev = ev.janitor.clean_names() return ev.to_pandas() if return_as_pandas else ev +def scoreboard_event_parsing(event): + event.get("competitions")[0].get("competitors")[0].get("team").pop("links", None) + event.get("competitions")[0].get("competitors")[1].get("team").pop("links", None) + if event.get("competitions")[0].get("competitors")[0].get("homeAway") == "home": + event = __extract_home_away(event, 0, "home") + event = __extract_home_away(event, 1, "away") + else: + event = __extract_home_away(event, 0, "away") + event = __extract_home_away(event, 1, "home") + del_keys = ["geoBroadcasts", "headlines", "series", "situation", "tickets", "odds", "leaders"] + for k in del_keys: + event.get("competitions")[0].pop(k, None) + event.get("competitions")[0]["notes_type"] = ( + event.get("competitions")[0]["notes"][0].get("type") if len(event.get("competitions")[0]["notes"]) > 0 else "" + ) + event.get("competitions")[0]["notes_headline"] = ( + event.get("competitions")[0]["notes"][0].get("headline").replace('"', "") + if len(event.get("competitions")[0]["notes"]) > 0 + else "" + ) + event.get("competitions")[0]["broadcast_market"] = ( + event.get("competitions")[0].get("broadcasts", [])[0].get("market", "") + if len(event.get("competitions")[0].get("broadcasts")) > 0 + else "" + ) + event.get("competitions")[0]["broadcast_name"] = ( + event.get("competitions")[0].get("broadcasts", [])[0].get("names", [])[0] + if len(event.get("competitions")[0].get("broadcasts")) > 0 + else "" + ) + event.get("competitions")[0].pop("broadcasts", None) + event.get("competitions")[0].pop("notes", None) + event.get("competitions")[0].pop("competitors", None) + return event + + def __extract_home_away(event, arg1, arg2): event["competitions"][0][arg2] = event.get("competitions")[0].get("competitors")[arg1].get("team") event["competitions"][0][arg2]["score"] = event.get("competitions")[0].get("competitors")[arg1].get("score") diff --git a/sportsdataverse/wbb/wbb_schedule.py b/sportsdataverse/wbb/wbb_schedule.py index 031fd57..3ec4008 100755 --- a/sportsdataverse/wbb/wbb_schedule.py +++ b/sportsdataverse/wbb/wbb_schedule.py @@ -3,7 +3,7 @@ import pandas as pd import polars as pl -from sportsdataverse.dl_utils import download, underscore +from sportsdataverse.dl_utils import download from sportsdataverse.errors import SeasonNotFoundError @@ -40,40 +40,7 @@ def espn_wbb_schedule( return pd.DataFrame() if return_as_pandas else pl.DataFrame() for event in events: - event.get("competitions")[0].get("competitors")[0].get("team").pop("links", None) - event.get("competitions")[0].get("competitors")[1].get("team").pop("links", None) - if event.get("competitions")[0].get("competitors")[0].get("homeAway") == "home": - event = __extract_home_away(event, 0, "home") - event = __extract_home_away(event, 1, "away") - else: - event = __extract_home_away(event, 0, "away") - event = __extract_home_away(event, 1, "home") - del_keys = ["geoBroadcasts", "headlines", "series", "situation", "tickets", "odds", "leaders"] - for k in del_keys: - event.get("competitions")[0].pop(k, None) - event.get("competitions")[0]["notes_type"] = ( - event.get("competitions")[0]["notes"][0].get("type") - if len(event.get("competitions")[0]["notes"]) > 0 - else "" - ) - event.get("competitions")[0]["notes_headline"] = ( - event.get("competitions")[0]["notes"][0].get("headline").replace('"', "") - if len(event.get("competitions")[0]["notes"]) > 0 - else "" - ) - event.get("competitions")[0]["broadcast_market"] = ( - event.get("competitions")[0].get("broadcasts", [])[0].get("market", "") - if len(event.get("competitions")[0].get("broadcasts")) > 0 - else "" - ) - event.get("competitions")[0]["broadcast_name"] = ( - event.get("competitions")[0].get("broadcasts", [])[0].get("names", [])[0] - if len(event.get("competitions")[0].get("broadcasts")) > 0 - else "" - ) - event.get("competitions")[0].pop("broadcasts", None) - event.get("competitions")[0].pop("notes", None) - event.get("competitions")[0].pop("competitors", None) + event = scoreboard_event_parsing(event) x = pl.from_pandas(pd.json_normalize(event.get("competitions")[0], sep="_")) x = x.with_columns( game_id=(pl.col("id").cast(pl.Int32)), @@ -92,11 +59,47 @@ def espn_wbb_schedule( x = x[[s.name for s in x if s.null_count() != x.height]] ev = pd.concat([ev, x.to_pandas()], axis=0, ignore_index=True) ev = pl.from_pandas(ev) - ev.columns = [underscore(c) for c in ev.columns] + ev = ev.janitor.clean_names() return ev.to_pandas() if return_as_pandas else ev +def scoreboard_event_parsing(event): + event.get("competitions")[0].get("competitors")[0].get("team").pop("links", None) + event.get("competitions")[0].get("competitors")[1].get("team").pop("links", None) + if event.get("competitions")[0].get("competitors")[0].get("homeAway") == "home": + event = __extract_home_away(event, 0, "home") + event = __extract_home_away(event, 1, "away") + else: + event = __extract_home_away(event, 0, "away") + event = __extract_home_away(event, 1, "home") + del_keys = ["geoBroadcasts", "headlines", "series", "situation", "tickets", "odds", "leaders"] + for k in del_keys: + event.get("competitions")[0].pop(k, None) + event.get("competitions")[0]["notes_type"] = ( + event.get("competitions")[0]["notes"][0].get("type") if len(event.get("competitions")[0]["notes"]) > 0 else "" + ) + event.get("competitions")[0]["notes_headline"] = ( + event.get("competitions")[0]["notes"][0].get("headline").replace('"', "") + if len(event.get("competitions")[0]["notes"]) > 0 + else "" + ) + event.get("competitions")[0]["broadcast_market"] = ( + event.get("competitions")[0].get("broadcasts", [])[0].get("market", "") + if len(event.get("competitions")[0].get("broadcasts")) > 0 + else "" + ) + event.get("competitions")[0]["broadcast_name"] = ( + event.get("competitions")[0].get("broadcasts", [])[0].get("names", [])[0] + if len(event.get("competitions")[0].get("broadcasts")) > 0 + else "" + ) + event.get("competitions")[0].pop("broadcasts", None) + event.get("competitions")[0].pop("notes", None) + event.get("competitions")[0].pop("competitors", None) + return event + + def __extract_home_away(event, arg1, arg2): event["competitions"][0][arg2] = event.get("competitions")[0].get("competitors")[arg1].get("team") event["competitions"][0][arg2]["score"] = event.get("competitions")[0].get("competitors")[arg1].get("score") diff --git a/sportsdataverse/wnba/wnba_schedule.py b/sportsdataverse/wnba/wnba_schedule.py index 08d570d..5169baf 100755 --- a/sportsdataverse/wnba/wnba_schedule.py +++ b/sportsdataverse/wnba/wnba_schedule.py @@ -3,7 +3,7 @@ import pandas as pd import polars as pl -from sportsdataverse.dl_utils import download, underscore +from sportsdataverse.dl_utils import download def espn_wnba_schedule(dates=None, season_type=None, limit=500, return_as_pandas=False, **kwargs) -> pl.DataFrame: @@ -30,39 +30,7 @@ def espn_wnba_schedule(dates=None, season_type=None, limit=500, return_as_pandas return pd.DataFrame() if return_as_pandas else pl.DataFrame() for event in events: - event.get("competitions")[0].get("competitors")[0].get("team").pop("links", None) - event.get("competitions")[0].get("competitors")[1].get("team").pop("links", None) - if event.get("competitions")[0].get("competitors")[0].get("homeAway") == "home": - event = __extract_home_away(event, 0, "home") - event = __extract_home_away(event, 1, "away") - else: - event = __extract_home_away(event, 0, "away") - event = __extract_home_away(event, 1, "home") - - event.get("competitions")[0]["notes_type"] = ( - event.get("competitions")[0]["notes"][0].get("type") - if len(event.get("competitions")[0]["notes"]) > 0 - else "" - ) - event.get("competitions")[0]["notes_headline"] = ( - event.get("competitions")[0]["notes"][0].get("headline").replace('"', "") - if len(event.get("competitions")[0]["notes"]) > 0 - else "" - ) - event.get("competitions")[0]["broadcast_market"] = ( - event.get("competitions")[0].get("broadcasts", [])[0].get("market", "") - if len(event.get("competitions")[0].get("broadcasts")) > 0 - else "" - ) - event.get("competitions")[0]["broadcast_name"] = ( - event.get("competitions")[0].get("broadcasts", [])[0].get("names", [])[0] - if len(event.get("competitions")[0].get("broadcasts")) > 0 - else "" - ) - del_keys = ["geoBroadcasts", "headlines", "series", "situation", "tickets", "odds", "leaders"] - for k in del_keys: - event.get("competitions")[0].pop(k, None) - + event = scoreboard_event_parsing(event) x = pl.from_pandas(pd.json_normalize(event.get("competitions")[0], sep="_")) x = x.with_columns( game_id=(pl.col("id").cast(pl.Int32)), @@ -81,11 +49,47 @@ def espn_wnba_schedule(dates=None, season_type=None, limit=500, return_as_pandas x = x[[s.name for s in x if s.null_count() != x.height]] ev = pd.concat([ev, x.to_pandas()], axis=0, ignore_index=True) ev = pl.from_pandas(ev) - ev.columns = [underscore(c) for c in ev.columns] + ev = ev.janitor.clean_names() return ev.to_pandas() if return_as_pandas else ev +def scoreboard_event_parsing(event): + event.get("competitions")[0].get("competitors")[0].get("team").pop("links", None) + event.get("competitions")[0].get("competitors")[1].get("team").pop("links", None) + if event.get("competitions")[0].get("competitors")[0].get("homeAway") == "home": + event = __extract_home_away(event, 0, "home") + event = __extract_home_away(event, 1, "away") + else: + event = __extract_home_away(event, 0, "away") + event = __extract_home_away(event, 1, "home") + del_keys = ["geoBroadcasts", "headlines", "series", "situation", "tickets", "odds", "leaders"] + for k in del_keys: + event.get("competitions")[0].pop(k, None) + event.get("competitions")[0]["notes_type"] = ( + event.get("competitions")[0]["notes"][0].get("type") if len(event.get("competitions")[0]["notes"]) > 0 else "" + ) + event.get("competitions")[0]["notes_headline"] = ( + event.get("competitions")[0]["notes"][0].get("headline").replace('"', "") + if len(event.get("competitions")[0]["notes"]) > 0 + else "" + ) + event.get("competitions")[0]["broadcast_market"] = ( + event.get("competitions")[0].get("broadcasts", [])[0].get("market", "") + if len(event.get("competitions")[0].get("broadcasts")) > 0 + else "" + ) + event.get("competitions")[0]["broadcast_name"] = ( + event.get("competitions")[0].get("broadcasts", [])[0].get("names", [])[0] + if len(event.get("competitions")[0].get("broadcasts")) > 0 + else "" + ) + event.get("competitions")[0].pop("broadcasts", None) + event.get("competitions")[0].pop("notes", None) + event.get("competitions")[0].pop("competitors", None) + return event + + def __extract_home_away(event, arg1, arg2): event["competitions"][0][arg2] = event.get("competitions")[0].get("competitors")[arg1].get("team") event["competitions"][0][arg2]["score"] = event.get("competitions")[0].get("competitors")[arg1].get("score") From e45322d8b2a71d7665058d5baf3853fd070809fb Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Tue, 8 Aug 2023 11:12:32 -0400 Subject: [PATCH 79/79] add corrupt pbp check for completed games --- sportsdataverse/cfb/cfb_pbp.py | 61 ++++++++++++++++++++++++++++++---- sportsdataverse/nfl/nfl_pbp.py | 48 ++++++++++++++++++++++++-- 2 files changed, 100 insertions(+), 9 deletions(-) diff --git a/sportsdataverse/cfb/cfb_pbp.py b/sportsdataverse/cfb/cfb_pbp.py index f37a037..927006e 100755 --- a/sportsdataverse/cfb/cfb_pbp.py +++ b/sportsdataverse/cfb/cfb_pbp.py @@ -49,7 +49,7 @@ qbr_model = Booster({"nthread": 4}) # init model qbr_model.load_model(qbr_model_file) -logger = logging.getLogger(__name__) +logger = logging.getLogger("sdv.cfb_pbp") logger.addHandler(logging.NullHandler()) @@ -170,6 +170,10 @@ def cfb_pbp_disk(self): self.json = pbp_txt return self.json + def cfb_pbp_json(self, **kwargs): + self.json = json + return self.json + def __helper_cfb_pbp_drives(self, pbp_txt): pbp_txt, init = self.__helper_cfb_pbp(pbp_txt) @@ -219,7 +223,7 @@ def __helper_cfb_pbp_features(self, pbp_txt, init): meta_prefix="drive.", errors="ignore", ) - pbp_txt["plays"] = pl.concat([pbp_txt["plays"], pl.from_pandas(prev_drives)], how="vertical") + pbp_txt["plays"] = pl.concat([pbp_txt["plays"], pl.from_pandas(prev_drives)], how="diagonal") pbp_txt["timeouts"] = { init["homeTeamId"]: {"1": [], "2": []}, @@ -229,6 +233,16 @@ def __helper_cfb_pbp_features(self, pbp_txt, init): logging.debug(f"{self.gameId}: plays_df length - {len(pbp_txt['plays'])}") if len(pbp_txt["plays"]) == 0: return pbp_txt + if (len(pbp_txt["plays"]) < 50) and ( + pbp_txt.get("header").get("competitions")[0].get("status").get("type").get("completed") == True + ): + logging.debug(f"{self.gameId}: appear to be too few plays ({len(pbp_txt['plays'])}) for a completed game") + return pbp_txt + if (len(pbp_txt["plays"]) > 500) and ( + pbp_txt.get("header").get("competitions")[0].get("status").get("type").get("completed") == True + ): + logging.debug(f"{self.gameId}: appear to be too many plays ({len(pbp_txt['plays'])}) for a completed game") + return pbp_txt pbp_txt["plays"] = ( pbp_txt["plays"] .with_columns( @@ -4532,9 +4546,14 @@ def run_processing_pipeline(self): } self.json = pbp_json self.plays_json = pbp_txt["plays"] - if ( - pbp_json.get("header").get("competitions")[0].get("playByPlaySource") != "none" - and len(pbp_txt["plays"]) > 0 + + confirmed_corrupt = self.corrupt_pbp_check() + + if confirmed_corrupt: + return self.json if self.return_keys is None else {k: self.json.get(f"{k}") for k in self.return_keys} + + if (pbp_json.get("header").get("competitions")[0].get("playByPlaySource") != "none") and ( + len(pbp_txt["drives"]) > 0 ): self.plays_json = ( self.plays_json.pipe(self.__add_downs_data) @@ -4554,7 +4573,7 @@ def run_processing_pipeline(self): .pipe(self.__process_qbr) ) self.ran_pipeline = True - advBoxScore = self.create_box_score(self.plays_json) + advBoxScore = self.plays_json.pipe(self.create_box_score) self.plays_json = self.plays_json.to_dicts() pbp_json = { "gameId": int(self.gameId), @@ -4616,9 +4635,15 @@ def run_cleaning_pipeline(self): } self.json = pbp_json self.plays_json = pbp_txt["plays"] + + confirmed_corrupt = self.corrupt_pbp_check() + + if confirmed_corrupt: + return self.json if self.return_keys is None else {k: self.json.get(f"{k}") for k in self.return_keys} + if ( pbp_json.get("header").get("competitions")[0].get("playByPlaySource") != "none" - and len(pbp_txt["plays"]) > 0 + and len(pbp_txt["drives"]) > 0 ): self.plays_json = ( self.plays_json.pipe(self.__add_downs_data) @@ -4661,3 +4686,25 @@ def run_cleaning_pipeline(self): self.json = pbp_json self.ran_cleaning_pipeline = True return self.json + + def corrupt_pbp_check(self): + if len(self.json["plays"]) == 0: + logging.debug( + f"{self.gameId}: appear to be too no plays available ({len(self.json['plays'])}). run_processing_pipeline did not run" + ) + return True + if (len(self.json["plays"]) < 50) and ( + self.json.get("header").get("competitions")[0].get("status").get("type").get("completed") == True + ): + logging.debug( + f"{self.gameId}: appear to be too few plays ({len(self.json['plays'])}) for a completed game. run_processing_pipeline did not run" + ) + return True + if (len(self.json["plays"]) > 500) and ( + self.json.get("header").get("competitions")[0].get("status").get("type").get("completed") == True + ): + logging.debug( + f"{self.gameId}: appear to be too many plays ({len(self.json['plays'])}) for a completed game. run_processing_pipeline did not run" + ) + return True + return False diff --git a/sportsdataverse/nfl/nfl_pbp.py b/sportsdataverse/nfl/nfl_pbp.py index 88b585e..6e622d0 100755 --- a/sportsdataverse/nfl/nfl_pbp.py +++ b/sportsdataverse/nfl/nfl_pbp.py @@ -56,7 +56,7 @@ qbr_model = Booster({"nthread": 4}) # init model qbr_model.load_model(qbr_model_file) -logger = logging.getLogger(__name__) +logger = logging.getLogger("sdv.nfl_pbp") logger.addHandler(logging.NullHandler()) @@ -224,7 +224,7 @@ def __helper_nfl_pbp_features(self, pbp_txt, init): meta_prefix="drive.", errors="ignore", ) - pbp_txt["plays"] = pl.concat([pbp_txt["plays"], pl.from_pandas(prev_drives)], how="vertical") + pbp_txt["plays"] = pl.concat([pbp_txt["plays"], pl.from_pandas(prev_drives)], how="diagonal") pbp_txt["timeouts"] = { init["homeTeamId"]: {"1": [], "2": []}, @@ -234,6 +234,16 @@ def __helper_nfl_pbp_features(self, pbp_txt, init): logging.debug(f"{self.gameId}: plays_df length - {len(pbp_txt['plays'])}") if len(pbp_txt["plays"]) == 0: return pbp_txt + if (len(pbp_txt["plays"]) < 50) and ( + pbp_txt.get("header").get("competitions")[0].get("status").get("type").get("completed") == True + ): + logging.debug(f"{self.gameId}: appear to be too few plays ({len(pbp_txt['plays'])}) for a completed game") + return pbp_txt + if (len(pbp_txt["plays"]) > 500) and ( + pbp_txt.get("header").get("competitions")[0].get("status").get("type").get("completed") == True + ): + logging.debug(f"{self.gameId}: appear to be too many plays ({len(pbp_txt['plays'])}) for a completed game") + return pbp_txt pbp_txt["plays"] = ( pbp_txt["plays"] @@ -4538,6 +4548,12 @@ def run_processing_pipeline(self): } self.json = pbp_json self.plays_json = pbp_txt["plays"] + + confirmed_corrupt = self.corrupt_pbp_check() + + if confirmed_corrupt: + return self.json if self.return_keys is None else {k: self.json.get(f"{k}") for k in self.return_keys} + if ( pbp_json.get("header").get("competitions")[0].get("playByPlaySource") != "none" and len(pbp_txt["plays"]) > 0 @@ -4622,6 +4638,12 @@ def run_cleaning_pipeline(self): } self.json = pbp_json self.plays_json = pbp_txt["plays"] + + confirmed_corrupt = self.corrupt_pbp_check() + + if confirmed_corrupt: + return self.json if self.return_keys is None else {k: self.json.get(f"{k}") for k in self.return_keys} + if pbp_json.get("header").get("competitions")[0].get("playByPlaySource") != "none": self.plays_json = ( self.plays_json.pipe(self.__add_downs_data) @@ -4664,3 +4686,25 @@ def run_cleaning_pipeline(self): self.json = pbp_json self.ran_cleaning_pipeline = True return self.json + + def corrupt_pbp_check(self): + if len(self.json["plays"]) == 0: + logging.debug( + f"{self.gameId}: appear to be too no plays available ({len(self.json['plays'])}). run_processing_pipeline did not run" + ) + return True + if (len(self.json["plays"]) < 50) and ( + self.json.get("header").get("competitions")[0].get("status").get("type").get("completed") == True + ): + logging.debug( + f"{self.gameId}: appear to be too few plays ({len(self.json['plays'])}) for a completed game. run_processing_pipeline did not run" + ) + return True + if (len(self.json["plays"]) > 500) and ( + self.json.get("header").get("competitions")[0].get("status").get("type").get("completed") == True + ): + logging.debug( + f"{self.gameId}: appear to be too many plays ({len(self.json['plays'])}) for a completed game. run_processing_pipeline did not run" + ) + return True + return False

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 c*!Tyi2F0O~s`Y^1yKrZ!7R!tLeyTRt<8 delta 6731 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 J=R35Y{Xb!65e)zU diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.nfl.md b/Sphinx-docs/_build/markdown/sportsdataverse.nfl.md index af8e011..c028450 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.nfl.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.nfl.md @@ -502,17 +502,17 @@ Returns: ## sportsdataverse.nfl.nfl_pbp module -### class sportsdataverse.nfl.nfl_pbp.NFLPlayProcess(gameId=0, raw=False, path_to_json='/') +### class sportsdataverse.nfl.nfl_pbp.NFLPlayProcess(gameId=0, raw=False, path_to_json='/', return_keys=None, \*\*kwargs) Bases: `object` -#### \__init__(gameId=0, raw=False, path_to_json='/') +#### \__init__(gameId=0, raw=False, path_to_json='/', return_keys=None, \*\*kwargs) Initialize self. See help(type(self)) for accurate signature. #### create_box_score(play_df) -#### espn_nfl_pbp() +#### espn_nfl_pbp(\*\*kwargs) espn_nfl_pbp() - Pull the game by id. Data from API endpoints: nfl/playbyplay, nfl/summary Args: @@ -544,6 +544,8 @@ Example: #### raw( = False) +#### return_keys( = None) + #### run_cleaning_pipeline() #### run_processing_pipeline() diff --git a/docs/docs/nfl/index.md b/docs/docs/nfl/index.md index af8e011..c028450 100755 --- a/docs/docs/nfl/index.md +++ b/docs/docs/nfl/index.md @@ -502,17 +502,17 @@ Returns: ## sportsdataverse.nfl.nfl_pbp module -### class sportsdataverse.nfl.nfl_pbp.NFLPlayProcess(gameId=0, raw=False, path_to_json='/') +### class sportsdataverse.nfl.nfl_pbp.NFLPlayProcess(gameId=0, raw=False, path_to_json='/', return_keys=None, \*\*kwargs) Bases: `object` -#### \__init__(gameId=0, raw=False, path_to_json='/') +#### \__init__(gameId=0, raw=False, path_to_json='/', return_keys=None, \*\*kwargs) Initialize self. See help(type(self)) for accurate signature. #### create_box_score(play_df) -#### espn_nfl_pbp() +#### espn_nfl_pbp(\*\*kwargs) espn_nfl_pbp() - Pull the game by id. Data from API endpoints: nfl/playbyplay, nfl/summary Args: @@ -544,6 +544,8 @@ Example: #### raw( = False) +#### return_keys( = None) + #### run_cleaning_pipeline() #### run_processing_pipeline() diff --git a/sportsdataverse/nfl/nfl_pbp.py b/sportsdataverse/nfl/nfl_pbp.py index ea7bac6..e1cd57e 100755 --- a/sportsdataverse/nfl/nfl_pbp.py +++ b/sportsdataverse/nfl/nfl_pbp.py @@ -63,16 +63,18 @@ class NFLPlayProcess(object): ran_cleaning_pipeline = False raw = False path_to_json = "/" + return_keys = None - def __init__(self, gameId=0, raw=False, path_to_json="/"): + def __init__(self, gameId=0, raw=False, path_to_json="/", return_keys=None, **kwargs): self.gameId = int(gameId) # self.logger = logger self.ran_pipeline = False self.ran_cleaning_pipeline = False self.raw = raw self.path_to_json = path_to_json + self.return_keys = return_keys - def espn_nfl_pbp(self): + def espn_nfl_pbp(self, **kwargs): """espn_nfl_pbp() - Pull the game by id. Data from API endpoints: `nfl/playbyplay`, `nfl/summary` Args: @@ -88,8 +90,7 @@ def espn_nfl_pbp(self): `nfl_df = sportsdataverse.nfl.NFLPlayProcess(gameId=401220403).espn_nfl_pbp()` """ cache_buster = int(time.time() * 1000) - pbp_txt = {} - pbp_txt["timeouts"] = {} + pbp_txt = {"timeouts": {}} # summary endpoint for pickcenter array summary_url = ( f"http://site.api.espn.com/apis/site/v2/sports/football/nfl/summary?event={self.gameId}&{cache_buster}" From 5b21788d1c30c7a8a64f9ad4e4b036c7a719068e Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Mon, 31 Jul 2023 17:19:45 -0400 Subject: [PATCH 58/79] removing unused vars --- sportsdataverse/cfb/cfb_pbp.py | 23 ++++++++++------------- sportsdataverse/mbb/mbb_pbp.py | 2 +- sportsdataverse/nba/nba_pbp.py | 26 +++++++++++++------------- sportsdataverse/nfl/nfl_pbp.py | 23 ++++++++++------------- sportsdataverse/wbb/wbb_pbp.py | 2 +- sportsdataverse/wnba/wnba_pbp.py | 26 +++++++++++++------------- 6 files changed, 48 insertions(+), 54 deletions(-) diff --git a/sportsdataverse/cfb/cfb_pbp.py b/sportsdataverse/cfb/cfb_pbp.py index 804b8ee..1be539a 100755 --- a/sportsdataverse/cfb/cfb_pbp.py +++ b/sportsdataverse/cfb/cfb_pbp.py @@ -109,16 +109,16 @@ def espn_cfb_pbp(self, **kwargs): "standings", ] dict_keys_expected = ["boxscore", "format", "gameInfo", "drives", "predictor", "header", "standings"] - array_keys_expected = [ - "leaders", - "broadcasts", - "pickcenter", - "againstTheSpread", - "odds", - "winprobability", - "scoringPlays", - "videos", - ] + # array_keys_expected = [ + # "leaders", + # "broadcasts", + # "pickcenter", + # "againstTheSpread", + # "odds", + # "winprobability", + # "scoringPlays", + # "videos", + # ] if self.raw == True: # reorder keys in raw format, appending empty keys which are defined later to the end pbp_json = {} @@ -3198,7 +3198,6 @@ def __process_epa(self, play_df): EP_end=pl.lit(EP_end), ) - kick = "kick)" play_df = ( play_df.with_columns( EP_start=pl.when( @@ -3730,7 +3729,6 @@ def __process_wpa(self, play_df): return play_df def __add_drive_data(self, play_df): - base_groups = play_df.groupby(["drive.id"]) play_df = ( play_df.with_columns( drive_start=pl.when(pl.col("start.pos_team.id") == pl.col("homeTeamId")) @@ -3865,7 +3863,6 @@ def create_box_score(self, play_df): & (pl.col("scrimmage_play") == True) & (pl.col("athlete_name").is_in(qbs_list)) ) - pass_qbr_box_df = pass_qbr_box.groupby(by=["pos_team", "athlete_name"]) pass_qbr = ( pass_qbr_box.groupby(by=["pos_team", "athlete_name"]) .agg( diff --git a/sportsdataverse/mbb/mbb_pbp.py b/sportsdataverse/mbb/mbb_pbp.py index c6a7aa3..dfc3798 100755 --- a/sportsdataverse/mbb/mbb_pbp.py +++ b/sportsdataverse/mbb/mbb_pbp.py @@ -67,7 +67,7 @@ def espn_mbb_pbp(game_id: int, raw=False, **kwargs) -> Dict: "espnWP", "leaders", ] - array_keys_expected = ["boxscore", "format", "gameInfo", "predictor", "article", "header", "season", "standings"] + # array_keys_expected = ["boxscore", "format", "gameInfo", "predictor", "article", "header", "season", "standings"] if raw == True: # reorder keys in raw format, appending empty keys which are defined later to the end pbp_json = {} diff --git a/sportsdataverse/nba/nba_pbp.py b/sportsdataverse/nba/nba_pbp.py index 764caa5..fc3407f 100755 --- a/sportsdataverse/nba/nba_pbp.py +++ b/sportsdataverse/nba/nba_pbp.py @@ -56,19 +56,19 @@ def espn_nba_pbp(game_id: int, raw=False, **kwargs) -> Dict: "timeouts", ] dict_keys_expected = ["boxscore", "format", "gameInfo", "predictor", "article", "header", "season", "standings"] - array_keys_expected = [ - "plays", - "seasonseries", - "videos", - "broadcasts", - "pickcenter", - "againstTheSpread", - "odds", - "winprobability", - "teamInfo", - "espnWP", - "leaders", - ] + # array_keys_expected = [ + # "plays", + # "seasonseries", + # "videos", + # "broadcasts", + # "pickcenter", + # "againstTheSpread", + # "odds", + # "winprobability", + # "teamInfo", + # "espnWP", + # "leaders", + # ] if raw == True: # reorder keys in raw format, appending empty keys which are defined later to the end pbp_json = {} diff --git a/sportsdataverse/nfl/nfl_pbp.py b/sportsdataverse/nfl/nfl_pbp.py index e1cd57e..5cd6705 100755 --- a/sportsdataverse/nfl/nfl_pbp.py +++ b/sportsdataverse/nfl/nfl_pbp.py @@ -115,16 +115,16 @@ def espn_nfl_pbp(self, **kwargs): "standings", ] dict_keys_expected = ["boxscore", "format", "gameInfo", "drives", "predictor", "header", "standings"] - array_keys_expected = [ - "leaders", - "broadcasts", - "pickcenter", - "againstTheSpread", - "odds", - "winprobability", - "scoringPlays", - "videos", - ] + # array_keys_expected = [ + # "leaders", + # "broadcasts", + # "pickcenter", + # "againstTheSpread", + # "odds", + # "winprobability", + # "scoringPlays", + # "videos", + # ] if self.raw == True: # reorder keys in raw format, appending empty keys which are defined later to the end pbp_json = {} @@ -3204,7 +3204,6 @@ def __process_epa(self, play_df): EP_end=pl.lit(EP_end), ) - kick = "kick)" play_df = ( play_df.with_columns( EP_start=pl.when( @@ -3736,7 +3735,6 @@ def __process_wpa(self, play_df): return play_df def __add_drive_data(self, play_df): - base_groups = play_df.groupby(["drive.id"]) play_df = ( play_df.with_columns( drive_start=pl.when(pl.col("start.pos_team.id") == pl.col("homeTeamId")) @@ -3871,7 +3869,6 @@ def create_box_score(self, play_df): & (pl.col("scrimmage_play") == True) & (pl.col("athlete_name").is_in(qbs_list)) ) - pass_qbr_box_df = pass_qbr_box.groupby(by=["pos_team", "athlete_name"]) pass_qbr = ( pass_qbr_box.groupby(by=["pos_team", "athlete_name"]) .agg( diff --git a/sportsdataverse/wbb/wbb_pbp.py b/sportsdataverse/wbb/wbb_pbp.py index fda5700..641c79f 100755 --- a/sportsdataverse/wbb/wbb_pbp.py +++ b/sportsdataverse/wbb/wbb_pbp.py @@ -67,7 +67,7 @@ def espn_wbb_pbp(game_id: int, raw=False, **kwargs) -> Dict: "espnWP", "leaders", ] - array_keys_expected = ["boxscore", "format", "gameInfo", "predictor", "article", "header", "season", "standings"] + # array_keys_expected = ["boxscore", "format", "gameInfo", "predictor", "article", "header", "season", "standings"] if raw == True: # reorder keys in raw format, appending empty keys which are defined later to the end pbp_json = {} diff --git a/sportsdataverse/wnba/wnba_pbp.py b/sportsdataverse/wnba/wnba_pbp.py index 1d89409..08a9636 100755 --- a/sportsdataverse/wnba/wnba_pbp.py +++ b/sportsdataverse/wnba/wnba_pbp.py @@ -55,19 +55,19 @@ def espn_wnba_pbp(game_id: int, raw=False, **kwargs) -> Dict: "timeouts", ] dict_keys_expected = ["boxscore", "format", "gameInfo", "predictor", "article", "header", "season", "standings"] - array_keys_expected = [ - "plays", - "seasonseries", - "videos", - "broadcasts", - "pickcenter", - "againstTheSpread", - "odds", - "winprobability", - "teamInfo", - "espnWP", - "leaders", - ] + # array_keys_expected = [ + # "plays", + # "seasonseries", + # "videos", + # "broadcasts", + # "pickcenter", + # "againstTheSpread", + # "odds", + # "winprobability", + # "teamInfo", + # "espnWP", + # "leaders", + # ] if raw == True: # reorder keys in raw format, appending empty keys which are defined later to the end pbp_json = {} From 892ec65492b8927f21d8c28440ea3f85d1edb20b Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Mon, 31 Jul 2023 21:14:13 -0400 Subject: [PATCH 59/79] updating schedule documentation per PR comments --- .../_build/doctrees/environment.pickle | Bin 935061 -> 930977 bytes .../doctrees/sportsdataverse.cfb.doctree | Bin 106736 -> 106811 bytes .../doctrees/sportsdataverse.mbb.doctree | Bin 85780 -> 85855 bytes .../doctrees/sportsdataverse.nba.doctree | Bin 85989 -> 86061 bytes .../doctrees/sportsdataverse.nfl.doctree | Bin 210375 -> 210450 bytes .../doctrees/sportsdataverse.nhl.doctree | Bin 102595 -> 102667 bytes .../doctrees/sportsdataverse.wbb.doctree | Bin 86255 -> 86330 bytes .../doctrees/sportsdataverse.wnba.doctree | Bin 84126 -> 84201 bytes Sphinx-docs/_build/doctrees/tests.cfb.doctree | Bin 3131 -> 3513 bytes Sphinx-docs/_build/doctrees/tests.mbb.doctree | Bin 3131 -> 3513 bytes .../_build/markdown/sportsdataverse.cfb.md | 4 ++-- .../_build/markdown/sportsdataverse.mbb.md | 5 ++--- .../_build/markdown/sportsdataverse.nba.md | 6 ++---- .../_build/markdown/sportsdataverse.nfl.md | 4 ++-- .../_build/markdown/sportsdataverse.nhl.md | 6 ++---- .../_build/markdown/sportsdataverse.wbb.md | 5 ++--- .../_build/markdown/sportsdataverse.wnba.md | 4 ++-- docs/docs/cfb/index.md | 4 ++-- docs/docs/mbb/index.md | 5 ++--- docs/docs/nba/index.md | 6 ++---- docs/docs/nfl/index.md | 4 ++-- docs/docs/nhl/index.md | 6 ++---- docs/docs/wbb/index.md | 5 ++--- docs/docs/wnba/index.md | 4 ++-- sportsdataverse/cfb/cfb_schedule.py | 4 ++-- sportsdataverse/mbb/mbb_schedule.py | 5 ++--- sportsdataverse/nba/nba_schedule.py | 6 ++---- sportsdataverse/nfl/nfl_schedule.py | 4 ++-- sportsdataverse/nhl/nhl_schedule.py | 6 ++---- sportsdataverse/wbb/wbb_schedule.py | 5 ++--- sportsdataverse/wnba/wnba_schedule.py | 4 ++-- 31 files changed, 42 insertions(+), 60 deletions(-) diff --git a/Sphinx-docs/_build/doctrees/environment.pickle b/Sphinx-docs/_build/doctrees/environment.pickle index 42c54880d84c1a8a5c24d6905b92834168b53097..8c866376cd3ba5441fdefa9b3cd8b6a4f3ebbe12 100755 GIT binary patch delta 63983 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 delta 64056 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 diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.cfb.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.cfb.doctree index 1e882ab8af0867203d6819174a6cb46d340515cc..a18488a96bf703a84895102262c9923abf0eea31 100755 GIT binary patch delta 303 zcmexxkZt!NwuUK;f=o&aw+b^F_zLCdxg?e(x)mknrdlZk=evmIS4ilosU`TX7X&H(h*seIBERqyki(f?s}KszPR(LSDW?I?x@( plN%c>r+4Qv=80tEHNh082~#rIW2a<@_2Ac!+XdT~=P`O30RX;(XioqD delta 93 zcmdmei0#8cwuUK;f=t&JY!zlS@SVQ4ol$dojxXb-=_@)QERE^?evDkxnLDA}?XUb8 ndE_SRZ8Di|o5u*2Wu4sCU^0D4E@R&GWgUz=xBty!v@-$#VJRYm diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.mbb.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.mbb.doctree index e5ceb7a537bf3100a0c833a49d63c3dd2c490522..3ee2e6f799023e660cb8e15c5a3c6a2ac9a95833 100755 GIT binary patch delta 379 zcmbO-kM;gM)`lsJyi8vHQ~4Pu3FYXyB$gz)6(#1TS}6qN=Oh+Qo);%RU4e;FW9lcy zT1L+41&oY3c$8^Qf5*tkiATY8HqH#`EH07B3s#CvPFRyQy_T8LnUQ<@L1xBgMm8Rx zT&>7t##qhi#jK1*Y}{bA)Bh+i>TbWm%D7KLwj?OEq_iln*ovzFhhyax7&Rmnpvn~d z^7B#^GSd|D@)gp7zAB!+(UQ?}y0$!Ho=7%66HIZMFeQUMc1ng=4_@uK9kAV9f$^*! E0CC=X=>Px# delta 171 zcmcaVk9EpC)`lsJyiEW7rt&jRnw&L3gOO{x0u!Ug)K84HKxP3Wqt4{iiBM6^>F*dB zIVT@n3FTjBV5QjPgf&^yYnd6H8F{uJWM*t;Wa95_(6g(dUC=9gl$e`pr4W#xlUOu4F;0B?Tt>#FY@8X= zS!p7ZCr(lo2`a#?P-FT5CPpqiCTUEUW@cQ^$T|HqGvf(1ZlI}CL8f+0S7c@U#>NBW zFBh4<(UMVfx*Hp#5mQoCw2M) zCPoEG1*m!jzx=#Zh0HXCynKappa+X52NsJ=jz6qAy<3*inUQPyd09p+k!+$2HN|P@ klnnORDH&ot#F>NJano1IF|KFh0)|Ga7})dO@{Cai06F532mk;8 delta 255 zcmZ3xfc5Eo)`lsJ^BL`?E?~SeIj3KPk!$)~M#iOVTp7|?X(E#+PEwtmIzbB}sxkcl z6C>B;L*-Ca8q=kj8P@~lPBSx}VB-Pmm5%l^h`!ZZjiP7T*b*5sVSv7sS2rOsd**Sm>FdzD_06njz6qA zeX%T~GaEP1xandb2QdauK3J}aO$SE?d+d}9u^wCs)TaNDV_d?>y?voPV~+s&;|4dxlV;(Tc$;y@lrIwTylg1Y1q#|HqUkl8sBRDK5RmQ!?0Nr(}rr;M6LKQ|or^|4bZ=oKw3^8Nguj$DhJe R-Iy&VD_HYwoyu%v3IK-Ocw+zn delta 141 zcmbRAhUfTeo`x32EljKKO?LRMIsM#yCXMO8?=h{KY;+pTGlDTRr{CJg$hAG@0h65U z^d>bXlj-aJGHFbnwA6CC#y_S!#-Qm3?U=Nuum8)W&N(H6J$6clSPzoe^qcz_xu-Y& cXZkz6U=5@2)LG0HoKqc48NgucC1wXx0Kp?VdH?_b diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.nhl.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.nhl.doctree index 5e3c96ec73b35b6e10ff6498c1648ff2e9f1cb41..83c9310830125322388600963fb4e32272bd91c8 100755 GIT binary patch delta 429 zcmX@SkgaX0O(`B6*mw=61Bsn?Yj@x9r!&cMXT^OC& zxPewMOHDp_xMRA6E8{mtp6UEKLLi}H%CxNz9dQJkESno^pR zI(?2Kqk^OYRK0>WECwuVPf4o)-(W7UVNl8L-c?i89#~ hr^!4sI7t?}=kv&&avmCZ4g~5CCepme&9P delta 264 zcmeBP#CCWgTf-E_N=B!xRg51UrbksVYA|w5?{{LiyCugLll;)%=q?V=Tl}umg$S6H|qK?R9n_H^W z+hT!6ft%pd7mY)1jMtA$Rc*ame02NtS Ai~s-t diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.wbb.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.wbb.doctree index a26d89c576d683ad72937ecb47e0f71d6236f3dc..4a4540e95ddae3f5950010ac20f3da9835c10ab0 100755 GIT binary patch delta 386 zcmaFAkagE0)`lsJ224zWQ;ire3+3p!B$gz)6(#1TS}6qN=Oh+Q&sSg+pRUKqxRjMM zLpm#XvcNwzk)Q%R3N>V=WU$9h$q?(ouWb4{1xD`a+nE@bFmg_hVP-tR$_=!j31q?K z1Cx`dZ(?Em#>N8_nj$i}aFXWq>#U4MY#=3yB9jwqbhq2EF&>nVEeT33DJ{w?w&KF! zCVNQ*s1gOg{Jd0!%ru3(e1&wN4~r)Y7K%>xKdLf)l00LcNH#75OmP|j^C(WOxb5B! I^u4120J>s;lmGw# delta 187 zcmdnBi1qzK)`lsJ229%nrW!F`o}4McJ3Zf;QJRr!x*j9rQdX`E>8#+%0{_$|=TFpv z2x?3|G|>pkoW6#MaXpY7!_0Vsl?SM{39ROgKU(UzC(dgV{+30k;#5XRj03(XUqdysSOc5G|?E$ O09nVqokfw+%m4r&jXa6~ diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.wnba.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.wnba.doctree index 0ab24dde6066b8eafc2a680ab0fbd8e15e790dbe..69d8a68a55e0a78dc3be4699f427e4644a171dc0 100755 GIT binary patch delta 316 zcmbO?k@e+7)`lsJYK*(5sxw{@%F%O4EJ<`LO3Y2QQV7V;Ni3TDaiaKic_v1U={}5% ztMDi=!Xu|Sy-tRaYx`OzMtKR@lAzR*(xSX#E3N|U)=iUU)R0tws#5UF&r4OvOjF3q zS4amst9UZwDbdLvCa6qLlxEBm$;M-bDNZw{WU$9h$q?(oryI8mw%5uqD(V0L##NJBCW3iJFox###Y~Kv5{%K4 qFRV14CJj{PD9yNha??a@5QlwA27Bz346(_L6LmoR?I&ay6?FiY8z6N6 diff --git a/Sphinx-docs/_build/doctrees/tests.cfb.doctree b/Sphinx-docs/_build/doctrees/tests.cfb.doctree index c688cc369753ab407f6c5d638f06aaab327c63ce..c1aa336ca6a267f5cf0c1f15b3dc9196b765a023 100755 GIT binary patch delta 445 zcmb7=F-yZx6orjxQYsd8Q>(Q_#1?6PfL(-wfw+)x~1nFl@)BhEeYJw=K8niDynb=eB;`M)pAR8w6`H zTwZV>U~^yZnyy0yp~p$Im$;pl9->z@k}t*>p^6>oIKmJEpQRkLLukJ%g2Y$G8mby} z4MWXGK2}DU75Xw&PYU5gLnI8E|pEpkguJhk)aqnrH4N^KczG$RW~_5uOv0E zq{ypko#C9}kP({^l~I;aT-w8#mzbMcQdy8%JOyM#onMBt iUq-B7Mwwq}h9XF$I)ewOUKgm|Eu%OiVRIT!Fe3mKnK8Hk diff --git a/Sphinx-docs/_build/doctrees/tests.mbb.doctree b/Sphinx-docs/_build/doctrees/tests.mbb.doctree index dbc0874a42c5835b9752290f276256d553db20dd..ddd06a71d7a7a5285b19ad1620e8d23e2b30c734 100755 GIT binary patch delta 445 zcmb7=F-yZx6orjxS}GQGQ>(Q_#EP^(z%D{TK_P?Pq#-8FvnODZ+Pr`r#5fc>yh0(* zpWq@QNI($(kgMS0=8IYtoE+|QIQQP~oTvH6yb>x`ce#7zT9(SujT|mwf+@CZkux6g zjZ1#c%TSO;M&r?ov~a*O#HC%E_*~;%5kmSlpMO}ra zVxR^nCCU)f!e0^o9rOilh%8G}quVC*Pcz>n2o0=i9BX3M#cXI?wCOuJ&*Fn5`P(>g Q?ca&Vodst6Jk^li0p2X8rT_o{ delta 135 zcmdlfy<38{fpw|@&qmgz%#-VwxF>I8E|pEpkguJhk)aqnrH4N^KczG$RW~_5uOv0E zq{ypko#C9}kP({^l~I;aT-w8#mzbMcQdy8%JOyM#onMBt iUq-B7Mwwq}h9XF$I)ewOUKgm|Eu%OiVRIT!Fe3mKnK8Hk diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.cfb.md b/Sphinx-docs/_build/markdown/sportsdataverse.cfb.md index 2ef6f5f..25700e4 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.cfb.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.cfb.md @@ -232,7 +232,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing calendar dates for the requested season. + pl.DataFrame: Polars dataframe containing calendar dates for the requested season. Raises: @@ -253,7 +253,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. + pl.DataFrame: Polars dataframe containing schedule dates for the requested season. Returns None if no games ### sportsdataverse.cfb.cfb_schedule.most_recent_cfb_season() diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.mbb.md b/Sphinx-docs/_build/markdown/sportsdataverse.mbb.md index 0776063..57fd208 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.mbb.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.mbb.md @@ -181,8 +181,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing - calendar dates for the requested season. + pl.DataFrame: Polars dataframe containing calendar dates for the requested season. Raises: @@ -202,7 +201,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. + pl.DataFrame: Polars dataframe containing schedule dates for the requested season. Returns None if no games ### sportsdataverse.mbb.mbb_schedule.most_recent_mbb_season() diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.nba.md b/Sphinx-docs/_build/markdown/sportsdataverse.nba.md index facfb9f..7d7d008 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.nba.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.nba.md @@ -182,8 +182,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing - calendar dates for the requested season. + pl.DataFrame: Polars dataframe containing calendar dates for the requested season. Raises: @@ -203,8 +202,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing - schedule events for the requested season. + pl.DataFrame: Polars dataframe containing schedule dates for the requested season. Returns None if no games ### sportsdataverse.nba.nba_schedule.most_recent_nba_season() diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.nfl.md b/Sphinx-docs/_build/markdown/sportsdataverse.nfl.md index c028450..666adf9 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.nfl.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.nfl.md @@ -563,7 +563,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing calendar dates for the requested season. + pl.DataFrame: Polars dataframe containing calendar dates for the requested season. Raises: @@ -583,7 +583,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. + pl.DataFrame: Polars dataframe containing schedule dates for the requested season. Returns None if no games ### sportsdataverse.nfl.nfl_schedule.get_current_week() diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.nhl.md b/Sphinx-docs/_build/markdown/sportsdataverse.nhl.md index add82ee..d093c6a 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.nhl.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.nhl.md @@ -232,8 +232,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing - calendar dates for the requested season. + pl.DataFrame: Polars dataframe containing calendar dates for the requested season. Raises: @@ -252,8 +251,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing - schedule events for the requested season. + pl.DataFrame: Polars dataframe containing schedule dates for the requested season. Returns None if no games ### sportsdataverse.nhl.nhl_schedule.most_recent_nhl_season() diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.wbb.md b/Sphinx-docs/_build/markdown/sportsdataverse.wbb.md index c8a7173..32743c0 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.wbb.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.wbb.md @@ -183,8 +183,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing - calendar dates for the requested season. + pl.DataFrame: Polars dataframe containing calendar dates for the requested season. Raises: @@ -204,7 +203,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. + pl.DataFrame: Polars dataframe containing schedule dates for the requested season. Returns None if no games ### sportsdataverse.wbb.wbb_schedule.most_recent_wbb_season() diff --git a/Sphinx-docs/_build/markdown/sportsdataverse.wnba.md b/Sphinx-docs/_build/markdown/sportsdataverse.wnba.md index 50684d0..c701f7a 100755 --- a/Sphinx-docs/_build/markdown/sportsdataverse.wnba.md +++ b/Sphinx-docs/_build/markdown/sportsdataverse.wnba.md @@ -180,7 +180,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing calendar dates for the requested season. + pl.DataFrame: Polars dataframe containing calendar dates for the requested season. Raises: @@ -198,7 +198,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. + pl.DataFrame: Polars dataframe containing schedule dates for the requested season. Returns None if no games ### sportsdataverse.wnba.wnba_schedule.most_recent_wnba_season() diff --git a/docs/docs/cfb/index.md b/docs/docs/cfb/index.md index 2ef6f5f..25700e4 100755 --- a/docs/docs/cfb/index.md +++ b/docs/docs/cfb/index.md @@ -232,7 +232,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing calendar dates for the requested season. + pl.DataFrame: Polars dataframe containing calendar dates for the requested season. Raises: @@ -253,7 +253,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. + pl.DataFrame: Polars dataframe containing schedule dates for the requested season. Returns None if no games ### sportsdataverse.cfb.cfb_schedule.most_recent_cfb_season() diff --git a/docs/docs/mbb/index.md b/docs/docs/mbb/index.md index 0776063..57fd208 100755 --- a/docs/docs/mbb/index.md +++ b/docs/docs/mbb/index.md @@ -181,8 +181,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing - calendar dates for the requested season. + pl.DataFrame: Polars dataframe containing calendar dates for the requested season. Raises: @@ -202,7 +201,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. + pl.DataFrame: Polars dataframe containing schedule dates for the requested season. Returns None if no games ### sportsdataverse.mbb.mbb_schedule.most_recent_mbb_season() diff --git a/docs/docs/nba/index.md b/docs/docs/nba/index.md index facfb9f..7d7d008 100755 --- a/docs/docs/nba/index.md +++ b/docs/docs/nba/index.md @@ -182,8 +182,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing - calendar dates for the requested season. + pl.DataFrame: Polars dataframe containing calendar dates for the requested season. Raises: @@ -203,8 +202,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing - schedule events for the requested season. + pl.DataFrame: Polars dataframe containing schedule dates for the requested season. Returns None if no games ### sportsdataverse.nba.nba_schedule.most_recent_nba_season() diff --git a/docs/docs/nfl/index.md b/docs/docs/nfl/index.md index c028450..666adf9 100755 --- a/docs/docs/nfl/index.md +++ b/docs/docs/nfl/index.md @@ -563,7 +563,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing calendar dates for the requested season. + pl.DataFrame: Polars dataframe containing calendar dates for the requested season. Raises: @@ -583,7 +583,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. + pl.DataFrame: Polars dataframe containing schedule dates for the requested season. Returns None if no games ### sportsdataverse.nfl.nfl_schedule.get_current_week() diff --git a/docs/docs/nhl/index.md b/docs/docs/nhl/index.md index add82ee..d093c6a 100755 --- a/docs/docs/nhl/index.md +++ b/docs/docs/nhl/index.md @@ -232,8 +232,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing - calendar dates for the requested season. + pl.DataFrame: Polars dataframe containing calendar dates for the requested season. Raises: @@ -252,8 +251,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing - schedule events for the requested season. + pl.DataFrame: Polars dataframe containing schedule dates for the requested season. Returns None if no games ### sportsdataverse.nhl.nhl_schedule.most_recent_nhl_season() diff --git a/docs/docs/wbb/index.md b/docs/docs/wbb/index.md index c8a7173..32743c0 100755 --- a/docs/docs/wbb/index.md +++ b/docs/docs/wbb/index.md @@ -183,8 +183,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing - calendar dates for the requested season. + pl.DataFrame: Polars dataframe containing calendar dates for the requested season. Raises: @@ -204,7 +203,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. + pl.DataFrame: Polars dataframe containing schedule dates for the requested season. Returns None if no games ### sportsdataverse.wbb.wbb_schedule.most_recent_wbb_season() diff --git a/docs/docs/wnba/index.md b/docs/docs/wnba/index.md index 50684d0..c701f7a 100755 --- a/docs/docs/wnba/index.md +++ b/docs/docs/wnba/index.md @@ -180,7 +180,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing calendar dates for the requested season. + pl.DataFrame: Polars dataframe containing calendar dates for the requested season. Raises: @@ -198,7 +198,7 @@ Args: Returns: - pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. + pl.DataFrame: Polars dataframe containing schedule dates for the requested season. Returns None if no games ### sportsdataverse.wnba.wnba_schedule.most_recent_wnba_season() diff --git a/sportsdataverse/cfb/cfb_schedule.py b/sportsdataverse/cfb/cfb_schedule.py index c6d7285..2f5d38b 100755 --- a/sportsdataverse/cfb/cfb_schedule.py +++ b/sportsdataverse/cfb/cfb_schedule.py @@ -20,7 +20,7 @@ def espn_cfb_schedule( return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. + pl.DataFrame: Polars dataframe containing schedule dates for the requested season. Returns None if no games """ params = { @@ -151,7 +151,7 @@ def espn_cfb_calendar(season=None, groups=None, ondays=None, return_as_pandas=Fa return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing calendar dates for the requested season. + pl.DataFrame: Polars dataframe containing calendar dates for the requested season. Raises: ValueError: If `season` is less than 2002. diff --git a/sportsdataverse/mbb/mbb_schedule.py b/sportsdataverse/mbb/mbb_schedule.py index f4dac2b..641759a 100755 --- a/sportsdataverse/mbb/mbb_schedule.py +++ b/sportsdataverse/mbb/mbb_schedule.py @@ -19,7 +19,7 @@ def espn_mbb_schedule( limit (int): number of records to return, default: 500. return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. + pl.DataFrame: Polars dataframe containing schedule dates for the requested season. Returns None if no games """ url = "http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/scoreboard" params = { @@ -139,8 +139,7 @@ def espn_mbb_calendar(season=None, ondays=None, return_as_pandas=False, **kwargs return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing - calendar dates for the requested season. + pl.DataFrame: Polars dataframe containing calendar dates for the requested season. Raises: ValueError: If `season` is less than 2002. diff --git a/sportsdataverse/nba/nba_schedule.py b/sportsdataverse/nba/nba_schedule.py index b55f080..778eb18 100755 --- a/sportsdataverse/nba/nba_schedule.py +++ b/sportsdataverse/nba/nba_schedule.py @@ -17,8 +17,7 @@ def espn_nba_schedule(dates=None, season_type=None, limit=500, return_as_pandas= return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing - schedule events for the requested season. + pl.DataFrame: Polars dataframe containing schedule dates for the requested season. Returns None if no games """ url = "http://site.api.espn.com/apis/site/v2/sports/basketball/nba/scoreboard" params = {"dates": dates, "seasonType": season_type, "limit": limit} @@ -132,8 +131,7 @@ def espn_nba_calendar(season=None, ondays=None, return_as_pandas=False, **kwargs return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing - calendar dates for the requested season. + pl.DataFrame: Polars dataframe containing calendar dates for the requested season. Raises: ValueError: If `season` is less than 2002. diff --git a/sportsdataverse/nfl/nfl_schedule.py b/sportsdataverse/nfl/nfl_schedule.py index f99bd65..c6d4103 100755 --- a/sportsdataverse/nfl/nfl_schedule.py +++ b/sportsdataverse/nfl/nfl_schedule.py @@ -19,7 +19,7 @@ def espn_nfl_schedule( return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. + pl.DataFrame: Polars dataframe containing schedule dates for the requested season. Returns None if no games """ params = {"week": week, "dates": dates, "seasonType": season_type, "limit": limit} @@ -141,7 +141,7 @@ def espn_nfl_calendar(season=None, ondays=None, return_as_pandas=False, **kwargs return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing calendar dates for the requested season. + pl.DataFrame: Polars dataframe containing calendar dates for the requested season. Raises: ValueError: If `season` is less than 2002. diff --git a/sportsdataverse/nhl/nhl_schedule.py b/sportsdataverse/nhl/nhl_schedule.py index aa7bd5a..9487b76 100755 --- a/sportsdataverse/nhl/nhl_schedule.py +++ b/sportsdataverse/nhl/nhl_schedule.py @@ -16,8 +16,7 @@ def espn_nhl_schedule(dates=None, season_type=None, limit=500, return_as_pandas= return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing - schedule events for the requested season. + pl.DataFrame: Polars dataframe containing schedule dates for the requested season. Returns None if no games """ @@ -132,8 +131,7 @@ def espn_nhl_calendar(season=None, ondays=None, return_as_pandas=False, **kwargs return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing - calendar dates for the requested season. + pl.DataFrame: Polars dataframe containing calendar dates for the requested season. Raises: ValueError: If `season` is less than 2002. diff --git a/sportsdataverse/wbb/wbb_schedule.py b/sportsdataverse/wbb/wbb_schedule.py index 5dc6aa9..031fd57 100755 --- a/sportsdataverse/wbb/wbb_schedule.py +++ b/sportsdataverse/wbb/wbb_schedule.py @@ -20,7 +20,7 @@ def espn_wbb_schedule( return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. + pl.DataFrame: Polars dataframe containing schedule dates for the requested season. Returns None if no games """ url = "http://site.api.espn.com/apis/site/v2/sports/basketball/womens-college-basketball/scoreboard" params = { @@ -144,8 +144,7 @@ def espn_wbb_calendar(season=None, ondays=None, return_as_pandas=False, **kwargs return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing - calendar dates for the requested season. + pl.DataFrame: Polars dataframe containing calendar dates for the requested season. Raises: ValueError: If `season` is less than 2002. diff --git a/sportsdataverse/wnba/wnba_schedule.py b/sportsdataverse/wnba/wnba_schedule.py index 717a980..08d570d 100755 --- a/sportsdataverse/wnba/wnba_schedule.py +++ b/sportsdataverse/wnba/wnba_schedule.py @@ -15,7 +15,7 @@ def espn_wnba_schedule(dates=None, season_type=None, limit=500, return_as_pandas limit (int): number of records to return, default: 500. Returns: - pd.DataFrame: Pandas dataframe containing schedule dates for the requested season. + pl.DataFrame: Polars dataframe containing schedule dates for the requested season. Returns None if no games """ url = "http://site.api.espn.com/apis/site/v2/sports/basketball/wnba/scoreboard" params = {"dates": dates, "seasonType": season_type, "limit": limit} @@ -128,7 +128,7 @@ def espn_wnba_calendar(season=None, ondays=None, return_as_pandas=False, **kwarg ondays (boolean): Used to return dates for calendar ondays Returns: - pd.DataFrame: Pandas dataframe containing calendar dates for the requested season. + pl.DataFrame: Polars dataframe containing calendar dates for the requested season. Raises: ValueError: If `season` is less than 2002. From 5171d273608e9c51e63f8296199ec02b8ab32ff0 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Mon, 31 Jul 2023 21:15:23 -0400 Subject: [PATCH 60/79] adding tests/function renaming --- pytest.ini | 5 ++ tests/__init__.py | 0 tests/cfb/__init__.py | 0 tests/cfb/test_cfb_pbp.py | 83 ++++++---------------------------- tests/cfb/test_cfb_schedule.py | 14 +++--- tests/mbb/__init__.py | 0 tests/mbb/test_mbb_schedule.py | 10 ++-- tests/nfl/__init__.py | 0 tests/nfl/test_nfl_pbp.py | 40 +++++++++++++++- tests/test_dl_utils.py | 10 ++-- 10 files changed, 74 insertions(+), 88 deletions(-) create mode 100644 pytest.ini create mode 100644 tests/__init__.py create mode 100644 tests/cfb/__init__.py create mode 100644 tests/mbb/__init__.py create mode 100644 tests/nfl/__init__.py diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..9089c9e --- /dev/null +++ b/pytest.ini @@ -0,0 +1,5 @@ +[pytest] +markers = + sequential +testpaths = + tests diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cfb/__init__.py b/tests/cfb/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cfb/test_cfb_pbp.py b/tests/cfb/test_cfb_pbp.py index 55e6545..b024cb3 100644 --- a/tests/cfb/test_cfb_pbp.py +++ b/tests/cfb/test_cfb_pbp.py @@ -2,15 +2,10 @@ import pytest from sportsdataverse.cfb.cfb_pbp import CFBPlayProcess -from sportsdataverse.cfb.cfb_schedule import ( - espn_cfb_calendar, - espn_cfb_schedule, - most_recent_cfb_season, -) @pytest.fixture() -def generated_data(): +def generated_cfb_data(): test = CFBPlayProcess(gameId=401301025) test.espn_cfb_pbp() test.run_processing_pipeline() @@ -18,22 +13,22 @@ def generated_data(): @pytest.fixture() -def box_score(generated_data): - yield generated_data.create_box_score(pl.DataFrame(generated_data.plays_json, infer_schema_length=400)) +def cfb_box_score(generated_cfb_data): + yield generated_cfb_data.create_box_score(pl.DataFrame(generated_cfb_data.plays_json, infer_schema_length=400)) -def test_basic_pbp(generated_data): - assert generated_data.json is not None +def test_basic_cfb_pbp(generated_cfb_data): + assert generated_cfb_data.json is not None - generated_data.run_processing_pipeline() - assert len(generated_data.plays_json) > 0 - assert generated_data.ran_pipeline == True - assert isinstance(pl.DataFrame(generated_data.plays_json, infer_schema_length=400), pl.DataFrame) + generated_cfb_data.run_processing_pipeline() + assert len(generated_cfb_data.plays_json) > 0 + assert generated_cfb_data.ran_pipeline == True + assert isinstance(pl.DataFrame(generated_cfb_data.plays_json, infer_schema_length=400), pl.DataFrame) -def test_adv_box_score(box_score): - assert box_score is not None - assert not set(box_score.keys()).difference( +def test_cfb_adv_box_score(cfb_box_score): + assert cfb_box_score is not None + assert not set(cfb_box_score.keys()).difference( { "win_pct", "pass", @@ -48,8 +43,8 @@ def test_adv_box_score(box_score): ) -def test_havoc_rate(box_score): - defense_home = box_score["defensive"][0] +def test_havoc_rate(cfb_box_score): + defense_home = cfb_box_score["defensive"][0] # print(defense_home) passes_defended = defense_home.get("pass_breakups", 0) home_int = defense_home.get("Int", 0) @@ -218,53 +213,3 @@ def test_expected_turnovers(iu_play_base_box): ) assert round(away_exp_xTO, 4) == round(away_actual_xTO, 4) assert round(home_exp_xTO, 4) == round(home_actual_xTO, 4) - - -@pytest.fixture() -def calendar_data(): - yield espn_cfb_calendar(season=most_recent_cfb_season(), return_as_pandas=False) - - -def calendar_data_check(calendar_data): - assert isinstance(calendar_data, pl.DataFrame) - assert len(calendar_data) > 0 - - -@pytest.fixture() -def calendar_ondays_data(): - yield espn_cfb_calendar(season=2021, ondays=True, return_as_pandas=False) - - -def calendar_ondays_data_check(calendar_ondays_data): - assert isinstance(calendar_ondays_data, pl.DataFrame) - assert len(calendar_ondays_data) > 0 - - -@pytest.fixture() -def schedule_data(): - yield espn_cfb_schedule(return_as_pandas=False) - - -def schedule_data_check(schedule_data): - assert isinstance(schedule_data, pl.DataFrame) - assert len(schedule_data) > 0 - - -@pytest.fixture() -def schedule_data2(): - yield espn_cfb_schedule(dates=20220901, return_as_pandas=False) - - -def schedule_data_check2(schedule_data2): - assert isinstance(schedule_data, pl.DataFrame) - assert len(schedule_data) > 0 - - -@pytest.fixture() -def week_1_cfb_schedule(): - yield espn_cfb_schedule(dates=2022, week=1, return_as_pandas=False) - - -def week_1_schedule_check(week_1_cfb_schedule): - assert isinstance(week_1_cfb_schedule, pl.DataFrame) - assert len(week_1_cfb_schedule) > 0 diff --git a/tests/cfb/test_cfb_schedule.py b/tests/cfb/test_cfb_schedule.py index 099464b..83ad4da 100644 --- a/tests/cfb/test_cfb_schedule.py +++ b/tests/cfb/test_cfb_schedule.py @@ -13,7 +13,7 @@ def cfb_calendar_data(): yield espn_cfb_calendar(season=most_recent_cfb_season(), return_as_pandas=False) -def cfb_calendar_data_check(cfb_calendar_data): +def test_cfb_calendar_data_check(cfb_calendar_data): assert isinstance(cfb_calendar_data, pl.DataFrame) assert len(cfb_calendar_data) > 0 @@ -23,7 +23,7 @@ def cfb_calendar_ondays_data(): yield espn_cfb_calendar(season=2021, ondays=True, return_as_pandas=False) -def cfb_calendar_ondays_data_check(cfb_calendar_ondays_data): +def test_cfb_calendar_ondays_data_check(cfb_calendar_ondays_data): assert isinstance(cfb_calendar_ondays_data, pl.DataFrame) assert len(cfb_calendar_ondays_data) > 0 @@ -33,7 +33,7 @@ def cfb_schedule_data(): yield espn_cfb_schedule(return_as_pandas=False) -def cfb_schedule_data_check(cfb_schedule_data): +def test_cfb_schedule_data_check(cfb_schedule_data): assert isinstance(cfb_schedule_data, pl.DataFrame) assert len(cfb_schedule_data) > 0 @@ -43,9 +43,9 @@ def cfb_schedule_data2(): yield espn_cfb_schedule(dates=20220901, return_as_pandas=False) -def cfb_schedule_data_check2(cfb_schedule_data2): - assert isinstance(cfb_schedule_data, pl.DataFrame) - assert len(cfb_schedule_data) > 0 +def test_cfb_schedule_data_check2(cfb_schedule_data2): + assert isinstance(cfb_schedule_data2, pl.DataFrame) + assert len(cfb_schedule_data2) > 0 @pytest.fixture() @@ -53,6 +53,6 @@ def week_1_cfb_schedule(): yield espn_cfb_schedule(dates=2022, week=1, return_as_pandas=False) -def week_1_cfb_schedule_check(week_1_cfb_schedule): +def test_week_1_cfb_schedule_check(week_1_cfb_schedule): assert isinstance(week_1_cfb_schedule, pl.DataFrame) assert len(week_1_cfb_schedule) > 0 diff --git a/tests/mbb/__init__.py b/tests/mbb/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/mbb/test_mbb_schedule.py b/tests/mbb/test_mbb_schedule.py index 16cec8c..a4230c4 100644 --- a/tests/mbb/test_mbb_schedule.py +++ b/tests/mbb/test_mbb_schedule.py @@ -13,7 +13,7 @@ def mbb_calendar_data(): yield espn_mbb_calendar(season=most_recent_mbb_season(), return_as_pandas=False) -def mbb_calendar_data_check(mbb_calendar_data): +def test_mbb_calendar_data_check(mbb_calendar_data): assert isinstance(mbb_calendar_data, pl.DataFrame) assert len(mbb_calendar_data) > 0 @@ -23,7 +23,7 @@ def mbb_calendar_ondays_data(): yield espn_mbb_calendar(season=2021, ondays=True, return_as_pandas=False) -def mbb_calendar_ondays_data_check(mbb_calendar_ondays_data): +def test_mbb_calendar_ondays_data_check(mbb_calendar_ondays_data): assert isinstance(mbb_calendar_ondays_data, pl.DataFrame) assert len(mbb_calendar_ondays_data) > 0 @@ -33,7 +33,7 @@ def mbb_schedule_data(): yield espn_mbb_schedule(return_as_pandas=False) -def mbb_schedule_data_check(mbb_schedule_data): +def test_mbb_schedule_data_check(mbb_schedule_data): assert isinstance(mbb_schedule_data, pl.DataFrame) assert len(mbb_schedule_data) > 0 @@ -43,7 +43,7 @@ def mbb_schedule_data2(): yield espn_mbb_schedule(dates=20221201, return_as_pandas=False) -def mbb_schedule_data_check2(mbb_schedule_data2): +def test_mbb_schedule_data_check2(mbb_schedule_data2): assert isinstance(mbb_schedule_data2, pl.DataFrame) assert len(mbb_schedule_data2) > 0 @@ -53,6 +53,6 @@ def january_schedule(): yield espn_mbb_schedule(dates=20230123, return_as_pandas=False) -def january_schedule_check(january_schedule): +def test_january_schedule_check(january_schedule): assert isinstance(january_schedule, pl.DataFrame) assert len(january_schedule) > 0 diff --git a/tests/nfl/__init__.py b/tests/nfl/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/nfl/test_nfl_pbp.py b/tests/nfl/test_nfl_pbp.py index dcd32cf..cf8794a 100644 --- a/tests/nfl/test_nfl_pbp.py +++ b/tests/nfl/test_nfl_pbp.py @@ -6,16 +6,52 @@ @pytest.fixture() def generated_nfl_data(): - test = NFLPlayProcess(gameId=401437777) + test = NFLPlayProcess(gameId=401220403) test.espn_nfl_pbp() test.run_processing_pipeline() yield test +@pytest.fixture() +def nfl_box_score(generated_nfl_data): + yield generated_nfl_data.create_box_score(pl.DataFrame(generated_nfl_data.plays_json, infer_schema_length=400)) + + def test_basic_nfl_pbp(generated_nfl_data): assert generated_nfl_data.json is not None generated_nfl_data.run_processing_pipeline() assert len(generated_nfl_data.plays_json) > 0 assert generated_nfl_data.ran_pipeline == True - assert isinstance(generated_nfl_data.plays_json, pl.DataFrame) + assert isinstance(pl.DataFrame(generated_nfl_data.plays_json, infer_schema_length=400), pl.DataFrame) + + +def test_nfl_adv_box_score(nfl_box_score): + assert nfl_box_score is not None + assert not set(nfl_box_score.keys()).difference( + { + "win_pct", + "pass", + "team", + "situational", + "rush", + "receiver", + "defensive", + "turnover", + "drives", + } + ) + + +def test_havoc_rate(nfl_box_score): + defense_home = nfl_box_score["defensive"][0] + # print(defense_home) + passes_defended = defense_home.get("pass_breakups", 0) + home_int = defense_home.get("Int", 0) + tfl = defense_home.get("TFL", 0) + fum = defense_home.get("fumbles", 0) + plays = defense_home.get("scrimmage_plays", 0) + + assert plays > 0 + assert defense_home["havoc_total"] == (passes_defended + home_int + tfl + fum) + assert round(defense_home["havoc_total_rate"], 4) == round(((passes_defended + home_int + tfl + fum) / plays), 4) diff --git a/tests/test_dl_utils.py b/tests/test_dl_utils.py index 05b6e9b..2489ea9 100644 --- a/tests/test_dl_utils.py +++ b/tests/test_dl_utils.py @@ -26,11 +26,11 @@ def test_download_valid_url_custom_headers(self): assert response.status_code == 200 # Tests that the function can download a valid URL with a proxy - def test_download_valid_url_with_proxy(self): - url = "https://jsonplaceholder.typicode.com/posts" - proxy = {"https": "https://localhost:8080"} - response = download(url, proxy=proxy) - assert response.status_code == 200 + # def test_download_valid_url_with_proxy(self): + # url = "https://jsonplaceholder.typicode.com/posts" + # proxy = {"https": "https://localhost:8080"} + # response = download(url, proxy=proxy) + # assert response.status_code == 200 # Tests that the function can download a valid URL with a very short timeout def test_download_valid_url_with_short_timeout(self): From 652f7bd666748cc65b097168ee2b1fb8c0322db6 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Mon, 31 Jul 2023 21:18:01 -0400 Subject: [PATCH 61/79] fixing timeouts and formatting per PR comments --- sportsdataverse/cfb/cfb_pbp.py | 104 ++++++++++----------------------- sportsdataverse/nfl/nfl_pbp.py | 104 ++++++++++----------------------- 2 files changed, 64 insertions(+), 144 deletions(-) diff --git a/sportsdataverse/cfb/cfb_pbp.py b/sportsdataverse/cfb/cfb_pbp.py index 1be539a..99b9c1e 100755 --- a/sportsdataverse/cfb/cfb_pbp.py +++ b/sportsdataverse/cfb/cfb_pbp.py @@ -300,6 +300,7 @@ def __helper_cfb_pbp_features(self, pbp_txt, init): .and_(pl.col("start.yardsToEndzone") == pl.col("lead_start_yardsToEndzone")) .and_(pl.col("start.distance") == pl.col("lead_start_distance")) .and_(pl.col("text") == pl.col("lead_text")) + .and_(pl.col("type.text") != "Timeout") ) .then(pl.lit(True)) .when( @@ -308,6 +309,7 @@ def __helper_cfb_pbp_features(self, pbp_txt, init): .and_(pl.col("start.yardsToEndzone") == pl.col("lead_start_yardsToEndzone")) .and_(pl.col("start.distance") == pl.col("lead_start_distance")) .and_(pl.col("text").is_in(pl.col("lead_text"))) + .and_(pl.col("type.text") != "Timeout") ) .then(pl.lit(True)) .otherwise(pl.lit(False)) @@ -435,82 +437,40 @@ def __helper_cfb_pbp_features(self, pbp_txt, init): pbp_txt["plays"] = ( pbp_txt["plays"] .with_columns( - pl.when( - ( - (pbp_txt["timeouts"][init["homeTeamId"]]["1"] <= pl.col("id")).and_( - pl.col("period.number") <= 2, len(pbp_txt["timeouts"][init["homeTeamId"]]["1"]) == 1 - ) - ).or_( - (pbp_txt["timeouts"][init["homeTeamId"]]["2"] <= pl.col("id")).and_( - pl.col("period.number") > 2, len(pbp_txt["timeouts"][init["homeTeamId"]]["2"]) == 1 - ) - ) - ) - .then(2) - .when( - ( - (pbp_txt["timeouts"][init["homeTeamId"]]["1"] <= pl.col("id")).and_( - pl.col("period.number") <= 2, len(pbp_txt["timeouts"][init["homeTeamId"]]["1"]) == 2 - ) - ).or_( - (pbp_txt["timeouts"][init["homeTeamId"]]["2"] <= pl.col("id")).and_( - pl.col("period.number") > 2, len(pbp_txt["timeouts"][init["homeTeamId"]]["2"]) == 2 - ) - ) - ) - .then(1) - .when( - ( - (pbp_txt["timeouts"][init["homeTeamId"]]["1"] <= pl.col("id")).and_( - pl.col("period.number") <= 2, len(pbp_txt["timeouts"][init["homeTeamId"]]["1"]) == 3 - ) - ).or_( - (pbp_txt["timeouts"][init["homeTeamId"]]["2"] <= pl.col("id")).and_( - pl.col("period.number") > 2, len(pbp_txt["timeouts"][init["homeTeamId"]]["2"]) == 3 - ) - ) - ) - .then(0) - .otherwise(3) - .alias("end.homeTeamTimeouts"), - pl.when( - ( - (pbp_txt["timeouts"][init["awayTeamId"]]["1"] <= pl.col("id")).and_( - pl.col("period.number") <= 2, len(pbp_txt["timeouts"][init["awayTeamId"]]["1"]) == 1 - ) - ).or_( - (pbp_txt["timeouts"][init["awayTeamId"]]["2"] <= pl.col("id")).and_( - pl.col("period.number") > 2, len(pbp_txt["timeouts"][init["awayTeamId"]]["2"]) == 1 - ) - ) - ) - .then(2) - .when( - ( - (pbp_txt["timeouts"][init["awayTeamId"]]["1"] <= pl.col("id")).and_( - pl.col("period.number") <= 2, len(pbp_txt["timeouts"][init["awayTeamId"]]["1"]) == 2 - ) - ).or_( - (pbp_txt["timeouts"][init["awayTeamId"]]["2"] <= pl.col("id")).and_( - pl.col("period.number") > 2, len(pbp_txt["timeouts"][init["awayTeamId"]]["2"]) == 2 + ( + 3 + - pl.struct(pl.col(["id", "period.number"])).apply( + lambda x: ( + sum( + (i <= x["id"]) & (x["period.number"] <= 2) + for i in pbp_txt["timeouts"][int(init["homeTeamId"])]["1"] + ) ) + | ( + sum( + (i <= x["id"]) & (x["period.number"] > 2) + for i in pbp_txt["timeouts"][int(init["homeTeamId"])]["2"] + ) + ), ) - ) - .then(1) - .when( - ( - (pbp_txt["timeouts"][init["awayTeamId"]]["1"] <= pl.col("id")).and_( - pl.col("period.number") <= 2, len(pbp_txt["timeouts"][init["awayTeamId"]]["1"]) == 3 - ) - ).or_( - (pbp_txt["timeouts"][init["awayTeamId"]]["2"] <= pl.col("id")).and_( - pl.col("period.number") > 2, len(pbp_txt["timeouts"][init["awayTeamId"]]["2"]) == 3 + ).alias("end.homeTeamTimeouts"), + ( + 3 + - pl.struct(pl.col(["id", "period.number"])).apply( + lambda x: ( + sum( + (i <= x["id"]) & (x["period.number"] <= 2) + for i in pbp_txt["timeouts"][int(init["awayTeamId"])]["1"] + ) ) + | ( + sum( + (i <= x["id"]) & (x["period.number"] > 2) + for i in pbp_txt["timeouts"][int(init["awayTeamId"])]["2"] + ) + ), ) - ) - .then(0) - .otherwise(3) - .alias("end.awayTeamTimeouts"), + ).alias("end.awayTeamTimeouts"), ) .with_columns( pl.col("end.homeTeamTimeouts").shift_and_fill(periods=1, fill_value=3).alias("start.homeTeamTimeouts"), diff --git a/sportsdataverse/nfl/nfl_pbp.py b/sportsdataverse/nfl/nfl_pbp.py index 5cd6705..f433a9e 100755 --- a/sportsdataverse/nfl/nfl_pbp.py +++ b/sportsdataverse/nfl/nfl_pbp.py @@ -306,6 +306,7 @@ def __helper_nfl_pbp_features(self, pbp_txt, init): .and_(pl.col("start.yardsToEndzone") == pl.col("lead_start_yardsToEndzone")) .and_(pl.col("start.distance") == pl.col("lead_start_distance")) .and_(pl.col("text") == pl.col("lead_text")) + .and_(pl.col("type.text") != "Timeout") ) .then(pl.lit(True)) .when( @@ -314,6 +315,7 @@ def __helper_nfl_pbp_features(self, pbp_txt, init): .and_(pl.col("start.yardsToEndzone") == pl.col("lead_start_yardsToEndzone")) .and_(pl.col("start.distance") == pl.col("lead_start_distance")) .and_(pl.col("text").is_in(pl.col("lead_text"))) + .and_(pl.col("type.text") != "Timeout") ) .then(pl.lit(True)) .otherwise(pl.lit(False)) @@ -441,82 +443,40 @@ def __helper_nfl_pbp_features(self, pbp_txt, init): pbp_txt["plays"] = ( pbp_txt["plays"] .with_columns( - pl.when( - ( - (pbp_txt["timeouts"][init["homeTeamId"]]["1"] <= pl.col("id")).and_( - pl.col("period.number") <= 2, len(pbp_txt["timeouts"][init["homeTeamId"]]["1"]) == 1 - ) - ).or_( - (pbp_txt["timeouts"][init["homeTeamId"]]["2"] <= pl.col("id")).and_( - pl.col("period.number") > 2, len(pbp_txt["timeouts"][init["homeTeamId"]]["2"]) == 1 - ) - ) - ) - .then(2) - .when( - ( - (pbp_txt["timeouts"][init["homeTeamId"]]["1"] <= pl.col("id")).and_( - pl.col("period.number") <= 2, len(pbp_txt["timeouts"][init["homeTeamId"]]["1"]) == 2 - ) - ).or_( - (pbp_txt["timeouts"][init["homeTeamId"]]["2"] <= pl.col("id")).and_( - pl.col("period.number") > 2, len(pbp_txt["timeouts"][init["homeTeamId"]]["2"]) == 2 - ) - ) - ) - .then(1) - .when( - ( - (pbp_txt["timeouts"][init["homeTeamId"]]["1"] <= pl.col("id")).and_( - pl.col("period.number") <= 2, len(pbp_txt["timeouts"][init["homeTeamId"]]["1"]) == 3 - ) - ).or_( - (pbp_txt["timeouts"][init["homeTeamId"]]["2"] <= pl.col("id")).and_( - pl.col("period.number") > 2, len(pbp_txt["timeouts"][init["homeTeamId"]]["2"]) == 3 - ) - ) - ) - .then(0) - .otherwise(3) - .alias("end.homeTeamTimeouts"), - pl.when( - ( - (pbp_txt["timeouts"][init["awayTeamId"]]["1"] <= pl.col("id")).and_( - pl.col("period.number") <= 2, len(pbp_txt["timeouts"][init["awayTeamId"]]["1"]) == 1 - ) - ).or_( - (pbp_txt["timeouts"][init["awayTeamId"]]["2"] <= pl.col("id")).and_( - pl.col("period.number") > 2, len(pbp_txt["timeouts"][init["awayTeamId"]]["2"]) == 1 - ) - ) - ) - .then(2) - .when( - ( - (pbp_txt["timeouts"][init["awayTeamId"]]["1"] <= pl.col("id")).and_( - pl.col("period.number") <= 2, len(pbp_txt["timeouts"][init["awayTeamId"]]["1"]) == 2 - ) - ).or_( - (pbp_txt["timeouts"][init["awayTeamId"]]["2"] <= pl.col("id")).and_( - pl.col("period.number") > 2, len(pbp_txt["timeouts"][init["awayTeamId"]]["2"]) == 2 + ( + 3 + - pl.struct(pl.col(["id", "period.number"])).apply( + lambda x: ( + sum( + (i <= x["id"]) & (x["period.number"] <= 2) + for i in pbp_txt["timeouts"][int(init["homeTeamId"])]["1"] + ) ) + | ( + sum( + (i <= x["id"]) & (x["period.number"] > 2) + for i in pbp_txt["timeouts"][int(init["homeTeamId"])]["2"] + ) + ), ) - ) - .then(1) - .when( - ( - (pbp_txt["timeouts"][init["awayTeamId"]]["1"] <= pl.col("id")).and_( - pl.col("period.number") <= 2, len(pbp_txt["timeouts"][init["awayTeamId"]]["1"]) == 3 - ) - ).or_( - (pbp_txt["timeouts"][init["awayTeamId"]]["2"] <= pl.col("id")).and_( - pl.col("period.number") > 2, len(pbp_txt["timeouts"][init["awayTeamId"]]["2"]) == 3 + ).alias("end.homeTeamTimeouts"), + ( + 3 + - pl.struct(pl.col(["id", "period.number"])).apply( + lambda x: ( + sum( + (i <= x["id"]) & (x["period.number"] <= 2) + for i in pbp_txt["timeouts"][int(init["awayTeamId"])]["1"] + ) ) + | ( + sum( + (i <= x["id"]) & (x["period.number"] > 2) + for i in pbp_txt["timeouts"][int(init["awayTeamId"])]["2"] + ) + ), ) - ) - .then(0) - .otherwise(3) - .alias("end.awayTeamTimeouts"), + ).alias("end.awayTeamTimeouts"), ) .with_columns( pl.col("end.homeTeamTimeouts").shift_and_fill(periods=1, fill_value=3).alias("start.homeTeamTimeouts"), From c5d110a362ad9d11bb275b5f472a70e808624f37 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Mon, 31 Jul 2023 22:44:24 -0400 Subject: [PATCH 62/79] remove mlb --- .pre-commit-config.yaml | 16 +- .../_build/doctrees/environment.pickle | Bin 930977 -> 844847 bytes .../doctrees/sportsdataverse.cfb.doctree | Bin 106811 -> 106830 bytes .../_build/doctrees/sportsdataverse.doctree | Bin 57651 -> 57625 bytes .../doctrees/sportsdataverse.mbb.doctree | Bin 85855 -> 85874 bytes .../doctrees/sportsdataverse.mlb.doctree | Bin 167254 -> 5864 bytes .../doctrees/sportsdataverse.nba.doctree | Bin 86061 -> 86080 bytes .../doctrees/sportsdataverse.nfl.doctree | Bin 210450 -> 221714 bytes .../doctrees/sportsdataverse.nhl.doctree | Bin 102667 -> 103489 bytes .../doctrees/sportsdataverse.wbb.doctree | Bin 86330 -> 85913 bytes .../doctrees/sportsdataverse.wnba.doctree | Bin 84201 -> 84223 bytes Sphinx-docs/_build/doctrees/tests.doctree | Bin 11817 -> 3512 bytes Sphinx-docs/_build/markdown/index.md | 33 - Sphinx-docs/_build/markdown/modules.md | 33 - .../_build/markdown/sportsdataverse.cfb.md | 16 +- .../_build/markdown/sportsdataverse.mbb.md | 12 +- .../_build/markdown/sportsdataverse.md | 33 - .../_build/markdown/sportsdataverse.mlb.md | 602 ----------------- .../_build/markdown/sportsdataverse.nba.md | 12 +- .../_build/markdown/sportsdataverse.nfl.md | 108 ++- .../_build/markdown/sportsdataverse.nhl.md | 24 +- .../_build/markdown/sportsdataverse.wbb.md | 17 +- .../_build/markdown/sportsdataverse.wnba.md | 12 +- Sphinx-docs/_build/markdown/tests.md | 15 - Sphinx-docs/sportsdataverse.rst | 1 - {sportsdataverse => archive}/mlb/__init__.py | 0 .../mlb/mlb_loaders.py | 0 .../mlb/mlbam_games.py | 0 .../mlb/mlbam_players.py | 0 .../mlb/mlbam_reports.py | 0 .../mlb/mlbam_stats.py | 0 .../mlb/mlbam_teams.py | 0 .../mlb/retrosheet.py | 0 .../mlb/retrosplits.py | 0 create_docs.sh | 1 - docs/docs/cfb/index.md | 16 +- docs/docs/mbb/index.md | 12 +- docs/docs/mlb/index.md | 623 ------------------ docs/docs/nba/index.md | 12 +- docs/docs/nfl/index.md | 108 ++- docs/docs/nhl/index.md | 24 +- docs/docs/wbb/index.md | 17 +- docs/docs/wnba/index.md | 12 +- examples/test_mlbam.py | 390 ----------- sportsdataverse/__init__.py | 3 +- sportsdataverse/cfb/cfb_game_rosters.py | 2 +- sportsdataverse/cfb/cfb_loaders.py | 12 +- sportsdataverse/cfb/cfb_teams.py | 2 +- sportsdataverse/mbb/mbb_game_rosters.py | 2 +- sportsdataverse/mbb/mbb_loaders.py | 8 +- sportsdataverse/mbb/mbb_teams.py | 2 +- sportsdataverse/nba/nba_game_rosters.py | 2 +- sportsdataverse/nba/nba_loaders.py | 8 +- sportsdataverse/nba/nba_teams.py | 2 +- sportsdataverse/nfl/nfl_game_rosters.py | 2 +- sportsdataverse/nfl/nfl_loaders.py | 93 ++- sportsdataverse/nfl/nfl_teams.py | 2 +- sportsdataverse/nhl/nhl_api.py | 10 +- sportsdataverse/nhl/nhl_game_rosters.py | 2 +- sportsdataverse/nhl/nhl_loaders.py | 10 +- sportsdataverse/nhl/nhl_pbp.py | 4 +- sportsdataverse/nhl/nhl_teams.py | 2 +- sportsdataverse/wbb/wbb_game_rosters.py | 2 +- sportsdataverse/wbb/wbb_loaders.py | 8 +- sportsdataverse/wbb/wbb_pbp.py | 4 +- sportsdataverse/wbb/wbb_teams.py | 2 +- sportsdataverse/wnba/wnba_game_rosters.py | 2 +- sportsdataverse/wnba/wnba_loaders.py | 8 +- sportsdataverse/wnba/wnba_teams.py | 2 +- 69 files changed, 381 insertions(+), 1964 deletions(-) rename {sportsdataverse => archive}/mlb/__init__.py (100%) mode change 100755 => 100644 rename {sportsdataverse => archive}/mlb/mlb_loaders.py (100%) mode change 100755 => 100644 rename {sportsdataverse => archive}/mlb/mlbam_games.py (100%) mode change 100755 => 100644 rename {sportsdataverse => archive}/mlb/mlbam_players.py (100%) mode change 100755 => 100644 rename {sportsdataverse => archive}/mlb/mlbam_reports.py (100%) mode change 100755 => 100644 rename {sportsdataverse => archive}/mlb/mlbam_stats.py (100%) mode change 100755 => 100644 rename {sportsdataverse => archive}/mlb/mlbam_teams.py (100%) mode change 100755 => 100644 rename {sportsdataverse => archive}/mlb/retrosheet.py (100%) mode change 100755 => 100644 rename {sportsdataverse => archive}/mlb/retrosplits.py (100%) mode change 100755 => 100644 delete mode 100755 docs/docs/mlb/index.md delete mode 100755 examples/test_mlbam.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 17311d9..39e6634 100755 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,14 +1,14 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks repos: - - repo: local - hooks: - - id: update-docs - name: update-docs - entry: bash create_docs.sh - language: system - types: [python] - pass_filenames: false + # - repo: local + # hooks: + # - id: update-docs + # name: update-docs + # entry: bash create_docs.sh + # language: system + # types: [python] + # pass_filenames: false - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.0.1 diff --git a/Sphinx-docs/_build/doctrees/environment.pickle b/Sphinx-docs/_build/doctrees/environment.pickle index 8c866376cd3ba5441fdefa9b3cd8b6a4f3ebbe12..36f81940cfdfbcaab64b28ac00639eb8b7eccff2 100755 GIT binary patch delta 59340 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 delta 138050 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

)s&Wc_e&t@rc&gr&*-AyR;cItGyZvA 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 0 else "" ) - del_keys = [ - "geoBroadcasts", - "headlines", - "series", - "situation", - "tickets", - "odds", - "broadcasts", - "notes", - "competitors", - ] + del_keys = ["geoBroadcasts", "headlines", "series", "situation", "tickets", "odds", "leaders"] for k in del_keys: event.get("competitions")[0].pop(k, None) diff --git a/sportsdataverse/nfl/nfl_schedule.py b/sportsdataverse/nfl/nfl_schedule.py index df008d2..4f522f6 100755 --- a/sportsdataverse/nfl/nfl_schedule.py +++ b/sportsdataverse/nfl/nfl_schedule.py @@ -42,7 +42,7 @@ def espn_nfl_schedule( else: event = _extract_home_away(event, 0, "away") event = _extract_home_away(event, 1, "home") - del_keys = ["geoBroadcasts", "headlines", "series", "situation", "tickets", "odds"] + del_keys = ["geoBroadcasts", "headlines", "series", "situation", "tickets", "odds", "leaders"] for k in del_keys: event.get("competitions")[0].pop(k, None) event.get("competitions")[0]["notes_type"] = ( diff --git a/sportsdataverse/nhl/nhl_schedule.py b/sportsdataverse/nhl/nhl_schedule.py index b6f29e8..641de46 100755 --- a/sportsdataverse/nhl/nhl_schedule.py +++ b/sportsdataverse/nhl/nhl_schedule.py @@ -61,17 +61,7 @@ def espn_nhl_schedule(dates=None, season_type=None, limit=500, return_as_pandas= if len(event.get("competitions")[0].get("broadcasts")) > 0 else "" ) - del_keys = [ - "geoBroadcasts", - "headlines", - "series", - "situation", - "tickets", - "odds", - "broadcasts", - "notes", - "competitors", - ] + del_keys = ["geoBroadcasts", "headlines", "series", "situation", "tickets", "odds", "leaders"] for k in del_keys: event.get("competitions")[0].pop(k, None) diff --git a/sportsdataverse/wbb/wbb_schedule.py b/sportsdataverse/wbb/wbb_schedule.py index c33c2a5..d5c6b5a 100755 --- a/sportsdataverse/wbb/wbb_schedule.py +++ b/sportsdataverse/wbb/wbb_schedule.py @@ -46,7 +46,7 @@ def espn_wbb_schedule( else: event = __extract_home_away(event, 0, "away") event = __extract_home_away(event, 1, "home") - del_keys = ["geoBroadcasts", "headlines", "series", "situation", "tickets", "odds"] + del_keys = ["geoBroadcasts", "headlines", "series", "situation", "tickets", "odds", "leaders"] for k in del_keys: event.get("competitions")[0].pop(k, None) event.get("competitions")[0]["notes_type"] = ( diff --git a/sportsdataverse/wnba/wnba_schedule.py b/sportsdataverse/wnba/wnba_schedule.py index c34183b..f124a10 100755 --- a/sportsdataverse/wnba/wnba_schedule.py +++ b/sportsdataverse/wnba/wnba_schedule.py @@ -57,17 +57,7 @@ def espn_wnba_schedule(dates=None, season_type=None, limit=500, return_as_pandas if len(event.get("competitions")[0].get("broadcasts")) > 0 else "" ) - del_keys = [ - "geoBroadcasts", - "headlines", - "series", - "situation", - "tickets", - "odds", - "broadcasts", - "notes", - "competitors", - ] + del_keys = ["geoBroadcasts", "headlines", "series", "situation", "tickets", "odds", "leaders"] for k in del_keys: event.get("competitions")[0].pop(k, None) From 285f2e3f8c39aa77a2a2dd800ae446e3afef7d66 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Mon, 31 Jul 2023 05:58:50 -0400 Subject: [PATCH 49/79] changelog --- CHANGELOG.md | 2 +- docs/src/pages/CHANGELOG.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96a1831..b6bd97c 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ ## 0.0.36-7 Release: July 9, 2023 -- Switched most under the hood dataframe operations to use the python `polars` library and many functions now have a parameter `return_as_pandas` which defaults to `True` but can be set to `False` to return a polars dataframe instead of a pandas dataframe. This is set this way for backward compatibility but will be changed to default to `False` in a future release. +- Switched most under the hood dataframe operations to use the python `polars` library and many functions now have a parameter `return_as_pandas` which defaults to `False` but can be set to `True` to return a pandas dataframe instead of a polars dataframe. This is a **breaking change.** - Added `**kwargs` which pass arguments to the `dl_utils.download()` function, including `headers`, `proxy`, `timeout` (default 30s), `num_retries` (default = 15), `logger` (default = None) - Function `espn_cfb_game_rosters()` added. - Function `espn_nba_game_rosters()` added. diff --git a/docs/src/pages/CHANGELOG.md b/docs/src/pages/CHANGELOG.md index 96a1831..b6bd97c 100755 --- a/docs/src/pages/CHANGELOG.md +++ b/docs/src/pages/CHANGELOG.md @@ -1,5 +1,5 @@ ## 0.0.36-7 Release: July 9, 2023 -- Switched most under the hood dataframe operations to use the python `polars` library and many functions now have a parameter `return_as_pandas` which defaults to `True` but can be set to `False` to return a polars dataframe instead of a pandas dataframe. This is set this way for backward compatibility but will be changed to default to `False` in a future release. +- Switched most under the hood dataframe operations to use the python `polars` library and many functions now have a parameter `return_as_pandas` which defaults to `False` but can be set to `True` to return a pandas dataframe instead of a polars dataframe. This is a **breaking change.** - Added `**kwargs` which pass arguments to the `dl_utils.download()` function, including `headers`, `proxy`, `timeout` (default 30s), `num_retries` (default = 15), `logger` (default = None) - Function `espn_cfb_game_rosters()` added. - Function `espn_nba_game_rosters()` added. From cb71e1a2de380a9e28dea6a2d900f02a8e0f52a0 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Mon, 31 Jul 2023 07:16:40 -0400 Subject: [PATCH 50/79] Function `cfb.load_cfb_betting_lines()` added. --- CHANGELOG.md | 1 + .../doctrees/sportsdataverse.cfb.doctree | Bin 102268 -> 106790 bytes .../_build/markdown/sportsdataverse.cfb.md | 18 ++++++++++++- docs/docs/cfb/index.md | 18 ++++++++++++- docs/src/pages/CHANGELOG.md | 1 + sportsdataverse/cfb/cfb_loaders.py | 25 +++++++++++++++++- sportsdataverse/config.py | 1 + 7 files changed, 61 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6bd97c..b6ef881 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Function `espn_nhl_game_rosters()` added. - Function `espn_wbb_game_rosters()` added. - Function `espn_wnba_game_rosters()` added. +- Function `load_cfb_betting_lines()` added (only 2006 through 2019). ## 0.0.34-35 Release: May 7-9, 2023 - Reconfigured some imports diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.cfb.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.cfb.doctree index 2fb40523624dc532f97d6ab0de93ed96f3120a92..2bb36849f6b5a105b63c97327018db7640d77e9a 100755 GIT binary patch delta 11930 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 IZGg$}|G>J6KL7v# delta 10878 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 pl.DataFra return data.to_pandas(use_pyarrow_extension_array=True) if return_as_pandas else data +def load_cfb_betting_lines(return_as_pandas=False) -> pl.DataFrame: + """Load college football betting lines information + + Example: + `cfb_df = sportsdataverse.cfb.load_cfb_betting_lines()` + + Args: + return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. + + Returns: + pd.DataFrame: Pandas dataframe containing teams available for the requested seasons. + """ + + return ( + pl.read_parquet(CFB_BETTING_LINES_URL, use_pyarrow=True, columns=None).to_pandas( + use_pyarrow_extension_array=True + ) + if return_as_pandas + else pl.read_parquet(CFB_BETTING_LINES_URL, use_pyarrow=True, columns=None) + ) + + def get_cfb_teams(return_as_pandas=False) -> pl.DataFrame: """Load college football team ID information and logos Example: - `cfb_df = sportsdataverse.cfb.cfb_teams()` + `cfb_df = sportsdataverse.cfb.get_cfb_teams()` Args: return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. diff --git a/sportsdataverse/config.py b/sportsdataverse/config.py index 3da5428..c2123b6 100755 --- a/sportsdataverse/config.py +++ b/sportsdataverse/config.py @@ -6,6 +6,7 @@ CFB_TEAM_LOGO_URL = SGITHUB + "cfbfastR-data/main/teams/teams_colors_logos.parquet" CFB_TEAM_SCHEDULE_URL = SGITHUB + "cfbfastR-data/main/schedules/parquet/cfb_schedules_{season}.parquet" CFB_TEAM_INFO_URL = SGITHUB + "cfbfastR-data/main/team_info/parquet/cfb_team_info_{season}.parquet" +CFB_BETTING_LINES_URL = SGITHUB + "cfbfastR-data/main/betting/parquet/cfb_line_odds.parquet" NHL_BASE_URL = SGITHUB + "fastRhockey-data/main/nhl/pbp/parquet/play_by_play_{season}.parquet" NHL_PLAYER_BOX_URL = SGITHUB + "fastRhockey-data/main/nhl/player_box/parquet/player_box_{season}.parquet" From 8f2acb47d0e63296b8bcd39aeeb01ee6a06d99d6 Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Mon, 31 Jul 2023 07:19:35 -0400 Subject: [PATCH 51/79] docs --- .../doctrees/sportsdataverse.cfb.doctree | Bin 106790 -> 106736 bytes .../_build/markdown/sportsdataverse.cfb.md | 4 ++-- docs/docs/cfb/index.md | 4 ++-- docs/src/pages/CHANGELOG.md | 2 +- sportsdataverse/cfb/cfb_loaders.py | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Sphinx-docs/_build/doctrees/sportsdataverse.cfb.doctree b/Sphinx-docs/_build/doctrees/sportsdataverse.cfb.doctree index 2bb36849f6b5a105b63c97327018db7640d77e9a..1e882ab8af0867203d6819174a6cb46d340515cc 100755 GIT binary patch delta 183 zcmZ2>i0#8cHkJm~sjM4WA|e=_Cx6UzV$_>Hk&jVy`U77^mC2tY@))fqKRBa3xh|WJ zOK(aBd+d}9u^zb4W}&Ei{EVqUea1XVsU; pl.DataFrame: return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing teams available for the requested seasons. + pd.DataFrame: Pandas dataframe containing betting lines available for the available seasons. """ return ( @@ -158,7 +158,7 @@ def get_cfb_teams(return_as_pandas=False) -> pl.DataFrame: return_as_pandas (bool): If True, returns a pandas dataframe. If False, returns a polars dataframe. Returns: - pd.DataFrame: Pandas dataframe containing teams available for the requested seasons. + pd.DataFrame: Pandas dataframe containing teams available. """ return ( From 9e2c8ac10147a2d9aee40fa594a9e8bb54560e9a Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Mon, 31 Jul 2023 08:16:25 -0400 Subject: [PATCH 52/79] schedule --- sportsdataverse/cfb/cfb_schedule.py | 2 ++ sportsdataverse/mbb/mbb_schedule.py | 2 ++ sportsdataverse/nba/nba_schedule.py | 2 ++ sportsdataverse/nfl/nfl_schedule.py | 2 ++ sportsdataverse/nhl/nhl_schedule.py | 2 ++ sportsdataverse/wbb/wbb_schedule.py | 2 ++ sportsdataverse/wnba/wnba_schedule.py | 2 ++ 7 files changed, 14 insertions(+) diff --git a/sportsdataverse/cfb/cfb_schedule.py b/sportsdataverse/cfb/cfb_schedule.py index dd9c6d2..fefb043 100755 --- a/sportsdataverse/cfb/cfb_schedule.py +++ b/sportsdataverse/cfb/cfb_schedule.py @@ -37,6 +37,8 @@ def espn_cfb_schedule( ev = pl.DataFrame() events_txt = resp.json() events = events_txt.get("events") + if events is None: + return pd.DataFrame() if return_as_pandas else pl.DataFrame() if len(events) == 0: return pd.DataFrame() if return_as_pandas else pl.DataFrame() diff --git a/sportsdataverse/mbb/mbb_schedule.py b/sportsdataverse/mbb/mbb_schedule.py index 3f245b4..d09205d 100755 --- a/sportsdataverse/mbb/mbb_schedule.py +++ b/sportsdataverse/mbb/mbb_schedule.py @@ -33,6 +33,8 @@ def espn_mbb_schedule( ev = pl.DataFrame() events_txt = resp.json() events = events_txt.get("events") + if events is None: + return pd.DataFrame() if return_as_pandas else pl.DataFrame() if len(events) == 0: return pd.DataFrame() if return_as_pandas else pl.DataFrame() diff --git a/sportsdataverse/nba/nba_schedule.py b/sportsdataverse/nba/nba_schedule.py index 04a34d9..0e1db4f 100755 --- a/sportsdataverse/nba/nba_schedule.py +++ b/sportsdataverse/nba/nba_schedule.py @@ -27,6 +27,8 @@ def espn_nba_schedule(dates=None, season_type=None, limit=500, return_as_pandas= ev = pl.DataFrame() events_txt = resp.json() events = events_txt.get("events") + if events is None: + return pd.DataFrame() if return_as_pandas else pl.DataFrame() if len(events) == 0: return pd.DataFrame() if return_as_pandas else pl.DataFrame() diff --git a/sportsdataverse/nfl/nfl_schedule.py b/sportsdataverse/nfl/nfl_schedule.py index 4f522f6..5f4ef3c 100755 --- a/sportsdataverse/nfl/nfl_schedule.py +++ b/sportsdataverse/nfl/nfl_schedule.py @@ -30,6 +30,8 @@ def espn_nfl_schedule( ev = pl.DataFrame() events_txt = resp.json() events = events_txt.get("events") + if events is None: + return pd.DataFrame() if return_as_pandas else pl.DataFrame() if len(events) == 0: return pd.DataFrame() if return_as_pandas else pl.DataFrame() diff --git a/sportsdataverse/nhl/nhl_schedule.py b/sportsdataverse/nhl/nhl_schedule.py index 641de46..bea0d66 100755 --- a/sportsdataverse/nhl/nhl_schedule.py +++ b/sportsdataverse/nhl/nhl_schedule.py @@ -28,6 +28,8 @@ def espn_nhl_schedule(dates=None, season_type=None, limit=500, return_as_pandas= ev = pl.DataFrame() events_txt = resp.json() events = events_txt.get("events") + if events is None: + return pd.DataFrame() if return_as_pandas else pl.DataFrame() if len(events) == 0: return pd.DataFrame() if return_as_pandas else pl.DataFrame() diff --git a/sportsdataverse/wbb/wbb_schedule.py b/sportsdataverse/wbb/wbb_schedule.py index d5c6b5a..09654c0 100755 --- a/sportsdataverse/wbb/wbb_schedule.py +++ b/sportsdataverse/wbb/wbb_schedule.py @@ -34,6 +34,8 @@ def espn_wbb_schedule( ev = pl.DataFrame() events_txt = resp.json() events = events_txt.get("events") + if events is None: + return pd.DataFrame() if return_as_pandas else pl.DataFrame() if len(events) == 0: return pd.DataFrame() if return_as_pandas else pl.DataFrame() diff --git a/sportsdataverse/wnba/wnba_schedule.py b/sportsdataverse/wnba/wnba_schedule.py index f124a10..e14fd3d 100755 --- a/sportsdataverse/wnba/wnba_schedule.py +++ b/sportsdataverse/wnba/wnba_schedule.py @@ -24,6 +24,8 @@ def espn_wnba_schedule(dates=None, season_type=None, limit=500, return_as_pandas ev = pl.DataFrame() events_txt = resp.json() events = events_txt.get("events") + if events is None: + return pd.DataFrame() if return_as_pandas else pl.DataFrame() if len(events) == 0: return pd.DataFrame() if return_as_pandas else pl.DataFrame() From 1b1c2541b5595d78c1ffb60b461207f0db67b19b Mon Sep 17 00:00:00 2001 From: saiemgilani Date: Mon, 31 Jul 2023 14:25:22 -0400 Subject: [PATCH 53/79] messing around with schedule things... it's a working mess --- .../_build/doctrees/environment.pickle | Bin 924066 -> 930542 bytes setup.py | 2 +- sportsdataverse.egg-info/PKG-INFO | 83 ------ sportsdataverse.egg-info/SOURCES.txt | 256 ------------------ sportsdataverse.egg-info/dependency_links.txt | 0 sportsdataverse.egg-info/requires.txt | 57 ---- sportsdataverse.egg-info/top_level.txt | 1 - sportsdataverse/cfb/cfb_schedule.py | 38 ++- sportsdataverse/mbb/mbb_schedule.py | 25 +- sportsdataverse/nba/nba_schedule.py | 29 +- sportsdataverse/nfl/nfl_schedule.py | 29 +- sportsdataverse/nhl/nhl_schedule.py | 30 +- sportsdataverse/wbb/wbb_schedule.py | 29 +- sportsdataverse/wnba/wnba_schedule.py | 33 ++- 14 files changed, 162 insertions(+), 450 deletions(-) delete mode 100755 sportsdataverse.egg-info/PKG-INFO delete mode 100755 sportsdataverse.egg-info/SOURCES.txt delete mode 100755 sportsdataverse.egg-info/dependency_links.txt delete mode 100755 sportsdataverse.egg-info/requires.txt delete mode 100755 sportsdataverse.egg-info/top_level.txt diff --git a/Sphinx-docs/_build/doctrees/environment.pickle b/Sphinx-docs/_build/doctrees/environment.pickle index 78b0af5bb735ae10a7b7d2ca970b62cca25d3595..9cd05c7d856e9838c9c829d50a290c571c2bb9a5 100755 GIT binary patch delta 65265 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