Skip to content

Commit

Permalink
Merge pull request #9 from poprl/feature-imports
Browse files Browse the repository at this point in the history
Feature imports
  • Loading branch information
manfreddiaz authored Aug 11, 2023
2 parents edb8915 + c3dfe9e commit b9d05d5
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 38 deletions.
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@
author_email='',
url='https://www.python.org/sigs/distutils-sig/',
package_dir={'': 'src'},
packages=['poprank'],
packages=['poprank', 'poprank.functional',
'poprank.functional._trueskill'],
)
25 changes: 13 additions & 12 deletions src/poprank/functional/_trueskill/factor_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
from scipy.stats import norm
from typing import Callable
from poprank import Rate
from typing import List

INF: float = float("inf")

Expand Down Expand Up @@ -178,8 +179,8 @@ def update_value(self, factor: "Factor", pi: float = 0,
class Factor():
"""A factor in the factor graph"""

def __init__(self, variables: list[Variable]) -> None:
self.variables: list[Variable] = variables
def __init__(self, variables: List[Variable]) -> None:
self.variables: List[Variable] = variables
for variable in variables:
variable.messages[self] = Gaussian()

Expand Down Expand Up @@ -235,22 +236,22 @@ def pass_message_up(self) -> float:
class SumFactor(Factor):

def __init__(self, sum_variable: Variable,
term_variables: list[Variable],
weights: list[int]):
term_variables: List[Variable],
weights: List[int]):
super(SumFactor, self).__init__([sum_variable] +
term_variables)
self.sum: Variable = sum_variable
self.terms: list[Variable] = term_variables
self.weights: list[int] = weights
self.terms: List[Variable] = term_variables
self.weights: List[int] = weights

