From 8bc4844527af7ce1ff6536a627d6006d6ba24668 Mon Sep 17 00:00:00 2001 From: jordansaltaccount Date: Sun, 8 Feb 2026 14:05:49 -0500 Subject: [PATCH 1/2] Playoff Odds --- pybaseball/playoff_odds.py | 51 +++++++++++++++++++ .../pybaseball/test_playoff_odds.py | 38 ++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 pybaseball/playoff_odds.py create mode 100644 tests/integration/pybaseball/test_playoff_odds.py diff --git a/pybaseball/playoff_odds.py b/pybaseball/playoff_odds.py new file mode 100644 index 00000000..39017ec7 --- /dev/null +++ b/pybaseball/playoff_odds.py @@ -0,0 +1,51 @@ +from io import StringIO +from typing import List, Optional + +import pandas as pd +from bs4 import BeautifulSoup, Comment, PageElement, ResultSet + +from . import cache +from .utils import most_recent_season +from .datasources.bref import BRefSession + +session = BRefSession() + + +def playoff_odds(season): + + if season >= 2020: + session = BRefSession() + url = f'https://www.baseball-reference.com/leagues/majors/{season}-playoff-odds.shtml' + s = session.get(url).content + soup = BeautifulSoup(s, "lxml") + # Find the specific table by id + table = soup.find('table', {'id': 'playoff_prob_mlb'}) + + if table is None: + print(f"Table with id 'playoff_scenarios_mlb' not found for season {season}") + df = None + + # Use pandas' read_html for easier parsing + df = pd.read_html(StringIO(str(table)))[0] + df = df.iloc[:, [1, 2, 20, 21, 22, 23, 24, 25]] + df = df.reset_index(drop=True) + + division_names = ['NL East', 'NL Central', 'NL West', 'AL East', 'AL Central', 'AL West', 'Tm'] + new_df = pd.DataFrame(columns=df.columns) + + for index, row in df.iterrows(): + # Check if first column (index 0) does not contain division names + if row.iloc[0] not in division_names: + new_df = pd.concat([new_df, row.to_frame().T], ignore_index=True) + new_df = new_df.dropna() + #there is a BYE column in the most current season + new_df.columns = ['Tm', 'Lg', 'WC', 'Div', 'LDS', 'LCS', 'Pennant', 'Win WS'] + + else: + new_df = None + print(f"Playoff odds not found for {season}") + + return new_df + + + diff --git a/tests/integration/pybaseball/test_playoff_odds.py b/tests/integration/pybaseball/test_playoff_odds.py new file mode 100644 index 00000000..dca6fbde --- /dev/null +++ b/tests/integration/pybaseball/test_playoff_odds.py @@ -0,0 +1,38 @@ +from time import sleep +from typing import Generator, Optional + +import pandas as pd +import pytest + +from pybaseball.playoff_odds import playoff_odds +from pybaseball.utils import most_recent_season + +@pytest.fixture(autouse=True) +def before_after_each() -> Generator[None, None, None]: + # before each test + yield + # after each test + sleep(6) # BBRef will throttle us if we make more than 10 calls per minute + +def get_division_counts_by_season(season: Optional[int]) -> int: + if season is None: + season = most_recent_season() - 1 + + if season >= 1994: + return 6 + if season >= 1969: + return 4 + return 1 + +class TestBRefPlayoffOdds: + @pytest.mark.parametrize( + "season", [2024] # Changed to test only 2024 + ) + def test_odds(self, season: Optional[int]) -> None: + season_playoff_odds = playoff_odds(season) + + assert season_playoff_odds is not None + #assert len(season_playoff_odds) == get_division_counts_by_season(season) + assert season_playoff_odds is not None + assert len(season_playoff_odds.columns) > 0 + assert len(season_playoff_odds.index) > 0 \ No newline at end of file From 6e63fcd527292c95376af7f61d3028e560b8bbae Mon Sep 17 00:00:00 2001 From: jordansaltaccount Date: Mon, 9 Feb 2026 22:06:11 -0500 Subject: [PATCH 2/2] Playoff Odds v2 --- pybaseball/playoff_odds.py | 82 +++++++++++-------- .../pybaseball/test_playoff_odds.py | 12 --- 2 files changed, 48 insertions(+), 46 deletions(-) diff --git a/pybaseball/playoff_odds.py b/pybaseball/playoff_odds.py index 39017ec7..4f5b33bf 100644 --- a/pybaseball/playoff_odds.py +++ b/pybaseball/playoff_odds.py @@ -10,42 +10,56 @@ session = BRefSession() - def playoff_odds(season): - - if season >= 2020: - session = BRefSession() - url = f'https://www.baseball-reference.com/leagues/majors/{season}-playoff-odds.shtml' - s = session.get(url).content - soup = BeautifulSoup(s, "lxml") - # Find the specific table by id - table = soup.find('table', {'id': 'playoff_prob_mlb'}) - - if table is None: - print(f"Table with id 'playoff_scenarios_mlb' not found for season {season}") - df = None - - # Use pandas' read_html for easier parsing - df = pd.read_html(StringIO(str(table)))[0] - df = df.iloc[:, [1, 2, 20, 21, 22, 23, 24, 25]] - df = df.reset_index(drop=True) - - division_names = ['NL East', 'NL Central', 'NL West', 'AL East', 'AL Central', 'AL West', 'Tm'] - new_df = pd.DataFrame(columns=df.columns) - - for index, row in df.iterrows(): - # Check if first column (index 0) does not contain division names - if row.iloc[0] not in division_names: - new_df = pd.concat([new_df, row.to_frame().T], ignore_index=True) - new_df = new_df.dropna() - #there is a BYE column in the most current season - new_df.columns = ['Tm', 'Lg', 'WC', 'Div', 'LDS', 'LCS', 'Pennant', 'Win WS'] - - else: - new_df = None + if season < 2020: print(f"Playoff odds not found for {season}") - - return new_df + return None + + session = BRefSession() + url = f'https://www.baseball-reference.com/leagues/majors/{season}-playoff-odds.shtml' + s = session.get(url).content + soup = BeautifulSoup(s, "lxml") + + # Find the specific table by id + table = soup.find('table', {'id': 'playoff_prob_mlb'}) + if table is None: + print(f"Table with id 'playoff_prob_mlb' not found for season {season}") + return None + + # HTML → DataFrame + df = pd.read_html(StringIO(str(table)))[0] + + # DataFrame → JSON string + json_str = df.to_json(orient="records") + # JSON string → DataFrame + new_df = pd.read_json(StringIO(json_str)) + + cols = [ + ('Unnamed: 1_level_0', 'Tm'), + ('Unnamed: 2_level_0', 'Lg'), + ('Unnamed: 20_level_0', 'WC'), + ('Unnamed: 21_level_0', 'Div'), + ('Unnamed: 22_level_0', 'LDS'), + ('Unnamed: 23_level_0', 'LCS'), + ('Unnamed: 24_level_0', 'Pennant'), + ('Unnamed: 25_level_0', 'Win WS') + ] + + df_filtered = df[cols].dropna() + + # Remove division header rows + division_names = [ + 'NL East', 'NL Central', 'NL West', + 'AL East', 'AL Central', 'AL West', 'Tm' + ] + + mask = ~df_filtered.iloc[:, 0].isin(division_names) + new_df = df_filtered[mask].reset_index(drop=True) + + # Clean column names + new_df.columns = ['Tm', 'Lg', 'WC', 'Div', 'LDS', 'LCS', 'Pennant', 'Win WS'] + + return new_df diff --git a/tests/integration/pybaseball/test_playoff_odds.py b/tests/integration/pybaseball/test_playoff_odds.py index dca6fbde..da1a3a5e 100644 --- a/tests/integration/pybaseball/test_playoff_odds.py +++ b/tests/integration/pybaseball/test_playoff_odds.py @@ -14,25 +14,13 @@ def before_after_each() -> Generator[None, None, None]: # after each test sleep(6) # BBRef will throttle us if we make more than 10 calls per minute -def get_division_counts_by_season(season: Optional[int]) -> int: - if season is None: - season = most_recent_season() - 1 - - if season >= 1994: - return 6 - if season >= 1969: - return 4 - return 1 - class TestBRefPlayoffOdds: @pytest.mark.parametrize( "season", [2024] # Changed to test only 2024 ) def test_odds(self, season: Optional[int]) -> None: season_playoff_odds = playoff_odds(season) - assert season_playoff_odds is not None - #assert len(season_playoff_odds) == get_division_counts_by_season(season) assert season_playoff_odds is not None assert len(season_playoff_odds.columns) > 0 assert len(season_playoff_odds.index) > 0 \ No newline at end of file