Module rating.manager

Classes

class Manager (rating_system: RatingSystem = None, player_database: PlayerDatabase = None, game_database: GameDatabase = None, tournament_database: TournamentDatabase = None, stat_manager: StatManager = None, rating_period: RatingPeriod = None, rating_period_type: int = 0, custom_timedelta: datetime.timedelta = datetime.timedelta(days=7), last_date_update: datetime.datetime = None, do_recompute: bool = True, recompute: bool = False, add_home_advantage: bool = True, forfeit_keep_points: bool = True)

The Manager class is responsible for managing the player database, game database, tournament database, rating system, and the statistics manager.

Attributes

  • rating_system (RatingSystem): The rating system to use.
  • player_database (PlayerDatabase): The player database.
  • game_database (GameDatabase): The game database.
  • tournament_database (TournamentDatabase): The tournament database.
  • stat_manager (StatManager): The statistics manager.
  • rating_period (RatingPeriod): The rating period.
  • rating_period_type (RatingPeriodEnum): The rating period type.
  • custom_timedelta (timedelta): The custom time delta for rating periods.
  • last_date_update (datetime): The last date of rating update.
  • do_recompute (bool): Whether to recompute ratings if games/players/tournaments are removed/added before the last update.
  • recompute (bool): Whether to recompute ratings.
  • add_home_advantage (bool): Whether to add a home advantage to the games when added.
  • forfeit_keep_points (bool, optional): If True, points associated with each player are counted as the given points in case of a forfeit. This allows for custom match results that do not fit the normal points system.

Args

  • rating_system (RatingSystem, optional): The rating system to use. Defaults to PolyratingCrossEntropy().
  • player_database (PlayerDatabase, optional): The player database. Defaults to PlayerDatabase().
  • game_database (GameDatabase, optional): The game database. Defaults to GameDatabase().
  • tournament_database (TournamentDatabase, optional): The tournament database. Defaults to TournamentDatabase().
  • stat_manager (StatManager, optional): The statistics manager. Defaults to StatManager().
  • rating_period (RatingPeriod, optional): The rating period. Defaults to RatingPeriod().
  • rating_period_type (RatingPeriodEnum, optional): The rating period type. Defaults to RatingPeriodEnum.TOURNAMENT.
  • custom_timedelta (timedelta, optional): The custom time delta for rating periods. Defaults to timedelta(days=7).
  • last_date_update (datetime, optional): The last date of rating update. Defaults to None.
  • recompute (bool, optional): Whether to recompute ratings. Defaults to False.
  • do_recompute (bool): Whether to recompute ratings if games/players/tournaments are removed/added before the last update.
  • add_home_advantage (bool, optional): Whether to add a home advantage to the games when added. Defaults to True.
  • forfeit_keep_points (bool, optional): If True, points associated with each player are counted as the given points in case of a forfeit. This allows for custom match results that do not fit the normal points system.