def pass_message_down(self) -> float:
msgs: list[Gaussian] = [var.messages[self] for var
msgs: List[Gaussian] = [var.messages[self] for var
in self.terms]
return self.update(self.sum, self.terms, msgs, self.weights)

def pass_message_up(self, index: int = 0) -> float:
weight: float = self.weights[index]
weights: list[float] = []
weights: List[float] = []
for i, w in enumerate(self.weights):
weights.append(0. if weight == 0
else 1. / weight if i == index
Expand All @@ -260,8 +261,8 @@ def pass_message_up(self, index: int = 0) -> float:
msgs = [var.messages[self] for var in values]
return self.update(self.terms[index], values, msgs, weights)

def update(self, variable: Variable, values: list[Variable],
msgs: list[Gaussian], weights: list[float]) -> float:
def update(self, variable: Variable, values: List[Variable],
msgs: List[Gaussian], weights: List[float]) -> float:
pi_inv: float = 0
mu: float = 0
for value, msg, weight in zip(values, msgs, weights):
Expand Down Expand Up @@ -343,11 +344,11 @@ def w_draw(diff: float, draw_margin: float) -> float:
return (v ** 2) + (a * norm.pdf(a) - b * norm.pdf(b)) / denom


def flatten(array: list) -> list:
def flatten(array: List) -> List:
"""return a flattened copy of an array"""
new_array = []
for x in array:
if isinstance(x, list):
if isinstance(x, List):
new_array.extend(flatten(x))
else:
new_array.append(x)
Expand Down
33 changes: 28 additions & 5 deletions src/poprank/functional/elo.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

def elo(
players: "list[str]", interactions: "list[Interaction]",
elos: "list[EloRate]", k_factor: float, wdl: bool = False
elos: "list[EloRate]", k_factor: float = 20, wdl: bool = False
) -> "list[EloRate]":
"""Rates players by calculating their new elo after a set of interactions.
Expand Down Expand Up @@ -93,7 +93,7 @@ def elo(
loss_value=0)]
# New elo values
rates: "list[EloRate]" = \
[EloRate(e.mu + k_factor*(true_scores[i] - expected_scores[i]), 0)
[EloRate(e.mu + k_factor*(true_scores[i] - expected_scores[i]), e.std)
for i, e in enumerate(elos)]

return rates
Expand Down Expand Up @@ -578,6 +578,11 @@ def bayeselo(
list[EloRate]: The updated ratings of all players
"""

# This check is necessary, otherwise the algorithm raises a
# divide by 0 error
if len(interactions) == 0:
return elos

if len(players) != len(elos):
raise ValueError(f"Players and elos length mismatch\
: {len(players)} != {len(elos)}")
Expand All @@ -586,7 +591,11 @@ def bayeselo(
if not isinstance(elo, EloRate):
raise TypeError("elos must be of type list[EloRate]")

players_in_interactions = set()

for interaction in interactions:
players_in_interactions = \
players_in_interactions.union(interaction.players)
if len(interaction.players) != 2 or len(interaction.outcomes) != 2:
raise ValueError("Bayeselo only accepts interactions involving \
both a pair of players and a pair of outcomes")
Expand All @@ -608,14 +617,20 @@ def bayeselo(
spreads are not compatible (expected base {elo_base}, spread {elo_spread} but \
got base {e.base}, spread {e.spread})")

players_in_interactions = [p for p in players if
p in players_in_interactions]
elos_to_update = [e for e, p in zip(elos, players)
if p in players_in_interactions]

pairwise_stats: PopulationPairwiseStatistics = \
PopulationPairwiseStatistics.from_interactions(
players=players,
players=players_in_interactions,
interactions=interactions
)

bt: BayesEloRating = BayesEloRating(
pairwise_stats, elos, elo_draw=elo_draw, elo_advantage=elo_advantage,
pairwise_stats, elos=elos_to_update, elo_draw=elo_draw,
elo_advantage=elo_advantage,
base=elo_base, spread=elo_spread
)

Expand All @@ -630,4 +645,12 @@ def bayeselo(

bt.rescale_elos()

return bt.elos
new_elos = []
for i, p in enumerate(players):
if p in players_in_interactions:
new_elos.append(bt.elos[0])
bt.elos = bt.elos[1:]
else:
new_elos.append(elos[i])

return new_elos
37 changes: 19 additions & 18 deletions src/poprank/functional/glicko.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import Tuple
from popcore import Interaction
from poprank import GlickoRate, Glicko2Rate
from typing import List


def _compute_skill_improvement(
Expand Down Expand Up @@ -35,11 +36,11 @@ def _interaction_to_match_outcome(interaction: Interaction) -> Tuple[float]:


def _improvements_from_interactions(
players: list[str], ratings: list[GlickoRate],
interactions: list[Interaction]
players: List[str], ratings: List[GlickoRate],
interactions: List[Interaction]
):
skill_improvements: "list[float]" = [0. for p in players]
skill_variance: "list[float]" = [0. for p in players]
skill_improvements: "List[float]" = [0. for p in players]
skill_variance: "List[float]" = [0. for p in players]

for interaction in interactions:
id_player: int = players.index(interaction.players[0])
Expand Down Expand Up @@ -74,11 +75,11 @@ def _improvements_from_interactions(


def glicko(
players: "list[str]", interactions: "list[Interaction]",
ratings: "list[GlickoRate]", uncertainty_increase: float = 34.6,
players: "List[str]", interactions: "List[Interaction]",
ratings: "List[GlickoRate]", uncertainty_increase: float = 34.6,
rating_deviation_unrated: float = 350.0, base: float = 10.0,
spread: float = 400.0
) -> "list[GlickoRate]":
) -> "List[GlickoRate]":
"""Rates players by calculating their new glicko after a set of
interactions.
Expand All @@ -89,11 +90,11 @@ def glicko(
See also: :meth:`poprank.functional.glicko.glicko2`
Args:
players (list[str]): a list containing all unique player identifiers
interactions (list[Interaction]): a list containing the interactions to
players (List[str]): a list containing all unique player identifiers
interactions (List[Interaction]): a list containing the interactions to
get a rating from. Every interaction should be between exactly 2
players.
ratings (list[Glicko1Rate]): the initial ratings of the players.
ratings (List[Glicko1Rate]): the initial ratings of the players.
uncertainty_increase (float, optional): constant governing the
increase in uncerntainty between rating periods. Defaults to 34.6.
rating_deviation_unrated (float, optional): The rating deviation of
Expand All @@ -104,10 +105,10 @@ def glicko(
Defaults to 400.0.
Returns:
list[Glicko1Rate]: the updated ratings of all players
List[Glicko1Rate]: the updated ratings of all players
"""

new_ratings: "list[GlickoRate]" = []
new_ratings: "List[GlickoRate]" = []

# Update rating deviations
for rating in ratings:
Expand Down Expand Up @@ -145,11 +146,11 @@ def glicko(


def glicko2(
players: "list[str]", interactions: "list[Interaction]",
ratings: "list[Glicko2Rate]", rating_deviation_unrated: float = 350.0,
players: "List[str]", interactions: "List[Interaction]",
ratings: "List[Glicko2Rate]", rating_deviation_unrated: float = 350.0,
volatility_constraint: float = 0.5, epsilon: float = 1e-6,
unrated_player_rate: float = 1500.0, conversion_std: float = 173.7178
) -> "list[Glicko2Rate]":
) -> "List[Glicko2Rate]":

"""Rates players by calculating their new glicko2 after a set of
interactions.
Expand All @@ -161,11 +162,11 @@ def glicko2(
See also: :meth:`poprank.functional.glicko.glicko`
Args:
players (list[str]): a list containing all unique player identifiers
interactions (list[Interaction]): a list containing the interactions to
players (List[str]): a list containing all unique player identifiers
interactions (List[Interaction]): a list containing the interactions to
get a rating from. Every interaction should be between exactly 2
players.
ratings (list[Glicko2Rate]): the initial ratings of the players.
ratings (List[Glicko2Rate]): the initial ratings of the players.
RD_unrated (float, optional): The rating deviation of unrated players.
Defaults to 350.0.
tau (float, optional): Constant constraining the volatility over time.
Expand Down
13 changes: 11 additions & 2 deletions test/test_bayeselo.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def test_implementation_against_bayeselo(self):
with open(games_filepath, "r") as f:
games = json.load(f)

self.assertEquals(len(games), 7999) # Sanity check
self.assertEqual(len(games), 7999) # Sanity check

players = []
interactions = []
Expand Down Expand Up @@ -105,7 +105,7 @@ def test_implementation_full_scale(self):
actual_elos = [EloRate(x, 0) for x in expected_results_500k["ratings"]]
actual_ranking = expected_results_500k["actual_ranking"]

self.assertEquals(len(games), 549907) # Sanity check
self.assertEqual(len(games), 549907) # Sanity check

players = []
interactions = []
Expand Down Expand Up @@ -160,4 +160,13 @@ def test_loss(self):
self.assertListEqual(expected_results,
[round(x.mu) for x in results])

def test_no_interaction(self):
players = ["a", "b", "c"]
interactions = [Interaction(players=["a", "b"], outcomes=(0, 1))]
elos = [EloRate(0., 0.) for x in players]
results = bayeselo(players, interactions, elos)
expected_results = [-48, 48, 0]
self.assertListEqual(expected_results,
[round(x.mu) for x in results])

# TODO: Test that it works for players that already have a rating

0 comments on commit b9d05d5

Please sign in to comment.