Skip to content

Commit 06da2ed

Browse files
author
Pierre-François MONVILLE
committed
1.5
add proper tests use black formatting modify inspect so that it’s coherent across save and load add utils script to contain error classes and utility functions modify playerday elo to a property attribute modify player log_likelihood computation to comply with tokoharu pull request modify player hessian, covariance and update_by_ndim_newton computation to comply with tokoharu pull request
1 parent 3e785f0 commit 06da2ed

File tree

6 files changed

+789
-651
lines changed

6 files changed

+789
-651
lines changed

tests/whr_test.py

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import sys
2+
import os
3+
import pytest
4+
5+
sys.path.append(os.path.join(os.path.dirname(__file__), "../"))
6+
from whr import whole_history_rating
7+
from whr import utils
8+
9+
10+
def setup_game_with_elo(white_elo, black_elo, handicap):
11+
whr = whole_history_rating.Base()
12+
game = whr.create_game("black", "white", "W", 1, handicap)
13+
game.black_player.days[0].elo = black_elo
14+
game.white_player.days[0].elo = white_elo
15+
return game
16+
17+
18+
def test_even_game_between_equal_strength_players_should_have_white_winrate_of_50_percent():
19+
game = setup_game_with_elo(500, 500, 0)
20+
assert abs(0.5 - game.white_win_probability()) <= 0.0001
21+
22+
23+
def test_handicap_should_confer_advantage():
24+
game = setup_game_with_elo(500, 500, 1)
25+
assert game.black_win_probability() > 0.5
26+
27+
28+
def test_higher_rank_should_confer_advantage():
29+
game = setup_game_with_elo(600, 500, 0)
30+
assert game.white_win_probability() > 0.5
31+
32+
33+
def test_winrates_are_equal_for_same_elo_delta():
34+
game = setup_game_with_elo(100, 200, 0)
35+
game2 = setup_game_with_elo(200, 300, 0)
36+
assert abs(game.white_win_probability() - game2.white_win_probability()) <= 0.0001
37+
38+
39+
def test_winrates_for_twice_as_strong_player():
40+
game = setup_game_with_elo(100, 200, 0)
41+
assert abs(0.359935 - game.white_win_probability()) <= 0.0001
42+
43+
44+
def test_winrates_should_be_inversely_proportional_with_unequal_ranks():
45+
game = setup_game_with_elo(600, 500, 0)
46+
assert (
47+
abs(game.white_win_probability() - (1 - game.black_win_probability())) <= 0.0001
48+
)
49+
50+
51+
def test_winrates_should_be_inversely_proportional_with_handicap():
52+
game = setup_game_with_elo(500, 500, 4)
53+
assert (
54+
abs(game.white_win_probability() - (1 - game.black_win_probability())) <= 0.0001
55+
)
56+
57+
58+
def test_output():
59+
whr = whole_history_rating.Base()
60+
whr.create_game("shusaku", "shusai", "B", 1, 0)
61+
whr.create_game("shusaku", "shusai", "W", 2, 0)
62+
whr.create_game("shusaku", "shusai", "W", 3, 0)
63+
whr.create_game("shusaku", "shusai", "W", 4, 0)
64+
whr.create_game("shusaku", "shusai", "W", 4, 0)
65+
whr.iterate(50)
66+
assert [
67+
[1, -92, 71],
68+
[2, -94, 71],
69+
[3, -95, 71],
70+
[4, -96, 72],
71+
] == whr.ratings_for_player("shusaku")
72+
assert [
73+
[1, 92, 71],
74+
[2, 94, 71],
75+
[3, 95, 71],
76+
[4, 96, 72],
77+
] == whr.ratings_for_player("shusai")
78+
79+
80+
def test_unstable_exception_raised_in_certain_cases():
81+
whr = whole_history_rating.Base()
82+
for _ in range(10):
83+
whr.create_game("anchor", "player", "B", 1, 0)
84+
whr.create_game("anchor", "player", "W", 1, 0)
85+
for _ in range(10):
86+
whr.create_game("anchor", "player", "B", 180, 600)
87+
whr.create_game("anchor", "player", "W", 180, 600)
88+
with pytest.raises(utils.UnstableRatingException):
89+
whr.iterate(10)
90+
91+
92+
def test_log_likelihood():
93+
whr = whole_history_rating.Base()
94+
whr.create_game("shusaku", "shusai", "B", 1, 0)
95+
whr.create_game("shusaku", "shusai", "W", 4, 0)
96+
whr.create_game("shusaku", "shusai", "W", 10, 0)
97+
player = whr.players["shusaku"]
98+
player.days[0].r = 1
99+
player.days[1].r = 2
100+
player.days[2].r = 0
101+
assert abs(-69.65648196168772 - player.log_likelihood()) <= 0.0001
102+
assert abs(-1.9397850625546684 - player.days[0].log_likelihood()) <= 0.0001
103+
assert abs(-2.1269280110429727 - player.days[1].log_likelihood()) <= 0.0001
104+
assert abs(-0.6931471805599453 - player.days[2].log_likelihood()) <= 0.0001
105+
106+
107+
def test_creating_games():
108+
# test creating the base with modified w2 and uncased
109+
whr = whole_history_rating.Base(config={"w2": 14, "uncased": True})
110+
# test creating one game
111+
assert isinstance(
112+
whr.create_game("shusaku", "shusai", "B", 4, 0), whole_history_rating.Game
113+
)
114+
# test creating one game with winner uncased (b instead of B)
115+
assert isinstance(
116+
whr.create_game("shusaku", "shusai", "w", 5, 0), whole_history_rating.Game
117+
)
118+
# test creating one game with cased letters (ShUsAkU instead of shusaku and ShUsAi instead of shusai)
119+
assert isinstance(
120+
whr.create_game("ShUsAkU", "ShUsAi", "W", 6, 0), whole_history_rating.Game
121+
)
122+
123+
124+
def test_loading_several_games_at_once(capsys):
125+
whr = whole_history_rating.Base()
126+
# test loading several games at once
127+
test_games = [
128+
"shusaku; shusai; B; 1",
129+
"shusaku;shusai;W;2;0",
130+
" shusaku ; shusai ;W ; 3; {'w2':300}",
131+
"shusaku;nobody;B;3;0;{'w2':300}",
132+
]
133+
whr.load_games(test_games, separator=";")
134+
assert len(whr.games) == 4
135+
# test auto iterating to get convergence
136+
whr.auto_iterate()
137+
# test getting ratings for player shusaku (day, elo, uncertainty)
138+
assert whr.ratings_for_player("shusaku") == [
139+
[1, 26.0, 70.0],
140+
[2, 25.0, 70.0],
141+
[3, 24.0, 70.0],
142+
]
143+
# test getting ratings for player shusai, only current elo and uncertainty
144+
assert whr.ratings_for_player("shusai", current=True) == (87.0, 84.0)
145+
# test getting probability of future match between shusaku and nobody2 (which default to 1 win 1 loss)
146+
assert whr.probability_future_match("shusai", "nobody2", 0) == (
147+
0.6224906898220315,
148+
0.3775093101779684,
149+
)
150+
# test getting log likelihood of base
151+
assert whr.log_likelihood() == 0.7431542354571272
152+
# test printing ordered ratings
153+
whr.print_ordered_ratings()
154+
display = "win probability: shusai:0.62%; nobody2:0.38%\nnobody => [-112.37545390067574]\nshusaku => [25.552142942931102, 24.669738398550702, 24.49953062693439]\nshusai => [84.74972643795506, 86.17200033461006, 86.88207745833284]\n"
155+
captured = capsys.readouterr()
156+
assert display == captured.out
157+
# test printing ordered ratings, only current elo
158+
whr.print_ordered_ratings(current=True)
159+
display = "nobody => -112.37545390067574\nshusaku => 24.49953062693439\nshusai => 86.88207745833284\n"
160+
captured = capsys.readouterr()
161+
assert display == captured.out
162+
# test getting ordered ratings, compact form
163+
assert whr.get_ordered_ratings(compact=True) == [
164+
[-112.37545390067574],
165+
[25.552142942931102, 24.669738398550702, 24.49953062693439],
166+
[84.74972643795506, 86.17200033461006, 86.88207745833284],
167+
]
168+
# test getting ordered ratings, only current elo with compact form
169+
assert whr.get_ordered_ratings(compact=True, current=True) == [
170+
-112.37545390067574,
171+
24.49953062693439,
172+
86.88207745833284,
173+
]
174+
# test saving base
175+
whole_history_rating.Base.save_base(whr, "test_whr.pkl")
176+
# test loading base
177+
whr2 = whole_history_rating.Base.load_base("test_whr.pkl")
178+
# test inspecting the first game
179+
whr_games = [x.inspect() for x in whr.games]
180+
whr2_games = [x.inspect() for x in whr2.games]
181+
assert whr_games == whr2_games

