Source code for pbpstats.resources.enhanced_pbp.enhanced_pbp_item

"""
``EnhancedPbpItem`` is an abstract base class for all enhanced pbp event types
"""
import abc

import pbpstats
from pbpstats.resources.enhanced_pbp import FieldGoal, Foul, FreeThrow, Rebound


[docs]class EnhancedPbpItem(metaclass=abc.ABCMeta): def __repr__(self): return f"<{type(self).__name__} GameId: {self.game_id}, Description: {self.description}, Time: {self.clock}, EventNum: {self.event_num}>" @abc.abstractproperty def is_possession_ending_event(self): """ returns True if event ends a possession, False otherwise """ pass @abc.abstractproperty def event_stats(self): """ returns list of dicts with all stats for event """ pass
[docs] @abc.abstractmethod def get_offense_team_id(self): """ returns team id for team on offense for event """ pass
@abc.abstractproperty def seconds_remaining(self): """ returns seconds remaining in period as a ``float`` """ pass @property def base_stats(self): """ returns list of dicts with all seconds played and possession count stats for event """ return ( self._get_seconds_played_stats_items() + self._get_possessions_played_stats_items() )
[docs] def get_all_events_at_current_time(self): """ returns list of all events that take place as the same time as the current event """ events = [self] # going backwards event = self while event is not None and self.seconds_remaining == event.seconds_remaining: if event != self: events.append(event) event = event.previous_event # going forwards event = self while event is not None and self.seconds_remaining == event.seconds_remaining: if event != self: events.append(event) event = event.next_event return sorted(events, key=lambda k: k.order)
@property def current_players(self): """ returns dict with list of player ids for each team with players on the floor for current event For all non subsitution events current players are just the same as previous event This gets overwritten in :obj:`~pbpstats.resources.enhanced_pbp.substitution.Substitution` since those are the only event types where players are not the same as the previous event """ return self.previous_event.current_players @property def score_margin(self): """ returns the score margin from perspective of offense team before the event took place """ if self.previous_event is None: score = self.score else: score = self.previous_event.score offense_team_id = self.get_offense_team_id() offense_points = score[offense_team_id] defense_points = 0 for team_id, points in score.items(): if team_id != offense_team_id: defense_points = points return offense_points - defense_points @property def lineup_ids(self): """ returns dict with lineup ids for each team for current event. Lineup ids are hyphen separated sorted player id strings. """ lineup_ids = {} for team_id, team_players in self.current_players.items(): players = [str(player_id) for player_id in team_players] sorted_player_ids = sorted(players) lineup_id = "-".join(sorted_player_ids) lineup_ids[team_id] = lineup_id return lineup_ids @property def seconds_since_previous_event(self): """ returns the number of seconds that have elapsed since the previous event """ if self.previous_event is None: return 0 if self.seconds_remaining == 720: # so that subs between periods for live don't have negative seconds return 0 if self.seconds_remaining == 300 and self.period > 4: # so that subs between periods for live don't have negative seconds return 0 return self.previous_event.seconds_remaining - self.seconds_remaining
[docs] def is_second_chance_event(self): """ returns True if the event takes place after an offensive rebound on the current possession, False otherwise """ event = self.previous_event if isinstance(event, Rebound) and event.is_real_rebound and event.oreb: return True while not (event is None or event.is_possession_ending_event): if isinstance(event, Rebound) and event.is_real_rebound and event.oreb: return True event = event.previous_event return False
[docs] def is_penalty_event(self): """ returns True if the team on offense is in the penalty, False otherwise """ if hasattr(self, "fouls_to_give"): team_ids = list(self.current_players.keys()) offense_team_id = self.get_offense_team_id() defense_team_id = ( team_ids[0] if offense_team_id == team_ids[1] else team_ids[1] ) if self.fouls_to_give[defense_team_id] == 0: if isinstance(self, (Foul, FreeThrow, Rebound)): # if foul or free throw or rebound on a missed ft # check foul event and should return false is foul # was shooting foul and team had a foul to give if isinstance(self, Foul): foul_event = self elif isinstance(self, FreeThrow): foul_event = self.foul_that_led_to_ft else: # if rebound is on missed ft, also need to look at foul that led to FT if not self.oreb and isinstance(self.missed_shot, FreeThrow): foul_event = self.missed_shot.foul_that_led_to_ft else: return True if foul_event is None: return True fouls_to_give_prior_to_foul = ( foul_event.previous_event.fouls_to_give[defense_team_id] ) if fouls_to_give_prior_to_foul > 0: return False return True return False
@property def count_as_possession(self): """ returns True if event is possession changing event that should count as a real possession, False otherwise. In order to not include possessions which a very low probability of scoring in possession counts, possession won't be counted as a possession if it starts with <= 2 seconds left and no points are scored before period ends """ if self.is_possession_ending_event: if self.seconds_remaining > 2: return True # check when previous possession ended prev_event = self.previous_event while prev_event is not None and not prev_event.is_possession_ending_event: prev_event = prev_event.previous_event if prev_event is None or prev_event.seconds_remaining > 2: return True # possession starts in final 2 seconds # return True if there is a FT or FGM between now and end of period next_event = prev_event.next_event while next_event is not None: if isinstance(next_event, FreeThrow) or ( isinstance(next_event, FieldGoal) and next_event.is_made ): return True next_event = next_event.next_event return False def _get_seconds_played_stats_items(self): """ makes event stats items for: - seconds played - seconds played for number of fouls - second chance seconds played - penalty seconds played """ stat_items = [] team_ids = list(self.current_players.keys()) offense_team_id = self.get_offense_team_id() is_penalty_event = self.is_penalty_event() is_second_chance_event = self.is_second_chance_event() if self.seconds_since_previous_event != 0: for team_id, players in self.previous_event.current_players.items(): seconds_stat_key = ( pbpstats.SECONDS_PLAYED_OFFENSE_STRING if team_id == offense_team_id else pbpstats.SECONDS_PLAYED_DEFENSE_STRING ) opponent_team_id = ( team_ids[0] if team_id == team_ids[1] else team_ids[1] ) previous_poss_lineup_ids = self.previous_event.lineup_ids for player_id in players: keys_to_add = [seconds_stat_key] player_fouls = self.previous_event.player_game_fouls[player_id] period = self.period if self.period <= 4 else "OT" foul_tracking_seconds_stat_key = ( f"Period{period}Fouls{player_fouls}{seconds_stat_key}" ) keys_to_add.append(foul_tracking_seconds_stat_key) if is_second_chance_event: seconds_chance_seconds_stat_key = ( f"{pbpstats.SECOND_CHANCE_STRING}{seconds_stat_key}" ) keys_to_add.append(seconds_chance_seconds_stat_key) if is_penalty_event: penalty_seconds_stat_key = ( f"{pbpstats.PENALTY_STRING}{seconds_stat_key}" ) keys_to_add.append(penalty_seconds_stat_key) for stat_key in keys_to_add: stat_item = { "player_id": player_id, "team_id": team_id, "opponent_team_id": opponent_team_id, "lineup_id": previous_poss_lineup_ids[team_id], "opponent_lineup_id": previous_poss_lineup_ids[ opponent_team_id ], "stat_key": stat_key, "stat_value": self.seconds_since_previous_event, } stat_items.append(stat_item) return stat_items def _get_possessions_played_stats_items(self): """ makes event stats items for: - possessions played - second chance possessions played - penalty possessions played """ stat_items = [] team_ids = list(self.current_players.keys()) offense_team_id = self.get_offense_team_id() is_penalty_event = self.is_penalty_event() is_second_chance_event = self.is_second_chance_event() if self.count_as_possession: if isinstance(self, FreeThrow): current_players = self.event_for_efficiency_stats.current_players lineup_ids = self.event_for_efficiency_stats.lineup_ids else: current_players = self.current_players lineup_ids = self.lineup_ids for team_id, players in current_players.items(): possessions_stat_key = ( pbpstats.OFFENSIVE_POSSESSION_STRING if team_id == offense_team_id else pbpstats.DEFENSIVE_POSSESSION_STRING ) opponent_team_id = ( team_ids[0] if team_id == team_ids[1] else team_ids[1] ) for player_id in players: keys_to_add = [possessions_stat_key] if is_second_chance_event: seconds_chance_possessions_stat_key = ( f"{pbpstats.SECOND_CHANCE_STRING}{possessions_stat_key}" ) keys_to_add.append(seconds_chance_possessions_stat_key) if is_penalty_event: penalty_possessions_stat_key = ( f"{pbpstats.PENALTY_STRING}{possessions_stat_key}" ) keys_to_add.append(penalty_possessions_stat_key) for stat_key in keys_to_add: stat_item = { "player_id": player_id, "team_id": team_id, "opponent_team_id": opponent_team_id, "lineup_id": lineup_ids[team_id], "opponent_lineup_id": lineup_ids[opponent_team_id], "stat_key": stat_key, "stat_value": 1, } stat_items.append(stat_item) return stat_items