diff --git a/LICENCE.txt b/LICENCE.txt index df11a96..aea4e02 100644 --- a/LICENCE.txt +++ b/LICENCE.txt @@ -1,19 +1,20 @@ -Copyright (c) 2016 The Python Packaging Authority (PyPA) +Copyright (c) 2012 Pete Schwamb -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 4859826..b29c4e3 100644 --- a/README.md +++ b/README.md @@ -68,3 +68,7 @@ RĂ©mi Coulom in his paper, used w2=14 to get his [results](https://www.remi-coul whr = whole_history_rating.Base({'w2':14}) + +You can also set the base not case sensitive. "shusaku" and "ShUsAkU" will be the same. + + whr = whole_history_rating.Base({'uncased':True}) \ No newline at end of file diff --git a/whr/game.py b/whr/game.py index 59a1193..734cadf 100644 --- a/whr/game.py +++ b/whr/game.py @@ -6,7 +6,7 @@ def __init__(self,black, white, winner, time_step, handicap = 0, extras = None): self.day = time_step self.white_player = white self.black_player = black - self.winner = winner + self.winner = winner.upper() self.handicap = handicap self.handicap_proc = handicap if extras is None: diff --git a/whr/test_whr b/whr/test_whr deleted file mode 100644 index 85031dd..0000000 Binary files a/whr/test_whr and /dev/null differ diff --git a/whr/whole_history_rating.py b/whr/whole_history_rating.py index 54e3312..bee89de 100644 --- a/whr/whole_history_rating.py +++ b/whr/whole_history_rating.py @@ -4,13 +4,14 @@ from collections import defaultdict import time import math +import ast import pickle class UnstableRatingException(Exception): pass class Base: - def __init__(self, config= None): + def __init__(self, config=None): if config is None: self.config = defaultdict(lambda: None) else: @@ -19,11 +20,16 @@ def __init__(self, config= None): self.config["debug"] = False if self.config.get("w2") is None: self.config["w2"] = 300.0 + if self.config.get("uncased") is None: + self.config["uncased"] = False self.games = [] self.players = {} def print_ordered_ratings(self, current=False): """displays all ratings for each player (for each of his playing days) ordered + + Args: + current (bool, optional): True to let only the last estimation of the elo, False gets all estimation for each day played """ players = [x for x in self.players.values() if len(x.days) > 0] players.sort(key=lambda x: x.days[-1].gamma()) @@ -34,7 +40,7 @@ def print_ordered_ratings(self, current=False): else: print(f"{p.name} => {[x.elo() for x in p.days]}") - def get_ordered_ratings(self, current = False, compact = False): + def get_ordered_ratings(self, current=False, compact=False): """gets all ratings for each player (for each of his playing days) ordered Returns: @@ -49,7 +55,9 @@ def get_ordered_ratings(self, current = False, compact = False): players.sort(key=lambda x: x.days[-1].gamma()) for p in players: if len(p.days) > 0: - if current: + if current and compact: + result.append(p.days[-1].elo()) + elif current: result.append((p.name, p.days[-1].elo())) elif compact: result.append([x.elo() for x in p.days]) @@ -59,7 +67,7 @@ def get_ordered_ratings(self, current = False, compact = False): def log_likelihood(self): """gets the likelihood of the current state - + the more iteration you do the higher the likelihood becomes Returns: @@ -71,7 +79,7 @@ def log_likelihood(self): score += p.log_likelihood() return score - def player_by_name(self,name): + def player_by_name(self, name): """gets the player object corresponding to the name Args: @@ -80,26 +88,31 @@ def player_by_name(self,name): Returns: Player: the corresponding player """ + if self.config["uncased"]: + name = name.lower() if self.players.get(name, None) is None: self.players[name] = Player(name, self.config) return self.players[name] - def ratings_for_player(self,name, current = False): + def ratings_for_player(self, name, current = False): """gets all rating for each day played for the player Args: name (str): the player's name + current (bool, optional): True to let only the last estimation of the elo and uncertainty, False gets all estimation for each day played Returns: - list[list[int,float,float]]: for each day, the time_step the elo the uncertainty + list[list[int, float, float]]: for each day, the time_step the elo the uncertainty """ + if self.config["uncased"]: + name = name.lower() player = self.player_by_name(name) if current: return (round(player.days[-1].elo()), round(player.days[-1].uncertainty*100)) else: return [[d.day, round(d.elo()), round(d.uncertainty*100)] for d in player.days] - def _setup_game(self,black,white,winner,time_step,handicap,extras={}): + def _setup_game(self, black, white, winner, time_step, handicap, extras={}): if black == white: raise(AttributeError("Invalid game (black player == white player)")) return None @@ -108,7 +121,7 @@ def _setup_game(self,black,white,winner,time_step,handicap,extras={}): game = Game(black_player, white_player, winner, time_step, handicap, extras) return game - def create_game(self, black, white, winner, time_step, handicap, extras = {}): + def create_game(self, black, white, winner, time_step, handicap, extras={}): """creates a new game to be added to the base Args: @@ -122,6 +135,9 @@ def create_game(self, black, white, winner, time_step, handicap, extras = {}): Returns: Game: the added game """ + if self.config["uncased"]: + black = black.lower() + white = white.lower() game = self._setup_game(black, white, winner, time_step, handicap, extras) return self._add_game(game) @@ -144,8 +160,9 @@ def iterate(self, count): for name, player in self.players.items(): player.update_uncertainty() - def auto_iterate(self, time_limit = 10, precision = 10E-3): - """Summary + def auto_iterate(self, time_limit=10, precision=10E-3): + """iterates automatically until it converges or reaches the time limit + iterates iteratively ten by ten Args: time_limit (int, optional): the maximal time after which no more iteration are launched @@ -168,7 +185,7 @@ def auto_iterate(self, time_limit = 10, precision = 10E-3): return i, False a = b - def _test_stability(self,v1,v2, precision = 10E-3): + def _test_stability(self, v1, v2, precision=10E-3): """tests if two lists of lists of floats are equal but a certain precision Args: @@ -186,21 +203,24 @@ def _test_stability(self,v1,v2, precision = 10E-3): return False return True - def probability_future_match(self, name1, name2, handicap = 0, extras = {}): + def probability_future_match(self, name1, name2, handicap=0, extras={}): """gets the probability of winning for an hypothetical match against name1 and name2 - + displays the probability of winning for name1 and name2 in percent rounded to the second decimal - + Args: - name1 (str): name1's name - name2 (str): name2's name - handicap (int, optional): the handicap (in elo) - extras (dict, optional): extra parameters - + name1 (str): name1's name + name2 (str): name2's name + handicap (int, optional): the handicap (in elo) + extras (dict, optional): extra parameters + Returns: - tuple(int,int): the probability between 0 and 1 for name1 first then name2 + tuple(int, int): the probability between 0 and 1 for name1 first then name2 """ # Avoid self-played games (no info) + if self.config["uncased"]: + name1 = name1.lower() + name2 = name2.lower() if name1 == name2: raise(AttributeError("Invalid game (black == white)")) return None @@ -237,15 +257,16 @@ def load_games(self, games, separator=' '): this function loads all games in the base all match must comply to this format: "black_name white_name winner time_step handicap extras" - black_name is required - white_name is required - winner is B or W is required - time_step is required - handicap is optional (default 0) - extras is a dict {} and optional - + black_name (required) + white_name (required) + winner is B or W (required) + time_step (required) + handicap (optional: default 0) + extras is a dict (optional) + Args: - games (str|list[str]): Description + games (str|list[str]): a path or a list of string representing games + separator (str, optional): the separator between all elements of a game, space by default (every element will be trim eventually) """ data = None if isinstance(games, str): @@ -259,13 +280,17 @@ def load_games(self, games, separator=' '): arguments = [x.strip() for x in line.split(separator)] is_correct = False if len(arguments) == 6: - black, white, winner, time_step, handicap, extras = arguments - extras = eval(extras) - is_correct = True + try: + black, white, winner, time_step, handicap, extras = arguments + extras = last = ast.literal_eval(extras) + if isinstance(extras,dict): + is_correct = True + except Exception as e: + raise(AttributeError(f"the extras argument couldn't be evaluated as a dict: {extras}\n{e}")) if len(arguments) == 5: black, white, winner, time_step, last = arguments try: - eval_last = eval(last) + eval_last = ast.literal_eval(last) if isinstance(eval_last,dict): extras = eval_last is_correct = True @@ -273,13 +298,16 @@ def load_games(self, games, separator=' '): handicap = eval_last is_correct = True except Exception as e: - raise(AttributeError(f"the last argument couldn't be evaluated as an int or a dict: {last}")) + raise(AttributeError(f"the last argument couldn't be evaluated as an int or a dict: {last}\n{e}")) if len(arguments) == 4: black, white, winner, time_step = arguments is_correct = True if not is_correct: - raise(AttributeError(f"loaded game must have this format: 'black_name white_name winner time_step handicap extras' with handicap and extras optional. the handicap|extras argument is: {last}")) + raise(AttributeError(f"loaded game must have this format: 'black_name white_name winner time_step handicap extras' with handicap (int or dict) and extras (dict) optional. the handicap|extras argument is: {last}")) time_step, handicap = int(time_step), int(handicap) + if self.config["uncased"]: + black = black.lower() + white = white.lower() self.create_game(black, white, winner, time_step, handicap, extras=extras) def save_base(self, path): @@ -308,23 +336,72 @@ def load_base(path): if __name__ == "__main__": - whr = Base(config={"w2":14}) + print("test creating the base with modified w2 and uncased") + whr = Base(config={"w2":14, "uncased":True}) + print("Done") + print("-------------------------------------------------------") + print("test creating one game") + whr.create_game("shusaku", "shusai", "B", 4, 0) + print("Done") + print("-------------------------------------------------------") + print("test creating one game with winner uncased (b instead of B)") + whr.create_game("shusaku", "shusai", "w", 5, 0) + print("Done") + print("-------------------------------------------------------") + print("test creating one game with cased letters (ShUsAkU instead of shusaku and ShUsAi instead of shusai)") + game = whr.create_game("ShUsAkU", "ShUsAi", "W", 6, 0) + print("Done") + print("-------------------------------------------------------") + print("test loading several games at once") games = ["shusaku; shusai; B; 1", "shusaku;shusai;W;2;0", " shusaku ; shusai ;W ; 3; {'w2':300}", "shusaku;nobody;B;3;0;{'w2':300}"] - # whr.create_game("shusaku", "shusai", "B", 1, 0) - # whr.create_game("shusaku", "shusai", "W", 2, 0) - # whr.create_game("shusaku", "shusai", "W", 3, 0) - # a = whr.create_game("shusaku", "nobody", "B", 3, 0) - # print(a.bpd) whr.load_games(games, separator=";") + print("Done") + print("-------------------------------------------------------") + print("test auto iterating to get convergence") print(whr.auto_iterate()) + print("Done") + print("-------------------------------------------------------") + print("test getting ratings for player shusaku (day, elo, uncertainty)") print(whr.ratings_for_player("shusaku")) - print(whr.ratings_for_player("shusai")) + print("Done") + print("-------------------------------------------------------") + print("test getting ratings for player shusai, only current elo and uncertainty") + print(whr.ratings_for_player("shusai", current=True)) + print("Done") + print("-------------------------------------------------------") + print("test getting probability of future match between shusaku and nobody2 (which default to 1 win 1 loss)") print(whr.probability_future_match("shusai", "nobody2", 0)) + print("Done") + print("-------------------------------------------------------") + print("test getting log likelihood of base") print(whr.log_likelihood()) + print("Done") + print("-------------------------------------------------------") + print("test printing ordered ratings") whr.print_ordered_ratings() + print("Done") + print("-------------------------------------------------------") + print("test printing ordered ratings, only current elo") whr.print_ordered_ratings(current=True) + print("Done") + print("-------------------------------------------------------") + print("test getting ordered ratings, compact form") + print(whr.get_ordered_ratings(compact=True)) + print("Done") + print("-------------------------------------------------------") + print("test getting ordered ratings, only current elo with compact form") + print(whr.get_ordered_ratings(compact=True, current=True)) + print("Done") + print("-------------------------------------------------------") + print("test saving base") Base.save_base(whr,"test_whr") + print("Done") + print("-------------------------------------------------------") + print("test loading base") whr2 = Base.load_base('test_whr') + print("Done") + print("-------------------------------------------------------") + print("test inspecting the first game") print(whr2.games[0].inspect()) - - + print("Done") + print("-------------------------------------------------------")