From e366d463ab169a3f819e3c136a879579f73cc707 Mon Sep 17 00:00:00 2001 From: ss77995ss Date: Sun, 17 Nov 2024 16:01:10 +0800 Subject: [PATCH 1/2] feat(Statcast): Implement Catcher Throwing function --- example.py | 4 ++ src/baseball_stats_python/__init__.py | 2 + .../constants/__init__.py | 1 + src/baseball_stats_python/constants/common.py | 0 .../enums/statcast_leaderboard.py | 7 +++ .../statcast/catcher_throwing.py | 59 +++++++++++++++++++ .../statcast/mlbam_id_search.py | 2 +- src/baseball_stats_python/utils/statcast.py | 8 +-- tests/statcast/test_catcher_throwing.py | 25 ++++++++ tests/utils/test_statcast.py | 4 +- 10 files changed, 105 insertions(+), 7 deletions(-) create mode 100644 src/baseball_stats_python/constants/__init__.py create mode 100644 src/baseball_stats_python/constants/common.py create mode 100644 src/baseball_stats_python/enums/statcast_leaderboard.py create mode 100644 src/baseball_stats_python/statcast/catcher_throwing.py create mode 100644 tests/statcast/test_catcher_throwing.py diff --git a/example.py b/example.py index f128071..a68abf9 100644 --- a/example.py +++ b/example.py @@ -7,6 +7,7 @@ ) from src.baseball_stats_python.enums.minor import MinorGameType from src.baseball_stats_python.enums.statcast import GameType, MlbTeam, Month +from src.baseball_stats_python.statcast.catcher_throwing import catcher_throwing def example(): @@ -35,3 +36,6 @@ def mlbam_id_example(): # example() # minor_example() # mlbam_id_example() + +df = catcher_throwing('669257', game_type=123) +print(df) diff --git a/src/baseball_stats_python/__init__.py b/src/baseball_stats_python/__init__.py index 1642b25..0ab6095 100644 --- a/src/baseball_stats_python/__init__.py +++ b/src/baseball_stats_python/__init__.py @@ -1,3 +1,4 @@ +from .statcast.catcher_throwing import catcher_throwing from .statcast.minor_statcast_search import ( minor_statcast_batter_search, minor_statcast_pitcher_search, @@ -18,4 +19,5 @@ 'minor_statcast_pitcher_search', 'minor_statcast_batter_search', 'mlbam_id_search', + 'catcher_throwing', ] diff --git a/src/baseball_stats_python/constants/__init__.py b/src/baseball_stats_python/constants/__init__.py new file mode 100644 index 0000000..3d2d398 --- /dev/null +++ b/src/baseball_stats_python/constants/__init__.py @@ -0,0 +1 @@ +DEFAULT_SEASON = 2024 diff --git a/src/baseball_stats_python/constants/common.py b/src/baseball_stats_python/constants/common.py new file mode 100644 index 0000000..e69de29 diff --git a/src/baseball_stats_python/enums/statcast_leaderboard.py b/src/baseball_stats_python/enums/statcast_leaderboard.py new file mode 100644 index 0000000..23765d7 --- /dev/null +++ b/src/baseball_stats_python/enums/statcast_leaderboard.py @@ -0,0 +1,7 @@ +from .enum_base import EnumBase + + +class GameType(EnumBase): + REGULAR_SEASON = 'R' + PLAYOFFS = 'PO' + ALL = 'All' diff --git a/src/baseball_stats_python/statcast/catcher_throwing.py b/src/baseball_stats_python/statcast/catcher_throwing.py new file mode 100644 index 0000000..d332e99 --- /dev/null +++ b/src/baseball_stats_python/statcast/catcher_throwing.py @@ -0,0 +1,59 @@ +import pandas as pd +import requests + +from ..constants import DEFAULT_SEASON +from ..enums.statcast_leaderboard import GameType + +session = requests.Session() + +API_URL = 'https://baseballsavant.mlb.com/leaderboard/services/catcher-throwing' + + +def catcher_throwing( + catcher_id: str, + game_type: str | GameType = GameType.REGULAR_SEASON, + season: str = str(DEFAULT_SEASON), +) -> pd.DataFrame: + """ + Get catcher throwing data from each stolen base attempt for a specific catcher. + ref: https://baseballsavant.mlb.com/leaderboard/catcher-throwing + + Args: + catcher_id (str): The MLBAM ID of the catcher. (Required) + game_type (str): The game type to filter by. + n (str): The number of results to return. + season (str): The season to filter by. The earliest season available is 2016. + + Returns: + pd.DataFrame: A DataFrame containing the catcher throwing data. + """ + + if not catcher_id: + raise ValueError('catcher_id is required') + + if not isinstance(game_type, str) and not isinstance(game_type, GameType): + raise ValueError(f'Invalid type for game_type: {type(game_type)}') + + if not GameType.has_value(game_type): + raise ValueError(f'Invalid game type: {game_type}') + + if int(season) < 2016: + raise ValueError( + f'Invalid season: {season}, The earliest season available is 2016' + ) + + params = { + 'gameType': game_type, + 'season': season, + 'n': 0, + } + + response = session.get(f'{API_URL}/{catcher_id}', params=params) + + if response.status_code == 200: + result = response.json() + return pd.DataFrame(result['data']) + else: + raise Exception( + f'Failed to fetch data: {response.status_code} - {response.text}' + ) diff --git a/src/baseball_stats_python/statcast/mlbam_id_search.py b/src/baseball_stats_python/statcast/mlbam_id_search.py index 750e63f..5d9b3ad 100644 --- a/src/baseball_stats_python/statcast/mlbam_id_search.py +++ b/src/baseball_stats_python/statcast/mlbam_id_search.py @@ -12,7 +12,7 @@ def mlbam_id_search(player_name: str, debug: bool = False) -> pd.DataFrame: """ - Search for MLBAM ID(s) by player name. + Search for MLBAM ID(s) by player's name. Args: player_name (str): The player name to search for. (Required) diff --git a/src/baseball_stats_python/utils/statcast.py b/src/baseball_stats_python/utils/statcast.py index f741a17..a99e9e6 100644 --- a/src/baseball_stats_python/utils/statcast.py +++ b/src/baseball_stats_python/utils/statcast.py @@ -1,12 +1,12 @@ +from ..constants import DEFAULT_SEASON from ..enums.statcast import GameType, MlbTeam, Month -CURRENT_SEASON = 2024 START_SEASON = 2008 STATCAST_START_SEASON = 2015 -ALL_SEASONS = [str(year) for year in range(START_SEASON, CURRENT_SEASON + 1)] +ALL_SEASONS = [str(year) for year in range(START_SEASON, DEFAULT_SEASON + 1)] STATCAST_SEASONS = [ - str(year) for year in range(STATCAST_START_SEASON, CURRENT_SEASON + 1) + str(year) for year in range(STATCAST_START_SEASON, DEFAULT_SEASON + 1) ] @@ -20,7 +20,7 @@ def get_season_param_str(season: str | list[str]) -> str: return '|'.join(season) if season == '': - return str(CURRENT_SEASON) + return str(DEFAULT_SEASON) if season == 'all': return '|'.join(ALL_SEASONS) if season == 'statcast': diff --git a/tests/statcast/test_catcher_throwing.py b/tests/statcast/test_catcher_throwing.py new file mode 100644 index 0000000..cca6f6d --- /dev/null +++ b/tests/statcast/test_catcher_throwing.py @@ -0,0 +1,25 @@ +import pytest + +from baseball_stats_python.statcast.catcher_throwing import catcher_throwing + + +def test_catcher_throwing_invalid(): + with pytest.raises(ValueError) as e: + catcher_throwing('') + assert str(e.value) == 'catcher_id is required' + + with pytest.raises(ValueError) as e: + catcher_throwing('669257', 'invalid') + assert str(e.value) == 'Invalid game type: invalid' + + with pytest.raises(ValueError) as e: + catcher_throwing('669257', 123) + assert str(e.value) == f'Invalid type for game_type: {int}' + + with pytest.raises(ValueError) as e: + catcher_throwing('669257', season='2015') + assert str(e.value) == 'Invalid season: 2015, The earliest season available is 2016' + + with pytest.raises(ValueError) as e: + catcher_throwing('669257', game_type='RRR') + assert str(e.value) == 'Invalid game type: RRR' diff --git a/tests/utils/test_statcast.py b/tests/utils/test_statcast.py index 4591185..e663a42 100644 --- a/tests/utils/test_statcast.py +++ b/tests/utils/test_statcast.py @@ -2,9 +2,9 @@ import pytest +from baseball_stats_python.constants import DEFAULT_SEASON from baseball_stats_python.enums.statcast import GameType, MlbTeam, Month from baseball_stats_python.utils.statcast import ( - CURRENT_SEASON, get_game_type_param_str, get_month_param_str, get_season_param_str, @@ -15,7 +15,7 @@ def test_get_season_param_str(): assert get_season_param_str('2024') == '2024' assert get_season_param_str(['2024', '2023']) == '2024|2023' - assert get_season_param_str('') == str(CURRENT_SEASON) + assert get_season_param_str('') == str(DEFAULT_SEASON) def test_get_season_param_str_invalid(): From 95942d56293da74ced039f3d0f43e5e1c6658857 Mon Sep 17 00:00:00 2001 From: ss77995ss Date: Mon, 9 Dec 2024 22:34:32 +0800 Subject: [PATCH 2/2] docs(Statcast): Add `catcher_throwing` function documentation --- docs/catcher_throwing.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 docs/catcher_throwing.md diff --git a/docs/catcher_throwing.md b/docs/catcher_throwing.md new file mode 100644 index 0000000..0a1bac6 --- /dev/null +++ b/docs/catcher_throwing.md @@ -0,0 +1,32 @@ +# Statcast Catcher Throwing + +## `catcher_throwing` + +Function to get catcher throwing data from each stolen base attempt for a specific catcher. Based on Baseball Savant's [Catcher Throwing](https://baseballsavant.mlb.com/leaderboard/catcher-throwing). + +**Examples** + +```python +from baseball_stats_python import catcher_throwing + +# Get Will Smith's catcher throwing data +catcher_throwing('669257') + +# Get Will Smith's catcher throwing data in 2023 +catcher_throwing('669257', season='2023') + +# Get Will Smith's catcher throwing data in playoffs +catcher_throwing('669257', game_type=GameType.PLAYOFFS) +``` + +**Arguments** + +| Argument | Data Type | Description | +| --------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| catcher_id (Required) | `str` | The MLBAM ID of the catcher. | +| game_type | `str` or `GameType` | The game type to filter by. Can be `R` for regular season, `PO` for playoffs, or `All` for all games. Check enum [GameType](../enums/statcast_leaderboard.py) | +| season | `str` | The season to filter by. The earliest season available is 2016. | + +**Return** + +A DataFrame with columns that related to the [Catcher Throwing](https://baseballsavant.mlb.com/leaderboard/catcher-throwing) leaderboard. The DataFrame will represent each stolen base attempt for a specific catcher which contains data like `pop_time`, `throw_type`, `r_primary_lead`, etc.