Source code for pbpstats.resources.enhanced_pbp.stats_nba.enhanced_pbp_item

"""
``StatsEnhancedPbpItem`` is the base class for all stats.nba.com enhanced pbp event types
"""
from collections import defaultdict

import requests

from pbpstats import HEADERS, REQUEST_TIMEOUT
from pbpstats.resources.enhanced_pbp import (
    FieldGoal,
    Foul,
    FreeThrow,
    JumpBall,
    Rebound,
    StartOfPeriod,
    Turnover,
    Violation,
)
from pbpstats.resources.enhanced_pbp.enhanced_pbp_item import EnhancedPbpItem

KEY_ATTR_MAPPER = {
    "GAME_ID": "game_id",
    "EVENTNUM": "event_num",
    "PCTIMESTRING": "clock",
    "PERIOD": "period",
    "EVENTMSGACTIONTYPE": "event_action_type",
    "EVENTMSGTYPE": "event_type",
    "PLAYER1_ID": "player1_id",
    "PLAYER1_TEAM_ID": "team_id",
    "PLAYER2_ID": "player2_id",
    "PLAYER3_ID": "player3_id",
    "VIDEO_AVAILABLE_FLAG": "video_available",
}


[docs]class StatsEnhancedPbpItem(EnhancedPbpItem): """ Base class for enhanced pbp events from stats.nba.com :param dict event: dict with event data :param int order: sequential order in which event occurs """ def __init__(self, event, order): for key, value in KEY_ATTR_MAPPER.items(): if event.get(key) is not None: setattr(self, value, event.get(key)) if ( event.get("HOMEDESCRIPTION") is not None and event.get("VISITORDESCRIPTION") is not None ): self.description = ( f"{event.get('HOMEDESCRIPTION')}: {event.get('VISITORDESCRIPTION')}" ) elif event.get("HOMEDESCRIPTION") is not None: self.description = f"{event.get('HOMEDESCRIPTION')}" elif event.get("VISITORDESCRIPTION") is not None: self.description = f"{event.get('VISITORDESCRIPTION')}" elif event.get("NEUTRALDESCRIPTION") is not None: self.description = f"{event.get('NEUTRALDESCRIPTION')}" else: self.description = "" if ( event.get("PLAYER1_TEAM_ID") is None and event.get("PLAYER1_ID") is not None and event.get("EVENTMSGTYPE") != 18 ): # need to set team id in these cases where player id is team id # EVENTMSGTYPE 18 is replay event - it is ignored because it has no team id and unknown player id self.team_id = event.get("PLAYER1_ID", 0) self.player1_id = 0 # fix team/player ids on some event types so they are consistent with DataPbpItem if self.event_type == 10: # jump ball PLAYER3_TEAM_ID is player who ball gets tipped to self.player2_id = event["PLAYER3_ID"] self.player3_id = event["PLAYER2_ID"] if event["PLAYER3_TEAM_ID"] is not None: self.team_id = event["PLAYER3_TEAM_ID"] else: # when jump ball is tipped out of bounds, winning team is PLAYER3_ID self.team_id = event["PLAYER3_ID"] if hasattr(self, "player2_id"): delattr(self, "player2_id") elif self.event_type in [5, 6]: # steals need to change PLAYER2_ID to player3_id - this is player who turned ball over # fouls need to change PLAYER2_ID to player3_id - this is player who drew foul if hasattr(self, "player2_id"): delattr(self, "player2_id") if event.get("PLAYER2_ID") is not None: self.player3_id = event["PLAYER2_ID"] if hasattr(self, "player2_id") and self.player2_id == 0: delattr(self, "player2_id") if hasattr(self, "player3_id") and self.player3_id == 0: delattr(self, "player3_id") self.order = order self.player_game_fouls = defaultdict(int) self.possession_changing_override = False self.non_possession_changing_override = False self.score = defaultdict(int) @property def data(self): """ returns event as a dict """ return self.__dict__ @property def seconds_remaining(self): """ returns seconds remaining in period as a ``float`` """ split = self.clock.split(":") return float(split[0]) * 60 + float(split[1]) @property def video_url(self): """ returns url for mp4 video of play, if available """ if self.video_available == 1: parameters = {"GameEventID": self.event_num, "GameID": self.game_id} base_url = "https://stats.nba.com/stats/videoeventsasset" response = requests.get( base_url, parameters, headers=HEADERS, timeout=REQUEST_TIMEOUT ) if response.status_code == 200: response_json = response.json() video_urls = response_json["resultSets"]["Meta"]["videoUrls"] if len(video_urls) == 1: return video_urls[0]["murl"] else: response.raise_for_status() return None
[docs] def get_offense_team_id(self): """ returns team id for team on offense for event """ if isinstance(self, Foul) and (self.is_charge or self.is_offensive_foul): # offensive foul returns team id # this isn't separate method in Foul class because some fouls can be committed # on offense or defense (loose ball, flagrant, technical) return self.team_id event_to_check = self.previous_event team_ids = list(self.current_players.keys()) while event_to_check is not None and not ( isinstance(event_to_check, (FieldGoal, JumpBall)) or ( isinstance(event_to_check, Turnover) and not event_to_check.is_no_turnover ) or (isinstance(event_to_check, Rebound) and event_to_check.is_real_rebound) or ( isinstance(event_to_check, FreeThrow) and not event_to_check.is_technical_ft ) ): event_to_check = event_to_check.previous_event if event_to_check is None and self.next_event is not None: # should only get here on first possession of period when first event is non-offensive foul, # FieldGoal, FreeThrow, Rebound, Turnover, JumpBall return self.next_event.get_offense_team_id() if isinstance(event_to_check, Turnover) and not event_to_check.is_no_turnover: return ( team_ids[0] if team_ids[1] == event_to_check.get_offense_team_id() else team_ids[1] ) if isinstance(event_to_check, Rebound) and event_to_check.is_real_rebound: if not event_to_check.oreb: return ( team_ids[0] if team_ids[1] == event_to_check.get_offense_team_id() else team_ids[1] ) return event_to_check.get_offense_team_id() if isinstance(event_to_check, (FieldGoal, FreeThrow)): if event_to_check.is_possession_ending_event: return ( team_ids[0] if team_ids[1] == event_to_check.get_offense_team_id() else team_ids[1] ) return event_to_check.get_offense_team_id() if isinstance(event_to_check, JumpBall): if event_to_check.count_as_possession: team_ids = list(self.current_players.keys()) return ( team_ids[0] if team_ids[1] == event_to_check.get_offense_team_id() else team_ids[1] ) return event_to_check.get_offense_team_id()
@property def is_possession_ending_event(self): """ returns True if event ends a possession, False otherwise """ if self.next_event is None: return True if self.possession_changing_override: return True if self.non_possession_changing_override: return False if isinstance(self, Rebound) and self.is_real_rebound and not self.oreb: return True if isinstance(self, Turnover) and not self.is_no_turnover: return True if isinstance(self, FieldGoal) and self.is_made: # no possession change on flagrant foul next_event_is_flagrant_drawn = ( isinstance(self.next_event, Foul) and self.next_event.is_flagrant and self.team_id != self.next_event.team_id and self.clock == self.next_event.clock ) if ( not self.is_make_that_does_not_end_possession and not next_event_is_flagrant_drawn ): return True if isinstance(self, FreeThrow) and self.is_made and self.is_end_ft: next_event_is_foul_drawn_at_ft_time = ( isinstance(self.next_event, Foul) and self.clock == self.next_event.clock and self.team_id != self.next_event.team_id and ( self.next_event.is_loose_ball_foul or self.next_event.is_personal_foul or self.next_event.is_away_from_play_foul or self.next_event.is_flagrant ) ) if ( not self.is_away_from_play_ft and not self.is_inbound_foul_ft and not self.is_transition_take_foul_ft and not next_event_is_foul_drawn_at_ft_time ): return True if not isinstance(self.previous_event, StartOfPeriod) and isinstance( self, JumpBall ): return self._is_jump_ball_possession_ending_event() return False def _is_jump_ball_possession_ending_event(self): """ need to check for rare case where possession changes on jump ball but there is no turnover/rebound """ if ( isinstance(self.next_event, Turnover) and not self.next_event.is_no_turnover and self.next_event.clock == self.clock ): # if next event is steal at same time of pbp don't need to change possession since steal takes care of it return False elif ( isinstance(self.previous_event, Turnover) and not self.previous_event.is_no_turnover and self.previous_event.clock == self.clock ): # if previous event is steal at same time of pbp don't need to change possession since steal takes care of it return False elif ( isinstance(self.next_event, Violation) and self.next_event.is_jumpball_violation ): # jump ball violation - turnover will be possession changing event return False elif isinstance(self.next_event, Foul) and self.next_event.clock == self.clock: next_event = self.next_event.next_event if ( isinstance(next_event, Turnover) and not next_event.is_no_turnover and next_event.clock == self.clock ): # foul turnover on jump ball - turnover will trigger change of possession return False jump_ball_winning_team_id = self.team_id 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: return False if isinstance(prev_event, Rebound): jump_ball_winning_team_started_possession_with_ball = ( jump_ball_winning_team_id == prev_event.team_id ) else: jump_ball_winning_team_started_possession_with_ball = ( jump_ball_winning_team_id != prev_event.team_id ) next_event_rebound = ( isinstance(self.next_event, Rebound) and self.next_event.is_real_rebound ) if not jump_ball_winning_team_started_possession_with_ball and not ( next_event_rebound or isinstance(self.next_event, JumpBall) ): # ignore jump ball if next event is a rebound or jump ball since that will trigger possession change return True return False