Expand source code
class Manager(BaseClass):
    def __init__(self, rating_system : RatingSystem = None, 
                 player_database : PlayerDatabase = None, 
                 game_database : GameDatabase = None, 
                 tournament_database : TournamentDatabase = None, 
                 stat_manager : StatManager = None, 
                 rating_period : RatingPeriod = None, 
                 rating_period_type : int = RatingPeriodEnum.TOURNAMENT, 
                 custom_timedelta : timedelta = timedelta(days=7), 
                 last_date_update : datetime = None, do_recompute : bool = True, 
                 recompute : bool = False, 
                 add_home_advantage : bool = True, 
                 forfeit_keep_points : bool = True) -> 'Manager':
        """
        The Manager class is responsible for managing the player database, game database, tournament database,
        rating system, and the statistics manager.

        Attributes:
            - rating_system (RatingSystem): The rating system to use.
            - player_database (PlayerDatabase): The player database.
            - game_database (GameDatabase): The game database.
            - tournament_database (TournamentDatabase): The tournament database.
            - stat_manager (StatManager): The statistics manager.
            - rating_period (RatingPeriod): The rating period.
            - rating_period_type (RatingPeriodEnum): The rating period type.
            - custom_timedelta (timedelta): The custom time delta for rating periods.
            - last_date_update (datetime): The last date of rating update.
            - do_recompute (bool): Whether to recompute ratings if games/players/tournaments are removed/added before the last update.
            - recompute (bool): Whether to recompute ratings.
            - add_home_advantage (bool): Whether to add a home advantage to the games when added.
            - forfeit_keep_points (bool, optional): If True, points associated with each player are counted as the given points in case of a forfeit. This allows for custom match results that do not fit the normal points system.

        Args:
            - rating_system (RatingSystem, optional): The rating system to use. Defaults to PolyratingCrossEntropy().
            - player_database (PlayerDatabase, optional): The player database. Defaults to PlayerDatabase().
            - game_database (GameDatabase, optional): The game database. Defaults to GameDatabase().
            - tournament_database (TournamentDatabase, optional): The tournament database. Defaults to TournamentDatabase().
            - stat_manager (StatManager, optional): The statistics manager. Defaults to StatManager().
            - rating_period (RatingPeriod, optional): The rating period. Defaults to RatingPeriod().
            - rating_period_type (RatingPeriodEnum, optional): The rating period type. Defaults to RatingPeriodEnum.TOURNAMENT.
            - custom_timedelta (timedelta, optional): The custom time delta for rating periods. Defaults to timedelta(days=7).
            - last_date_update (datetime, optional): The last date of rating update. Defaults to None.
            - recompute (bool, optional): Whether to recompute ratings. Defaults to False.
            - do_recompute (bool): Whether to recompute ratings if games/players/tournaments are removed/added before the last update.
            - add_home_advantage (bool, optional): Whether to add a home advantage to the games when added. Defaults to True.
            - forfeit_keep_points (bool, optional): If True, points associated with each player are counted as the given points in case of a forfeit. This allows for custom match results that do not fit the normal points system.
        """
        if rating_system is None:
            rating_system = PolyratingCrossEntropy(
                linearized=10,
                epsilon=1e-2
            )
        if player_database is None:
            player_database = PlayerDatabase()
        if game_database is None:
            game_database = GameDatabase()
        if tournament_database is None:
            tournament_database = TournamentDatabase()
        if stat_manager is None:
            stat_manager = StatManager()
        if rating_period is None:
            rating_period = RatingPeriod()

        super().__init__(rating_system=rating_system, player_database=player_database,
                         game_database=game_database, tournament_database=tournament_database, 
                         stat_manager=stat_manager, rating_period=rating_period,
                         rating_period_type=rating_period_type, custom_timedelta=custom_timedelta, 
                         last_date_update=last_date_update, recompute=recompute, do_recompute=do_recompute, 
                         add_home_advantage=add_home_advantage, forfeit_keep_points=forfeit_keep_points)
        
    def generate_settings(self) -> dict:
        """
        Generate the settings dictionary for the manager.

        Returns:
            - dict: The generated settings dictionary.
        """
        settings = super().generate_settings()
        settings['custom_timedelta'] = self.custom_timedelta.total_seconds()
        settings['last_date_update'] = None
        if self.last_date_update is not None:
            settings['last_date_update'] = self.last_date_update.strftime("%Y-%m-%d - %H:%M:%S")
        return settings
    
    def clone(self) -> 'Manager':
        """
        Clone the manager.

        Returns:
            - Manager: The cloned manager.
        """
        return Manager.load_from_settings(self.generate_settings())
    
    def reset_and_recompute(self, rating_system : RatingSystem = None, rating_period_type : int = None, custom_timedelta : timedelta = None):
        """
        Reset the manager and recompute the ratings.

        Args:
            - rating_system (RatingSystem): The rating system to use. Defaults to None.
            - rating_period_type (int, optional): The rating period type. Defaults to None.
            - custom_timedelta (timedelta, optional): The custom time delta for rating periods. Defaults to None.
        """
        if rating_system is not None:
            self.rating_system = rating_system
        if custom_timedelta is not None:
            self.custom_timedelta = custom_timedelta
        if rating_period_type is not None:
            self.rating_period_type = rating_period_type
            if self.rating_period_type == RatingPeriodEnum.TOURNAMENT:
                self.rating_period = RatingPeriod()
                for tournament in self.tournament_database:
                    self.rating_period.trigger_new_period(tournament.get_date())
            elif self.rating_period_type == RatingPeriodEnum.TIMEDELTA:
                self.rating_period = RatingPeriod()
                self.rating_period.trigger_new_period(self.game_database.get_earliest_date())
        
        self.recompute = True
        self.update_rating()
    
    @classmethod
    def load_from_settings(cls, settings : dict) -> 'Manager':
        """
        Load the rating manager from the given settings.

        Args:
            - cls (class): The class of the rating manager.
            - settings (dict): The settings to load from.

        Returns:
            - Manager: An instance of the rating manager.

        """
        kwargs = super().get_input_parameters(settings)
        kwargs['custom_timedelta'] = timedelta(seconds=kwargs['custom_timedelta'])
        if kwargs['last_date_update'] is not None:
            kwargs['last_date_update'] = datetime.strptime(kwargs['last_date_update'], "%Y-%m-%d - %H:%M:%S")
        return cls(**kwargs)
        
    def trigger_new_period(self, tournament : Tournament = None):
        """
        Triggers a new rating period based on the specified tournament or timedelta.

        Args:
            - tournament (Tournament, optional): The tournament object representing the new period. Defaults to None.
        """
        if self.rating_period_type == RatingPeriodEnum.TOURNAMENT and tournament is not None:
            self.rating_period.trigger_new_period(tournament.get_date())
        elif self.rating_period_type == RatingPeriodEnum.TIMEDELTA:
            if self.last_date_update is None or len(self.rating_period) == 0:
                self.rating_period.trigger_new_period(self.game_database.get_earliest_date() + self.custom_timedelta)
            
            while self.rating_period.get_last_period() < self.game_database.get_latest_date():
                self.rating_period.trigger_new_period(self.rating_period.get_last_period() + self.custom_timedelta)
        else:
            self.rating_period.trigger_new_period(datetime.now())

    def compute_statistics(self, tournament : Tournament = None, 
                           data_folder : str = "data", 
                           history_folder : str = "history", 
                           tournament_folder : str = "tournaments"):
        """
        Computes tournament results and statistics.

        Args:
            - tournament (Tournament): The tournament object.
            - data_folder (str, optional): The folder where the data is stored. Defaults to "data".
            - history_folder (str, optional): The folder where the historical tournament data is stored. Defaults to "history".
            - tournament_folder (str, optional): The folder where the tournament data is stored. Defaults to "tournaments".
        """
        logger.info("Computing tournament results...")
        if tournament is not None:
            tournament.compute_tournament_results(self.game_database, self.player_database, self.rating_system)

        logger.info("Computing statistics...")
        latest_date_str = self.last_date_update.strftime("%Y_%m_%d") if self.last_date_update is not None else "no_date"
        self.stat_manager.run(self.player_database, self.game_database, self.tournament_database, self.rating_system,
                              data_folder, history_folder, tournament_folder, latest_date_str)

    def update_rating(self):
        """
        Perform one iteration of the rating update process.
        """
        logger.info("Updating ratings...")
        
        if len(self.rating_period) == 0:
            logger.info("No rating period set. Automatically triggering a new period.")
            self.trigger_new_period()
        if self.recompute:
            for player in self.player_database:
                player.clear_rating_history()
                player.get_rating().reset()
                self.last_date_update = None
            self.recompute = False
        for period_dates in self.rating_period.iterate_periods(self.last_date_update):
            logger.info(f"Updating ratings for period {period_dates[-1]}...")
            if (len(period_dates) == 1 and self.game_database.get_n_games_between_dates(period_dates[0]) == 0):
                continue
            elif len(period_dates) > 1 and self.game_database.get_n_games_between_dates(period_dates[-1], period_dates[-2]) == 0:
                continue
            self.rating_system.period_update(self.player_database, self.game_database, period_dates)
            for player in self.player_database:
                player.store_rating(period_dates[-1])
        self.last_date_update = self.rating_period[-1]

    def add_tournament(self, tournament_path : str = None, tournament_name : str = None, force : bool = False, tournament : Tournament = None) -> Tournament:
        """
        Adds a tournament to the rating manager. Also computes updated ratings and statistics.

        Args:
            - tournament_path (str): The path to the tournament file. Defaults to None.
            - tournament_name (str, optional): The name of the tournament. If not provided, the name will be extracted from the file.
            - force (bool, optional): If set to True, allows adding a tournament with the same name as an existing one. Defaults to False.
            - tournament (Tournament, optional): The tournament object to add. Defaults to None.
        """
        logger.info(f"Adding tournament from {tournament_path}...")
        if tournament is None:
            tournament = extract_tournament(tournament_path)
            if tournament_name is not None:
                tournament.name = tournament_name
        if tournament is None:
            raise ValueError(f"No tournament found in {tournament_path}.")
        
        if self.tournament_database.check_duplicate(tournament) and not force:
            raise ValueError(f"Tournament {tournament.name} already exists in the database. If you think this is a mistake, use the force option to add as a new tournament.")
        
        self.tournament_database.add(tournament)

        if tournament_path is not None:
            logger.info(f"Extracting players from {tournament_path}")
            players, tie_breaks, tie_break_names = extract_players(tournament_path)
            for player in players:
                existing_player = self.player_database.get_player_by_name(player.name)
                if existing_player is None:
                    self.player_database.add(player)
                else:
                    tie_breaks[existing_player.id] = tie_breaks[player.id]

            tournament.set_tie_breaks(tie_breaks, tie_break_names)

            logger.info(f"Extracting games from {tournament_path}")
            extract_games(tournament_path, tournament, self.game_database, self.player_database, 
                          add_home_advantage=self.add_home_advantage)
        self.player_database.clear_empty(self.game_database)
        self.trigger_new_period(tournament)
        was_false = not self.recompute
        self.recompute = self.last_date_update is not None and tournament.get_date() <= self.last_date_update and self.do_recompute
        if self.recompute and was_false:
            logger.warning(f"You have added a tournament from {tournament.get_date()} which is earlier than the last tournament in the rating period. Next recomputation of the ratings will need a full recompute.")
        return tournament
    
    def remove_tournament(self, tournament_name : str = None, tournament : Tournament = None):
        """
        Remove a tournament from the rating manager. Also removes all games associated with the tournament, and players that only have played in that tournament.

        Args:
            - tournament_name (str): The name of the tournament to remove. Defaults to None.
            - tournament (Tournament, optional): The tournament object to remove. Defaults to None.
        """
        if tournament is None:
            tournament = self.tournament_database.get_tournament_by_name(tournament_name)
        if tournament is None:
            raise ValueError(f"Tournament {tournament_name} not found in the database.")
        for game in self.game_database.get_games_per_tournament(tournament.id):
            if self.last_date_update is not None and game.get_date() < self.last_date_update and self.do_recompute:
                self.recompute = True
                logger.warning(f"You have removed a game from {game.get_date()} which is earlier than the last game in the rating period. Next recomputation of the ratings will need a full recompute.")
            self.game_database.remove(game)
        self.tournament_database.remove(tournament)
        self.player_database.clear_empty(self.game_database)

    def remove_player(self, player_name : str = None, player : Player = None):
        """
        Remove a player from the player database. Also removes all games associated with the player.

        Args:
            - player_name (str): The name of the player to remove. Defaults to None.
            - player (Player, optional): The player object to remove. Defaults to None.
        """
        if player is None:
            player = self.player_database.get_player_by_name(player_name)
        if player is None:
            raise ValueError(f"Player {player_name} not found in the database.")
        for game in self.game_database.get_games_per_player(player.id):
            if self.last_date_update is not None and game.get_date() < self.last_date_update and self.do_recompute:
                self.recompute = True
                logger.warning(f"You have removed a game from {game.get_date()} which is earlier than the last game in the rating period. Next recomputation of the ratings will need a full recompute.")
            self.game_database.remove(self.game_database[game.id])
        self.player_database.remove(player)

    def add_player(self, player_name : str = None, player : Player = None) -> Player:
        """
        Add a player to the player database.

        Args:
            - player_name (str): The name of the player to add.
            - player (Player, optional): The player object to add. Defaults to None.

        Returns:
            - Player: The player object.
        """
        if player is None:
            player = self.player_database.get_player_by_name(player_name)
            if player is None:
                player = Player(player_name)
                self.player_database.add(player)
        else:
            self.player_database.add(player)
        return player

    def remove_game(self, game_id : int = None, game : Game = None):
        """
        Remove a game from the game database.

        Args:
            - game_id (int): The ID of the game to remove. Defaults to None.
            - game (Game, optional): The game object to remove. Defaults to None.
        """
        if game is None:
            game = self.game_database[game_id]
        if game is None:
            raise ValueError(f"Game {game_id} not found in the database.")
        if self.last_date_update is not None and game.get_date() < self.last_date_update and self.do_recompute:
            self.recompute = True
            logger.warning(f"You have removed a game from {game.get_date()} which is earlier than the last game in the rating period. Next recomputation of the ratings will need a full recompute.")
        self.game_database.remove(game)

    def add_game(self, home_name : str = None, out_name : str = None, result_str : str = None, 
                 date : datetime = None, tournament_id : int = None, 
                 allow_new_players : bool = True, force_new_players : bool = False, 
                 game : Game = None) -> Game:
        """
        Adds a game to the manager's databases.

        Args:
            - home_name (str): The name of the home player.
            - out_name (str): The name of the out player.
            - result_str (str): The result of the game. Either 1-0, 0-1, 1/2-1/2, 1F-0, 0-1F, 0F-0F.
            - date (datetime, optional): The date of the game. Defaults to None.
            - tournament_id (int, optional): The ID of the tournament. Defaults to None.
            - allow_new_players (bool, optional): If set to True, allows adding new players to the database. Defaults to True.
            - force_new_players (bool, optional): If set to True, forces that the players are new players. Defaults to False.
            - game (Game, optional): The game object to add. Defaults to None.
        Returns:
            - Game: The game object.
        """
        if game is not None:
            self.game_database.add(game)
            return game
        logger.debug(f"Adding game {home_name} vs {out_name} with result {result_str}...")
        home = self.player_database.get_player_by_name(home_name)
        out = self.player_database.get_player_by_name(out_name)
        if home is None:
            if self.player_database.get_player_by_name(home_name) is None:
                if not allow_new_players:
                    raise ValueError(f"Player {home_name} not found in the database.")
                home = Player(home_name)
                self.player_database.add(home)
            elif force_new_players:
                raise ValueError(f"Player {home_name} found in the database.")
        if out is None:
            if self.player_database.get_player_by_name(out_name) is None:
                if not allow_new_players:
                    raise ValueError(f"Player {out_name} not found in the database.")
                out = Player(out_name)
                self.player_database.add(out)
            elif force_new_players:
                raise ValueError(f"Player {out_name} found in the database.")
        game = Game(home=home.id, out=out.id, result=result_str, date=date, tournament_id=tournament_id,
                    add_home_advantage=self.add_home_advantage, forfeit_keep_points=self.forfeit_keep_points)
        self.game_database.add(game)
        was_false = not self.recompute
        self.recompute = self.last_date_update is not None and game.get_date() <= self.last_date_update and self.do_recompute
        if self.recompute and was_false:
            logger.warning(f"You have added a game from {game.get_date()} which is earlier than the last game in the rating period. Next recomputation of the ratings will need a full recompute.")
        return game