whr/game.py

Lines changed: 62 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,66 @@
11
import sys
22

3+
34
class Game:
5+
def __init__(self, black, white, winner, time_step, handicap=0, extras=None):
6+
self.day = time_step
7+
self.white_player = white
8+
self.black_player = black
9+
self.winner = winner.upper()
10+
self.handicap = handicap
11+
self.handicap_proc = handicap
12+
self.bpd = None
13+
self.wpd = None
14+
if extras is None:
15+
self.extras = dict()
16+
self.extras["komi"] = 6.5
17+
else:
18+
self.extras = extras
19+
if self.extras.get("komi") is None:
20+
self.extras["komi"] = 6.5
21+
22+
def opponents_adjusted_gamma(self, player):
23+
if player == self.white_player:
24+
opponent_elo = self.bpd.elo + self.handicap
25+
elif player == self.black_player:
26+
opponent_elo = self.wpd.elo - self.handicap
27+
else:
28+
raise (
29+
AttributeError(
30+
f"No opponent for {player.__str__()}, since they're not in this game: {self.__str__()}."
31+
)
32+
)
33+
rval = 10 ** (opponent_elo / 400.0)
34+
if rval == 0 or rval > sys.maxsize:
35+
raise AttributeError("bad adjusted gamma")
36+
return rval
37+
38+
def opponent(self, player):
39+
if player == self.white_player:
40+
return self.black_player
41+
return self.white_player
42+
43+
def prediction_score(self):
44+
if self.white_win_probability() == 0.5:
45+
return 0.5
46+
return (
47+
1.0
48+
if (
49+
(self.winner == "W" and self.white_win_probability() > 0.5)
50+
or (self.winner == "B" and self.white_win_probability() < 0.5)
51+
)
52+
else 0.0
53+
)
54+
55+
def inspect(self):
56+
return f"W:{self.white_player.name}(r={self.wpd.r if self.wpd is not None else '?'}) B:{self.black_player.name}(r={self.bpd.r if self.bpd is not None else '?'}) winner = {self.winner}, komi = {self.extras['komi']}, handicap = {self.handicap}"
57+
58+
def white_win_probability(self):
59+
return self.wpd.gamma() / (
60+
self.wpd.gamma() + self.opponents_adjusted_gamma(self.white_player)
61+
)
462

