Source code for pbpstats.resources.enhanced_pbp.free_throw

import abc

import pbpstats
from pbpstats.resources.enhanced_pbp import Foul


[docs]class FreeThrow(metaclass=abc.ABCMeta): """ Class for free throw events """ shot_type = pbpstats.FREE_THROW_STRING @abc.abstractproperty def is_made(self): """ returns True if shot was made, False otherwise """ pass @abc.abstractproperty def is_ft_1_of_1(self): pass @abc.abstractproperty def is_ft_1_of_2(self): pass @abc.abstractproperty def is_ft_2_of_2(self): pass @abc.abstractproperty def is_ft_1_of_3(self): pass @abc.abstractproperty def is_ft_2_of_3(self): pass @abc.abstractproperty def is_ft_3_of_3(self): pass @property def is_first_ft(self): """ returns True if free throw is first of trip to the free throw line, False otherwise """ return ( self.is_ft_1_of_1 or self.is_ft_1_of_2 or self.is_ft_1_of_3 or self.is_ft_1pt or self.is_ft_2pt or self.is_ft_3pt ) @property def is_end_ft(self): """ returns True if free throw is last of trip to the free throw line, False otherwise """ return ( self.is_ft_1_of_1 or self.is_ft_2_of_2 or self.is_ft_3_of_3 or self.is_ft_1pt or self.is_ft_2pt or self.is_ft_3pt ) and not self.is_flagrant_ft @abc.abstractproperty def is_technical_ft(self): pass @abc.abstractproperty def is_ft_1pt(self): pass @abc.abstractproperty def is_ft_2pt(self): pass @abc.abstractproperty def is_ft_3pt(self): pass @property def shot_value(self): """ returns shot value of a free throw Starting in 2019-20 season, the G-League added 2 and 3 point FTs """ if self.is_ft_2pt: return 2 if self.is_ft_3pt: return 3 return 1 @property def is_away_from_play_ft(self): """ returns True if free throw is from an away from the play foul, False otherwise. """ foul = self.foul_that_led_to_ft if ( ( (self.is_ft_1_of_1 or self.is_ft_1pt) or (self.is_ft_2_of_2 or self.is_ft_2pt) ) and foul is not None and foul.is_away_from_play_foul ): made_shots_at_event_time = [] fts_by_other_player_at_event_time = [] events_at_event_time = self.get_all_events_at_current_time() for event in events_at_event_time: if hasattr(event, "is_made") and event.is_made: if not isinstance(event, FreeThrow): made_shots_at_event_time.append(event) if ( isinstance(event, FreeThrow) and event.player1_id != self.player1_id ): fts_by_other_player_at_event_time.append(event) if len(made_shots_at_event_time) == 0: # check for made free throw by other player - this is where player is fouled going for rebound on made FT if len(fts_by_other_player_at_event_time) == 0: return True for ft in fts_by_other_player_at_event_time: if ft.team_id != self.team_id: return True else: made_shots_at_event_time = sorted( made_shots_at_event_time, key=lambda k: k.order ) if (made_shots_at_event_time[0].team_id == foul.team_id) and ( self.player1_id != made_shots_at_event_time[0].player1_id ): # make sure player who made shot is not player who shot FT return True return False @property def is_inbound_foul_ft(self): """ returns True if free throw is from an inbound foul, False otherwise. """ if self.is_ft_1_of_1 or self.is_ft_1pt: events_at_event_time = self.get_all_events_at_current_time() for event in events_at_event_time: if isinstance(event, Foul) and event.is_inbound_foul: return True return False @property def is_transition_take_foul_ft(self): """ returns True if free throw is from an transition take foul, False otherwise. """ if self.is_ft_1_of_1 or self.is_ft_1pt: events_at_event_time = self.get_all_events_at_current_time() for event in events_at_event_time: if isinstance(event, Foul) and event.is_transition_take_foul: return True return False @property def foul_that_led_to_ft(self): """ returns :obj:`~pbpstats.resources.enhanced_pbp.foul.Foul` object for the foul that resulted in the free throw """ clock = self.clock # foul should be before FT so start by going backwards event = self while ( event is not None and event.clock == clock and not ( isinstance(event, Foul) and not event.is_technical and not event.is_double_technical ) ): event = event.previous_event if ( isinstance(event, Foul) and not event.is_technical and not event.is_double_technical and event.clock == clock ): return event # bug in pbp where foul is after FT event = self while ( event is not None and event.clock == clock and not ( isinstance(event, Foul) and not event.is_technical and not event.is_double_technical ) ): event = event.next_event if ( isinstance(event, Foul) and not event.is_technical and not event.is_double_technical and event.clock == clock ): return event return None @property def num_ft_for_trip(self): """ returns number of shots for the trip to the free throw line """ if "of 1" in self.description: return 1 elif "of 2" in self.description: return 2 elif "of 3" in self.description: return 3 @property def free_throw_type(self): """ returns string description of free throw type """ if self.is_technical_ft: return "Technical" num_fts = self.num_ft_for_trip if num_fts is None: num_fts = self.shot_value if num_fts == 1: # check for shot before FT at same time as FT previous_event = self while ( previous_event is not None and previous_event.clock == self.clock and not ( hasattr(previous_event, "is_made") and previous_event.is_made and not isinstance(previous_event, FreeThrow) ) ): previous_event = previous_event.previous_event if ( previous_event is not None and previous_event.clock == self.clock and ( hasattr(previous_event, "is_made") and previous_event.is_made and not isinstance(previous_event, FreeThrow) ) ): and1_shot = previous_event if self.player1_id == and1_shot.player1_id: return f"{and1_shot.shot_value}pt And 1" else: return "1 Shot Away From Play" else: return "1 Shot Away From Play" foul_event = self.foul_that_led_to_ft if foul_event.is_shooting_foul or foul_event.is_shooting_block_foul: return f"{num_fts}pt Shooting Foul" elif foul_event.is_flagrant: if num_fts is None: # assume 2 shot flagrant if num_fts is None num_fts = 2 return f"{num_fts} Shot Flagrant" elif foul_event.is_away_from_play_foul: return f"{num_fts} Shot Away From Play" elif foul_event.is_inbound_foul: return f"{num_fts} Shot Inbound Foul" elif foul_event.is_clear_path_foul: return f"{num_fts} Shot Clear Path" elif num_fts == 3: return "3pt Shooting Foul" else: return "Penalty" @property def event_for_efficiency_stats(self): """ returns :obj:`~pbpstats.resources.enhanced_pbp.foul.Foul` object for the foul that resulted in the free throw. Plus/minus points should go to the players on the floor at the time of the foul, not the free throw. """ clock = self.clock # foul should be before FT so start by going backwards event = self while ( event is not None and event.clock == clock and not isinstance(event, Foul) ): event = event.previous_event if isinstance(event, Foul) and event.clock == clock: return event # bug in pbp where foul is after FT event = self while ( event is not None and event.clock == clock and not isinstance(event, Foul) ): event = event.next_event if isinstance(event, Foul) and event.clock == clock: return event return self @property def event_stats(self): """ returns list of dicts with all stats for event """ stats = [] team_ids = list(self.current_players.keys()) is_penalty_event = self.is_penalty_event() is_second_chance_event = self.is_second_chance_event() lineup_ids = self.event_for_efficiency_stats.lineup_ids if self.is_made: stats += self._get_made_ft_stat_items( is_second_chance_event, is_penalty_event ) # add plus minus and opponent points - used for lineup/wowy stats to get net rating points = self.shot_value for ( team_id, players, ) in self.event_for_efficiency_stats.current_players.items(): multiplier = 1 if team_id == self.team_id else -1 opponent_team_id = ( team_ids[0] if team_id == team_ids[1] else team_ids[1] ) for player_id in players: 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": pbpstats.PLUS_MINUS_STRING, "stat_value": points * multiplier, } stats.append(stat_item) if multiplier == -1: opponent_points_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": pbpstats.OPPONENT_POINTS, "stat_value": points, } stats.append(opponent_points_stat_item) if self.is_first_ft or self.is_technical_ft: stats += self._get_ft_trip_stat_items( is_second_chance_event, is_penalty_event ) if not self.is_made: stats += self._get_missed_ft_stat_items( is_second_chance_event, is_penalty_event ) opponent_team_id = team_ids[0] if self.team_id == team_ids[1] else team_ids[1] for stat in stats: if "lineup_id" not in stat.keys(): opponent_team_id = ( team_ids[0] if stat["team_id"] == team_ids[1] else team_ids[1] ) stat["lineup_id"] = lineup_ids[stat["team_id"]] stat["opponent_team_id"] = opponent_team_id stat["opponent_lineup_id"] = lineup_ids[opponent_team_id] return self.base_stats + stats def _get_made_ft_stat_items(self, is_second_chance_event, is_penalty_event): stats = [] if self.is_ft_3pt: free_throw_key = pbpstats.FT_3_PT_MADE_STRING elif self.is_ft_2pt: free_throw_key = pbpstats.FT_2_PT_MADE_STRING elif self.is_ft_1pt: free_throw_key = pbpstats.FT_1_PT_MADE_STRING elif self.is_technical_ft: free_throw_key = pbpstats.TECHNICAL_FTS_MADE_STRING else: free_throw_key = pbpstats.FTS_MADE_STRING stats.append( { "player_id": self.player1_id, "team_id": self.team_id, "stat_key": free_throw_key, "stat_value": 1, } ) if is_second_chance_event: second_chance_stat_key = f"{pbpstats.SECOND_CHANCE_STRING}{free_throw_key}" stats.append( { "player_id": self.player1_id, "team_id": self.team_id, "stat_key": second_chance_stat_key, "stat_value": 1, } ) if is_penalty_event: penalty_stat_key = f"{pbpstats.PENALTY_STRING}{free_throw_key}" stats.append( { "player_id": self.player1_id, "team_id": self.team_id, "stat_key": penalty_stat_key, "stat_value": 1, } ) foul_event = self.foul_that_led_to_ft if ( foul_event is not None and foul_event.is_personal_take_foul and self.seconds_remaining < 60 and self.period >= 4 ): final_minute_take_foul_stat_key = ( f"{pbpstats.FINAL_MINUTE_PENALTY_TAKE_FOUL_STRING}{free_throw_key}" ) stats.append( { "player_id": self.player1_id, "team_id": self.team_id, "stat_key": final_minute_take_foul_stat_key, "stat_value": 1, } ) return stats def _get_missed_ft_stat_items(self, is_second_chance_event, is_penalty_event): stats = [] if self.is_ft_3pt: free_throw_key = pbpstats.FT_3_PT_MISSED_STRING elif self.is_ft_2pt: free_throw_key = pbpstats.FT_2_PT_MISSED_STRING elif self.is_ft_1pt: free_throw_key = pbpstats.FT_1_PT_MISSED_STRING else: free_throw_key = pbpstats.FTS_MISSED_STRING stats.append( { "player_id": self.player1_id, "team_id": self.team_id, "stat_key": free_throw_key, "stat_value": 1, } ) if is_second_chance_event: second_chance_stat_key = f"{pbpstats.SECOND_CHANCE_STRING}{free_throw_key}" stats.append( { "player_id": self.player1_id, "team_id": self.team_id, "stat_key": second_chance_stat_key, "stat_value": 1, } ) if is_penalty_event: penalty_stat_key = f"{pbpstats.PENALTY_STRING}{free_throw_key}" stats.append( { "player_id": self.player1_id, "team_id": self.team_id, "stat_key": penalty_stat_key, "stat_value": 1, } ) foul_event = self.foul_that_led_to_ft if ( foul_event is not None and foul_event.is_personal_take_foul and self.seconds_remaining < 60 and self.period >= 4 ): final_minute_take_foul_stat_key = ( f"{pbpstats.FINAL_MINUTE_PENALTY_TAKE_FOUL_STRING}{free_throw_key}" ) stats.append( { "player_id": self.player1_id, "team_id": self.team_id, "stat_key": final_minute_take_foul_stat_key, "stat_value": 1, } ) return stats def _get_ft_trip_stat_items(self, is_second_chance_event, is_penalty_event): stats = [] free_throw_trip_key = self.free_throw_type + " Free Throw Trips" stats.append( { "player_id": self.player1_id, "team_id": self.team_id, "stat_key": free_throw_trip_key, "stat_value": 1, } ) if is_second_chance_event: second_chance_stat_key = ( f"{pbpstats.SECOND_CHANCE_STRING}{free_throw_trip_key}" ) stats.append( { "player_id": self.player1_id, "team_id": self.team_id, "stat_key": second_chance_stat_key, "stat_value": 1, } ) if is_penalty_event: penalty_stat_key = f"{pbpstats.PENALTY_STRING}{free_throw_trip_key}" stats.append( { "player_id": self.player1_id, "team_id": self.team_id, "stat_key": penalty_stat_key, "stat_value": 1, } ) foul_event = self.foul_that_led_to_ft if ( foul_event is not None and foul_event.is_personal_take_foul and self.seconds_remaining < 60 and self.period >= 4 ): current_players = self.event_for_efficiency_stats.current_players offense_team_id = self.get_offense_team_id() for team_id, players in current_players.items(): final_minute_take_foul_possessions_stat_key = ( f"{pbpstats.FINAL_MINUTE_PENALTY_TAKE_FOUL_STRING}{pbpstats.OFFENSIVE_POSSESSION_STRING}" if team_id == offense_team_id else f"{pbpstats.FINAL_MINUTE_PENALTY_TAKE_FOUL_STRING}{pbpstats.DEFENSIVE_POSSESSION_STRING}" ) for player_id in players: stat_item = { "player_id": player_id, "team_id": team_id, "stat_key": final_minute_take_foul_possessions_stat_key, "stat_value": 1, } stats.append(stat_item) return stats