Ancestors

Static methods

def load_from_settings(settings: dict) ‑> Manager

Load the rating manager from the given settings.

Args

  • cls (class): The class of the rating manager.
  • settings (dict): The settings to load from.

Returns

  • Manager: An instance of the rating manager.

Methods

def add_game(self, home_name: str = None, out_name: str = None, result_str: str = None, date: datetime.datetime = None, tournament_id: int = None, allow_new_players: bool = True, force_new_players: bool = False, game: Game = None) ‑> Game

Adds a game to the manager's databases.

Args

  • home_name (str): The name of the home player.
  • out_name (str): The name of the out player.
  • result_str (str): The result of the game. Either 1-0, 0-1, 1/2-1/2, 1F-0, 0-1F, 0F-0F.
  • date (datetime, optional): The date of the game. Defaults to None.
  • tournament_id (int, optional): The ID of the tournament. Defaults to None.
  • allow_new_players (bool, optional): If set to True, allows adding new players to the database. Defaults to True.
  • force_new_players (bool, optional): If set to True, forces that the players are new players. Defaults to False.
  • game (Game, optional): The game object to add. Defaults to None.

Returns

  • Game: The game object.
def add_player(self, player_name: str = None, player: Player = None) ‑> Player

Add a player to the player database.

Args

  • player_name (str): The name of the player to add.
  • player (Player, optional): The player object to add. Defaults to None.

