diff --git a/analysis/analyze_glicko2_daily_windows.py b/analysis/analyze_glicko2_daily_windows.py index 00e87cd..7b846c7 100755 --- a/analysis/analyze_glicko2_daily_windows.py +++ b/analysis/analyze_glicko2_daily_windows.py @@ -45,7 +45,9 @@ def process_game(self, game: GameRecord) -> Glicko2Analytics: black_base, [ ( - opponent.copy((1 if past_game.black_id != game.black_id else -1) * get_handicap_adjustment(opponent.rating, past_game.handicap)), + opponent.copy((1 if past_game.black_id != game.black_id else -1) * get_handicap_adjustment(opponent.rating, past_game.handicap, + komi=past_game.komi, size=past_game.size, rules=past_game.rules, + )), past_game.winner_id == game.black_id ) for past_game, opponent in self._storage.get_matches_newer_or_equal_to( @@ -58,7 +60,9 @@ def process_game(self, game: GameRecord) -> Glicko2Analytics: white_base, [ ( - opponent.copy((1 if past_game.black_id != game.white_id else -1) * get_handicap_adjustment(opponent.rating, past_game.handicap)), + opponent.copy((1 if past_game.black_id != game.white_id else -1) * get_handicap_adjustment(opponent.rating, past_game.handicap, + komi=past_game.komi, size=past_game.size, rules=past_game.rules, + )), past_game.winner_id == game.white_id ) for past_game, opponent in self._storage.get_matches_newer_or_equal_to( @@ -76,7 +80,9 @@ def process_game(self, game: GameRecord) -> Glicko2Analytics: skipped=False, game=game, expected_win_rate=black_cur.expected_win_probability( - white_cur, get_handicap_adjustment(black_cur.rating, game.handicap), ignore_g=True + white_cur, get_handicap_adjustment(black_cur.rating, game.handicap, + komi=game.komi, size=game.size, rules=game.rules, + ), ignore_g=True ), black_rating=black_cur.rating, white_rating=white_cur.rating, diff --git a/analysis/analyze_glicko2_glickman_weekly_window.py b/analysis/analyze_glicko2_glickman_weekly_window.py index 742c6bd..bc53115 100755 --- a/analysis/analyze_glicko2_glickman_weekly_window.py +++ b/analysis/analyze_glicko2_glickman_weekly_window.py @@ -53,7 +53,9 @@ def process_game(self, game: GameRecord) -> Glicko2Analytics: black_base, [ ( - opponent.copy((1 if past_game.black_id != game.black_id else -1) * get_handicap_adjustment(opponent.rating, past_game.handicap)), + opponent.copy((1 if past_game.black_id != game.black_id else -1) * get_handicap_adjustment(opponent.rating, past_game.handicap, + komi=past_game.komi, size=past_game.size, rules=past_game.rules, + )), past_game.winner_id == past_game.black_id ) for past_game, opponent in self._storage.get_matches_newer_or_equal_to( @@ -66,7 +68,9 @@ def process_game(self, game: GameRecord) -> Glicko2Analytics: white_base, [ ( - opponent.copy((1 if past_game.black_id != game.white_id else -1) * get_handicap_adjustment(opponent.rating, past_game.handicap)), + opponent.copy((1 if past_game.black_id != game.white_id else -1) * get_handicap_adjustment(opponent.rating, past_game.handicap, + komi=past_game.komi, size=past_game.size, rules=past_game.rules, + )), past_game.winner_id == past_game.white_id ) for past_game, opponent in self._storage.get_matches_newer_or_equal_to( @@ -84,7 +88,9 @@ def process_game(self, game: GameRecord) -> Glicko2Analytics: skipped=False, game=game, expected_win_rate=black_cur.expected_win_probability( - white_cur, get_handicap_adjustment(black_cur.rating, game.handicap), ignore_g=True + white_cur, get_handicap_adjustment(black_cur.rating, game.handicap, + komi=game.komi, size=game.size, rules=game.rules, + ), ignore_g=True ), black_rating=black_cur.rating, white_rating=white_cur.rating, diff --git a/analysis/analyze_glicko2_one_game_at_a_time.py b/analysis/analyze_glicko2_one_game_at_a_time.py index fcc3ef9..40dbe58 100755 --- a/analysis/analyze_glicko2_one_game_at_a_time.py +++ b/analysis/analyze_glicko2_one_game_at_a_time.py @@ -35,12 +35,14 @@ def process_game(self, game: GameRecord) -> Glicko2Analytics: black = self._storage.get(game.black_id) white = self._storage.get(game.white_id) - updated_black = glicko2_update( black, [ ( - white.copy(-get_handicap_adjustment(white.rating, game.handicap)), + white.copy(-get_handicap_adjustment(white.rating, game.handicap, + komi=game.komi, size=game.size, + rules=game.rules, + )), game.winner_id == game.black_id, ) ], @@ -50,7 +52,10 @@ def process_game(self, game: GameRecord) -> Glicko2Analytics: white, [ ( - black.copy(get_handicap_adjustment(black.rating, game.handicap)), + black.copy(get_handicap_adjustment(black.rating, game.handicap, + komi=game.komi, size=game.size, + rules=game.rules, + )), game.winner_id == game.white_id, ) ], @@ -65,7 +70,10 @@ def process_game(self, game: GameRecord) -> Glicko2Analytics: skipped=False, game=game, expected_win_rate=black.expected_win_probability( - white, get_handicap_adjustment(black.rating, game.handicap), ignore_g=True + white, get_handicap_adjustment(black.rating, game.handicap, + komi=game.komi, size=game.size, + rules=game.rules, + ), ignore_g=True ), black_rating=black.rating, white_rating=white.rating, diff --git a/analysis/analyze_glicko2_one_game_at_a_time_rating_grid.py b/analysis/analyze_glicko2_one_game_at_a_time_rating_grid.py index 5302db4..eb9a2d0 100755 --- a/analysis/analyze_glicko2_one_game_at_a_time_rating_grid.py +++ b/analysis/analyze_glicko2_one_game_at_a_time_rating_grid.py @@ -63,7 +63,9 @@ def process_game(self, game: GameRecord) -> Dict[str, Glicko2Analytics]: black, [ ( - src_white.copy(-get_handicap_adjustment(src_white.rating, game.handicap)), + src_white.copy(-get_handicap_adjustment(src_white.rating, game.handicap, + komi=game.komi, size=game.size, rules=game.rules, + )), game.winner_id == game.black_id, ) ], @@ -73,7 +75,9 @@ def process_game(self, game: GameRecord) -> Dict[str, Glicko2Analytics]: white, [ ( - src_black.copy(get_handicap_adjustment(src_black.rating, game.handicap)), + src_black.copy(get_handicap_adjustment(src_black.rating, game.handicap, + komi=game.komi, size=game.size, rules=game.rules, + )), game.winner_id == game.white_id, ) ], @@ -89,7 +93,9 @@ def process_game(self, game: GameRecord) -> Dict[str, Glicko2Analytics]: skipped=False, game=game, expected_win_rate=black.expected_win_probability( - white, get_handicap_adjustment(black.rating, game.handicap), ignore_g=True + white, get_handicap_adjustment(black.rating, game.handicap, + komi=game.komi, size=game.size, rules=game.rules, + ), ignore_g=True ), black_rating=black.rating, white_rating=white.rating, diff --git a/analysis/analyze_glicko2_weekly_window_no_unxepected_changes.py b/analysis/analyze_glicko2_weekly_window_no_unxepected_changes.py index ce13db8..740d781 100755 --- a/analysis/analyze_glicko2_weekly_window_no_unxepected_changes.py +++ b/analysis/analyze_glicko2_weekly_window_no_unxepected_changes.py @@ -50,7 +50,9 @@ def process_game(self, game: GameRecord) -> Glicko2Analytics: black_base, [ ( - opponent.copy((1 if past_game.black_id != game.black_id else -1) * get_handicap_adjustment(opponent.rating, past_game.handicap)), + opponent.copy((1 if past_game.black_id != game.black_id else -1) * get_handicap_adjustment(opponent.rating, past_game.handicap, + komi=past_game.komi, size=past_game.size, rules=past_game.rules, + )), past_game.winner_id == past_game.black_id ) for past_game, opponent in self._storage.get_matches_newer_or_equal_to( @@ -63,7 +65,9 @@ def process_game(self, game: GameRecord) -> Glicko2Analytics: white_base, [ ( - opponent.copy((1 if past_game.black_id != game.white_id else -1) * get_handicap_adjustment(opponent.rating, past_game.handicap)), + opponent.copy((1 if past_game.black_id != game.white_id else -1) * get_handicap_adjustment(opponent.rating, past_game.handicap, + komi=past_game.komi, size=past_game.size, rules=past_game.rules, + )), past_game.winner_id == past_game.white_id ) for past_game, opponent in self._storage.get_matches_newer_or_equal_to( @@ -96,7 +100,9 @@ def process_game(self, game: GameRecord) -> Glicko2Analytics: skipped=False, game=game, expected_win_rate=black_cur.expected_win_probability( - white_cur, get_handicap_adjustment(black_cur.rating, game.handicap), ignore_g=True + white_cur, get_handicap_adjustment(black_cur.rating, game.handicap, + komi=game.komi, size=game.size, rules=game.rules, + ), ignore_g=True ), black_rating=black_cur.rating, white_rating=white_cur.rating, diff --git a/analysis/analyze_glicko2_weekly_window_reduce_rating_movement.py b/analysis/analyze_glicko2_weekly_window_reduce_rating_movement.py index e5ee645..072510c 100755 --- a/analysis/analyze_glicko2_weekly_window_reduce_rating_movement.py +++ b/analysis/analyze_glicko2_weekly_window_reduce_rating_movement.py @@ -55,7 +55,9 @@ def process_game(self, game: GameRecord) -> Glicko2Analytics: black_base, [ ( - opponent.copy((1 if past_game.black_id != game.black_id else -1) * get_handicap_adjustment(opponent.rating, past_game.handicap)), + opponent.copy((1 if past_game.black_id != game.black_id else -1) * get_handicap_adjustment(opponent.rating, past_game.handicap, + komi=past_game.komi, size=past_game.size, rules=past_game.rules, + )), past_game.winner_id == past_game.black_id ) for past_game, opponent in self._storage.get_matches_newer_or_equal_to( @@ -68,7 +70,9 @@ def process_game(self, game: GameRecord) -> Glicko2Analytics: white_base, [ ( - opponent.copy((1 if past_game.black_id != game.white_id else -1) * get_handicap_adjustment(opponent.rating, past_game.handicap)), + opponent.copy((1 if past_game.black_id != game.white_id else -1) * get_handicap_adjustment(opponent.rating, past_game.handicap, + komi=past_game.komi, size=past_game.size, rules=past_game.rules, + )), past_game.winner_id == past_game.white_id ) for past_game, opponent in self._storage.get_matches_newer_or_equal_to( @@ -112,7 +116,9 @@ def process_game(self, game: GameRecord) -> Glicko2Analytics: skipped=False, game=game, expected_win_rate=black_cur.expected_win_probability( - white_cur, get_handicap_adjustment(black_cur.rating, game.handicap), ignore_g=True + white_cur, get_handicap_adjustment(black_cur.rating, game.handicap, + komi=game.komi, size=game.size, rules=game.rules, + ), ignore_g=True ), black_rating=black_cur.rating, white_rating=white_cur.rating, diff --git a/analysis/analyze_gor.py b/analysis/analyze_gor.py index d2e431a..0018be5 100755 --- a/analysis/analyze_gor.py +++ b/analysis/analyze_gor.py @@ -44,15 +44,19 @@ def process_game(self, game: GameRecord) -> GorAnalytics: updated_black = gor_update( - black.with_handicap(get_handicap_adjustment(black.rating, game.handicap)), - #white.with_handicap(-get_handicap_adjustment(white.rating, game.handicap)), + black.with_handicap(get_handicap_adjustment(black.rating, game.handicap, + komi=game.komi, size=game.size, rules=game.rules, + )), + #white.with_handicap(-get_handicap_adjustment(white.rating, game.handicap, ...)), white, 1 if game.winner_id == game.black_id else 0, ) updated_white = gor_update( white, - black.with_handicap(get_handicap_adjustment(black.rating, game.handicap)), + black.with_handicap(get_handicap_adjustment(black.rating, game.handicap, + komi=game.komi, size=game.size, rules=game.rules, + )), 1 if game.winner_id == game.white_id else 0, ) @@ -65,8 +69,10 @@ def process_game(self, game: GameRecord) -> GorAnalytics: return GorAnalytics( skipped=False, game=game, - expected_win_rate=black.with_handicap(get_handicap_adjustment(white.rating, game.handicap)).expected_win_probability( - #white.copy(-get_handicap_adjustment(white.rating, game.handicap)) + expected_win_rate=black.with_handicap(get_handicap_adjustment(white.rating, game.handicap, + komi=game.komi, size=game.size, rules=game.rules, + )).expected_win_probability( + #white.copy(-get_handicap_adjustment(white.rating, game.handicap, ...)) white ), black_rating=black.rating, diff --git a/analysis/util/RatingMath.py b/analysis/util/RatingMath.py index 7f2ac62..315a32a 100644 --- a/analysis/util/RatingMath.py +++ b/analysis/util/RatingMath.py @@ -8,6 +8,7 @@ "rank_to_rating", "rating_to_rank", "get_handicap_adjustment", + "get_handicap_rank_difference", "rating_config", "set_optimizer_rating_points", "set_exhaustive_log_parameters", @@ -33,6 +34,8 @@ P = 1 HALF_STONE_HANDICAP = False HALF_STONE_HANDICAP_FOR_ALL_RANKS = False +COMPUTE_HANDICAP_VIA_KOMI_SMALL = False +COMPUTE_HANDICAP_VIA_KOMI_19X19 = False cli.add_argument( "--half-stone-handicap", dest="half_stone_handicap", const=1, default=False, action="store_const", help="Use a 0.5 rank adjustment for hc1", @@ -40,6 +43,16 @@ cli.add_argument( "--half-stone-handicap-for-all-ranks", dest="half_stone_handicap_for_all_ranks", const=1, default=False, action="store_const", help="use rankdiff -0.5 for handicap", ) +cli.add_argument( + "--compute-handicap-via-komi-small", + dest="compute_handicap_via_komi_small", const=1, default=False, action="store_const", + help="compute effective handicap from komi for small boards", +) +cli.add_argument( + "--compute-handicap-via-komi-19x19", + dest="compute_handicap_via_komi_19x19", const=1, default=False, action="store_const", + help="compute effective handicap from komi for 19x19 boards", +) logarithmic = cli.add_argument_group( "logarithmic ranking variables", "rating to ranks converted with `(log(rating / a) ** p) * c + d`", @@ -77,14 +90,66 @@ def set_exhaustive_log_parameters(a: float, c:float, d:float, p:float = 1.0) -> D = d P = p -def get_handicap_adjustment(rating: float, handicap: int) -> float: + +def get_handicap_rank_difference(handicap: int, size: int, komi: float, rules: str) -> float: global HALF_STONE_HANDICAP global HALF_STONE_HANDICAP_FOR_ALL_RANKS + global COMPUTE_HANDICAP_VIA_KOMI_SMALL + global COMPUTE_HANDICAP_VIA_KOMI_19X19 + + if (COMPUTE_HANDICAP_VIA_KOMI_19X19 and size == 19) or (COMPUTE_HANDICAP_VIA_KOMI_SMALL and size != 19): + # The territorial value of a free stone. + stone_value = 12 + + # Number of extra moves black makes before white responds. + num_extra_moves = handicap - 1 if handicap > 1 else 0 + + if rules == "japanese" or rules == "korean": + # Territory scoring. + area_bonus = 0 + komi_bonus = 0 + else: + # Bonus for the area value of a stone in area scoring. + area_bonus = 1 + + # Chinese and AGA rules add extra komi when there's a handicap but + # don't store it in the 'komi' field. + if rules == "chinese": + komi_bonus = 1 * handicap + elif rules == "aga": + komi_bonus = 1 * num_extra_moves + else: + komi_bonus = 0 + + # Figure out the point value of black's head start, if any, by + # subtracting the actual komi from the fair komi for an even game, and + # adding the point value of any extra moves. + fair_komi = stone_value / 2 + area_bonus + 0.5 + actual_komi = komi + komi_bonus + value_extra_moves = (stone_value + area_bonus) * num_extra_moves + head_start = fair_komi - actual_komi + value_extra_moves + + # Convert back to a fractional handicap, using scaling factors of 3x + # and 6x for 9x9 and 13x13. + if size == 9: + return head_start * 6 / stone_value + if size == 13: + return head_start * 3 / stone_value + return head_start / stone_value + if HALF_STONE_HANDICAP_FOR_ALL_RANKS: - return rank_to_rating(rating_to_rank(rating) + (handicap - 0.5 if handicap > 0 else 0)) - rating + return handicap - 0.5 if handicap > 0 else 0 if HALF_STONE_HANDICAP: - return rank_to_rating(rating_to_rank(rating) + (0.5 if handicap == 1 else handicap)) - rating - return rank_to_rating(rating_to_rank(rating) + handicap) - rating + return 0.5 if handicap == 1 else handicap + return handicap + + +def get_handicap_adjustment(rating: float, handicap: int, size: int, komi: float, rules: str) -> float: + rank_difference = get_handicap_rank_difference(handicap, size, komi, rules) + effective_rank = min(39, max(0, rating_to_rank(rating) + rank_difference)) + effective_rank = rating_to_rank(rating) + return rank_to_rating(effective_rank) - rating + def set_optimizer_rating_points(points: List[float]) -> None: global optimizer_rating_control_points @@ -99,9 +164,13 @@ def configure_rating_to_rank(args: argparse.Namespace) -> None: global D global HALF_STONE_HANDICAP global HALF_STONE_HANDICAP_FOR_ALL_RANKS + global COMPUTE_HANDICAP_VIA_KOMI_SMALL + global COMPUTE_HANDICAP_VIA_KOMI_19X19 HALF_STONE_HANDICAP = args.half_stone_handicap HALF_STONE_HANDICAP_FOR_ALL_RANKS = args.half_stone_handicap_for_all_ranks + COMPUTE_HANDICAP_VIA_KOMI_SMALL = args.compute_handicap_via_komi_small + COMPUTE_HANDICAP_VIA_KOMI_19X19 = args.compute_handicap_via_komi_19x19 system: str = args.ranks a: float = args.a c: float = args.c @@ -260,7 +329,9 @@ def __rating_to_rank(rating: float) -> float: else: raise NotImplementedError - assert round(get_handicap_adjustment(1000.0, 0), 8) == 0 + for size in [9, 13, 19]: + assert round(get_handicap_adjustment(1000.0, 0, size=size, rules="japanese", komi=6.5), 8) == 0 + assert round(get_handicap_adjustment(1000.0, 0, size=size, rules="aga", komi=7.5), 8) == 0 def lerp(x:float, y:float, a:float): diff --git a/analysis/util/SkipLogic.py b/analysis/util/SkipLogic.py index b155641..3daaaf5 100644 --- a/analysis/util/SkipLogic.py +++ b/analysis/util/SkipLogic.py @@ -3,6 +3,8 @@ Storage, ) +from .RatingMath import get_handicap_rank_difference + __all__ = [ "should_skip_game", ] @@ -20,4 +22,12 @@ def should_skip_game(game: GameRecord, storage: Storage) -> bool: elif game.speed == 3: # correspondence non timeout, clear flags for both storage.set_timeout_flag(game.black_id, False) storage.set_timeout_flag(game.white_id, False) + + # Skip games with effective handicap > 9, which shouldn't be rated, since + # they're too noisy. This is mostly old 9x9 and 13x13 games. + if get_handicap_rank_difference( + handicap=game.handicap, size=game.size, + komi=game.komi, rules=game.rules) > 9: + return True + return False diff --git a/analysis/util/TallyGameAnalytics.py b/analysis/util/TallyGameAnalytics.py index 812de9c..6f8abed 100644 --- a/analysis/util/TallyGameAnalytics.py +++ b/analysis/util/TallyGameAnalytics.py @@ -17,7 +17,7 @@ from .Glicko2Analytics import Glicko2Analytics from .GorAnalytics import GorAnalytics from .InMemoryStorage import InMemoryStorage -from .RatingMath import rating_config, rating_to_rank +from .RatingMath import rating_config, rating_to_rank, get_handicap_rank_difference from .EGFGameData import EGFGameData from .AGAGameData import AGAGameData @@ -73,7 +73,10 @@ def add_glicko2_analytics(self, result: Glicko2Analytics) -> None: self.games_ignored += 1 return - if abs(result.black_rank + result.game.handicap - result.white_rank) > 1: + handicap_rank_difference = get_handicap_rank_difference( + handicap=result.game.handicap, size=result.game.size, + komi=result.game.komi, rules=result.game.rules) + if abs(result.black_rank + handicap_rank_difference - result.white_rank) > 1: self.games_ignored += 1 return @@ -89,7 +92,7 @@ def add_glicko2_analytics(self, result: Glicko2Analytics) -> None: ]: for handicap in [ALL, result.game.handicap]: if isinstance(rank, int) or isinstance(rank, str): # this is just to make mypy happy - if abs(result.black_rank + result.game.handicap - result.white_rank) <= 1: + if abs(result.black_rank + handicap_rank_difference - result.white_rank) <= 1: self.count_black_wins[size][speed][rank][handicap] += 1 if black_won: self.black_wins[size][speed][rank][handicap] += 1 @@ -110,7 +113,10 @@ def add_gor_analytics(self, result: GorAnalytics) -> None: self.games_ignored += 1 return - if abs(result.black_rank + result.game.handicap - result.white_rank) > 1: + handicap_rank_difference = get_handicap_rank_difference( + handicap=result.game.handicap, size=result.game.size, + komi=result.game.komi, rules=result.game.rules) + if abs(result.black_rank + handicap_rank_difference - result.white_rank) > 1: self.games_ignored += 1 return