Skip to content

Commit 5664aea

Browse files
authored
Window Sizes + Ties Fixed (#161)
* Ties now reflect properly in updated mu and sigma. Fixes #155 Signed-off-by: Vivek Joshy <daegontaven@gmail.com> * Upgrade tox deps Signed-off-by: Vivek Joshy <daegontaven@gmail.com> * Fix mypy typing and add changelog fragments Signed-off-by: Vivek Joshy <daegontaven@gmail.com> --------- Signed-off-by: Vivek Joshy <daegontaven@gmail.com>
1 parent 43a8ffa commit 5664aea

20 files changed

+884
-671
lines changed

SECURITY.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44

55
| Version | Supported |
66
|---------|--------------------|
7-
| 6.0.0 | :white_check_mark: |
8-
| 5.0.0 | :white_check_mark: |
9-
| < 5.0.0 | :x: |
7+
| ~=6.0 | :white_check_mark: |
8+
| ~=5.0 | :white_check_mark: |
9+
| <5.0 | :x: |
1010

1111
## Reporting a Vulnerability
1212

changes/161.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixes inconsistent updates from ties in free-for-all matches.

changes/161.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add `window_size` parameter that affect accuracy of partial pairing models.

docs/doc_requires.txt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
sphinx~=8.1
22
nbsphinx~=0.9
3-
pygments~=2.18
4-
shibuya~=2024.7
3+
pygments~=2.19
4+
shibuya~=2025.2
55
ipykernel~=6.29
6-
matplotlib~=3.9
6+
matplotlib~=3.10
77
myst_parser~=4.0 # Must Be Underscore, not Hyphen
8-
sphinx-intl~=2.2
8+
sphinx-intl~=2.3
99
sphinx_favicon~=1.0
1010
sphinx-docsearch~=0.1
1111
sphinx-copybutton~=0.5
1212
sphinx-autoapi~=3.1
1313
sphinxext-opengraph~=0.9
1414
sphinxcontrib-bibtex~=2.6
15-
sphinx-autodoc-typehints~=2.2
15+
sphinx-autodoc-typehints~=3.0

openskill/__init__.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
from typing import List
22

33
# Public API
4-
__all__: List[str] = [
5-
"models"
6-
]
4+
__all__: List[str] = ["models"]
75

86

97
# Metadata

openskill/models/weng_lin/bradley_terry_full.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -744,6 +744,12 @@ def _compute(
744744
team_ratings = self._calculate_team_ratings(teams, ranks=ranks)
745745
beta = self.beta
746746

747+
rank_groups: dict[int, list[int]] = {}
748+
for i, team_i in enumerate(team_ratings):
749+
if team_i.rank not in rank_groups:
750+
rank_groups[team_i.rank] = []
751+
rank_groups[team_i.rank].append(i)
752+
747753
result = []
748754
for i, team_i in enumerate(team_ratings):
749755
omega = 0.0
@@ -790,7 +796,6 @@ def _compute(
790796

791797
intermediate_result_per_team = []
792798
for j, j_players in enumerate(team_i.team):
793-
794799
if weights:
795800
weight = weights[i][j]
796801
else:
@@ -799,7 +804,7 @@ def _compute(
799804
mu = j_players.mu
800805
sigma = j_players.sigma
801806

802-
if omega > 0:
807+
if omega >= 0:
803808
mu += (sigma**2 / team_i.sigma_squared) * omega * weight
804809
sigma *= math.sqrt(
805810
max(
@@ -821,6 +826,16 @@ def _compute(
821826
modified_player.sigma = sigma
822827
intermediate_result_per_team.append(modified_player)
823828
result.append(intermediate_result_per_team)
829+
830+
for rank, indices in rank_groups.items():
831+
if len(indices) > 1:
832+
avg_mu_change = sum(
833+
result[i][0].mu - original_teams[i][0].mu for i in indices
834+
) / len(indices)
835+
for i in indices:
836+
for j in range(len(result[i])):
837+
result[i][j].mu = original_teams[i][j].mu + avg_mu_change
838+
824839
return result
825840

826841
def predict_win(self, teams: List[List[BradleyTerryFullRating]]) -> List[float]:

openskill/models/weng_lin/bradley_terry_part.py

Lines changed: 55 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
from openskill.models.common import _normalize, _unary_minus
1313
from openskill.models.weng_lin.common import (
14-
_ladder_pairs,
1514
_unwind,
1615
phi_major,
1716
phi_major_inverse,
@@ -281,6 +280,7 @@ def __init__(
281280
float,
282281
] = _gamma,
283282
tau: float = 25.0 / 300.0,
283+
window_size: int = 4,
284284
limit_sigma: bool = False,
285285
balance: bool = False,
286286
):
@@ -321,6 +321,10 @@ def __init__(
321321
322322
*Represented by:* :math:`\tau`
323323
324+
:param window_size: The sliding window size for partial pairing such
325+
that a larger window size tends to full pairing
326+
mode's accuracy.
327+
324328
:param limit_sigma: Boolean that determines whether to restrict
325329
the value of sigma from increasing.
326330
@@ -346,6 +350,7 @@ def __init__(
346350
] = gamma
347351

348352
self.tau: float = float(tau)
353+
self.window_size: int = int(window_size)
349354
self.limit_sigma: bool = limit_sigma
350355
self.balance: bool = balance
351356

@@ -748,87 +753,83 @@ def _compute(
748753
original_teams = teams
749754
team_ratings = self._calculate_team_ratings(teams, ranks=ranks)
750755
beta = self.beta
751-
adjacent_teams = _ladder_pairs(team_ratings)
752-
756+
num_teams = len(team_ratings)
757+
window = self.window_size
753758
result = []
754-
for i, (team_i, adjacent_i) in enumerate(zip(team_ratings, adjacent_teams)):
755-
omega = 0.0
756-
delta = 0.0
757759

758-
for q, team_q in enumerate(adjacent_i):
760+
for i, team_i in enumerate(team_ratings):
761+
omega_sum = 0.0
762+
delta_sum = 0.0
763+
comparisons = 0
764+
765+
start = max(0, i - window)
766+
end = min(num_teams, i + window + 1)
767+
for q in range(start, end):
759768
if q == i:
760769
continue
761-
770+
team_q = team_ratings[q]
762771
c_iq = math.sqrt(
763772
team_i.sigma_squared + team_q.sigma_squared + (2 * beta**2)
764773
)
765774
p_iq = 1 / (1 + math.exp((team_q.mu - team_i.mu) / c_iq))
766-
sigma_squared_to_ciq = team_i.sigma_squared / c_iq
775+
sigma_to_ciq = team_i.sigma_squared / c_iq
767776

768777
s = 0.0
769778
if team_q.rank > team_i.rank:
770-
s = 1
779+
s = 1.0
771780
elif team_q.rank == team_i.rank:
772781
s = 0.5
773782

774-
omega += sigma_squared_to_ciq * (s - p_iq)
775-
if weights:
776-
gamma_value = self.gamma(
777-
c_iq,
778-
len(team_ratings),
779-
team_i.mu,
780-
team_i.sigma_squared,
781-
team_i.team,
782-
team_i.rank,
783-
weights[i],
784-
)
785-
else:
786-
gamma_value = self.gamma(
787-
c_iq,
788-
len(team_ratings),
789-
team_i.mu,
790-
team_i.sigma_squared,
791-
team_i.team,
792-
team_i.rank,
793-
None,
794-
)
795-
delta += (
796-
((gamma_value * sigma_squared_to_ciq) / c_iq) * p_iq * (1 - p_iq)
783+
omega_sum += sigma_to_ciq * (s - p_iq)
784+
current_weights = weights[i] if weights else None
785+
gamma_value = self.gamma(
786+
c_iq,
787+
num_teams,
788+
team_i.mu,
789+
team_i.sigma_squared,
790+
team_i.team,
791+
team_i.rank,
792+
current_weights,
797793
)
794+
delta_sum += (gamma_value * sigma_to_ciq / c_iq) * p_iq * (1 - p_iq)
795+
comparisons += 1
798796

799-
intermediate_result_per_team = []
800-
for j, j_players in enumerate(team_i.team):
801-
802-
if weights:
803-
weight = weights[i][j]
804-
else:
805-
weight = 1
806-
807-
mu = j_players.mu
808-
sigma = j_players.sigma
797+
if comparisons > 0:
798+
omega = omega_sum / comparisons
799+
delta = delta_sum / comparisons
800+
else:
801+
omega = 0.0
802+
delta = 0.0
809803

810-
if omega > 0:
811-
mu += (sigma**2 / team_i.sigma_squared) * omega * weight
812-
sigma *= math.sqrt(
804+
intermediate_result_per_team = []
805+
for j, player in enumerate(team_i.team):
806+
w = weights[i][j] if weights else 1
807+
new_mu = player.mu
808+
new_sigma = player.sigma
809+
810+
if omega >= 0:
811+
new_mu += (new_sigma**2 / team_i.sigma_squared) * omega * w
812+
new_sigma *= math.sqrt(
813813
max(
814-
1 - (sigma**2 / team_i.sigma_squared) * delta * weight,
814+
1 - (new_sigma**2 / team_i.sigma_squared) * delta * w,
815815
self.kappa,
816-
),
816+
)
817817
)
818818
else:
819-
mu += (sigma**2 / team_i.sigma_squared) * omega / weight
820-
sigma *= math.sqrt(
819+
new_mu += (new_sigma**2 / team_i.sigma_squared) * omega / w
820+
new_sigma *= math.sqrt(
821821
max(
822-
1 - (sigma**2 / team_i.sigma_squared) * delta / weight,
822+
1 - (new_sigma**2 / team_i.sigma_squared) * delta / w,
823823
self.kappa,
824-
),
824+
)
825825
)
826826

827827
modified_player = original_teams[i][j]
828-
modified_player.mu = mu
829-
modified_player.sigma = sigma
828+
modified_player.mu = new_mu
829+
modified_player.sigma = new_sigma
830830
intermediate_result_per_team.append(modified_player)
831831
result.append(intermediate_result_per_team)
832+
832833
return result
833834

834835
def predict_win(self, teams: List[List[BradleyTerryPartRating]]) -> List[float]:

openskill/models/weng_lin/plackett_luce.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -746,6 +746,12 @@ def _compute(
746746
sum_q = self._sum_q(team_ratings, c)
747747
a = self._a(team_ratings)
748748

749+
rank_groups: dict[int, list[int]] = {}
750+
for i, team_i in enumerate(team_ratings):
751+
if team_i.rank not in rank_groups:
752+
rank_groups[team_i.rank] = []
753+
rank_groups[team_i.rank].append(i)
754+
749755
result = []
750756
for i, team_i in enumerate(team_ratings):
751757
omega = 0.0
@@ -758,7 +764,7 @@ def _compute(
758764
delta += (
759765
i_mu_over_ce_over_sum_q * (1 - i_mu_over_ce_over_sum_q) / a[q]
760766
)
761-
if q == i:
767+
if team_q.rank == team_i.rank:
762768
omega += (1 - i_mu_over_ce_over_sum_q) / a[q]
763769
else:
764770
omega -= i_mu_over_ce_over_sum_q / a[q]
@@ -790,7 +796,6 @@ def _compute(
790796

791797
intermediate_result_per_team = []
792798
for j, j_players in enumerate(team_i.team):
793-
794799
if weights:
795800
weight = weights[i][j]
796801
else:
@@ -799,7 +804,7 @@ def _compute(
799804
mu = j_players.mu
800805
sigma = j_players.sigma
801806

802-
if omega > 0:
807+
if omega >= 0:
803808
mu += (sigma**2 / team_i.sigma_squared) * omega * weight
804809
sigma *= math.sqrt(
805810
max(
@@ -821,6 +826,16 @@ def _compute(
821826
modified_player.sigma = sigma
822827
intermediate_result_per_team.append(modified_player)
823828
result.append(intermediate_result_per_team)
829+
830+
for rank, indices in rank_groups.items():
831+
if len(indices) > 1:
832+
avg_mu_change = sum(
833+
result[i][0].mu - original_teams[i][0].mu for i in indices
834+
) / len(indices)
835+
for i in indices:
836+
for j in range(len(result[i])):
837+
result[i][j].mu = original_teams[i][j].mu + avg_mu_change
838+
824839
return result
825840

826841
def predict_win(self, teams: List[List[PlackettLuceRating]]) -> List[float]:

openskill/models/weng_lin/thurstone_mosteller_full.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -767,6 +767,12 @@ def _compute(
767767
team_ratings = self._calculate_team_ratings(teams, ranks=ranks)
768768
beta = self.beta
769769

770+
rank_groups: dict[int, list[int]] = {}
771+
for i, team_i in enumerate(team_ratings):
772+
if team_i.rank not in rank_groups:
773+
rank_groups[team_i.rank] = []
774+
rank_groups[team_i.rank].append(i)
775+
770776
result = []
771777
for i, team_i in enumerate(team_ratings):
772778
omega = 0.0
@@ -829,7 +835,6 @@ def _compute(
829835

830836
intermediate_result_per_team = []
831837
for j, j_players in enumerate(team_i.team):
832-
833838
if weights:
834839
weight = weights[i][j]
835840
else:
@@ -838,7 +843,7 @@ def _compute(
838843
mu = j_players.mu
839844
sigma = j_players.sigma
840845

841-
if omega > 0:
846+
if omega >= 0:
842847
mu += (sigma**2 / team_i.sigma_squared) * omega * weight
843848
sigma *= math.sqrt(
844849
max(
@@ -860,6 +865,16 @@ def _compute(
860865
modified_player.sigma = sigma
861866
intermediate_result_per_team.append(modified_player)
862867
result.append(intermediate_result_per_team)
868+
869+
for rank, indices in rank_groups.items():
870+
if len(indices) > 1:
871+
avg_mu_change = sum(
872+
result[i][0].mu - original_teams[i][0].mu for i in indices
873+
) / len(indices)
874+
for i in indices:
875+
for j in range(len(result[i])):
876+
result[i][j].mu = original_teams[i][j].mu + avg_mu_change
877+
863878
return result
864879

865880
def predict_win(

0 commit comments

Comments
 (0)