Returns

  • Player: The player object.
def add_tournament(self, tournament_path: str = None, tournament_name: str = None, force: bool = False, tournament: Tournament = None) ‑> Tournament

Adds a tournament to the rating manager. Also computes updated ratings and statistics.

Args

  • tournament_path (str): The path to the tournament file. Defaults to None.
  • tournament_name (str, optional): The name of the tournament. If not provided, the name will be extracted from the file.
  • force (bool, optional): If set to True, allows adding a tournament with the same name as an existing one. Defaults to False.
  • tournament (Tournament, optional): The tournament object to add. Defaults to None.
def clone(self) ‑> Manager

Clone the manager.

Returns

  • Manager: The cloned manager.
def compute_statistics(self, tournament: Tournament = None, data_folder: str = 'data', history_folder: str = 'history', tournament_folder: str = 'tournaments')

Computes tournament results and statistics.

Args

  • tournament (Tournament): The tournament object.
  • data_folder (str, optional): The folder where the data is stored. Defaults to "data".
  • history_folder (str, optional): The folder where the historical tournament data is stored. Defaults to "history".
  • tournament_folder (str, optional): The folder where the tournament data is stored. Defaults to "tournaments".
def generate_settings(self) ‑> dict

Generate the settings dictionary for the manager.

Returns

  • dict: The generated settings dictionary.