5-
def __init__(self,black, white, winner, time_step, handicap = 0, extras = None):
6-
self.day = time_step
7-
self.white_player = white
8-
self.black_player = black
9-
self.winner = winner.upper()
10-
self.handicap = handicap
11-
self.handicap_proc = handicap
12-
if extras is None:
13-
self.extras = dict()
14-
self.extras["komi"] = 6.5
15-
else:
16-
self.extras = extras
17-
if self.extras.get("komi") is None:
18-
self.extras["komi"] = 6.5
19-
20-
21-
def opponents_adjusted_gamma(self, player):
22-
if player == self.white_player:
23-
opponent_elo = self.bpd.elo() + self.handicap
24-
elif player == self.black_player:
25-
opponent_elo = self.wpd.elo() - self.handicap
26-
else:
27-
raise(AttributeError(f"No opponent for {player.__str__()}, since they're not in this game: {self.__str__()}."))
28-
rval = 10**(opponent_elo/400.0)
29-
if rval == 0 or rval > sys.maxsize:
30-
raise(AttributeError("bad adjusted gamma"))
31-
return rval
32-
33-
def opponent(self, player):
34-
if player == self.white_player:
35-
return self.black_player
36-
elif player == self.black_player:
37-
return self.white_player
38-
39-
def prediction_score(self):
40-
if self.white_win_probability() == 0.5:
41-
return 0.5
42-
else:
43-
return 1.0 if ((self.winner == "W" and self.white_win_probability() > 0.5) or (self.winner == "B" and self.white_win_probability()<0.5)) else 0.0
44-
45-
def inspect(self):
46-
return f"{self.__str__()} : W:{self.white_player.name}(r={self.wpd.r if self.wpd is not None else '?'}) B:{self.black_player.name}(r={self.bpd.r if self.bpd is not None else '?'}) winner = {self.winner}, komi = {self.extras['komi']}, handicap = {self.handicap}"
47-
48-
def white_win_probability(self):
49-
return self.wpd.gamma()/(self.wpd.gamma() + self.opponents_adjusted_gamma(self.white_player))
50-
51-
def black_win_probability(self):
52-
return self.bpd.gamma()/(self.bpd.gamma() + self.opponents_adjusted_gamma(self.black_player))
63+
def black_win_probability(self):
64+
return self.bpd.gamma() / (
65+
self.bpd.gamma() + self.opponents_adjusted_gamma(self.black_player)
66+
)

0 commit comments

Comments
 (0)