From 63e390d91f84c21b1507425015ec889c210dbf39 Mon Sep 17 00:00:00 2001 From: DesiPilla Date: Fri, 15 Dec 2023 23:49:00 -0500 Subject: [PATCH 01/12] feat: Add method to calculate historical standings for any week Subject to the league's specific tiebreaker rules --- espn_api/football/league.py | 275 +++++++++++++++++++++++++++++++++++- 1 file changed, 272 insertions(+), 3 deletions(-) diff --git a/espn_api/football/league.py b/espn_api/football/league.py index a07a8166..2fd1ef20 100644 --- a/espn_api/football/league.py +++ b/espn_api/football/league.py @@ -1,7 +1,9 @@ -import datetime -import time import json -from typing import List, Tuple, Union +from itertools import combinations +from typing import Callable, Dict, List, Tuple, Union + +import pandas as pd + from ..base_league import BaseLeague from .team import Team @@ -15,6 +17,7 @@ from .utils import power_points, two_step_dominance from .constant import POSITION_MAP, ACTIVITY_MAP + class League(BaseLeague): '''Creates a League instance for Public/Private ESPN league''' def __init__(self, league_id: int, year: int, espn_s2=None, swid=None, fetch_league=True, debug=False): @@ -116,6 +119,108 @@ def standings(self) -> List[Team]: standings = sorted(self.teams, key=lambda x: x.final_standing if x.final_standing != 0 else x.standing, reverse=False) return standings + def standings_weekly(self, week: int) -> pd.DataFrame: + """This is the main function to get the standings for a given week. + + It controls the tiebreaker hierarchy and calls the recursive League()._sort_standings_df function. + First, the division winners must be determined. Then, the rest of the teams are sorted. + + The standard tiebreaker hierarchy is: + 1. Head-to-head record among the tied teams + 2. Total points scored for the season + 3. Division record (if all tied teams are in the same division) + 4. Total points scored against for the season + 5. Coin flip + + Args: + week (int): Week to get the standings for + + Returns: + pd.DataFrame: Sorted standings DataFrame + """ + # Build the standings DataFrame for the given week + standings_df = pd.DataFrame() + standings_df["team_id"] = [team.team_id for team in self.teams] + standings_df["team_name"] = [team.team_name for team in self.teams] + standings_df["division_id"] = [team.division_id for team in self.teams] + standings_df["division_name"] = [team.division_name for team in self.teams] + standings_df["wins"] = [ + sum([1 for outcome in team.outcomes[:week] if outcome == "W"]) + for team in self.teams + ] + standings_df["ties"] = [ + sum([1 for outcome in team.outcomes[:week] if outcome == "T"]) + for team in self.teams + ] + standings_df["losses"] = [ + sum([1 for outcome in team.outcomes[:week] if outcome == "L"]) + for team in self.teams + ] + standings_df["points_for"] = [sum(team.scores[:week]) for team in self.teams] + standings_df["points_against"] = [ + sum([team.schedule[w].scores[w] for w in range(week)]) + for team in self.teams + ] + standings_df["win_pct"] = ( + standings_df["wins"] + standings_df["ties"] / 2 + ) / standings_df[["wins", "losses", "ties"]].sum(axis=1) + standings_df.set_index("team_id", inplace=True) + + if self.settings.playoff_seed_tie_rule == "TOTAL_POINTS_SCORED": + tiebreaker_hierarchy = [ + (self._sort_by_win_pct, "win_pct"), + (self._sort_by_points_for, "points_for"), + (self._sort_by_head_to_head, "h2h_wins"), + (self._sort_by_division_record, "division_record"), + (self._sort_by_points_against, "points_against"), + (self._sort_by_coin_flip, "coin_flip"), + ] + elif self.settings.playoff_seed_tie_rule == "H2H_RECORD": + tiebreaker_hierarchy = [ + (self._sort_by_win_pct, "win_pct"), + (self._sort_by_head_to_head, "h2h_wins"), + (self._sort_by_points_for, "points_for"), + (self._sort_by_division_record, "division_record"), + (self._sort_by_points_against, "points_against"), + (self._sort_by_coin_flip, "coin_flip"), + ] + else: + raise ValueError( + "Unkown tiebreaker_method: Must be either 'TOTAL_POINTS_SCORED' or 'H2H_RECORD'" + ) + + # First assign the division winners + division_winners = pd.DataFrame() + for division_id in standings_df.division_id.unique(): + division_standings = standings_df[(standings_df.division_id == division_id)] + division_winner = self._sort_standings_df( + division_standings, tiebreaker_hierarchy + ).iloc[0] + division_winners = pd.concat( + [division_winners, division_winner.to_frame().T] + ) + + # Sort the division winners + sorted_division_winners = self._sort_standings_df( + division_winners, tiebreaker_hierarchy + ) + + # Then sort the rest of the teams + rest_of_field = standings_df.loc[ + ~standings_df.index.isin(division_winners.index) + ] + sorted_rest_of_field = self._sort_standings_df( + rest_of_field, tiebreaker_hierarchy + ) + + # Combine all teams + sorted_standings = pd.concat( + [sorted_division_winners, sorted_rest_of_field], + axis=0, + ) + + return sorted_standings + def top_scorer(self) -> Team: most_pf = sorted(self.teams, key=lambda x: x.points_for, reverse=True) return most_pf[0] @@ -309,3 +414,167 @@ def message_board(self, msg_types: List[str] = None): messages.append(msg) return messages + def _build_division_record_dict(self) -> Dict: + """Create a DataFrame with each team's divisional record.""" + # Get the list of teams + team_ids = [team.team_id for team in self.teams] + + # Create a dictionary with each team's divisional record + div_outcomes = { + team.team_id: {"wins": 0, "divisional_games": 0} for team in self.teams + } + + # Loop through each team's schedule and outcomes and build the dictionary + for team in self.teams: + for opp, outcome in zip(team.schedule[:week], team.outcomes[:week]): + if team.division_id == opp.division_id: + if outcome == "W": + div_outcomes[team.team_id]["wins"] += 1 + if outcome == "T": + div_outcomes[team.team_id]["wins"] += 0.5 + + div_outcomes[team.team_id]["divisional_games"] += 1 + + # Calculate the divisional record + div_record = { + team_id: ( + div_outcomes[team_id]["wins"] + / max(div_outcomes[team_id]["divisional_games"], 1) + ) + for team_id in team_ids + } + + return div_record + + def _build_h2h_df(self) -> pd.DataFrame: + """Create a dataframe that contains the head-to-head victories between each team in the league""" + # Get the list of teams + team_ids = [team.team_id for team in self.teams] + + # Initialize the DataFrame + h2h_df = pd.DataFrame(index=team_ids, columns=team_ids).replace(np.nan, 0) + + # Loop through each team + for team in self.teams: + # Get the team's schedule + schedule = team.schedule + outcomes = team.outcomes + + # Loop through each week + for week, opp in enumerate(schedule): + # Update the DataFrame + if outcomes[week] == "W": + h2h_df.loc[team.team_id, opp.team_id] += 1 + + if outcomes[week] == "T": + h2h_df.loc[team.team_id, opp.team_id] += 0.5 + + return h2h_df + + def _sort_by_win_pct(self, standings_df: pd.DataFrame) -> pd.DataFrame: + """Take a DataFrame of standings and sort it using the TOTAL_POINTS_SCORED tiebreaker""" + return standings_df.sort_values(by="win_pct", ascending=False) + + def _sort_by_points_for(self, standings_df: pd.DataFrame) -> pd.DataFrame: + """Take a DataFrame of standings and sort it using the TOTAL_POINTS_SCORED tiebreaker""" + return standings_df.sort_values(by="points_for", ascending=False) + + def _sort_by_division_record(self, standings_df: pd.DataFrame) -> pd.DataFrame: + division_records = build_division_record_df(self) + standings_df["division_record"] = standings_df.index.map(division_records) + return standings_df.sort_values(by="division_record", ascending=False) + + def _sort_by_points_against(self, standings_df: pd.DataFrame) -> pd.DataFrame: + """Take a DataFrame of standings and sort it using the 3rd level tiebreaker""" + return standings_df.sort_values(by="points_against", ascending=True) + + def _sort_by_coin_flip(self, standings_df: pd.DataFrame) -> pd.DataFrame: + standings_df["coin_flip"] = np.random.rand(len(standings_df)) + return standings_df.sort_values(by="coin_flip", ascending=False) + + def _sort_by_head_to_head( + self, + standings_df: pd.DataFrame, + ) -> pd.DataFrame: + """Take a DataFrame of standings and sort it using the H2H_RECORD tiebreaker""" + # If there is only one team, return the dataframe as-is + if len(standings_df) == 1: + return standings_df + + # Filter the H2H DataFrame to only include the teams in question + team_ids = standings_df.index.tolist() + h2h_df = self._build_h2h_df().loc[team_ids, team_ids] + + # If there are only two teams, sort descending by H2H wins + if len(h2h_df) == 2: + standings_df["h2h_wins"] = h2h_df.sum(axis=1) + return standings_df.sort_values(by="h2h_wins", ascending=False) + + # If there are more than two teams... + if len(h2h_df) > 2: + # Check if the teams have all played each other an equal number of times + matchup_combos = list(combinations(team_ids, 2)) + matchup_counts = [ + h2h_df.loc[t1, t2] + h2h_df.loc[t2, t1] for t1, t2 in matchup_combos + ] + if len(set(matchup_counts)) == 1: + # All teams have played each other an equal number of times + # Sort the teams by total H2H wins against each other + standings_df["h2h_wins"] = h2h_df.sum(axis=1) + return standings_df.sort_values(by="h2h_wins", ascending=False) + else: + # All teams have not played each other an equal number of times + # This tiebreaker is invalid + standings_df["h2h_wins"] = 0 + return standings_df + + def _sort_standings_df( + self, + standings_df: pd.DataFrame, + tiebreaker_hierarchy: List[Tuple[Callable, str]], + ) -> pd.DataFrame: + """This recursive function sorts a standings DataFrame by the tiebreaker hierarchy. + It iterates through each tiebreaker, sorting any remaning ties by the next tiebreaker. + + Args: + standings_df (pd.DataFrame): Standings DataFrame to sort + tiebreaker_hierarchy (List[Tuple[Callable, str]]): List of tiebreaker functions and columns to sort by + + Returns: + pd.DataFrame: Sorted standings DataFrame + """ + # If there are no more tiebreakers, return the standings DataFrame as-is + if not tiebreaker_hierarchy: + return standings_df + + # If there is only one team to sort, return the standings DataFrame as-is + if len(standings_df) == 1: + return standings_df + + # Get the tiebreaker function and column name to group by + tiebreaker_function = tiebreaker_hierarchy[0][0] + tiebreaker_col = tiebreaker_hierarchy[0][1] + + # Apply the tiebreaker function to the standings DataFrame + standings_df = tiebreaker_function(standings_df) + + # Loop through each remaining unique tiebreaker value to see if ties remain + sorted_standings = pd.DataFrame() + for val in sorted(standings_df[tiebreaker_col].unique(), reverse=True): + standings_subset = standings_df[standings_df[tiebreaker_col] == val] + + # Sort all remaining teams with the next tiebreaker + sorted_subset = self._sort_standings_df( + standings_subset, + tiebreaker_hierarchy[1:], + ) + + # Append the sorted subset to the final sorted standings DataFrame + sorted_standings = pd.concat( + [ + sorted_standings, + sorted_subset, + ] + ) + + return sorted_standings From 37c4f220ab0087c36711c5983b8558009856eebc Mon Sep 17 00:00:00 2001 From: DesiPilla Date: Sat, 16 Dec 2023 00:31:03 -0500 Subject: [PATCH 02/12] fix: remove dependency on `pandas` I'm used to always having `pandas` and `numpy` available, made that change now. --- espn_api/football/league.py | 274 ++++++++++++++++++------------------ 1 file changed, 140 insertions(+), 134 deletions(-) diff --git a/espn_api/football/league.py b/espn_api/football/league.py index 2fd1ef20..b5cdcaa0 100644 --- a/espn_api/football/league.py +++ b/espn_api/football/league.py @@ -1,10 +1,7 @@ import json -from itertools import combinations +import random from typing import Callable, Dict, List, Tuple, Union -import pandas as pd - - from ..base_league import BaseLeague from .team import Team from .matchup import Matchup @@ -122,7 +119,7 @@ def standings(self) -> List[Team]: def standings_weekly(self, week: int) -> pd.DataFrame: """This is the main function to get the standings for a given week. - It controls the tiebreaker hierarchy and calls the recursive League()._sort_standings_df function. + It controls the tiebreaker hierarchy and calls the recursive League()._sort_team_data_list function. First, the division winners must be determined. Then, the rest of the teams are sorted. The standard tiebreaker hierarchy is: @@ -136,35 +133,29 @@ def standings_weekly(self, week: int) -> pd.DataFrame: week (int): Week to get the standings for Returns: - pd.DataFrame: Sorted standings DataFrame + pd.DataFrame: Sorted standings list """ - # Build the standings DataFrame for the given week - standings_df = pd.DataFrame() - standings_df["team_id"] = [team.team_id for team in self.teams] - standings_df["team_name"] = [team.team_name for team in self.teams] - standings_df["division_id"] = [team.division_id for team in self.teams] - standings_df["division_name"] = [team.division_name for team in self.teams] - standings_df["wins"] = [ - sum([1 for outcome in team.outcomes[:week] if outcome == "W"]) - for team in self.teams - ] - standings_df["ties"] = [ - sum([1 for outcome in team.outcomes[:week] if outcome == "T"]) - for team in self.teams - ] - standings_df["losses"] = [ - sum([1 for outcome in team.outcomes[:week] if outcome == "L"]) - for team in self.teams - ] - standings_df["points_for"] = [sum(team.scores[:week]) for team in self.teams] - standings_df["points_against"] = [ - sum([team.schedule[w].scores[w] for w in range(week)]) - for team in self.teams - ] - standings_df["win_pct"] = ( - standings_df["wins"] + standings_df["ties"] / 2 - ) / standings_df[["wins", "losses", "ties"]].sum(axis=1) - standings_df.set_index("team_id", inplace=True) + # Get standings data for each team up to the given week + list_of_team_data = [] + for team in self.teams: + team_data = { + "team": team, + "team_id": team.team_id, + "division_id": team.division_id, + "wins": sum([1 for outcome in team.outcomes[:week] if outcome == "W"]), + "ties": sum([1 for outcome in team.outcomes[:week] if outcome == "T"]), + "losses": sum( + [1 for outcome in team.outcomes[:week] if outcome == "L"] + ), + "points_for": sum(team.scores[:week]), + "points_against": sum( + [team.schedule[w].scores[w] for w in range(week)] + ), + } + team_data["win_pct"] = (team_data["wins"] + team_data["ties"] / 2) / sum( + [1 for outcome in team.outcomes[:week] if outcome in ["W", "T", "L"]] + ) + list_of_team_data.append(team_data) if self.settings.playoff_seed_tie_rule == "TOTAL_POINTS_SCORED": tiebreaker_hierarchy = [ @@ -190,36 +181,33 @@ def standings_weekly(self, week: int) -> pd.DataFrame: ) # First assign the division winners - division_winners = pd.DataFrame() - for division_id in standings_df.division_id.unique(): - division_standings = standings_df[(standings_df.division_id == division_id)] - division_winner = self._sort_standings_df( - division_standings, tiebreaker_hierarchy - ).iloc[0] - division_winners = pd.concat( - [division_winners, division_winner.to_frame().T] - ) + division_winners = [] + for division_id in list(self.settings.division_map.keys()): + division_teams = [ + team_data + for team_data in list_of_team_data + if team_data["division_id"] == division_id + ] + division_winner = self._sort_team_data_list( + division_teams, tiebreaker_hierarchy + )[0] + division_winners.append(division_winner) + list_of_team_data.remove(division_winner) # Sort the division winners - sorted_division_winners = self._sort_standings_df( + sorted_division_winners = self._sort_team_data_list( division_winners, tiebreaker_hierarchy ) # Then sort the rest of the teams - rest_of_field = standings_df.loc[ - ~standings_df.index.isin(division_winners.index) - ] - sorted_rest_of_field = self._sort_standings_df( - rest_of_field, tiebreaker_hierarchy + sorted_rest_of_field = self._sort_team_data_list( + list_of_team_data, tiebreaker_hierarchy ) # Combine all teams - sorted_standings = pd.concat( - [sorted_division_winners, sorted_rest_of_field], - axis=0, - ) + sorted_team_data = sorted_division_winners + sorted_rest_of_field - return sorted_standings + return [team_data["team"] for team_data in sorted_team_data] def top_scorer(self) -> Team: most_pf = sorted(self.teams, key=lambda x: x.points_for, reverse=True) @@ -446,135 +434,153 @@ def _build_division_record_dict(self) -> Dict: return div_record - def _build_h2h_df(self) -> pd.DataFrame: - """Create a dataframe that contains the head-to-head victories between each team in the league""" - # Get the list of teams - team_ids = [team.team_id for team in self.teams] - - # Initialize the DataFrame - h2h_df = pd.DataFrame(index=team_ids, columns=team_ids).replace(np.nan, 0) + def _build_h2h_dict(teams: List[Team]) -> Dict: + """Create a dictionary with each team's divisional record.""" - # Loop through each team - for team in self.teams: - # Get the team's schedule - schedule = team.schedule - outcomes = team.outcomes + # Get the list of teams + team_ids = [team.team_id for team in teams] - # Loop through each week - for week, opp in enumerate(schedule): - # Update the DataFrame - if outcomes[week] == "W": - h2h_df.loc[team.team_id, opp.team_id] += 1 + # Create a dictionary with each team's head to head record + h2h_outcomes = { + team.team_id: {opp.team_id: {"wins": 0, "h2h_games": 0} for opp in teams} + for team in teams + } - if outcomes[week] == "T": - h2h_df.loc[team.team_id, opp.team_id] += 0.5 + # Loop through each team's schedule and outcomes and build the dictionary + for team in teams: + for opp, outcome in zip(team.schedule[:week], team.outcomes[:week]): + if outcome == "W": + h2h_outcomes[team.team_id][opp.team_id]["wins"] += 1 + if outcome == "T": + h2h_outcomes[team.team_id][opp.team_id]["wins"] += 0.5 + + h2h_outcomes[team.team_id][opp.team_id]["h2h_games"] += 1 + + # Calculate the head to head record + h2h_record = { + team_id: { + opp_id: ( + h2h_outcomes[team_id][opp_id]["wins"] + / max(h2h_outcomes[team_id][opp_id]["h2h_games"], 1) + ) + for opp_id in team_ids + } + for team_id in team_ids + } - return h2h_df + return h2h_record - def _sort_by_win_pct(self, standings_df: pd.DataFrame) -> pd.DataFrame: - """Take a DataFrame of standings and sort it using the TOTAL_POINTS_SCORED tiebreaker""" - return standings_df.sort_values(by="win_pct", ascending=False) + def _sort_by_win_pct(self, team_data_list: List[Dict]) -> List[Dict]: + """Take a list of team standings data and sort it using the TOTAL_POINTS_SCORED tiebreaker""" + return sorted(team_data_list, key=lambda x: x["win_pct"], reverse=True) - def _sort_by_points_for(self, standings_df: pd.DataFrame) -> pd.DataFrame: - """Take a DataFrame of standings and sort it using the TOTAL_POINTS_SCORED tiebreaker""" - return standings_df.sort_values(by="points_for", ascending=False) + def _sort_by_points_for(self, team_data_list: List[Dict]) -> List[Dict]: + """Take a list of team standings data and sort it using the TOTAL_POINTS_SCORED tiebreaker""" + return sorted(team_data_list, key=lambda x: x["points_for"], reverse=True) - def _sort_by_division_record(self, standings_df: pd.DataFrame) -> pd.DataFrame: - division_records = build_division_record_df(self) - standings_df["division_record"] = standings_df.index.map(division_records) - return standings_df.sort_values(by="division_record", ascending=False) + def _sort_by_division_record(self, team_data_list: List[Dict]) -> List[Dict]: + """Take a list of team standings data and sort it using the 3rd level tiebreaker""" + division_records = self._build_division_record_dict(self) + for team_data in team_data_list: + team_data["division_record"] = division_records[team_data["team_id"]] + return sorted(team_data_list, key=lambda x: x["division_record"], reverse=True) - def _sort_by_points_against(self, standings_df: pd.DataFrame) -> pd.DataFrame: - """Take a DataFrame of standings and sort it using the 3rd level tiebreaker""" - return standings_df.sort_values(by="points_against", ascending=True) + def _sort_by_points_against(self, team_data_list: List[Dict]) -> List[Dict]: + """Take a list of team standings data and sort it using the 4th level tiebreaker""" + return sorted(team_data_list, key=lambda x: x["points_against"], reverse=True) - def _sort_by_coin_flip(self, standings_df: pd.DataFrame) -> pd.DataFrame: - standings_df["coin_flip"] = np.random.rand(len(standings_df)) - return standings_df.sort_values(by="coin_flip", ascending=False) + def _sort_by_coin_flip(self, team_data_list: List[Dict]) -> List[Dict]: + """Take a list of team standings data and sort it using the 5th level tiebreaker""" + for team_data in team_data_list: + team_data["coin_flip"] = random.random() + return sorted(team_data_list, key=lambda x: x["coin_flip"], reverse=True) def _sort_by_head_to_head( self, - standings_df: pd.DataFrame, - ) -> pd.DataFrame: - """Take a DataFrame of standings and sort it using the H2H_RECORD tiebreaker""" + team_data_list: List[Dict], + ) -> List[Dict]: + """Take a list of team standings data and sort it using the H2H_RECORD tiebreaker""" # If there is only one team, return the dataframe as-is - if len(standings_df) == 1: - return standings_df + if len(team_data_list) == 1: + return team_data_list # Filter the H2H DataFrame to only include the teams in question - team_ids = standings_df.index.tolist() - h2h_df = self._build_h2h_df().loc[team_ids, team_ids] + h2h_dict = self._build_h2h_dict(teams) # If there are only two teams, sort descending by H2H wins - if len(h2h_df) == 2: - standings_df["h2h_wins"] = h2h_df.sum(axis=1) - return standings_df.sort_values(by="h2h_wins", ascending=False) + if len(h2h_dict) == 2: + for team_data in team_data_list: + team_data["h2h_wins"] = h2h_dict[team_data["team_id"]]["h2h_wins"] + return sorted(team_data_list, key=lambda x: x["h2h_wins"], reverse=True) # If there are more than two teams... - if len(h2h_df) > 2: + if len(h2h_dict) > 2: # Check if the teams have all played each other an equal number of times - matchup_combos = list(combinations(team_ids, 2)) matchup_counts = [ - h2h_df.loc[t1, t2] + h2h_df.loc[t2, t1] for t1, t2 in matchup_combos + h2h_dict[opp]["h2h_games"] + for opp in h2h_dict[team].keys() + for team in h2h_dict.keys() ] if len(set(matchup_counts)) == 1: # All teams have played each other an equal number of times # Sort the teams by total H2H wins against each other - standings_df["h2h_wins"] = h2h_df.sum(axis=1) - return standings_df.sort_values(by="h2h_wins", ascending=False) + for team_data in team_data_list: + team_data["h2h_wins"] = h2h_dict[team_data["team_id"]]["h2h_wins"] + return sorted(team_data_list, key=lambda x: x["h2h_wins"], reverse=True) else: # All teams have not played each other an equal number of times # This tiebreaker is invalid - standings_df["h2h_wins"] = 0 - return standings_df + for team_data in team_data_list: + team_data["h2h_wins"] = 0 + return team_data_list - def _sort_standings_df( + def _sort_team_data_list( self, - standings_df: pd.DataFrame, + team_data_list: List[Dict], tiebreaker_hierarchy: List[Tuple[Callable, str]], - ) -> pd.DataFrame: - """This recursive function sorts a standings DataFrame by the tiebreaker hierarchy. + ) -> List[Dict]: + """This recursive function sorts a list of team standings data by the tiebreaker hierarchy. It iterates through each tiebreaker, sorting any remaning ties by the next tiebreaker. Args: - standings_df (pd.DataFrame): Standings DataFrame to sort + team_data_list (List[Dict]): List of team data dictionaries tiebreaker_hierarchy (List[Tuple[Callable, str]]): List of tiebreaker functions and columns to sort by Returns: - pd.DataFrame: Sorted standings DataFrame + List[Dict]: Sorted list of team data dictionaries """ - # If there are no more tiebreakers, return the standings DataFrame as-is + # If there are no more tiebreakers, return the standings list as-is if not tiebreaker_hierarchy: - return standings_df + return team_data_list - # If there is only one team to sort, return the standings DataFrame as-is - if len(standings_df) == 1: - return standings_df + # If there is only one team to sort, return the standings list as-is + if len(team_data_list) == 1: + return team_data_list # Get the tiebreaker function and column name to group by tiebreaker_function = tiebreaker_hierarchy[0][0] tiebreaker_col = tiebreaker_hierarchy[0][1] - # Apply the tiebreaker function to the standings DataFrame - standings_df = tiebreaker_function(standings_df) + # Apply the tiebreaker function to the standings list + team_data_list = tiebreaker_function(team_data_list) # Loop through each remaining unique tiebreaker value to see if ties remain - sorted_standings = pd.DataFrame() - for val in sorted(standings_df[tiebreaker_col].unique(), reverse=True): - standings_subset = standings_df[standings_df[tiebreaker_col] == val] + sorted_team_data_list = [] + for val in sorted( + set([team_data[tiebreaker_col] for team_data in team_data_list]), + reverse=True, + ): + # Filter the standings list to only include the teams with the current tiebreaker value + team_data_subset = [ + team_data + for team_data in team_data_list + if team_data[tiebreaker_col] == val + ] - # Sort all remaining teams with the next tiebreaker - sorted_subset = self._sort_standings_df( - standings_subset, + # Append the sorted subset to the final sorted standings list + sorted_team_data_list = sorted_team_data_list + self._sort_team_data_list( + team_data_subset, tiebreaker_hierarchy[1:], ) - # Append the sorted subset to the final sorted standings DataFrame - sorted_standings = pd.concat( - [ - sorted_standings, - sorted_subset, - ] - ) - - return sorted_standings + return sorted_team_data_list From 7a0ca8ba7b6b8e9dd1842c1f75b6f9b4daa0accf Mon Sep 17 00:00:00 2001 From: DesiPilla Date: Sat, 16 Dec 2023 00:34:27 -0500 Subject: [PATCH 03/12] lint --- espn_api/football/league.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/espn_api/football/league.py b/espn_api/football/league.py index b5cdcaa0..5bae2369 100644 --- a/espn_api/football/league.py +++ b/espn_api/football/league.py @@ -116,7 +116,7 @@ def standings(self) -> List[Team]: standings = sorted(self.teams, key=lambda x: x.final_standing if x.final_standing != 0 else x.standing, reverse=False) return standings - def standings_weekly(self, week: int) -> pd.DataFrame: + def standings_weekly(self, week: int) -> List[Dict]: """This is the main function to get the standings for a given week. It controls the tiebreaker hierarchy and calls the recursive League()._sort_team_data_list function. @@ -133,7 +133,7 @@ def standings_weekly(self, week: int) -> pd.DataFrame: week (int): Week to get the standings for Returns: - pd.DataFrame: Sorted standings list + List[Dict]: Sorted standings list """ # Get standings data for each team up to the given week list_of_team_data = [] @@ -157,6 +157,7 @@ def standings_weekly(self, week: int) -> pd.DataFrame: ) list_of_team_data.append(team_data) + # Identify the proper tiebreaker hierarchy if self.settings.playoff_seed_tie_rule == "TOTAL_POINTS_SCORED": tiebreaker_hierarchy = [ (self._sort_by_win_pct, "win_pct"), From 6cfd3313ae643006a409e316a5f5012bd5e5c07a Mon Sep 17 00:00:00 2001 From: DesiPilla Date: Sat, 16 Dec 2023 00:43:36 -0500 Subject: [PATCH 04/12] test: Add test case for coverage --- tests/football/unit/test_league.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/football/unit/test_league.py b/tests/football/unit/test_league.py index f3563bf8..9adb878b 100644 --- a/tests/football/unit/test_league.py +++ b/tests/football/unit/test_league.py @@ -81,7 +81,16 @@ def test_league_standings(self, m): standings = league.standings() self.assertEqual(standings[0].final_standing, 1) - @requests_mock.Mocker() + @requests_mock.Mocker() + def test_standings_weekly(self, m): + self.mock_setUp(m) + + league = League(self.league_id, self.season) + + valid_week = league.standings_weekly(1) + self.assertEqual(valid_week[0].wins > valid_week[-1].wins, True) + + @requests_mock.Mocker() def test_top_scorer(self, m): self.mock_setUp(m) From 005af2ad578816ad87199fc7226ade3fd9e34897 Mon Sep 17 00:00:00 2001 From: DesiPilla Date: Sat, 16 Dec 2023 01:22:38 -0500 Subject: [PATCH 05/12] test Had the wrong test expression --- tests/football/unit/test_league.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/football/unit/test_league.py b/tests/football/unit/test_league.py index 9adb878b..d2b935a3 100644 --- a/tests/football/unit/test_league.py +++ b/tests/football/unit/test_league.py @@ -87,8 +87,15 @@ def test_standings_weekly(self, m): league = League(self.league_id, self.season) - valid_week = league.standings_weekly(1) - self.assertEqual(valid_week[0].wins > valid_week[-1].wins, True) + week = 1 + standings = league.standings_weekly(1) + best_team_after_week = sum( + [1 for outcome in standings[0].outcomes[:week] if outcome == "W"] + ) + worst_team_after_week = sum( + [1 for outcome in standings[-1].outcomes[:week] if outcome == "W"] + ) + self.assertEqual(best_team_after_week >= worst_team_after_week, True) @requests_mock.Mocker() def test_top_scorer(self, m): From e200fd30c34df26f70a9eac931afeb3a62f5fa74 Mon Sep 17 00:00:00 2001 From: DesiPilla Date: Sat, 16 Dec 2023 20:52:49 -0500 Subject: [PATCH 06/12] test: add better test method Now explicitly tests for the exact standings of the test league --- tests/football/unit/test_league.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/tests/football/unit/test_league.py b/tests/football/unit/test_league.py index d2b935a3..10d7eb88 100644 --- a/tests/football/unit/test_league.py +++ b/tests/football/unit/test_league.py @@ -87,15 +87,17 @@ def test_standings_weekly(self, m): league = League(self.league_id, self.season) - week = 1 - standings = league.standings_weekly(1) - best_team_after_week = sum( - [1 for outcome in standings[0].outcomes[:week] if outcome == "W"] - ) - worst_team_after_week = sum( - [1 for outcome in standings[-1].outcomes[:week] if outcome == "W"] - ) - self.assertEqual(best_team_after_week >= worst_team_after_week, True) + # Test various weeks + week1_standings = [team.team_id for team in league.standings_weekly(1)] + self.assertEqual(week1_standings, [3, 11, 2, 10, 7, 8, 4, 5, 9, 1]) + + week4_standings = [team.team_id for team in league.standings_weekly(4)] + self.assertEqual(week4_standings, [2, 7, 11, 4, 3, 9, 1, 8, 5, 10]) + + # # Does not work with the playoffs + # week13_standings = [team.team_id for team in league.standings_weekly(13)] + # final_standings = [team.team_id for team in league.standings()] + # self.assertEqual(week13_standings, final_standings) @requests_mock.Mocker() def test_top_scorer(self, m): From e8573a7d69f7e237d3b66d79376e5adebd1a9edb Mon Sep 17 00:00:00 2001 From: DesiPilla Date: Tue, 19 Dec 2023 15:03:35 -0500 Subject: [PATCH 07/12] move functions to `helper.py` --- espn_api/football/helper.py | 198 +++++++++++++++++++++++++++++++ espn_api/football/league.py | 229 +++++------------------------------- 2 files changed, 226 insertions(+), 201 deletions(-) create mode 100644 espn_api/football/helper.py diff --git a/espn_api/football/helper.py b/espn_api/football/helper.py new file mode 100644 index 00000000..b0f70eb3 --- /dev/null +++ b/espn_api/football/helper.py @@ -0,0 +1,198 @@ +import random +from typing import Callable, Dict, List, Tuple + + +def build_division_record_dict(team_data_list: List[Dict]) -> Dict: + """Create a DataFrame with each team's divisional record.""" + # Create a dictionary with each team's divisional record + div_outcomes = { + team_data["team_id"]: {"wins": 0, "divisional_games": 0} + for team_data in team_data_list + } + + # Loop through each team's schedule and outcomes and build the dictionary + for team_data in team_data_list: + team = team_data["team"] + for opp, outcome in zip(team.schedule, team.outcomes): + if team.division_id == opp.division_id: + if outcome == "W": + div_outcomes[team_data["team_id"]]["wins"] += 1 + if outcome == "T": + div_outcomes[team_data["team_id"]]["wins"] += 0.5 + + div_outcomes[team_data["team_id"]]["divisional_games"] += 1 + + # Calculate the divisional record + div_record = { + team_data["team_id"]: ( + div_outcomes[team_data["team_id"]]["wins"] + / max(div_outcomes[team_data["team_id"]]["divisional_games"], 1) + ) + for team_data in team_data_list + } + + return div_record + + +def build_h2h_dict(team_data_list: List[Dict]) -> Dict: + """Create a dictionary with each team's divisional record.""" + # Create a dictionary with each team's head to head record + h2h_outcomes = { + team_data["team_id"]: { + opp["team_id"]: {"wins": 0, "h2h_games": 0} for opp in team_data_list + } + for team_data in team_data_list + } + + # Loop through each team's schedule and outcomes and build the dictionary + for team_data in team_data_list: + team = team_data["team"] + for opp, outcome in zip(team_data["schedule"], team_data["outcomes"]): + if outcome == "W": + h2h_outcomes[team.team_id][opp.team_id]["wins"] += 1 + if outcome == "T": + h2h_outcomes[team.team_id][opp.team_id]["wins"] += 0.5 + + h2h_outcomes[team.team_id][opp.team_id]["h2h_games"] += 1 + + # Calculate the head to head record + h2h_record = { + team_data["team_id"]: { + opp_data["team_id"]: ( + h2h_outcomes[team_data["team_id"]][opp_data["team_id"]]["wins"] + / max( + h2h_outcomes[team_data["team_id"]][opp_data["team_id"]][ + "h2h_games" + ], + 1, + ) + ) + for opp_data in team_data_list + } + for team_data in team_data_list + } + + return h2h_record + + +def sort_by_win_pct(team_data_list: List[Dict]) -> List[Dict]: + """Take a list of team standings data and sort it using the TOTAL_POINTS_SCORED tiebreaker""" + return sorted(team_data_list, key=lambda x: x["win_pct"], reverse=True) + + +def sort_by_points_for(team_data_list: List[Dict]) -> List[Dict]: + """Take a list of team standings data and sort it using the TOTAL_POINTS_SCORED tiebreaker""" + return sorted(team_data_list, key=lambda x: x["points_for"], reverse=True) + + +def sort_by_division_record(team_data_list: List[Dict]) -> List[Dict]: + """Take a list of team standings data and sort it using the 3rd level tiebreaker""" + division_records = build_division_record_dict(team_data_list) + for team_data in team_data_list: + team_data["division_record"] = division_records[team_data["team_id"]] + return sorted(team_data_list, key=lambda x: x["division_record"], reverse=True) + + +def sort_by_points_against(team_data_list: List[Dict]) -> List[Dict]: + """Take a list of team standings data and sort it using the 4th level tiebreaker""" + return sorted(team_data_list, key=lambda x: x["points_against"], reverse=True) + + +def sort_by_coin_flip(team_data_list: List[Dict]) -> List[Dict]: + """Take a list of team standings data and sort it using the 5th level tiebreaker""" + for team_data in team_data_list: + team_data["coin_flip"] = random.random() + return sorted(team_data_list, key=lambda x: x["coin_flip"], reverse=True) + + +def sort_by_head_to_head( + team_data_list: List[Dict], +) -> List[Dict]: + """Take a list of team standings data and sort it using the H2H_RECORD tiebreaker""" + # If there is only one team, return the dataframe as-is + if len(team_data_list) < 2: + return team_data_list + + # If there are only two teams, sort descending by H2H wins + elif len(h2h_dict) == 2: + # Filter the H2H DataFrame to only include the teams in question + h2h_dict = build_h2h_dict(team_data_list) + + for team_data in team_data_list: + team_data["h2h_wins"] = h2h_dict[team_data["team_id"]]["h2h_wins"] + return sorted(team_data_list, key=lambda x: x["h2h_wins"], reverse=True) + + # If there are more than two teams... + else: + # Filter the H2H DataFrame to only include the teams in question + h2h_dict = build_h2h_dict(team_data_list) + + # Check if the teams have all played each other an equal number of times + matchup_counts = [ + h2h_dict[opp]["h2h_games"] + for opp in h2h_dict[team_id].keys() + for team_id in h2h_dict.keys() + ] + if len(set(matchup_counts)) == 1: + # All teams have played each other an equal number of times + # Sort the teams by total H2H wins against each other + for team_data in team_data_list: + team_data["h2h_wins"] = h2h_dict[team_data["team_id"]]["h2h_wins"] + return sorted(team_data_list, key=lambda x: x["h2h_wins"], reverse=True) + else: + # All teams have not played each other an equal number of times + # This tiebreaker is invalid + for team_data in team_data_list: + team_data["h2h_wins"] = 0 + return team_data_list + + +def sort_team_data_list( + team_data_list: List[Dict], + tiebreaker_hierarchy: List[Tuple[Callable, str]], +) -> List[Dict]: + """This recursive function sorts a list of team standings data by the tiebreaker hierarchy. + It iterates through each tiebreaker, sorting any remaning ties by the next tiebreaker. + + Args: + team_data_list (List[Dict]): List of team data dictionaries + tiebreaker_hierarchy (List[Tuple[Callable, str]]): List of tiebreaker functions and columns to sort by + + Returns: + List[Dict]: Sorted list of team data dictionaries + """ + # If there are no more tiebreakers, return the standings list as-is + if not tiebreaker_hierarchy: + return team_data_list + + # If there is only one team to sort, return the standings list as-is + if len(team_data_list) == 1: + return team_data_list + + # Get the tiebreaker function and column name to group by + tiebreaker_function = tiebreaker_hierarchy[0][0] + tiebreaker_col = tiebreaker_hierarchy[0][1] + + # Apply the tiebreaker function to the standings list + team_data_list = tiebreaker_function(team_data_list) + + # Loop through each remaining unique tiebreaker value to see if ties remain + sorted_team_data_list = [] + for val in sorted( + set([team_data[tiebreaker_col] for team_data in team_data_list]), + reverse=True, + ): + # Filter the standings list to only include the teams with the current tiebreaker value + team_data_subset = [ + team_data + for team_data in team_data_list + if team_data[tiebreaker_col] == val + ] + + # Append the sorted subset to the final sorted standings list + sorted_team_data_list = sorted_team_data_list + sort_team_data_list( + team_data_subset, + tiebreaker_hierarchy[1:], + ) + + return sorted_team_data_list diff --git a/espn_api/football/league.py b/espn_api/football/league.py index 5bae2369..f11ab6fd 100644 --- a/espn_api/football/league.py +++ b/espn_api/football/league.py @@ -13,6 +13,15 @@ from .settings import Settings from .utils import power_points, two_step_dominance from .constant import POSITION_MAP, ACTIVITY_MAP +from .helper import ( + sort_by_coin_flip, + sort_by_division_record, + sort_by_head_to_head, + sort_by_points_against, + sort_by_points_for, + sort_by_win_pct, + sort_team_data_list, +) class League(BaseLeague): @@ -116,7 +125,7 @@ def standings(self) -> List[Team]: standings = sorted(self.teams, key=lambda x: x.final_standing if x.final_standing != 0 else x.standing, reverse=False) return standings - def standings_weekly(self, week: int) -> List[Dict]: + def standings_weekly(self, week: int) -> List[Team]: """This is the main function to get the standings for a given week. It controls the tiebreaker hierarchy and calls the recursive League()._sort_team_data_list function. @@ -151,6 +160,7 @@ def standings_weekly(self, week: int) -> List[Dict]: "points_against": sum( [team.schedule[w].scores[w] for w in range(week)] ), + "schedule": team.schedule[:week], } team_data["win_pct"] = (team_data["wins"] + team_data["ties"] / 2) / sum( [1 for outcome in team.outcomes[:week] if outcome in ["W", "T", "L"]] @@ -160,21 +170,21 @@ def standings_weekly(self, week: int) -> List[Dict]: # Identify the proper tiebreaker hierarchy if self.settings.playoff_seed_tie_rule == "TOTAL_POINTS_SCORED": tiebreaker_hierarchy = [ - (self._sort_by_win_pct, "win_pct"), - (self._sort_by_points_for, "points_for"), - (self._sort_by_head_to_head, "h2h_wins"), - (self._sort_by_division_record, "division_record"), - (self._sort_by_points_against, "points_against"), - (self._sort_by_coin_flip, "coin_flip"), + (sort_by_win_pct, "win_pct"), + (sort_by_points_for, "points_for"), + (sort_by_head_to_head, "h2h_wins"), + (sort_by_division_record, "division_record"), + (sort_by_points_against, "points_against"), + (sort_by_coin_flip, "coin_flip"), ] elif self.settings.playoff_seed_tie_rule == "H2H_RECORD": tiebreaker_hierarchy = [ - (self._sort_by_win_pct, "win_pct"), - (self._sort_by_head_to_head, "h2h_wins"), - (self._sort_by_points_for, "points_for"), - (self._sort_by_division_record, "division_record"), - (self._sort_by_points_against, "points_against"), - (self._sort_by_coin_flip, "coin_flip"), + (sort_by_win_pct, "win_pct"), + (sort_by_head_to_head, "h2h_wins"), + (sort_by_points_for, "points_for"), + (sort_by_division_record, "division_record"), + (sort_by_points_against, "points_against"), + (sort_by_coin_flip, "coin_flip"), ] else: raise ValueError( @@ -189,19 +199,19 @@ def standings_weekly(self, week: int) -> List[Dict]: for team_data in list_of_team_data if team_data["division_id"] == division_id ] - division_winner = self._sort_team_data_list( - division_teams, tiebreaker_hierarchy - )[0] + division_winner = sort_team_data_list(division_teams, tiebreaker_hierarchy)[ + 0 + ] division_winners.append(division_winner) list_of_team_data.remove(division_winner) # Sort the division winners - sorted_division_winners = self._sort_team_data_list( + sorted_division_winners = sort_team_data_list( division_winners, tiebreaker_hierarchy ) # Then sort the rest of the teams - sorted_rest_of_field = self._sort_team_data_list( + sorted_rest_of_field = sort_team_data_list( list_of_team_data, tiebreaker_hierarchy ) @@ -402,186 +412,3 @@ def message_board(self, msg_types: List[str] = None): for msg in msgs: messages.append(msg) return messages - - def _build_division_record_dict(self) -> Dict: - """Create a DataFrame with each team's divisional record.""" - # Get the list of teams - team_ids = [team.team_id for team in self.teams] - - # Create a dictionary with each team's divisional record - div_outcomes = { - team.team_id: {"wins": 0, "divisional_games": 0} for team in self.teams - } - - # Loop through each team's schedule and outcomes and build the dictionary - for team in self.teams: - for opp, outcome in zip(team.schedule[:week], team.outcomes[:week]): - if team.division_id == opp.division_id: - if outcome == "W": - div_outcomes[team.team_id]["wins"] += 1 - if outcome == "T": - div_outcomes[team.team_id]["wins"] += 0.5 - - div_outcomes[team.team_id]["divisional_games"] += 1 - - # Calculate the divisional record - div_record = { - team_id: ( - div_outcomes[team_id]["wins"] - / max(div_outcomes[team_id]["divisional_games"], 1) - ) - for team_id in team_ids - } - - return div_record - - def _build_h2h_dict(teams: List[Team]) -> Dict: - """Create a dictionary with each team's divisional record.""" - - # Get the list of teams - team_ids = [team.team_id for team in teams] - - # Create a dictionary with each team's head to head record - h2h_outcomes = { - team.team_id: {opp.team_id: {"wins": 0, "h2h_games": 0} for opp in teams} - for team in teams - } - - # Loop through each team's schedule and outcomes and build the dictionary - for team in teams: - for opp, outcome in zip(team.schedule[:week], team.outcomes[:week]): - if outcome == "W": - h2h_outcomes[team.team_id][opp.team_id]["wins"] += 1 - if outcome == "T": - h2h_outcomes[team.team_id][opp.team_id]["wins"] += 0.5 - - h2h_outcomes[team.team_id][opp.team_id]["h2h_games"] += 1 - - # Calculate the head to head record - h2h_record = { - team_id: { - opp_id: ( - h2h_outcomes[team_id][opp_id]["wins"] - / max(h2h_outcomes[team_id][opp_id]["h2h_games"], 1) - ) - for opp_id in team_ids - } - for team_id in team_ids - } - - return h2h_record - - def _sort_by_win_pct(self, team_data_list: List[Dict]) -> List[Dict]: - """Take a list of team standings data and sort it using the TOTAL_POINTS_SCORED tiebreaker""" - return sorted(team_data_list, key=lambda x: x["win_pct"], reverse=True) - - def _sort_by_points_for(self, team_data_list: List[Dict]) -> List[Dict]: - """Take a list of team standings data and sort it using the TOTAL_POINTS_SCORED tiebreaker""" - return sorted(team_data_list, key=lambda x: x["points_for"], reverse=True) - - def _sort_by_division_record(self, team_data_list: List[Dict]) -> List[Dict]: - """Take a list of team standings data and sort it using the 3rd level tiebreaker""" - division_records = self._build_division_record_dict(self) - for team_data in team_data_list: - team_data["division_record"] = division_records[team_data["team_id"]] - return sorted(team_data_list, key=lambda x: x["division_record"], reverse=True) - - def _sort_by_points_against(self, team_data_list: List[Dict]) -> List[Dict]: - """Take a list of team standings data and sort it using the 4th level tiebreaker""" - return sorted(team_data_list, key=lambda x: x["points_against"], reverse=True) - - def _sort_by_coin_flip(self, team_data_list: List[Dict]) -> List[Dict]: - """Take a list of team standings data and sort it using the 5th level tiebreaker""" - for team_data in team_data_list: - team_data["coin_flip"] = random.random() - return sorted(team_data_list, key=lambda x: x["coin_flip"], reverse=True) - - def _sort_by_head_to_head( - self, - team_data_list: List[Dict], - ) -> List[Dict]: - """Take a list of team standings data and sort it using the H2H_RECORD tiebreaker""" - # If there is only one team, return the dataframe as-is - if len(team_data_list) == 1: - return team_data_list - - # Filter the H2H DataFrame to only include the teams in question - h2h_dict = self._build_h2h_dict(teams) - - # If there are only two teams, sort descending by H2H wins - if len(h2h_dict) == 2: - for team_data in team_data_list: - team_data["h2h_wins"] = h2h_dict[team_data["team_id"]]["h2h_wins"] - return sorted(team_data_list, key=lambda x: x["h2h_wins"], reverse=True) - - # If there are more than two teams... - if len(h2h_dict) > 2: - # Check if the teams have all played each other an equal number of times - matchup_counts = [ - h2h_dict[opp]["h2h_games"] - for opp in h2h_dict[team].keys() - for team in h2h_dict.keys() - ] - if len(set(matchup_counts)) == 1: - # All teams have played each other an equal number of times - # Sort the teams by total H2H wins against each other - for team_data in team_data_list: - team_data["h2h_wins"] = h2h_dict[team_data["team_id"]]["h2h_wins"] - return sorted(team_data_list, key=lambda x: x["h2h_wins"], reverse=True) - else: - # All teams have not played each other an equal number of times - # This tiebreaker is invalid - for team_data in team_data_list: - team_data["h2h_wins"] = 0 - return team_data_list - - def _sort_team_data_list( - self, - team_data_list: List[Dict], - tiebreaker_hierarchy: List[Tuple[Callable, str]], - ) -> List[Dict]: - """This recursive function sorts a list of team standings data by the tiebreaker hierarchy. - It iterates through each tiebreaker, sorting any remaning ties by the next tiebreaker. - - Args: - team_data_list (List[Dict]): List of team data dictionaries - tiebreaker_hierarchy (List[Tuple[Callable, str]]): List of tiebreaker functions and columns to sort by - - Returns: - List[Dict]: Sorted list of team data dictionaries - """ - # If there are no more tiebreakers, return the standings list as-is - if not tiebreaker_hierarchy: - return team_data_list - - # If there is only one team to sort, return the standings list as-is - if len(team_data_list) == 1: - return team_data_list - - # Get the tiebreaker function and column name to group by - tiebreaker_function = tiebreaker_hierarchy[0][0] - tiebreaker_col = tiebreaker_hierarchy[0][1] - - # Apply the tiebreaker function to the standings list - team_data_list = tiebreaker_function(team_data_list) - - # Loop through each remaining unique tiebreaker value to see if ties remain - sorted_team_data_list = [] - for val in sorted( - set([team_data[tiebreaker_col] for team_data in team_data_list]), - reverse=True, - ): - # Filter the standings list to only include the teams with the current tiebreaker value - team_data_subset = [ - team_data - for team_data in team_data_list - if team_data[tiebreaker_col] == val - ] - - # Append the sorted subset to the final sorted standings list - sorted_team_data_list = sorted_team_data_list + self._sort_team_data_list( - team_data_subset, - tiebreaker_hierarchy[1:], - ) - - return sorted_team_data_list From a5edc823f676a5c5411312d0132af8fe9635f013 Mon Sep 17 00:00:00 2001 From: DesiPilla Date: Wed, 20 Dec 2023 13:28:04 -0500 Subject: [PATCH 08/12] fix: bug fixes & better test coverage I added some more comprehensive testing, and along the way uncovered some bugs (this is why we write test cases!) --- espn_api/football/helper.py | 12 ++- espn_api/football/league.py | 1 + tests/football/unit/test_league.py | 146 +++++++++++++++++++++++++++++ 3 files changed, 157 insertions(+), 2 deletions(-) diff --git a/espn_api/football/helper.py b/espn_api/football/helper.py index b0f70eb3..d93baeee 100644 --- a/espn_api/football/helper.py +++ b/espn_api/football/helper.py @@ -13,8 +13,8 @@ def build_division_record_dict(team_data_list: List[Dict]) -> Dict: # Loop through each team's schedule and outcomes and build the dictionary for team_data in team_data_list: team = team_data["team"] - for opp, outcome in zip(team.schedule, team.outcomes): - if team.division_id == opp.division_id: + for opp, outcome in zip(team_data["schedule"], team_data["outcomes"]): + if team_data["division_id"] == opp.division_id: if outcome == "W": div_outcomes[team_data["team_id"]]["wins"] += 1 if outcome == "T": @@ -48,6 +48,11 @@ def build_h2h_dict(team_data_list: List[Dict]) -> Dict: for team_data in team_data_list: team = team_data["team"] for opp, outcome in zip(team_data["schedule"], team_data["outcomes"]): + # Ignore teams that are not part of this tiebreaker + if opp.team_id not in h2h_outcomes[team.team_id].keys(): + continue + + # Add the outcome to the dictionary if outcome == "W": h2h_outcomes[team.team_id][opp.team_id]["wins"] += 1 if outcome == "T": @@ -109,6 +114,9 @@ def sort_by_head_to_head( team_data_list: List[Dict], ) -> List[Dict]: """Take a list of team standings data and sort it using the H2H_RECORD tiebreaker""" + # Create a dictionary with each team's head to head record + h2h_dict = build_h2h_dict(team_data_list) + # If there is only one team, return the dataframe as-is if len(team_data_list) < 2: return team_data_list diff --git a/espn_api/football/league.py b/espn_api/football/league.py index f11ab6fd..96f9deb1 100644 --- a/espn_api/football/league.py +++ b/espn_api/football/league.py @@ -161,6 +161,7 @@ def standings_weekly(self, week: int) -> List[Team]: [team.schedule[w].scores[w] for w in range(week)] ), "schedule": team.schedule[:week], + "outcomes": team.outcomes[:week], } team_data["win_pct"] = (team_data["wins"] + team_data["ties"] / 2) / sum( [1 for outcome in team.outcomes[:week] if outcome in ["W", "T", "L"]] diff --git a/tests/football/unit/test_league.py b/tests/football/unit/test_league.py index 10d7eb88..dd664843 100644 --- a/tests/football/unit/test_league.py +++ b/tests/football/unit/test_league.py @@ -1,9 +1,20 @@ from unittest import mock, TestCase from espn_api.football import League, BoxPlayer +from espn_api.football.helper import ( + build_division_record_dict, + build_h2h_dict, + sort_by_coin_flip, + sort_by_division_record, + sort_by_head_to_head, + sort_by_points_against, + sort_by_points_for, + sort_by_win_pct, +) import requests_mock import json import io + class LeagueTest(TestCase): def setUp(self): self.league_id = 123 @@ -99,6 +110,141 @@ def test_standings_weekly(self, m): # final_standings = [team.team_id for team in league.standings()] # self.assertEqual(week13_standings, final_standings) + @requests_mock.Mocker() + def test_standings_helpers(self, m): + self.mock_setUp(m) + + league = League(self.league_id, self.season) + + def get_list_of_team_data(league, week): + list_of_team_data = [] + for team in league.teams: + team_data = { + "team": team, + "team_id": team.team_id, + "division_id": team.division_id, + "wins": sum( + [1 for outcome in team.outcomes[:week] if outcome == "W"] + ), + "ties": sum( + [1 for outcome in team.outcomes[:week] if outcome == "T"] + ), + "losses": sum( + [1 for outcome in team.outcomes[:week] if outcome == "L"] + ), + "points_for": sum(team.scores[:week]), + "points_against": sum( + [team.schedule[w].scores[w] for w in range(week)] + ), + "schedule": team.schedule[:week], + "outcomes": team.outcomes[:week], + } + team_data["win_pct"] = ( + team_data["wins"] + team_data["ties"] / 2 + ) / sum( + [ + 1 + for outcome in team.outcomes[:week] + if outcome in ["W", "T", "L"] + ] + ) + list_of_team_data.append(team_data) + return list_of_team_data + + # Test build_h2h_dict and build_division_record_dict + # Week 1 - get data for teams 1 and 7 + week1_teams_data = get_list_of_team_data(league, 1) + list_of_team_data = [ + team for team in week1_teams_data if team["team_id"] in (1, 7) + ] + h2h_dict = build_h2h_dict(list_of_team_data) + division_record_dict = build_division_record_dict(list_of_team_data) + + self.assertEqual(h2h_dict[1][7], 0) # Team 1 has 0 wins out of 1 against Team 7 + self.assertEqual(h2h_dict[7][1], 1) # Team 7 has 1 win out of 1 against Team 1 + self.assertEqual(division_record_dict[1], 0) + self.assertEqual( + division_record_dict[7], + [ + team_data["win_pct"] + for team_data in list_of_team_data + if team_data["team_id"] == 7 + ][0], + ) + + # Week 10 - get data for teams 1 and 7 + week10_teams_data = get_list_of_team_data(league, 10) + list_of_team_data = [ + team for team in week10_teams_data if team["team_id"] in (1, 7) + ] + h2h_dict = build_h2h_dict(list_of_team_data) + division_record_dict = build_division_record_dict(week10_teams_data) + + self.assertEqual( + h2h_dict[1][7], 0.5 + ) # Team 1 has 1 win out of 2 against Team 7 + self.assertEqual( + h2h_dict[7][1], 0.5 + ) # Team 7 has 1 win out of 2 against Team 1 + self.assertEqual(division_record_dict[1], 0.6) + self.assertEqual( + division_record_dict[7], + [ + team_data["win_pct"] + for team_data in list_of_team_data + if team_data["team_id"] == 7 + ][0], + ) + + # Test sorting functions + # Assert that sort_by_win_pct is correct + sorted_list_of_team_data = sort_by_win_pct(week10_teams_data) + for i in range(len(sorted_list_of_team_data) - 1): + self.assertGreaterEqual( + sorted_list_of_team_data[i]["win_pct"], + sorted_list_of_team_data[i + 1]["win_pct"], + ) + + # Assert that sort_by_points_for is correct + sorted_list_of_team_data = sort_by_points_for(week10_teams_data) + for i in range(len(sorted_list_of_team_data) - 1): + self.assertGreaterEqual( + sorted_list_of_team_data[i]["points_for"], + sorted_list_of_team_data[i + 1]["points_for"], + ) + + # Assert that sort_by_head_to_head is correct + sorted_list_of_team_data = sort_by_head_to_head( + [team for team in list_of_team_data if team["team_id"] in (1, 2)] + ) + # Team 1 is undefeated vs team 2 + self.assertEqual(sorted_list_of_team_data[0]["team_id"], 1) + + # Assert that sort_by_division_record is correct + sorted_list_of_team_data = sort_by_division_record(week10_teams_data) + for i in range(len(sorted_list_of_team_data) - 1): + self.assertGreaterEqual( + division_record_dict[sorted_list_of_team_data[i]["team_id"]], + division_record_dict[sorted_list_of_team_data[i + 1]["team_id"]], + ) + + # Assert that sort_by_points_against is correct + sorted_list_of_team_data = sort_by_points_against(week10_teams_data) + for i in range(len(sorted_list_of_team_data) - 1): + self.assertGreaterEqual( + sorted_list_of_team_data[i]["points_against"], + sorted_list_of_team_data[i + 1]["points_against"], + ) + + # Assert that sort_by_coin_flip is not deterministic + standings_list = [] + for i in range(5): + sorted_list_of_team_data = sort_by_coin_flip(week10_teams_data) + standings_list.append( + (team["team_id"] for team in sorted_list_of_team_data) + ) + self.assertGreater(len(set(standings_list)), 1) + @requests_mock.Mocker() def test_top_scorer(self, m): self.mock_setUp(m) From da3da43677ca2fdccbcf6909e01197ba405a36a5 Mon Sep 17 00:00:00 2001 From: DesiPilla Date: Wed, 20 Dec 2023 15:12:17 -0500 Subject: [PATCH 09/12] fix: bug fixes and better test coverage --- espn_api/football/helper.py | 47 ++++++------ tests/football/unit/test_league.py | 113 +++++++++++++++++------------ 2 files changed, 90 insertions(+), 70 deletions(-) diff --git a/espn_api/football/helper.py b/espn_api/football/helper.py index d93baeee..0fd55c60 100644 --- a/espn_api/football/helper.py +++ b/espn_api/football/helper.py @@ -39,7 +39,7 @@ def build_h2h_dict(team_data_list: List[Dict]) -> Dict: # Create a dictionary with each team's head to head record h2h_outcomes = { team_data["team_id"]: { - opp["team_id"]: {"wins": 0, "h2h_games": 0} for opp in team_data_list + opp["team_id"]: {"h2h_wins": 0, "h2h_games": 0} for opp in team_data_list } for team_data in team_data_list } @@ -54,30 +54,26 @@ def build_h2h_dict(team_data_list: List[Dict]) -> Dict: # Add the outcome to the dictionary if outcome == "W": - h2h_outcomes[team.team_id][opp.team_id]["wins"] += 1 + h2h_outcomes[team.team_id][opp.team_id]["h2h_wins"] += 1 if outcome == "T": - h2h_outcomes[team.team_id][opp.team_id]["wins"] += 0.5 + h2h_outcomes[team.team_id][opp.team_id]["h2h_wins"] += 0.5 h2h_outcomes[team.team_id][opp.team_id]["h2h_games"] += 1 - # Calculate the head to head record - h2h_record = { - team_data["team_id"]: { - opp_data["team_id"]: ( - h2h_outcomes[team_data["team_id"]][opp_data["team_id"]]["wins"] - / max( - h2h_outcomes[team_data["team_id"]][opp_data["team_id"]][ - "h2h_games" - ], - 1, - ) - ) - for opp_data in team_data_list - } - for team_data in team_data_list - } + # # Calculate the head to head record + # for team_data in team_data_list: + # for opp_data in team_data_list: + # h2h_outcomes[team_data["team_id"]][opp_data["team_id"]]["h2h_record"] = ( + # h2h_outcomes[team_data["team_id"]][opp_data["team_id"]]["h2h_wins"] + # / max( + # h2h_outcomes[team_data["team_id"]][opp_data["team_id"]][ + # "h2h_games" + # ], + # 1, + # ) + # ) - return h2h_record + return h2h_outcomes def sort_by_win_pct(team_data_list: List[Dict]) -> List[Dict]: @@ -126,8 +122,12 @@ def sort_by_head_to_head( # Filter the H2H DataFrame to only include the teams in question h2h_dict = build_h2h_dict(team_data_list) + # Sum the H2H wins against all tied opponents for team_data in team_data_list: - team_data["h2h_wins"] = h2h_dict[team_data["team_id"]]["h2h_wins"] + team_data["h2h_wins"] = sum( + h2h_dict[team_data["team_id"]][opp_id]["h2h_wins"] + for opp_id in h2h_dict.keys() + ) return sorted(team_data_list, key=lambda x: x["h2h_wins"], reverse=True) # If there are more than two teams... @@ -145,7 +145,10 @@ def sort_by_head_to_head( # All teams have played each other an equal number of times # Sort the teams by total H2H wins against each other for team_data in team_data_list: - team_data["h2h_wins"] = h2h_dict[team_data["team_id"]]["h2h_wins"] + team_data["h2h_wins"] = sum( + h2h_dict[team_data["team_id"]][opp_id]["h2h_wins"] + for opp_id in h2h_dict.keys() + ) return sorted(team_data_list, key=lambda x: x["h2h_wins"], reverse=True) else: # All teams have not played each other an equal number of times diff --git a/tests/football/unit/test_league.py b/tests/football/unit/test_league.py index dd664843..1c939402 100644 --- a/tests/football/unit/test_league.py +++ b/tests/football/unit/test_league.py @@ -111,57 +111,70 @@ def test_standings_weekly(self, m): # self.assertEqual(week13_standings, final_standings) @requests_mock.Mocker() - def test_standings_helpers(self, m): + def test_build_h2h_dict(self, m): self.mock_setUp(m) league = League(self.league_id, self.season) - def get_list_of_team_data(league, week): - list_of_team_data = [] - for team in league.teams: - team_data = { - "team": team, - "team_id": team.team_id, - "division_id": team.division_id, - "wins": sum( - [1 for outcome in team.outcomes[:week] if outcome == "W"] - ), - "ties": sum( - [1 for outcome in team.outcomes[:week] if outcome == "T"] - ), - "losses": sum( - [1 for outcome in team.outcomes[:week] if outcome == "L"] - ), - "points_for": sum(team.scores[:week]), - "points_against": sum( - [team.schedule[w].scores[w] for w in range(week)] - ), - "schedule": team.schedule[:week], - "outcomes": team.outcomes[:week], - } - team_data["win_pct"] = ( - team_data["wins"] + team_data["ties"] / 2 - ) / sum( - [ - 1 - for outcome in team.outcomes[:week] - if outcome in ["W", "T", "L"] - ] - ) - list_of_team_data.append(team_data) - return list_of_team_data + # Test build_h2h_dict and build_division_record_dict + # Week 1 + ## Get data for teams 1 and 7 + week1_teams_data = self.get_list_of_team_data(league, 1) + list_of_team_data = [ + team for team in week1_teams_data if team["team_id"] in (1, 7) + ] + h2h_dict = build_h2h_dict(list_of_team_data) + + self.assertEqual(h2h_dict[1][7]["h2h_wins"], 0) # Team 1 is 0/1 vs Team 7 + self.assertEqual(h2h_dict[7][1]["h2h_wins"], 1) # Team 7 is 1/1 vs Team 1 + self.assertEqual(h2h_dict[1][7]["h2h_games"], 1) # Team 1 is 0/1 vs Team 7 + self.assertEqual(h2h_dict[7][1]["h2h_games"], 1) # Team 7 is 1/1 vs Team 1 + + ## Test 3 teams head-to-head + list_of_team_data = [ + team for team in week1_teams_data if team["team_id"] in (1, 2, 3) + ] + h2h_dict = build_h2h_dict(list_of_team_data) + self.assertEqual(h2h_dict[1][2]["h2h_games"], 0) # Teams have not played + self.assertEqual(h2h_dict[1][3]["h2h_games"], 0) # Teams have not played + self.assertEqual(h2h_dict[2][3]["h2h_games"], 0) # Teams have not played + + # Week 10 + ## Get data for teams 1 and 7 + week10_teams_data = self.get_list_of_team_data(league, 10) + list_of_team_data = [ + team for team in week10_teams_data if team["team_id"] in (1, 7) + ] + h2h_dict = build_h2h_dict(list_of_team_data) + + self.assertEqual(h2h_dict[1][7]["h2h_wins"], 1) # Team 1 is 1/2 vs Team 7 + self.assertEqual(h2h_dict[7][1]["h2h_wins"], 1) # Team 7 is 1/2 vs Team 1 + self.assertEqual(h2h_dict[1][7]["h2h_games"], 2) # Team 1 is 0/1 vs Team 7 + self.assertEqual(h2h_dict[7][1]["h2h_games"], 2) # Team 7 is 1/1 vs Team 1 + + # Test 3 teams head-to-head + list_of_team_data = [ + team for team in week10_teams_data if team["team_id"] in (1, 2, 3) + ] + h2h_dict = build_h2h_dict(list_of_team_data) + self.assertEqual(h2h_dict[1][2]["h2h_games"], 1) # Teams have played 1x + self.assertEqual(h2h_dict[1][3]["h2h_games"], 1) # Teams have played 1x + self.assertEqual(h2h_dict[2][3]["h2h_games"], 1) # Teams have played 1x + + @requests_mock.Mocker() + def test_build_division_records_dict(self, m): + self.mock_setUp(m) + + league = League(self.league_id, self.season) # Test build_h2h_dict and build_division_record_dict # Week 1 - get data for teams 1 and 7 - week1_teams_data = get_list_of_team_data(league, 1) + week1_teams_data = self.get_list_of_team_data(league, 1) list_of_team_data = [ team for team in week1_teams_data if team["team_id"] in (1, 7) ] - h2h_dict = build_h2h_dict(list_of_team_data) division_record_dict = build_division_record_dict(list_of_team_data) - self.assertEqual(h2h_dict[1][7], 0) # Team 1 has 0 wins out of 1 against Team 7 - self.assertEqual(h2h_dict[7][1], 1) # Team 7 has 1 win out of 1 against Team 1 self.assertEqual(division_record_dict[1], 0) self.assertEqual( division_record_dict[7], @@ -173,19 +186,12 @@ def get_list_of_team_data(league, week): ) # Week 10 - get data for teams 1 and 7 - week10_teams_data = get_list_of_team_data(league, 10) + week10_teams_data = self.get_list_of_team_data(league, 10) list_of_team_data = [ team for team in week10_teams_data if team["team_id"] in (1, 7) ] - h2h_dict = build_h2h_dict(list_of_team_data) division_record_dict = build_division_record_dict(week10_teams_data) - self.assertEqual( - h2h_dict[1][7], 0.5 - ) # Team 1 has 1 win out of 2 against Team 7 - self.assertEqual( - h2h_dict[7][1], 0.5 - ) # Team 7 has 1 win out of 2 against Team 1 self.assertEqual(division_record_dict[1], 0.6) self.assertEqual( division_record_dict[7], @@ -196,7 +202,18 @@ def get_list_of_team_data(league, week): ][0], ) - # Test sorting functions + @requests_mock.Mocker() + def test_sort_functions(self, m): + self.mock_setUp(m) + + league = League(self.league_id, self.season) + + week10_teams_data = self.get_list_of_team_data(league, 10) + list_of_team_data = [ + team for team in week10_teams_data if team["team_id"] in (1, 2) + ] + division_record_dict = build_division_record_dict(week10_teams_data) + # Assert that sort_by_win_pct is correct sorted_list_of_team_data = sort_by_win_pct(week10_teams_data) for i in range(len(sorted_list_of_team_data) - 1): From bf77794330650e251129a3488b893cd1dfe23616 Mon Sep 17 00:00:00 2001 From: DesiPilla Date: Wed, 20 Dec 2023 15:16:26 -0500 Subject: [PATCH 10/12] fix: test coverage --- tests/football/unit/test_league.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/football/unit/test_league.py b/tests/football/unit/test_league.py index 1c939402..d38604fe 100644 --- a/tests/football/unit/test_league.py +++ b/tests/football/unit/test_league.py @@ -110,6 +110,31 @@ def test_standings_weekly(self, m): # final_standings = [team.team_id for team in league.standings()] # self.assertEqual(week13_standings, final_standings) + def get_list_of_team_data(self, league: League, week: int): + list_of_team_data = [] + for team in league.teams: + team_data = { + "team": team, + "team_id": team.team_id, + "division_id": team.division_id, + "wins": sum([1 for outcome in team.outcomes[:week] if outcome == "W"]), + "ties": sum([1 for outcome in team.outcomes[:week] if outcome == "T"]), + "losses": sum( + [1 for outcome in team.outcomes[:week] if outcome == "L"] + ), + "points_for": sum(team.scores[:week]), + "points_against": sum( + [team.schedule[w].scores[w] for w in range(week)] + ), + "schedule": team.schedule[:week], + "outcomes": team.outcomes[:week], + } + team_data["win_pct"] = (team_data["wins"] + team_data["ties"] / 2) / sum( + [1 for outcome in team.outcomes[:week] if outcome in ["W", "T", "L"]] + ) + list_of_team_data.append(team_data) + return list_of_team_data + @requests_mock.Mocker() def test_build_h2h_dict(self, m): self.mock_setUp(m) From c4603430e2ae972f59a525a03eaa95c6666c5433 Mon Sep 17 00:00:00 2001 From: DesiPilla Date: Wed, 20 Dec 2023 15:31:40 -0500 Subject: [PATCH 11/12] fix: bug fixes & test coverage --- espn_api/football/helper.py | 12 +++++++----- tests/football/unit/test_league.py | 12 ++++++++++-- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/espn_api/football/helper.py b/espn_api/football/helper.py index 0fd55c60..7f8b1a48 100644 --- a/espn_api/football/helper.py +++ b/espn_api/football/helper.py @@ -39,7 +39,9 @@ def build_h2h_dict(team_data_list: List[Dict]) -> Dict: # Create a dictionary with each team's head to head record h2h_outcomes = { team_data["team_id"]: { - opp["team_id"]: {"h2h_wins": 0, "h2h_games": 0} for opp in team_data_list + opp["team_id"]: {"h2h_wins": 0, "h2h_games": 0} + for opp in team_data_list + if opp["team_id"] != team_data["team_id"] } for team_data in team_data_list } @@ -126,7 +128,7 @@ def sort_by_head_to_head( for team_data in team_data_list: team_data["h2h_wins"] = sum( h2h_dict[team_data["team_id"]][opp_id]["h2h_wins"] - for opp_id in h2h_dict.keys() + for opp_id in h2h_dict[team_data["team_id"]].keys() ) return sorted(team_data_list, key=lambda x: x["h2h_wins"], reverse=True) @@ -137,9 +139,9 @@ def sort_by_head_to_head( # Check if the teams have all played each other an equal number of times matchup_counts = [ - h2h_dict[opp]["h2h_games"] - for opp in h2h_dict[team_id].keys() + h2h_dict[team_id][opp_id]["h2h_games"] for team_id in h2h_dict.keys() + for opp_id in h2h_dict[team_id].keys() ] if len(set(matchup_counts)) == 1: # All teams have played each other an equal number of times @@ -147,7 +149,7 @@ def sort_by_head_to_head( for team_data in team_data_list: team_data["h2h_wins"] = sum( h2h_dict[team_data["team_id"]][opp_id]["h2h_wins"] - for opp_id in h2h_dict.keys() + for opp_id in h2h_dict[team_data["team_id"]].keys() ) return sorted(team_data_list, key=lambda x: x["h2h_wins"], reverse=True) else: diff --git a/tests/football/unit/test_league.py b/tests/football/unit/test_league.py index d38604fe..f4f21424 100644 --- a/tests/football/unit/test_league.py +++ b/tests/football/unit/test_league.py @@ -255,12 +255,20 @@ def test_sort_functions(self, m): sorted_list_of_team_data[i + 1]["points_for"], ) - # Assert that sort_by_head_to_head is correct + # Assert that sort_by_head_to_head is correct - 2 teams sorted_list_of_team_data = sort_by_head_to_head( - [team for team in list_of_team_data if team["team_id"] in (1, 2)] + [team for team in week10_teams_data if team["team_id"] in (1, 2)] + ) + self.assertEqual(sorted_list_of_team_data[0]["team_id"], 1) + + # Assert that sort_by_head_to_head is correct - 3 teams + sorted_list_of_team_data = sort_by_head_to_head( + [team for team in week10_teams_data if team["team_id"] in (1, 2, 3)] ) # Team 1 is undefeated vs team 2 self.assertEqual(sorted_list_of_team_data[0]["team_id"], 1) + self.assertEqual(sorted_list_of_team_data[1]["team_id"], 3) + self.assertEqual(sorted_list_of_team_data[2]["team_id"], 2) # Assert that sort_by_division_record is correct sorted_list_of_team_data = sort_by_division_record(week10_teams_data) From 73bab47a456b1ecde386d11967504201d42e0570 Mon Sep 17 00:00:00 2001 From: DesiPilla Date: Wed, 20 Dec 2023 15:43:39 -0500 Subject: [PATCH 12/12] tests: more test coverage --- tests/football/unit/test_league.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/tests/football/unit/test_league.py b/tests/football/unit/test_league.py index f4f21424..e4b589d7 100644 --- a/tests/football/unit/test_league.py +++ b/tests/football/unit/test_league.py @@ -110,6 +110,11 @@ def test_standings_weekly(self, m): # final_standings = [team.team_id for team in league.standings()] # self.assertEqual(week13_standings, final_standings) + # Test invalid playoff seeding rule + with self.assertRaises(Exception): + league.settings.playoff_seed_tie_rule = "NOT_A_REAL_RULE" + league.standings(week=1) + def get_list_of_team_data(self, league: League, week: int): list_of_team_data = [] for team in league.teams: @@ -233,10 +238,8 @@ def test_sort_functions(self, m): league = League(self.league_id, self.season) + week1_teams_data = self.get_list_of_team_data(league, 1) week10_teams_data = self.get_list_of_team_data(league, 10) - list_of_team_data = [ - team for team in week10_teams_data if team["team_id"] in (1, 2) - ] division_record_dict = build_division_record_dict(week10_teams_data) # Assert that sort_by_win_pct is correct @@ -255,21 +258,32 @@ def test_sort_functions(self, m): sorted_list_of_team_data[i + 1]["points_for"], ) + # Assert that sort_by_head_to_head is correct - 1 team + sorted_list_of_team_data = sort_by_head_to_head(week10_teams_data[:1].copy()) + self.assertEqual(sorted_list_of_team_data == week10_teams_data[:1], True) + # Assert that sort_by_head_to_head is correct - 2 teams sorted_list_of_team_data = sort_by_head_to_head( [team for team in week10_teams_data if team["team_id"] in (1, 2)] ) self.assertEqual(sorted_list_of_team_data[0]["team_id"], 1) - # Assert that sort_by_head_to_head is correct - 3 teams + # Assert that sort_by_head_to_head is correct - 3 teams, valid sorted_list_of_team_data = sort_by_head_to_head( [team for team in week10_teams_data if team["team_id"] in (1, 2, 3)] ) - # Team 1 is undefeated vs team 2 self.assertEqual(sorted_list_of_team_data[0]["team_id"], 1) self.assertEqual(sorted_list_of_team_data[1]["team_id"], 3) self.assertEqual(sorted_list_of_team_data[2]["team_id"], 2) + # Assert that sort_by_head_to_head is correct - 3 teams, invalid + sorted_list_of_team_data = sort_by_head_to_head( + [team for team in week1_teams_data if team["team_id"] in (1, 2, 3)] + ) + self.assertEqual(sorted_list_of_team_data[0]["h2h_wins"], 0) + self.assertEqual(sorted_list_of_team_data[1]["h2h_wins"], 0) + self.assertEqual(sorted_list_of_team_data[2]["h2h_wins"], 0) + # Assert that sort_by_division_record is correct sorted_list_of_team_data = sort_by_division_record(week10_teams_data) for i in range(len(sorted_list_of_team_data) - 1):