def remove_game(self, game_id: int = None, game: Game = None)

Remove a game from the game database.

Args

  • game_id (int): The ID of the game to remove. Defaults to None.
  • game (Game, optional): The game object to remove. Defaults to None.
def remove_player(self, player_name: str = None, player: Player = None)

Remove a player from the player database. Also removes all games associated with the player.

Args

  • player_name (str): The name of the player to remove. Defaults to None.
  • player (Player, optional): The player object to remove. Defaults to None.
def remove_tournament(self, tournament_name: str = None, tournament: Tournament = None)

Remove a tournament from the rating manager. Also removes all games associated with the tournament, and players that only have played in that tournament.

Args

  • tournament_name (str): The name of the tournament to remove. Defaults to None.
  • tournament (Tournament, optional): The tournament object to remove. Defaults to None.
def reset_and_recompute(self, rating_system: RatingSystem = None, rating_period_type: int = None, custom_timedelta: datetime.timedelta = None)

Reset the manager and recompute the ratings.

Args

  • rating_system (RatingSystem): The rating system to use. Defaults to None.
  • rating_period_type (int, optional): The rating period type. Defaults to None.
  • custom_timedelta (timedelta, optional): The custom time delta for rating periods. Defaults to None.
def trigger_new_period(self, tournament: Tournament = None)

Triggers a new rating period based on the specified tournament or timedelta.

Args

  • tournament (Tournament, optional): The tournament object representing the new period. Defaults to None.
def update_rating(self)

Perform one iteration of the rating update process.

Inherited members