"""
``StatsNbaPossessionLoader`` loads possession data for a game and
creates :obj:`~pbpstats.resources.possessions.possession.Possession` objects for each possession
The following code will load possession data for game id "0021900001" from a
pbp file located in the ``/pbp`` subdirectory of the ``/data`` directory
.. code-block:: python
from pbpstats.data_loader import StatsNbaPossessionLoader
possession_loader = StatsNbaPossessionLoader("0021900001", "file", "/data")
print(possession_loader.items[0].data) # prints dict with the first possession of the game
"""
import os
import json
from pbpstats import (
NBA_GAME_ID_PREFIX,
G_LEAGUE_GAME_ID_PREFIX,
WNBA_GAME_ID_PREFIX,
NBA_STRING,
G_LEAGUE_STRING,
WNBA_STRING,
)
from pbpstats.overrides import IntDecoder
from pbpstats.data_loader.stats_nba.enhanced_pbp_loader import StatsNbaEnhancedPbpLoader
from pbpstats.data_loader.nba_possession_loader import NbaPossessionLoader
from pbpstats.resources.possessions.possession import Possession
from pbpstats.resources.enhanced_pbp import Foul
[docs]class TeamHasBackToBackPossessionsException(Exception):
"""
Class for exception when a team is credited with back-to-back possessions.
You can manually edit the event order in the pbp file stored on disk or add
an event to the overrides file in your data directory to fix this.
"""
pass
[docs]class StatsNbaPossessionLoader(NbaPossessionLoader):
"""
Loads stats.nba.com source possession data for game.
Possessions are stored in items attribute as :obj:`~pbpstats.resources.possessions.possession.Possession` objects
:param str game_id: NBA Stats Game Id
:param str source: Where should data be loaded from. Options are 'web' or 'file'
:param str file_directory: (optional if source is 'web')
Directory in which data should be either stored (if source is web) or loaded from (if source is file).
The specific file location will be `stats_<game_id>.json` in the `/pbp` subdirectory.
If not provided response data will not be saved on disk.
:raises: :obj:`~pbpstats.data_loader.stats_nba.possessions_loader.TeamHasBackToBackPossessionsException`:
If team has the ball on back-to-back possessions.
"""
data_provider = "stats_nba"
resource = "Possessions"
parent_object = "Game"
def __init__(self, game_id, source, file_directory=None):
self.file_directory = file_directory
self.game_id = game_id
pbp_events = StatsNbaEnhancedPbpLoader(game_id, source, file_directory)
self.events = pbp_events.items
events_by_possession = self._split_events_by_possession()
self.items = [
Possession(possession_events) for possession_events in events_by_possession
]
self._add_extra_attrs_to_all_possessions()
self._load_bad_possession_overrides()
self._check_that_possessions_alternate()
@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
def _check_that_possessions_alternate(self):
"""
checks that a team doesn't have back-to-back possessions
usually caused by pbp events being out of order that can be fixed manually
"""
for possession in self.items:
if possession.previous_possession is not None:
poss = possession
prev_poss = possession.previous_possession
elif possession.next_possession is not None:
poss = possession.next_possession
prev_poss = possession
if poss.offense_team_id == prev_poss.offense_team_id:
game_id = (
self.game_id if self.league == NBA_STRING else int(self.game_id)
)
if not (
game_id in self.bad_pbp_cases.keys()
and poss.period in self.bad_pbp_cases[game_id].keys()
and poss.number in self.bad_pbp_cases[game_id][poss.period]
):
ignore_because_of_flagrant = False
events_to_check = [event for event in prev_poss.events]
if prev_poss.previous_possession is not None:
events_to_check += prev_poss.previous_possession.events
for event in events_to_check:
if isinstance(event, Foul) and event.is_flagrant:
ignore_because_of_flagrant = True
if not ignore_because_of_flagrant:
exception_text = (
f"GameId: {poss.game_id}, Period: {poss.period}, "
f"Number: {poss.number}, Events: {poss.events}, "
f"Previous Events: {prev_poss.events}>"
)
raise TeamHasBackToBackPossessionsException(exception_text)
def _load_bad_possession_overrides(self):
self.bad_pbp_cases = {}
if self.file_directory is not None:
bad_pbp_possessions_file_path = (
f"{self.file_directory}/overrides/bad_pbp_possessions.json"
)
if os.path.isfile(bad_pbp_possessions_file_path):
with open(bad_pbp_possessions_file_path) as f:
# bad pbp where event is missing in pbp causing back to back possessions for same team - this will prevent back to back possession exception from being raised
# {GameId: {Period:[EventNum]}}
self.bad_pbp_cases = json.loads(f.read(), cls=IntDecoder)