import abc
import math
import pbpstats
from pbpstats.resources.enhanced_pbp import Foul, FreeThrow, Turnover, Violation
[docs]class FieldGoal(object):
"""
Class for field goal events
"""
@abc.abstractproperty
def is_made(self):
"""
returns True if shot was made, False otherwise
"""
pass
@abc.abstractproperty
def shot_value(self):
"""
returns 3 if shot is a 3 point attempt, 2 otherwise
"""
pass
@property
def is_blocked(self):
"""
returns True if shot was blocked, False otherwise
"""
return not self.is_made and hasattr(self, "player3_id")
@property
def is_assisted(self):
"""
returns True if shot was assisted, False otherwise
"""
return self.is_made and hasattr(self, "player2_id")
@property
def rebound(self):
"""
returns :obj:`~pbpstats.resources.enhanced_pbp.rebound.Rebound` item
for the rebound of the shot, if it was missed, None otherwise
"""
if not self.is_made and self.next_event.is_real_rebound:
return self.next_event
return None
@property
def is_heave(self):
"""
returns True if shot was a last second heave, False otherwise
"""
return (
self.distance > pbpstats.HEAVE_DISTANCE_CUTOFF
and self.seconds_remaining < pbpstats.HEAVE_TIME_CUTOFF
)
@property
def is_corner_3(self):
"""
returns True if shot was a corner 3, False otherwise
"""
if self.shot_value != 3:
return False
if not hasattr(self, "locY") or self.locY is None:
return False
if self.locY <= 87:
return True
return False
@property
def distance(self):
"""
returns shot distance in feet
"""
if hasattr(self, "locX") and hasattr(self, "locY"):
x_squared = self.locX ** 2
y_squared = self.locY ** 2
# unit for distance is off by factor of 10, divide by 10 to convert to feet
shot_distance = math.sqrt(x_squared + y_squared) / 10
return round(shot_distance, 1)
else:
# no coordinates - get shot distance from event description
try:
return int(self.description.split("'")[0].split(" ")[-1])
except ValueError:
return None
@property
def shot_type(self):
"""
returns shot type string ('AtRim', 'ShortMidRange', 'LongMidRange', 'Arc3' or 'Corner3')
"""
if self.shot_value == 3:
if self.is_corner_3:
return pbpstats.CORNER_3_STRING
else:
return pbpstats.ARC_3_STRING
if self.distance is None:
return pbpstats.UNKNOWN_SHOT_DISTANCE_STRING
elif self.distance < pbpstats.AT_RIM_CUTOFF:
return pbpstats.AT_RIM_STRING
elif self.distance < pbpstats.SHORT_MID_RANGE_CUTOFF:
return pbpstats.SHORT_MID_RANGE_STRING
else:
return pbpstats.LONG_MID_RANGE_STRING
@property
def is_putback(self):
"""
returns True if shot is a 2pt attempt within 2 seconds of an
offensive rebound attempted by the same player who got the rebound
"""
if self.is_assisted or self.shot_value == 3:
return False
prev_evt = self.previous_event
if prev_evt is None:
return False
prev_evt_is_shooting_foul = isinstance(prev_evt, Foul) and (
prev_evt.is_shooting_foul or prev_evt.is_shooting_block_foul
)
prev_evt_is_goaltend = (
isinstance(prev_evt, Violation) and prev_evt.is_goaltend_violation
)
if (
prev_evt_is_shooting_foul or prev_evt_is_goaltend
) and self.clock == prev_evt.clock:
# sometimes foul event is between rebound and shot on an and 1 or goaltend is between rebound and made shot event
prev_evt = prev_evt.previous_event
if prev_evt is None:
return False
if not hasattr(prev_evt, "is_real_rebound"):
return False
if not prev_evt.is_real_rebound:
return False
return (
prev_evt.oreb
and prev_evt.player1_id == self.player1_id
and prev_evt.seconds_remaining - self.seconds_remaining <= 2
)
@property
def is_and1(self):
"""
returns True if shot was an and 1, False otherwise
"""
if self.is_make_that_does_not_end_possession:
free_throws = [
event
for event in self.get_all_events_at_current_time()
if isinstance(event, FreeThrow)
]
for ft in free_throws:
if "And 1" in ft.free_throw_type and ft.player1_id == self.player1_id:
return True
return False
@property
def shot_data(self):
"""
returns a dict with detailed shot data
"""
team_ids = list(self.current_players.keys())
opponent_team_id = team_ids[0] if self.team_id == team_ids[1] else team_ids[1]
shot_data = {
"PlayerId": self.player1_id,
"TeamId": self.team_id,
"OpponentTeamId": opponent_team_id,
"LineupId": self.lineup_ids[self.team_id],
"OpponentLineupId": self.lineup_ids[opponent_team_id],
"Made": self.is_made,
"X": self.locX if hasattr(self, "locX") else None,
"Y": self.locY if hasattr(self, "locY") else None,
"Time": self.seconds_remaining,
"ShotValue": self.shot_value,
"Putback": self.is_putback,
"ShotType": self.shot_type,
"ScoreMargin": self.score_margin,
"EventNum": self.event_num,
"IsAnd1": self.is_and1,
}
if self.is_made:
shot_data["Assisted"] = self.is_assisted
if self.is_assisted:
shot_data["AssistPlayerId"] = self.player2_id
if not self.is_made:
shot_data["Blocked"] = self.is_blocked
if self.is_blocked:
shot_data["BlockPlayerId"] = self.player3_id
if self.is_second_chance_event():
prev_event = self.previous_event
while not (
hasattr(prev_event, "is_real_rebound") and prev_event.is_real_rebound
):
prev_event = prev_event.previous_event
shot_data["SecondsSinceOReb"] = (
prev_event.seconds_remaining - self.seconds_remaining
)
shot_data["OrebShotPlayerId"] = prev_event.missed_shot.player1_id
shot_data["OrebReboundPlayerId"] = prev_event.player1_id
if prev_event.player1_id != 0:
rebound_shot_type = prev_event.missed_shot.shot_type
if (
isinstance(prev_event.missed_shot, FieldGoal)
and prev_event.missed_shot.is_blocked
):
rebound_shot_type += pbpstats.BLOCKED_STRING
elif (
isinstance(prev_event.missed_shot, FieldGoal)
and prev_event.missed_shot.is_blocked
):
# separate blocked from non blocked because blocked won't have shot clock reset
rebound_shot_type = "TeamBlocked"
else:
rebound_shot_type = "Team"
shot_data["OrebShotType"] = rebound_shot_type
return shot_data
def _check_if_make_ends_possession_when_1_foul(
self, foul_event, events_at_shot_time
):
"""
helper method for determining if made shot ends possession
when there was 1 foul at the time of the shot
"""
foul_team_team_id = foul_event.team_id
shooter_team_id = self.team_id
if foul_event.is_flagrant:
# flagrant foul and 1
if foul_team_team_id != shooter_team_id:
return True
other_makes_at_time_of_shot = []
for event in events_at_shot_time:
if (
isinstance(event, FieldGoal)
and event.event_num != self.event_num
and event.is_made
and self.team_id == event.team_id
):
other_makes_at_time_of_shot.append(event)
if shooter_team_id != foul_team_team_id:
# check FT 1 of 1s at time of shot
ft_1_of_1s_at_time_of_shot = []
for event in events_at_shot_time:
if (
isinstance(event, FreeThrow)
and (event.is_ft_1_of_1 or event.is_ft_1pt)
and not event.is_technical_ft
):
ft_1_of_1s_at_time_of_shot.append(event)
if (
len(other_makes_at_time_of_shot) == 1
and len(ft_1_of_1s_at_time_of_shot) == 1
):
if (
ft_1_of_1s_at_time_of_shot[0].player1_id
== other_makes_at_time_of_shot[0].player1_id
):
return False
elif ft_1_of_1s_at_time_of_shot[0].player1_id == self.player1_id:
return True
elif len(ft_1_of_1s_at_time_of_shot) > 0:
for ft_event in ft_1_of_1s_at_time_of_shot:
if ft_event.team_id == shooter_team_id:
return True
else:
# no free throws - check for lane violation and offensive goaltending
for event in events_at_shot_time:
if isinstance(event, Turnover) and (
event.is_lane_violation or event.is_offensive_goaltending
):
return True
if isinstance(event, Violation) and event.is_double_lane_violation:
return True
return False
def _check_if_make_ends_possession_when_not_1_foul(
self, events_at_shot_time, fouls_at_time_of_shot
):
"""
helper method for determining if made shot ends possession
when there was not 1 foul at the time of the shot
"""
shooter_team_id = self.team_id
if shooter_team_id not in [event.team_id for event in fouls_at_time_of_shot]:
ft_1_of_1s_at_time_of_shot = []
for event in events_at_shot_time:
if (
isinstance(event, FreeThrow)
and (event.is_ft_1_of_1 or event.is_ft_1pt)
and not event.is_technical_ft
):
ft_1_of_1s_at_time_of_shot.append(event)
if len(ft_1_of_1s_at_time_of_shot) == 1:
if ft_1_of_1s_at_time_of_shot[0].team_id == shooter_team_id:
return True
elif len(ft_1_of_1s_at_time_of_shot) > 1:
for ft_event in ft_1_of_1s_at_time_of_shot:
if ft_event.player1_id == self.player1_id:
return True
else:
opponent_fouls = [
event
for event in fouls_at_time_of_shot
if event.team_id != shooter_team_id
]
if 1 in [event.number_of_fta_for_foul for event in opponent_fouls]:
# rarely gets here but it is when there is an and1 and then another foul at the same time
# for example a loose ball foul going for the rebound on the FT
return True
return False
@property
def is_make_that_does_not_end_possession(self):
"""
returns True if shot is a made shot that does not end the possession due to a foul, False otherwise
"""
# check for foul at time of shot
fouls_at_time_of_shot = []
# ignore technical fouls and delay of game fouls when getting last foul
events_at_shot_time = self.get_all_events_at_current_time()
for event in events_at_shot_time:
if (
isinstance(event, Foul)
and not event.is_delay_of_game
and not event.is_technical
):
fouls_at_time_of_shot.append(event)
if len(fouls_at_time_of_shot) == 1:
foul = fouls_at_time_of_shot[0]
return self._check_if_make_ends_possession_when_1_foul(
foul, events_at_shot_time
)
return self._check_if_make_ends_possession_when_not_1_foul(
events_at_shot_time, fouls_at_time_of_shot
)
@property
def event_stats(self):
"""
returns list of dicts with all stats for event
"""
stats = []
is_penalty_event = self.is_penalty_event()
is_second_chance_event = self.is_second_chance_event()
if self.distance is not None:
stats += self._get_shot_distance_stat_items()
if self.is_heave:
stats.append(self._get_heave_stat_item())
team_ids = list(self.current_players.keys())
opponent_team_id = team_ids[0] if self.team_id == team_ids[1] else team_ids[1]
if self.is_made and not self.is_assisted:
stats += self._get_unassisted_stat_items(
is_second_chance_event, is_penalty_event
)
elif self.is_assisted:
stats += self._get_assisted_stat_items(
is_second_chance_event, is_penalty_event
)
elif self.is_blocked:
stats += self._get_blocked_stat_items(
is_second_chance_event, is_penalty_event, opponent_team_id
)
else:
stats += self._get_missed_stat_items(
is_second_chance_event, is_penalty_event
)
if self.is_made:
# add plus minus and opponent points - used for lineup/wowy stats to get net rating
for team_id, players in self.current_players.items():
multiplier = 1 if team_id == self.team_id else -1
for player_id in players:
stat_item = {
"player_id": player_id,
"team_id": team_id,
"stat_key": pbpstats.PLUS_MINUS_STRING,
"stat_value": self.shot_value * multiplier,
}
stats.append(stat_item)
if multiplier == -1:
opponent_points_stat_item = {
"player_id": player_id,
"team_id": team_id,
"stat_key": pbpstats.OPPONENT_POINTS,
"stat_value": self.shot_value,
}
stats.append(opponent_points_stat_item)
lineups_ids = self.lineup_ids
for stat in stats:
opponent_team_id = (
team_ids[0] if stat["team_id"] == team_ids[1] else team_ids[1]
)
stat["lineup_id"] = lineups_ids[stat["team_id"]]
stat["opponent_team_id"] = opponent_team_id
stat["opponent_lineup_id"] = lineups_ids[opponent_team_id]
return self.base_stats + stats
def _get_shot_distance_stat_items(self):
stats = []
if self.shot_value == 2:
distance_stat_key = pbpstats.TOTAL_2PT_SHOT_DISTANCE_STRING
count_stat_key = pbpstats.TOTAL_2PT_SHOTS_WITH_DISTANCE
else:
distance_stat_key = pbpstats.TOTAL_3PT_SHOT_DISTANCE_STRING
count_stat_key = pbpstats.TOTAL_3PT_SHOTS_WITH_DISTANCE
stats.append(
{
"player_id": self.player1_id,
"team_id": self.team_id,
"stat_key": distance_stat_key,
"stat_value": self.distance,
}
)
stats.append(
{
"player_id": self.player1_id,
"team_id": self.team_id,
"stat_key": count_stat_key,
"stat_value": 1,
}
)
return stats
def _get_heave_stat_item(self):
if self.is_made:
return {
"player_id": self.player1_id,
"team_id": self.team_id,
"stat_key": pbpstats.HEAVE_MAKES_STRING,
"stat_value": 1,
}
else:
return {
"player_id": self.player1_id,
"team_id": self.team_id,
"stat_key": pbpstats.HEAVE_MISSES_STRING,
"stat_value": 1,
}
def _get_unassisted_stat_items(self, is_second_chance_event, is_penalty_event):
stats = []
stat_key = f"{pbpstats.UNASSISTED_STRING}{self.shot_type}"
stats.append(
{
"player_id": self.player1_id,
"team_id": self.team_id,
"stat_key": stat_key,
"stat_value": 1,
}
)
if is_second_chance_event:
second_chance_stat_key = f"{pbpstats.SECOND_CHANCE_STRING}{stat_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}{stat_key}"
stats.append(
{
"player_id": self.player1_id,
"team_id": self.team_id,
"stat_key": penalty_stat_key,
"stat_value": 1,
}
)
if self.is_putback:
stats.append(
{
"player_id": self.player1_id,
"team_id": self.team_id,
"stat_key": pbpstats.PUTBACKS_STRING,
"stat_value": 1,
}
)
return stats
def _get_assisted_stat_items(self, is_second_chance_event, is_penalty_event):
stats = []
scorer_key = f"{pbpstats.ASSISTED_STRING}{self.shot_type}"
assist_key = f"{self.shot_type}{pbpstats.ASSISTS_STRING}"
stats.append(
{
"player_id": self.player1_id,
"team_id": self.team_id,
"stat_key": scorer_key,
"stat_value": 1,
}
)
stats.append(
{
"player_id": self.player2_id,
"team_id": self.team_id,
"stat_key": assist_key,
"stat_value": 1,
}
)
if is_second_chance_event:
second_chance_scorer_key = f"{pbpstats.SECOND_CHANCE_STRING}{scorer_key}"
second_chance_assist_key = f"{pbpstats.SECOND_CHANCE_STRING}{assist_key}"
stats.append(
{
"player_id": self.player1_id,
"team_id": self.team_id,
"stat_key": second_chance_scorer_key,
"stat_value": 1,
}
)
stats.append(
{
"player_id": self.player2_id,
"team_id": self.team_id,
"stat_key": second_chance_assist_key,
"stat_value": 1,
}
)
if is_penalty_event:
penalty_scorer_key = f"{pbpstats.PENALTY_STRING}{scorer_key}"
penalty_assist_key = f"{pbpstats.PENALTY_STRING}{assist_key}"
stats.append(
{
"player_id": self.player1_id,
"team_id": self.team_id,
"stat_key": penalty_scorer_key,
"stat_value": 1,
}
)
stats.append(
{
"player_id": self.player2_id,
"team_id": self.team_id,
"stat_key": penalty_assist_key,
"stat_value": 1,
}
)
assist_to_key = (
f"{self.player2_id}:AssistsTo:{self.player1_id}:{self.shot_type}"
)
stats.append(
{
"player_id": self.player2_id,
"team_id": self.team_id,
"stat_key": assist_to_key,
"stat_value": 1,
}
)
return stats
def _get_blocked_stat_items(
self, is_second_chance_event, is_penalty_event, opponent_team_id
):
stats = []
shot_key = f"{self.shot_type}{pbpstats.BLOCKED_STRING}"
stats.append(
{
"player_id": self.player1_id,
"team_id": self.team_id,
"stat_key": shot_key,
"stat_value": 1,
}
)
block_key = f"{pbpstats.BLOCKED_STRING}{self.shot_type}"
stats.append(
{
"player_id": self.player3_id,
"team_id": opponent_team_id,
"stat_key": block_key,
"stat_value": 1,
}
)
if is_second_chance_event:
second_chance_shot_key = f"{pbpstats.SECOND_CHANCE_STRING}{shot_key}"
second_chance_block_key = f"{pbpstats.SECOND_CHANCE_STRING}{block_key}"
stats.append(
{
"player_id": self.player1_id,
"team_id": self.team_id,
"stat_key": second_chance_shot_key,
"stat_value": 1,
}
)
stats.append(
{
"player_id": self.player3_id,
"team_id": opponent_team_id,
"stat_key": second_chance_block_key,
"stat_value": 1,
}
)
if is_penalty_event:
penalty_shot_key = f"{pbpstats.PENALTY_STRING}{shot_key}"
penalty_block_key = f"{pbpstats.PENALTY_STRING}{block_key}"
stats.append(
{
"player_id": self.player1_id,
"team_id": self.team_id,
"stat_key": penalty_shot_key,
"stat_value": 1,
}
)
stats.append(
{
"player_id": self.player3_id,
"team_id": opponent_team_id,
"stat_key": penalty_block_key,
"stat_value": 1,
}
)
return stats
def _get_missed_stat_items(self, is_second_chance_event, is_penalty_event):
stats = []
stat_key = f"{pbpstats.MISSED_STRING}{self.shot_type}"
stats.append(
{
"player_id": self.player1_id,
"team_id": self.team_id,
"stat_key": stat_key,
"stat_value": 1,
}
)
if is_second_chance_event:
second_chance_stat_key = f"{pbpstats.SECOND_CHANCE_STRING}{stat_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}{stat_key}"
stats.append(
{
"player_id": self.player1_id,
"team_id": self.team_id,
"stat_key": penalty_stat_key,
"stat_value": 1,
}
)
return stats