Source code for pbpstats.resources.enhanced_pbp.start_of_period

import abc
import json
import os

import requests

from pbpstats import (
    G_LEAGUE_GAME_ID_PREFIX,
    G_LEAGUE_STRING,
    HEADERS,
    NBA_GAME_ID_PREFIX,
    NBA_STRING,
    REQUEST_TIMEOUT,
    WNBA_GAME_ID_PREFIX,
    WNBA_STRING,
)
from pbpstats.overrides import IntDecoder
from pbpstats.resources.enhanced_pbp import (
    Ejection,
    EndOfPeriod,
    FieldGoal,
    Foul,
    FreeThrow,
    JumpBall,
    Substitution,
    Timeout,
    Turnover,
)


[docs]class InvalidNumberOfStartersException(Exception): """ Class for exception when a team's 5 period starters can't be determined. You can add the correct period starters to overrides/missing_period_starters.json in your data directory to fix this. """ pass
[docs]class StartOfPeriod(metaclass=abc.ABCMeta): """ Class for start of period events """
[docs] @abc.abstractclassmethod def get_period_starters(self, file_directory): """ Gets player ids of players who started the period for each team :param str file_directory: directory in which overrides subdirectory exists containing period starter overrides when period starters can't be determined from parsing pbp events :returns: dict with list of player ids for each team with players on the floor at start of period :raises: :obj:`~pbpstats.resources.enhanced_pbp.start_of_period.InvalidNumberOfStartersException`: If all 5 players that start the period for a team can't be determined. """ pass
@property def current_players(self): """ returns period starters """ return self.period_starters @property def league(self): """ Returns League for game id. First 2 in game id represent league - 00 for nba, 10 for wnba, 20 for g-league """ if self.game_id[0:2] == NBA_GAME_ID_PREFIX: return NBA_STRING elif self.game_id[0:2] == G_LEAGUE_GAME_ID_PREFIX: return G_LEAGUE_STRING elif self.game_id[0:2] == WNBA_GAME_ID_PREFIX: return WNBA_STRING @property def league_url_part(self): if self.game_id[0:2] == NBA_GAME_ID_PREFIX: return NBA_STRING elif self.game_id[0:2] == G_LEAGUE_GAME_ID_PREFIX: return f"{G_LEAGUE_STRING}.{NBA_STRING}" elif self.game_id[0:2] == WNBA_GAME_ID_PREFIX: return WNBA_STRING def _get_starters_from_boxscore_request(self): """ makes request to boxscore url for time from period start to first event to get period starters """ base_url = ( f"https://stats.{self.league_url_part}.com/stats/boxscoretraditionalv2" ) event = self while event is not None and event.seconds_remaining == self.seconds_remaining: event = event.next_event seconds_to_first_event = self.seconds_remaining - event.seconds_remaining if self.league == WNBA_STRING: seconds_in_period = 6000 else: seconds_in_period = 7200 if self.period == 1: start_range = 0 elif self.period <= 4: start_range = int(seconds_in_period * (self.period - 1)) else: start_range = int(4 * seconds_in_period + 3000 * (self.period - 5)) end_range = int(start_range + seconds_to_first_event * 10) params = { "GameId": self.game_id, "StartPeriod": 0, "EndPeriod": 0, "RangeType": 2, "StartRange": start_range, "EndRange": end_range, } starters_by_team = {} response = requests.get( base_url, params, headers=HEADERS, timeout=REQUEST_TIMEOUT ) if response.status_code == 200: response_json = response.json() else: response.raise_for_status() headers = response_json["resultSets"][0]["headers"] rows = response_json["resultSets"][0]["rowSet"] players = [dict(zip(headers, row)) for row in rows] starters = sorted( players, key=lambda k: int(k["MIN"].split(":")[1]), reverse=True ) if len(starters) < 10: raise InvalidNumberOfStartersException( f"GameId: {self.game_id}, Period: {self.period}, Starters: {starters}" ) for starter in starters[0:10]: team_id = starter["TEAM_ID"] player_id = starter["PLAYER_ID"] if team_id not in starters_by_team.keys(): starters_by_team[team_id] = [] starters_by_team[team_id].append(player_id) for team_id, starters in starters_by_team.items(): if len(starters) != 5: raise InvalidNumberOfStartersException( f"GameId: {self.game_id}, Period: {self.period}, TeamId: {team_id}, Players: {starters}" ) return starters_by_team
[docs] def get_team_starting_with_ball(self): """ returns team id for team on starting period with the ball """ if (self.period == 1 or self.period >= 5) and isinstance( self.next_event, JumpBall ): # period starts with jump ball - team that wins starts with the ball return self.next_event.team_id else: # find team id on first shot, non technical ft or turnover next_event = self.next_event while not ( isinstance(next_event, (FieldGoal, Turnover)) or ( isinstance(next_event, FreeThrow) and not next_event.is_technical_ft ) ): next_event = next_event.next_event return next_event.team_id
[docs] def get_offense_team_id(self): """ returns team id for team on starting period on offense """ return self.team_starting_with_ball
def _get_players_who_started_period_with_team_map(self): starters = [] subbed_in_players = [] player_team_map = {} # only player1 has team id in event, this is to track team event = self while event is not None and not isinstance(event, EndOfPeriod): if ( not isinstance(event, Timeout) and event.player1_id != 0 and hasattr(event, "team_id") ): player_id = event.player1_id if not isinstance(event, JumpBall): # on jump balls team id is winning team, not guaranteed to be player1 team player_team_map[player_id] = event.team_id if ( isinstance(event, Substitution) and event.incoming_player_id is not None ): player_team_map[event.incoming_player_id] = event.team_id if ( event.incoming_player_id not in starters and event.incoming_player_id not in subbed_in_players ): subbed_in_players.append(event.incoming_player_id) if player_id not in starters and player_id not in subbed_in_players: starters.append(player_id) is_technical_foul = isinstance(event, Foul) and ( event.is_technical or event.is_double_technical ) if player_id not in starters and player_id not in subbed_in_players: tech_ft_at_period_start = ( isinstance(event, FreeThrow) and event.clock == "12:00" ) if not ( is_technical_foul or isinstance(event, Ejection) or tech_ft_at_period_start ): # ignore all techs because a player could get a technical foul when they aren't in the game starters.append(player_id) # need player2_id and player3_id for players who play full period and never appear in an event as player_id - ex assists, blocks, steals, foul drawn if not isinstance(event, Substitution) and not ( is_technical_foul or isinstance(event, Ejection) ): # ignore all techs because a player could get a technical foul when they aren't in the game if ( hasattr(event, "player2_id") and event.player2_id not in starters and event.player2_id not in subbed_in_players ): starters.append(event.player2_id) if ( hasattr(event, "player3_id") and event.player3_id not in starters and event.player3_id not in subbed_in_players ): starters.append(event.player3_id) event = event.next_event return starters, player_team_map def _split_up_starters_by_team(self, starters, player_team_map): starters_by_team = {} # for players who don't appear in event as player1 - won't be in player_team_map dangling_starters = [] for player_id in starters: team_id = player_team_map.get(player_id) if team_id is not None: if team_id not in starters_by_team.keys(): starters_by_team[team_id] = [] starters_by_team[team_id].append(player_id) else: dangling_starters.append(player_id) # if there is one dangling starter we can add it to team missing a starter if len(dangling_starters) == 1 and len(starters) == 10: for _, team_starters in starters_by_team.items(): if len(team_starters) == 4: team_starters += dangling_starters return starters_by_team def _check_both_teams_have_5_starters(self, starters_by_team, file_directory): """ raises exception if either team does not have 5 starters """ for team_id, starters in starters_by_team.items(): if len(starters) != 5: # check if game and period are in overrides file if file_directory is None: raise InvalidNumberOfStartersException( f"GameId: {self.game_id}, Period: {self.period}, TeamId: {team_id}, Players: {starters}" ) missing_period_starters_file_path = ( f"{file_directory}/overrides/missing_period_starters.json" ) if not os.path.isfile(missing_period_starters_file_path): raise InvalidNumberOfStartersException( f"GameId: {self.game_id}, Period: {self.period}, TeamId: {team_id}, Players: {starters}" ) with open(missing_period_starters_file_path) as f: # hard code corrections for games with incorrect number of starters exceptions missing_period_starters = json.loads(f.read(), cls=IntDecoder) game_id = ( self.game_id if self.league == NBA_STRING else int(self.game_id) ) if ( game_id in missing_period_starters.keys() and self.period in missing_period_starters[game_id].keys() and team_id in missing_period_starters[game_id][self.period].keys() ): starters_by_team[team_id] = missing_period_starters[game_id][ self.period ][team_id] else: raise InvalidNumberOfStartersException( f"GameId: {game_id}, Period: {self.period}, TeamId: {team_id}, Players: {starters}" ) def _get_period_starters_from_period_events( self, file_directory, ignore_missing_starters=False ): starters, player_team_map = self._get_players_who_started_period_with_team_map() starters_by_team = self._split_up_starters_by_team(starters, player_team_map) if not ignore_missing_starters: self._check_both_teams_have_5_starters(starters_by_team, file_directory) return starters_by_team @property def event_stats(self): """ returns list of dicts with all stats for event """ return self.base_stats