Skip to content

Commit dc9e023

Browse files
authored
Predict ranks and their odds of entire match outcome. (#74)
1 parent 4070bee commit dc9e023

File tree

7 files changed

+232
-19
lines changed

7 files changed

+232
-19
lines changed

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,26 @@ You can compare two or more teams to get the probabilities of the match drawing.
110110
0.09025541153402594
111111
```
112112

113+
## Predicting Ranks
114+
115+
Sometimes you want to know what the likelihood is someone will place at a particular rank. You can use this library to predict those odds.
116+
117+
```python
118+
>>> from openskill import predict_rank, predict_draw
119+
>>> a1 = a2 = a3 = Rating(mu=34, sigma=0.25)
120+
>>> b1 = b2 = b3 = Rating(mu=32, sigma=0.5)
121+
>>> c1 = c2 = c3 = Rating(mu=30, sigma=1)
122+
>>> team_1, team_2, team_3 = [a1, a2, a3], [b1, b2, b3], [c1, c2, c3]
123+
>>> draw_probability = predict_draw(teams=[team_1, team_2, team_3])
124+
>>> draw_probability
125+
0.3295385074666581
126+
>>> rank_probability = predict_rank(teams=[team_1, team_2, team_3])
127+
>>> rank_probability
128+
[(1, 0.4450361350569973), (2, 0.19655022513040032), (3, 0.028875132345944337)]
129+
>>> sum([y for x, y in rank_probability]) + draw_probability
130+
1.0
131+
```
132+
113133
## Choosing Models
114134

115135
The default model is `PlackettLuce`. You can import alternate models from `openskill.models` like so:
Lines changed: 110 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
from typing import Union
55

66
import jsonlines
7+
import numpy as np
78
import trueskill
89
from prompt_toolkit import HTML
910
from prompt_toolkit import print_formatted_text as print
1011
from prompt_toolkit import prompt
1112
from prompt_toolkit.completion import WordCompleter
1213
from prompt_toolkit.shortcuts import ProgressBar
14+
from sklearn.model_selection import train_test_split
1315

1416
import openskill
1517
from openskill.models import (
@@ -24,11 +26,19 @@
2426
os_players = {}
2527
ts_players = {}
2628

29+
match_count = {}
30+
31+
matches = []
32+
training_set = {}
33+
test_set = {}
34+
valid_test_set_matches = []
35+
2736
# Counters
2837
os_correct_predictions = 0
2938
os_incorrect_predictions = 0
3039
ts_correct_predictions = 0
3140
ts_incorrect_predictions = 0
41+
confident_matches = 0
3242

3343

3444
print(HTML("<u><b>Benchmark Starting</b></u>"))
@@ -144,11 +154,15 @@ def predict_os_match(match: dict):
144154
for player in red_team:
145155
os_red_players[player] = os_players[player]
146156

147-
blue_win_probability, red_win_probability = openskill.predict_win(
157+
blue_win_probability, red_win_probability = openskill.predict_rank(
148158
[list(os_blue_players.values()), list(os_red_players.values())]
149159
)
150-
if (blue_win_probability > red_win_probability) == won:
151-
global os_correct_predictions
160+
blue_win_probability = blue_win_probability[0]
161+
red_win_probability = red_win_probability[0]
162+
global os_correct_predictions
163+
if (blue_win_probability < red_win_probability) == won:
164+
os_correct_predictions += 1
165+
elif blue_win_probability == red_win_probability: # Draw
152166
os_correct_predictions += 1
153167
else:
154168
global os_incorrect_predictions
@@ -179,7 +193,7 @@ def predict_ts_match(match: dict):
179193
ts_blue_players[player] = ts_players[player]
180194

181195
for player in red_team:
182-
ts_red_players[player] = os_players[player]
196+
ts_red_players[player] = ts_players[player]
183197

184198
blue_win_probability = win_probability(
185199
list(ts_blue_players.values()), list(ts_red_players.values())
@@ -193,6 +207,52 @@ def predict_ts_match(match: dict):
193207
ts_incorrect_predictions += 1
194208

195209

210+
def process_match(match: dict):
211+
teams: dict = match.get("teams")
212+
blue_team: dict = teams.get("blue")
213+
red_team: dict = teams.get("red")
214+
215+
for player in blue_team:
216+
match_count[player] = match_count.get(player, 0) + 1
217+
218+
for player in red_team:
219+
match_count[player] = match_count.get(player, 0) + 1
220+
221+
222+
def valid_test_set(match: dict):
223+
teams: dict = match.get("teams")
224+
blue_team: dict = teams.get("blue")
225+
red_team: dict = teams.get("red")
226+
227+
for player in blue_team:
228+
if player not in os_players:
229+
return False
230+
231+
for player in red_team:
232+
if player not in os_players:
233+
return False
234+
235+
return True
236+
237+
238+
def confident_in_match(match: dict) -> bool:
239+
teams: dict = match.get("teams")
240+
blue_team: dict = teams.get("blue")
241+
red_team: dict = teams.get("red")
242+
243+
global confident_matches
244+
for player in blue_team:
245+
if match_count[player] < 2:
246+
return False
247+
248+
for player in red_team:
249+
if match_count[player] < 2:
250+
return False
251+
252+
confident_matches += 1
253+
return True
254+
255+
196256
models = [
197257
BradleyTerryFull,
198258
BradleyTerryPart,
@@ -203,6 +263,7 @@ def predict_ts_match(match: dict):
203263
model_names = [m.__name__ for m in models]
204264
model_completer = WordCompleter(model_names)
205265
input_model = prompt("Enter Model: ", completer=model_completer)
266+
206267
if input_model in model_names:
207268
index = model_names.index(input_model)
208269
else:
@@ -211,41 +272,71 @@ def predict_ts_match(match: dict):
211272
with jsonlines.open("v2_jsonl_teams.jsonl") as reader:
212273
lines = list(reader.iter())
213274

214-
# Process OpenSkill Ratings
215-
title = HTML(f'Updating Ratings with <style fg="Green">{input_model}</style> Model')
275+
title = HTML(f'<style fg="Red">Processing Matches</style>')
216276
with ProgressBar(title=title) as progress_bar:
217-
os_process_time_start = time.time()
218277
for line in progress_bar(lines, total=len(lines)):
219278
if data_verified(match=line):
220-
process_os_match(match=line, model=models[index])
279+
process_match(match=line)
280+
281+
# Measure Confidence
282+
title = HTML(f'<style fg="Red">Splitting Data</style>')
283+
with ProgressBar(title=title) as progress_bar:
284+
for line in progress_bar(lines, total=len(lines)):
285+
if data_verified(match=line):
286+
if confident_in_match(match=line):
287+
matches.append(line)
288+
289+
# Split Data
290+
training_set, test_set = train_test_split(
291+
matches, test_size=0.33, random_state=True
292+
)
293+
294+
# Process OpenSkill Ratings
295+
title = HTML(
296+
f'Updating Ratings with <style fg="Green">{input_model}</style> Model:'
297+
)
298+
with ProgressBar(title=title) as progress_bar:
299+
os_process_time_start = time.time()
300+
for line in progress_bar(training_set, total=len(training_set)):
301+
process_os_match(match=line, model=models[index])
221302
os_process_time_stop = time.time()
222303
os_time = os_process_time_stop - os_process_time_start
223304

224305
# Process TrueSkill Ratings
225-
title = HTML(f'Updating Ratings with <style fg="Green">TrueSkill</style> Model')
306+
title = HTML(f'Updating Ratings with <style fg="Green">TrueSkill</style> Model:')
226307
with ProgressBar(title=title) as progress_bar:
227308
ts_process_time_start = time.time()
228-
for line in progress_bar(lines, total=len(lines)):
229-
if data_verified(match=line):
230-
process_ts_match(match=line)
309+
for line in progress_bar(training_set, total=len(training_set)):
310+
process_ts_match(match=line)
231311
ts_process_time_stop = time.time()
232312
ts_time = ts_process_time_stop - ts_process_time_start
233313

314+
# Process Test Set
315+
title = HTML(f'<style fg="Red">Processing Test Set</style>')
316+
with ProgressBar(title=title) as progress_bar:
317+
for line in progress_bar(test_set, total=len(test_set)):
318+
if valid_test_set(match=line):
319+
valid_test_set_matches.append(line)
320+
234321
# Predict OpenSkill Matches
235322
title = HTML(f'<style fg="Blue">Predicting OpenSkill Matches:</style>')
236323
with ProgressBar(title=title) as progress_bar:
237-
for line in progress_bar(lines, total=len(lines)):
238-
if data_verified(match=line):
239-
predict_os_match(match=line)
324+
for line in progress_bar(
325+
valid_test_set_matches, total=len(valid_test_set_matches)
326+
):
327+
predict_os_match(match=line)
240328

241329
# Predict TrueSkill Matches
242330
title = HTML(f'<style fg="Blue">Predicting TrueSkill Matches:</style>')
243331
with ProgressBar(title=title) as progress_bar:
244-
for line in progress_bar(lines, total=len(lines)):
245-
if data_verified(match=line):
246-
predict_ts_match(match=line)
332+
for line in progress_bar(
333+
valid_test_set_matches, total=len(valid_test_set_matches)
334+
):
335+
predict_ts_match(match=line)
247336

337+
mean = float(np.array(list(match_count.values())).mean())
248338

339+
print(HTML(f"Confident Matches: <style fg='Yellow'>{confident_matches}</style>"))
249340
print(
250341
HTML(
251342
f"Predictions Made with OpenSkill's <style fg='Green'><u>{input_model}</u></style> Model:"
@@ -281,3 +372,4 @@ def predict_ts_match(match: dict):
281372
)
282373
)
283374
print(HTML(f"Process Duration: <style fg='Yellow'>{ts_time}</style>"))
375+
print(HTML(f"Mean Matches: <style fg='Yellow'>{mean}</style>"))

docs/manual.rst

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,26 @@ You can compare two or more teams to get the probabilities of the match drawing.
119119
0.09025541153402594
120120
121121
122+
Predicting Ranks
123+
----------------
124+
125+
.. code:: python
126+
127+
>>> from openskill import predict_rank, predict_draw
128+
>>> a1 = a2 = a3 = Rating(mu=34, sigma=0.25)
129+
>>> b1 = b2 = b3 = Rating(mu=32, sigma=0.5)
130+
>>> c1 = c2 = c3 = Rating(mu=30, sigma=1)
131+
>>> team_1, team_2, team_3 = [a1, a2, a3], [b1, b2, b3], [c1, c2, c3]
132+
>>> draw_probability = predict_draw(teams=[team_1, team_2, team_3])
133+
>>> draw_probability
134+
0.3295385074666581
135+
>>> rank_probability = predict_rank(teams=[team_1, team_2, team_3])
136+
>>> rank_probability
137+
[(1, 0.4450361350569973), (2, 0.19655022513040032), (3, 0.028875132345944337)]
138+
>>> sum([y for x, y in rank_probability]) + draw_probability
139+
1.0
140+
141+
122142
Choosing Models
123143
---------------
124144

openskill/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
create_rating,
44
ordinal,
55
predict_draw,
6+
predict_rank,
67
predict_win,
78
rate,
89
team_rating,

openskill/rate.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
import itertools
33
import math
44
from functools import reduce
5-
from typing import List, Optional, Union
5+
from typing import List, Optional, Tuple, Union
6+
7+
from scipy.stats import rankdata
68

79
from openskill.constants import Constants, beta
810
from openskill.constants import mu as default_mu
@@ -412,3 +414,55 @@ def predict_draw(teams: List[List[Rating]], **options) -> Union[int, float]:
412414
denom = n * (n - 1)
413415

414416
return abs(sum(pairwise_probabilities)) / denom
417+
418+
419+
def predict_rank(
420+
teams: List[List[Rating]], **options
421+
) -> List[Tuple[int, Union[int, float]]]:
422+
"""
423+
Predict the shape of a match outcome.
424+
This algorithm has a time complexity of O(n!/(n - 2)!) where 'n' is the number of teams.
425+
426+
:param teams: A list of two or more teams, where teams are lists of :class:`~openskill.rate.Rating` objects.
427+
:return: A list of team ranks with their probabilities.
428+
"""
429+
if len(teams) < 2:
430+
raise ValueError(f"Expected at least two teams.")
431+
432+
n = len(teams)
433+
total_player_count = sum([len(_) for _ in teams])
434+
denom = (n * (n - 1)) / 2
435+
draw_probability = 1 / n
436+
draw_margin = (
437+
math.sqrt(total_player_count)
438+
* beta(**options)
439+
* phi_major_inverse((1 + draw_probability) / 2)
440+
)
441+
442+
pairwise_probabilities = []
443+
for pairwise_subset in itertools.permutations(teams, 2):
444+
current_team_a_rating = team_rating([pairwise_subset[0]])
445+
current_team_b_rating = team_rating([pairwise_subset[1]])
446+
mu_a = current_team_a_rating[0][0]
447+
sigma_a = current_team_a_rating[0][1]
448+
mu_b = current_team_b_rating[0][0]
449+
sigma_b = current_team_b_rating[0][1]
450+
pairwise_probabilities.append(
451+
phi_major(
452+
(mu_a - mu_b - draw_margin)
453+
/ math.sqrt(n * beta(**options) ** 2 + sigma_a**2 + sigma_b**2)
454+
)
455+
)
456+
win_probability = [
457+
(sum(team_prob) / denom)
458+
for team_prob in itertools.zip_longest(
459+
*[iter(pairwise_probabilities)] * (n - 1)
460+
)
461+
]
462+
463+
ranked_probability = [abs(_) for _ in win_probability]
464+
ranks = list(rankdata(ranked_probability, method="min"))
465+
max_ordinal = max(ranks)
466+
ranks = [abs(_ - max_ordinal) + 1 for _ in ranks]
467+
predictions = list(zip(ranks, ranked_probability))
468+
return predictions
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import pytest
2+
3+
from openskill import Rating
4+
from openskill.rate import predict_draw, predict_rank
5+
6+
7+
def test_predict_rank():
8+
a1 = Rating(mu=34, sigma=0.25)
9+
a2 = Rating(mu=32, sigma=0.25)
10+
a3 = Rating(mu=34, sigma=0.25)
11+
12+
b1 = Rating(mu=24, sigma=0.5)
13+
b2 = Rating(mu=22, sigma=0.5)
14+
b3 = Rating(mu=20, sigma=0.5)
15+
16+
team_1 = [a1, b1]
17+
team_2 = [a2, b2]
18+
team_3 = [a3, b3]
19+
20+
ranks = predict_rank(teams=[team_1, team_2, team_3])
21+
total_rank_probability = sum([y for x, y in ranks])
22+
draw_probability = predict_draw(teams=[team_1, team_2, team_3])
23+
assert total_rank_probability + draw_probability == pytest.approx(1)
24+
25+
with pytest.raises(ValueError):
26+
predict_rank(teams=[team_1])

0 commit comments

Comments
 (0)