From bd335a77c026b6d590e3ff9f6a4dcc12b7d42ba5 Mon Sep 17 00:00:00 2001 From: apiss2 Date: Fri, 21 Nov 2025 08:55:04 +0900 Subject: [PATCH 01/10] auto formatting by uv --- trueskillthroughtime/__init__.py | 623 +++++++++++++++++++------------ 1 file changed, 382 insertions(+), 241 deletions(-) diff --git a/trueskillthroughtime/__init__.py b/trueskillthroughtime/__init__.py index 15d255e..2e54f61 100644 --- a/trueskillthroughtime/__init__.py +++ b/trueskillthroughtime/__init__.py @@ -3,7 +3,7 @@ TrueskillThroughTime.py ~~~~~~~~~~~~~~~~~~~~~~~ -A Python implementation of the TrueSkill Through Time algorithm for +A Python implementation of the TrueSkill Through Time algorithm for estimating time-varying skill levels of players/agents in competitive games. This package supports: @@ -23,23 +23,33 @@ :license: BSD, see LICENSE for more details. """ +import copy import math - -inf = math.inf -sqrt2 = math.sqrt(2) -sqrt2pi = math.sqrt(2 * math.pi) -from scipy.stats import norm -from scipy.stats import truncnorm from collections import defaultdict -import math -from scipy.special import erfcinv as erfcinv_scipy -import scipy +from enum import Enum +from math import erfc + import numpy as np -from scipy.optimize import minimize -import copy +import scipy +from scipy.special import erfcinv +from scipy.stats import norm +__all__ = [ + "MU", + "SIGMA", + "Gaussian", + "N01", + "N00", + "Ninf", + "Nms", + "Player", + "Game", + "History", +] -__all__ = ['MU', 'SIGMA', 'Gaussian', 'N01', 'N00', 'Ninf', 'Nms', 'Player', 'Game', 'History' ] +inf = math.inf +sqrt2 = math.sqrt(2) +sqrt2pi = math.sqrt(2 * math.pi) MU = 0.0 SIGMA = 6 @@ -49,114 +59,116 @@ BETA = 1.0 # Performance noise std dev GAMMA = 0.03 # Skill drift rate -def erfc(x): - """Complementary error function.""" - return math.erfc(x) - -def erfcinv(y): - """Inverse complementary error function.""" - return erfcinv_scipy(y) def cdf(x, mu=0, sigma=1): """ Cumulative distribution function of a Gaussian distribution. - + Args: x: Value at which to evaluate the CDF mu: Mean of the distribution (default: 0) sigma: Standard deviation (default: 1) - + Returns: float: Probability that a sample from N(mu, sigma²) is less than or equal to x """ z = -(x - mu) / (sigma * sqrt2) - return (0.5 * erfc(z)) + return 0.5 * erfc(z) + def pdf(x, mu, sigma): """ Probability density function of a Gaussian distribution. - + Args: x: Value at which to evaluate the PDF mu: Mean of the distribution sigma: Standard deviation - + Returns: float: Probability density at x for N(mu, sigma²) """ - normalizer = (sqrt2pi * sigma)**-1 - functional = math.exp( -((x - mu)**2) / (2*sigma**2) ) + normalizer = (sqrt2pi * sigma) ** -1 + functional = math.exp(-((x - mu) ** 2) / (2 * sigma**2)) return normalizer * functional + def ppf(p, mu, sigma): """ Percent point function (inverse CDF) of a Gaussian distribution. - + Args: p: Probability value (between 0 and 1) mu: Mean of the distribution sigma: Standard deviation - + Returns: float: Value x such that P(X ≤ x) = p for X ~ N(mu, sigma²) """ - return mu - sigma * sqrt2 * erfcinv(2 * p) + return mu - sigma * sqrt2 * erfcinv(2 * p) + def v_w(mu, sigma, margin, tie): """ Compute correction factors v and w for truncated Gaussian moments. - + These factors are used in the expectation propagation approximation for updating beliefs based on win/loss/tie outcomes. - + Args: mu: Mean of the Gaussian sigma: Standard deviation margin: Draw margin (determines tie region) tie: Boolean indicating if the outcome is a tie - + Returns: tuple: (v, w) correction factors for mean and variance """ if not tie: - _alpha = (margin-mu)/sigma - v = pdf(-_alpha,0,1) / cdf(-_alpha,0,1) + _alpha = (margin - mu) / sigma + v = pdf(-_alpha, 0, 1) / cdf(-_alpha, 0, 1) w = v * (v + (-_alpha)) else: - _alpha = (-margin-mu)/sigma - _beta = ( margin-mu)/sigma - v = (pdf(_alpha,0,1)-pdf(_beta,0,1))/(cdf(_beta,0,1)-cdf(_alpha,0,1)) - u = (_alpha*pdf(_alpha,0,1)-_beta*pdf(_beta,0,1))/(cdf(_beta,0,1)-cdf(_alpha,0,1)) - w = - ( u - v**2 ) + _alpha = (-margin - mu) / sigma + _beta = (margin - mu) / sigma + v = (pdf(_alpha, 0, 1) - pdf(_beta, 0, 1)) / ( + cdf(_beta, 0, 1) - cdf(_alpha, 0, 1) + ) + u = (_alpha * pdf(_alpha, 0, 1) - _beta * pdf(_beta, 0, 1)) / ( + cdf(_beta, 0, 1) - cdf(_alpha, 0, 1) + ) + w = -(u - v**2) return v, w + def trunc(mu, sigma, margin, tie): """ Compute parameters of a truncated Gaussian distribution. - + Args: mu: Mean of the original Gaussian sigma: Standard deviation of the original Gaussian margin: Draw margin tie: Boolean indicating if the outcome is a tie - + Returns: tuple: (mu_trunc, sigma_trunc) parameters of the truncated distribution """ v, w = v_w(mu, sigma, margin, tie) mu_trunc = mu + sigma * v - sigma_trunc = sigma * math.sqrt(1-w) + sigma_trunc = sigma * math.sqrt(1 - w) return mu_trunc, sigma_trunc + def approx(N, margin, tie): """ Create a Gaussian approximation of a truncated distribution. - + Args: N: Gaussian object representing the prior distribution margin: Draw margin tie: Boolean indicating if the outcome is a tie - + Returns: Gaussian: Approximation of the truncated distribution """ @@ -164,32 +176,36 @@ def approx(N, margin, tie): return Gaussian(mu, sigma) -def fixed_point_approx(r: int, mu: float, sigma: float, max_iter: int = 16, tol: float = 1e-6) -> tuple[float, float]: +def fixed_point_approx( + r: int, mu: float, sigma: float, max_iter: int = 16, tol: float = 1e-6 +) -> tuple[float, float]: """ Gaussian approximation via fixed-point iteration for Poisson observations. - - Solves the fixed-point equations for the Gaussian approximation of p(d|r) - when the result r is a non-negative score r ~ Poisson(r|exp(d)) and the + + Solves the fixed-point equations for the Gaussian approximation of p(d|r) + when the result r is a non-negative score r ~ Poisson(r|exp(d)) and the difference of performance is Gaussian, d ~ N(d|mu, sigma²). - + Based on: Guo et al. (2012) "Score-based Bayesian skill learning" - + Args: r: Observed score difference (non-negative integer) mu: Prior mean of performance difference sigma: Prior standard deviation of performance difference max_iter: Maximum number of iterations (default: 16) tol: Convergence tolerance (default: 1e-6) - + Returns: tuple: (mu_new, sigma_new) parameters of the Gaussian approximation """ sigma2 = sigma**2 + def compute_kappa(k_prev): term = k_prev - mu - r * sigma2 - 1 - sqrt_term = math.sqrt(term**2 + 2*sigma2) - numerator = mu + r*sigma2 - 1 - k_prev + sqrt_term - return math.log(numerator/(2*sigma2)) + sqrt_term = math.sqrt(term**2 + 2 * sigma2) + numerator = mu + r * sigma2 - 1 - k_prev + sqrt_term + return math.log(numerator / (2 * sigma2)) + # # Initialize kappa kappa = 1 @@ -201,41 +217,43 @@ def compute_kappa(k_prev): kappa_new = compute_kappa(kappa) step = abs(kappa_new - kappa) kappa = kappa_new - #print(i, " ", step) + # print(i, " ", step) i += 1 # # Compute final mu_new and sigma2_new mu_new = mu + sigma2 * (r - math.exp(kappa)) sigma2_new = sigma2 / (1 + sigma2 * math.exp(kappa)) # - #print(mu_new, sigma2_new) + # print(mu_new, sigma2_new) return mu_new, math.sqrt(sigma2_new) + def compute_margin(p_draw, sd): """ Compute the draw margin based on draw probability. - + Args: p_draw: Probability of a draw (between 0 and 1) sd: Standard deviation of the performance difference - + Returns: float: Draw margin (positive value defining the tie region) """ - return abs(ppf(0.5-p_draw/2, 0.0, sd )) + return abs(ppf(0.5 - p_draw / 2, 0.0, sd)) + class Gaussian(object): """ Represents a Gaussian (normal) distribution with algebraic operations. - + This class implements a Gaussian distribution N(mu, sigma²) with support for addition, subtraction, multiplication, and division operations that correspond to common probabilistic operations. - + Attributes: mu (float): Mean of the distribution sigma (float): Standard deviation of the distribution - + Properties: pi (float): Precision (inverse variance) = 1/sigma² tau (float): Precision-adjusted mean = mu/sigma² @@ -244,11 +262,11 @@ class Gaussian(object): def __init__(self, mu=MU, sigma=SIGMA): """ Initialize a Gaussian distribution. - + Args: mu: Mean of the distribution (default: MU=0.0) sigma: Standard deviation (default: SIGMA=6) - + Raises: ValueError: If sigma is negative """ @@ -263,7 +281,7 @@ def __iter__(self): def __repr__(self): """String representation of the Gaussian.""" - return 'N(mu={:.6f}, sigma={:.6f})'.format(self.mu, self.sigma) + return "N(mu={:.6f}, sigma={:.6f})".format(self.mu, self.sigma) @property def pi(self): @@ -284,7 +302,7 @@ def tau(self): def __add__(self, M): """ Addition of independent Gaussians (convolution). - + If X ~ N(mu1, sigma1²) and Y ~ N(mu2, sigma2²) are independent, then X + Y ~ N(mu1+mu2, sigma1²+sigma2²) """ @@ -293,7 +311,7 @@ def __add__(self, M): def __sub__(self, M): """ Subtraction of independent Gaussians. - + If X ~ N(mu1, sigma1²) and Y ~ N(mu2, sigma2²) are independent, then X - Y ~ N(mu1-mu2, sigma1²+sigma2²) """ @@ -302,23 +320,23 @@ def __sub__(self, M): def __mul__(self, M): """ Multiplication of Gaussians or scalar multiplication. - + - Scalar multiplication: k*N(mu, sigma²) = N(k*mu, (k*sigma)²) - Gaussian multiplication (product of PDFs, normalized): N(mu1, sigma1²) * N(mu2, sigma2²) ∝ N(mu_new, sigma_new²) where precision-based formulas are used for numerical stability """ - if (type(M) == float) or (type(M) == int): + if isinstance(M, float) or isinstance(M, int): if M == inf: return Ninf else: - return Gaussian(M*self.mu, abs(M)*self.sigma) + return Gaussian(M * self.mu, abs(M) * self.sigma) if M.pi == 0: return self if self.pi == 0: return M _pi = self.pi + M.pi - return Gaussian((self.tau + M.tau) / _pi, _pi**(-1/2)) + return Gaussian((self.tau + M.tau) / _pi, _pi ** (-1 / 2)) def __rmul__(self, other): return self.__mul__(other) @@ -326,19 +344,19 @@ def __rmul__(self, other): def __truediv__(self, M): """ Division of Gaussians (removes the contribution of M from self). - + Used in message passing to compute marginals from joint distributions. Computed using precision: pi_new = pi_self - pi_M """ _pi = self.pi - M.pi - _sigma = inf if _pi == 0.0 else _pi**(-1/2) + _sigma = inf if _pi == 0.0 else _pi ** (-1 / 2) _mu = 0.0 if _pi == 0.0 else (self.tau - M.tau) / _pi return Gaussian(_mu, _sigma) def delta(self, M): """ Compute the difference between two Gaussians. - + Returns: tuple: (|mu1 - mu2|, |sigma1 - sigma2|) """ @@ -351,11 +369,11 @@ def cdf(self, x): def isapprox(self, M, tol=1e-5): """ Check if two Gaussians are approximately equal. - + Args: M: Another Gaussian object tol: Tolerance for comparison (default: 1e-5) - + Returns: bool: True if both mu and sigma are within tolerance """ @@ -365,10 +383,10 @@ def isapprox(self, M, tol=1e-5): def suma(Ns): """ Sum a list of independent Gaussian random variables. - + Args: Ns: List of Gaussian objects - + Returns: Gaussian: Sum of all Gaussians in the list """ @@ -381,10 +399,10 @@ def suma(Ns): def producto(Ns): """ Product of Gaussian PDFs (normalized). - + Args: Ns: List of Gaussian objects - + Returns: Gaussian: Product of all Gaussians in the list """ @@ -394,26 +412,28 @@ def producto(Ns): return res - N01 = Gaussian(0, 1) N00 = Gaussian(0, 0) Ninf = Gaussian(0, inf) Nms = Gaussian(MU, SIGMA) + class Player(object): """ Represents a player/agent with skill distribution and performance parameters. - + Attributes: prior (Gaussian): Player's skill distribution beta (float): Standard deviation of performance noise (within-game variability) gamma (float): Skill drift rate per time unit (between-game learning/decay) """ - def __init__(self, prior=Gaussian(MU, SIGMA), beta=BETA, gamma=GAMMA, prior_draw=Ninf): + def __init__( + self, prior=Gaussian(MU, SIGMA), beta=BETA, gamma=GAMMA, prior_draw=Ninf + ): """ Initialize a Player. - + Args: prior: Gaussian representing the player's skill (default: N(0, 6)) beta: Performance noise std dev (default: 1.0) @@ -423,10 +443,11 @@ def __init__(self, prior=Gaussian(MU, SIGMA), beta=BETA, gamma=GAMMA, prior_draw self.prior = prior self.beta = beta self.gamma = gamma + def performance(self): """ Generate a performance distribution: skill + noise. - + Returns: Gaussian: Performance ~ N(skill_mu, skill_sigma² + beta²) """ @@ -434,14 +455,18 @@ def performance(self): def __repr__(self): """String representation of the Player.""" - return 'Player(Gaussian(mu=%.3f, sigma=%.3f), beta=%.3f, gamma=%.3f)' % ( - self.prior.mu, self.prior.sigma, self.beta, self.gamma) + return "Player(Gaussian(mu=%.3f, sigma=%.3f), beta=%.3f, gamma=%.3f)" % ( + self.prior.mu, + self.prior.sigma, + self.beta, + self.gamma, + ) class team_variable(object): """ Internal class representing a team's performance variable in message passing. - + Attributes: prior: Team performance prior distribution likelihood_lose: Likelihood message from losing constraint @@ -456,7 +481,7 @@ def __init__(self, prior, likelihood_lose=Ninf, likelihood_win=Ninf): def __repr__(self): """String representation of the team variable.""" - return f'Team(prior={self.prior}, likelihood_lose={self.likelihood_lose}, likelihood_win={self.likelihood_win}' + return f"Team(prior={self.prior}, likelihood_lose={self.likelihood_lose}, likelihood_win={self.likelihood_win}" @property def p(self): @@ -482,7 +507,7 @@ def likelihood(self): class diff_messages(object): """ Internal class for performance difference variables in message passing. - + Attributes: prior: Prior distribution of performance difference likelihood: Likelihood message from outcome observation @@ -502,10 +527,10 @@ def p(self): class Game(object): """ Represents a single game/match with multiple teams and an outcome. - + This class performs Bayesian inference on player skills given a game result using expectation propagation with Gaussian message passing. - + Attributes: teams: List of teams, where each team is a list of Player objects result: Outcome for each team (lower is better, ties have equal values) @@ -514,7 +539,7 @@ class Game(object): obs: Observation model - "Ordinal" (ranking), "Continuous" (score), or "Discrete" (count) evidence: Marginal likelihood of the observed outcome likelihoods: Likelihood messages for each player's skill - + Example: >>> playerA = Player() >>> playerB = Player() @@ -528,10 +553,10 @@ class Game(object): def __init__(self, teams, result=[], p_draw=0.0, weights=[], obs="Ordinal"): """ Initialize a Game. - + Args: teams: List of teams, each team is a list of Player objects - result: List of outcomes (one per team). Lower is better. + result: List of outcomes (one per team). Lower is better. Equal values indicate ties. Default: decreasing order p_draw: Draw probability, between 0 and 1 (default: 0.0) weights: Player contribution weights (default: all 1.0) @@ -539,7 +564,9 @@ def __init__(self, teams, result=[], p_draw=0.0, weights=[], obs="Ordinal"): """ g = self g.teams = teams - g.result = result if len(result)==len(teams) else list(range(len(teams)-1,-1,-1)) + g.result = ( + result if len(result) == len(teams) else list(range(len(teams) - 1, -1, -1)) + ) if not weights: weights = [[1.0 for p in t] for t in teams] g.weights = weights @@ -547,8 +574,8 @@ def __init__(self, teams, result=[], p_draw=0.0, weights=[], obs="Ordinal"): g.o = g.orden() g.t = g.performance_teams() g.d = g.difference_teams() - if obs=="Ordinal": - g.tie = [g.result[g.o[e]]==g.result[g.o[e+1]] for e in range(len(g.d))] + if obs == "Ordinal": + g.tie = [g.result[g.o[e]] == g.result[g.o[e + 1]] for e in range(len(g.d))] g.margin = g.margin() else: g.tie = None @@ -559,7 +586,7 @@ def __init__(self, teams, result=[], p_draw=0.0, weights=[], obs="Ordinal"): def __repr__(self): """String representation of the Game.""" - return f'{self.teams}' + return f"{self.teams}" def __len__(self): """Return number of teams in the game.""" @@ -568,19 +595,23 @@ def __len__(self): def orden(self): """ Compute the ordering of teams by result (best to worst). - + Returns: list: Indices of teams sorted by result (descending order) """ - return [i[0] for i in sorted(enumerate(self.result), key=lambda x: x[1], reverse=True)] + return [ + i[0] + for i in sorted(enumerate(self.result), key=lambda x: x[1], reverse=True) + ] def margin(self): g = self res = [] for e in range(len(g.d)): - sd = math.sqrt(\ - sum([a.beta**2 for a in g.teams[g.o[e]]]) +\ - sum([a.beta**2 for a in g.teams[g.o[e+1]]])) + sd = math.sqrt( + sum([a.beta**2 for a in g.teams[g.o[e]]]) + + sum([a.beta**2 for a in g.teams[g.o[e + 1]]]) + ) compute_margin(g.p_draw, sd) res.append(0.0 if g.p_draw == 0.0 else compute_margin(g.p_draw, sd)) return res @@ -614,24 +645,36 @@ def partial_evidence(self, i_d): mu, sigma = g.d[i_d].prior if self.obs == "Ordinal": if g.tie[i_d]: - self.evidence *= cdf(g.margin[i_d], mu, sigma) - cdf(-g.margin[i_d], mu, sigma) + self.evidence *= cdf(g.margin[i_d], mu, sigma) - cdf( + -g.margin[i_d], mu, sigma + ) else: self.evidence *= 1 - cdf(g.margin[i_d], mu, sigma) elif self.obs == "Continuous": - self.evidence *= pdf(self.result[g.o[i_d]] - self.result[g.o[i_d + 1]], mu, sigma) + self.evidence *= pdf( + self.result[g.o[i_d]] - self.result[g.o[i_d + 1]], mu, sigma + ) elif self.obs == "Discrete": - r = self.result[g.o[i_d]]-self.result[g.o[i_d+1]] + r = self.result[g.o[i_d]] - self.result[g.o[i_d + 1]] # Monte Carlo Solution N = 5000 - hardcoded_lower_bound = 1/(2*N) - evidence = sum(r == scipy.stats.poisson.rvs(mu=np.exp(scipy.stats.norm.rvs(size=N,loc=mu,scale=sigma))))/N + hardcoded_lower_bound = 1 / (2 * N) + evidence = ( + sum( + r + == scipy.stats.poisson.rvs( + mu=np.exp(scipy.stats.norm.rvs(size=N, loc=mu, scale=sigma)) + ) + ) + / N + ) self.evidence *= hardcoded_lower_bound + evidence # # Version Guo et al: - #r=3 - #lmbda_i = math.exp(3) - #lmbda_j = math.exp(0) - #evidence = sum([math.exp(-(lmbda) * (lmbda)**(k/2) * np.i0(2*math.sqrt(lmbda)) for k in range(1,101)]) + # r=3 + # lmbda_i = math.exp(3) + # lmbda_j = math.exp(0) + # evidence = sum([math.exp(-(lmbda) * (lmbda)**(k/2) * np.i0(2*math.sqrt(lmbda)) for k in range(1,101)]) # def likelihood_difference(self, i_d): @@ -650,14 +693,18 @@ def likelihood_convergence(self): g = self for i in range(5): # Convergence iterations for e in range(len(g.d) - 1): - g.d[e].prior = g.t[g.o[e]].posterior_win - g.t[g.o[e + 1]].posterior_lose + g.d[e].prior = ( + g.t[g.o[e]].posterior_win - g.t[g.o[e + 1]].posterior_lose + ) if i == 0: g.partial_evidence(e) g.d[e].likelihood = g.likelihood_difference(e) likelihood_lose = g.t[g.o[e]].posterior_win - g.d[e].likelihood g.t[g.o[e + 1]].likelihood_lose = likelihood_lose for e in range(len(g.d) - 1, 0, -1): - g.d[e].prior = g.t[g.o[e]].posterior_win - g.t[g.o[e + 1]].posterior_lose + g.d[e].prior = ( + g.t[g.o[e]].posterior_win - g.t[g.o[e + 1]].posterior_lose + ) if i == 0 and e == len(g.d) - 1: g.partial_evidence(e) g.d[e].likelihood = g.likelihood_difference(e) @@ -684,7 +731,11 @@ def likelihood_performance(self): te = likelihood_teams[e] te_without_i = Gaussian( g.t[e].prior.mu - performance_individuals[e][i].mu, - math.sqrt(g.t[e].prior.sigma**2 - performance_individuals[e][i].sigma**2)) + math.sqrt( + g.t[e].prior.sigma ** 2 + - performance_individuals[e][i].sigma ** 2 + ), + ) w_i = g.weights[e][i] inv_w_i = inf if w_i == 0 else 1 / w_i res[-1].append(inv_w_i * (te - te_without_i)) @@ -710,14 +761,17 @@ def likelihood_analytic(self): res = [] for t in range(len(g.t)): res.append([]) - lose_case = (t == 1) + lose_case = t == 1 for i in range(len(g.teams[g.o[t]])): mu_i, sigma_i = g.teams[g.o[t]][i].prior w_i = g.weights[g.o[t]][i] inv_w_i = inf if w_i == 0 else 1 / w_i - mu_analytic = mu_i + inv_w_i * (-psi + psi_div) * (-1)**(lose_case) + mu_analytic = mu_i + inv_w_i * (-psi + psi_div) * (-1) ** (lose_case) sigma_analytic = math.sqrt( - (inv_w_i**2) * vartheta_div**2 + (inv_w_i**2) * vartheta**2 - sigma_i**2) + (inv_w_i**2) * vartheta_div**2 + + (inv_w_i**2) * vartheta**2 + - sigma_i**2 + ) res[-1].append(Gaussian(mu_analytic, sigma_analytic)) return [res[0], res[1]] if g.o[0] < g.o[1] else [res[1], res[0]] @@ -731,9 +785,9 @@ def likelihoods(self): def posteriors(self): """ Compute posterior skill distributions for all players. - + Combines each player's prior skill with the likelihood from the game outcome. - + Returns: list: Nested list of Gaussian posteriors, structured as [team][player] """ @@ -749,10 +803,10 @@ def posteriors(self): class Skill(object): """ Internal class representing a player's skill at a specific time point. - + Stores messages from forward/backward passes and likelihoods from games. Used internally by History for temporal inference. - + Attributes: bevents: Indices of games (within batch) this player participated in forward: Forward message from past games @@ -760,6 +814,7 @@ class Skill(object): likelihoods: List of likelihood messages from games at this time online: Online estimate (for online learning mode) """ + def __init__(self, bevents=[], forward=Ninf, backward=Ninf, likelihoods=[]): """Initialize a Skill variable.""" self.bevents = bevents @@ -770,7 +825,7 @@ def __init__(self, bevents=[], forward=Ninf, backward=Ninf, likelihoods=[]): def __repr__(self): """String representation of the Skill.""" - return f'Skill(events={self.bevents})' + return f"Skill(events={self.bevents})" @property def posterior(self): @@ -802,24 +857,23 @@ def update_likelihood(self, e, likelihood): def serialize(self): """Serialize skill object to dictionary.""" return { - 'bevents': self.bevents, - 'forward': (self.forward.mu, self.forward.sigma), - 'backward': (self.backward.mu, self.backward.sigma), - 'likelihoods': [(l.mu, l.sigma) for l in self.likelihoods], - 'online': None if not self.online else (self.online.mu, self.online.sigma) + "bevents": self.bevents, + "forward": (self.forward.mu, self.forward.sigma), + "backward": (self.backward.mu, self.backward.sigma), + "likelihoods": [(llhd.mu, llhd.sigma) for llhd in self.likelihoods], + "online": None if not self.online else (self.online.mu, self.online.sigma), } -from enum import Enum - class GameType(Enum): """ Enumeration of observation models for game outcomes. - + - Ordinal: Ranking/placement (win/loss/draw) - Continuous: Continuous scores (e.g., time, distance) - Discrete: Discrete counts (e.g., goals, points scored) """ + Ordinal = 0 Continuous = 1 Discrete = 2 @@ -828,11 +882,11 @@ class GameType(Enum): class History(object): """ Manages a sequence of games and performs temporal Bayesian skill inference. - + This is the main class for TrueSkill Through Time. It maintains a history of games and computes time-varying skill estimates for all players using Gaussian message passing with forward and backward iterations. - + Attributes: size: Total number of games batches: List of game compositions grouped by time @@ -846,7 +900,7 @@ class History(object): p_draw: Draw probability priors: Dictionary mapping player names to Player objects online: If True, use online learning mode - + Example: >>> composition = [[["a"], ["b"]], [["b"], ["c"]], [["c"], ["a"]]] >>> results = [[1, 2], [1, 2], [1, 2]] @@ -855,11 +909,24 @@ class History(object): >>> learning_curves = history.learning_curves() """ - def __init__(self, composition, results=[], times=[], priors=None, mu=0, sigma=3, beta=1, - gamma=0.15, p_draw=0.0, online=False, weights=[], obs=[]): + def __init__( + self, + composition, + results=[], + times=[], + priors=None, + mu=0, + sigma=3, + beta=1, + gamma=0.15, + p_draw=0.0, + online=False, + weights=[], + obs=[], + ): """ Initialize a History of games. - + Args: composition: List of games, where each game is a list of teams, and each team is a list of player names (strings) @@ -874,23 +941,41 @@ def __init__(self, composition, results=[], times=[], priors=None, mu=0, sigma=3 online: Enable online learning mode (default: False) weights: Player contribution weights for each game (default: all 1.0) obs: Observation model for each game - "Ordinal", "Continuous", or "Discrete" (default: all "Ordinal") - + Raises: ValueError: If input dimensions are inconsistent or parameters are invalid """ - self.check_input(composition, results, times, priors, mu, sigma, beta, gamma, p_draw, weights, obs) - + self.check_input( + composition, + results, + times, + priors, + mu, + sigma, + beta, + gamma, + p_draw, + weights, + obs, + ) + self.size = 0 - self.batches = []; self.bresults = []; self.btimes = [] - self.bskills = []; self.bweights = []; self.bobs = [] + self.batches = [] + self.bresults = [] + self.btimes = [] + self.bskills = [] + self.bweights = [] + self.bobs = [] self.bevidence = [] self.init_batches(composition, results, times, weights, obs) - self.mu = mu; self.sigma = sigma - self.beta = beta; self.gamma = gamma + self.mu = mu + self.sigma = sigma + self.beta = beta + self.gamma = gamma self.p_draw = p_draw self.priors = defaultdict( - lambda: Player(Gaussian(mu, sigma), beta, gamma), - priors if priors else {}) + lambda: Player(Gaussian(mu, sigma), beta, gamma), priors if priors else {} + ) self._last_message = None self._last_time = None self.online = online @@ -898,22 +983,46 @@ def __init__(self, composition, results=[], times=[], priors=None, mu=0, sigma=3 def __repr__(self): """String representation of the History.""" - return f'History(Events={self.size})' - - def check_input(self, composition, results, times, priors, mu, sigma, beta, gamma, p_draw, weights, obs): + return f"History(Events={self.size})" + + def check_input( + self, + composition, + results, + times, + priors, + mu, + sigma, + beta, + gamma, + p_draw, + weights, + obs, + ): """Validate input parameters.""" self.check_data(composition, results, times, priors, weights, obs) - if sigma < 0.0: raise ValueError("sigma < 0.0") - if beta < 0.0: raise ValueError("beta < 0.0") - if gamma < 0.0: raise ValueError("gamma < 0.0") - if p_draw < 0.0 or p_draw > 1.0 : raise ValueError("p_draw < 0.0 or p_draw > 1.0") + if sigma < 0.0: + raise ValueError("sigma < 0.0") + if beta < 0.0: + raise ValueError("beta < 0.0") + if gamma < 0.0: + raise ValueError("gamma < 0.0") + if p_draw < 0.0 or p_draw > 1.0: + raise ValueError("p_draw < 0.0 or p_draw > 1.0") + # def check_data(self, composition, results, times, priors, weights, obs): - if results and (len(composition) != len(results)): raise ValueError("len(composition) != len(results)") - if times and (len(composition) != len(times)): raise ValueError("len(composition) != len(times)") - if (not priors is None) and (not isinstance(priors, dict)): raise ValueError("not isinstance(priors, dict)") - if weights and (len(composition) != len(weights)): raise ValueError("len(composition) != len(weights)") - if obs and (len(composition) != len(obs)): raise ValueError("len(composition) != len(obs)") + if results and (len(composition) != len(results)): + raise ValueError("len(composition) != len(results)") + if times and (len(composition) != len(times)): + raise ValueError("len(composition) != len(times)") + if (priors is not None) and (not isinstance(priors, dict)): + raise ValueError("not isinstance(priors, dict)") + if weights and (len(composition) != len(weights)): + raise ValueError("len(composition) != len(weights)") + if obs and (len(composition) != len(obs)): + raise ValueError("len(composition) != len(obs)") + # def init_batches(self, composition, results, times, weights, obs): """Initialize batches from composition data.""" @@ -924,18 +1033,22 @@ def init_batches(self, composition, results, times, weights, obs): self.size += 1 t = times[i_e] if t != last_time: - e = 0 - self.btimes.append(t); self.batches.append([]) - self.bresults.append([]); self.bweights.append([]) - self.bskills.append({}); self.bobs.append([]) + self.btimes.append(t) + self.batches.append([]) + self.bresults.append([]) + self.bweights.append([]) + self.bskills.append({}) + self.bobs.append([]) self.bevidence.append([]) last_time = t self.add_to_batch(i_b, i_e, composition, results, times, weights, obs) - def add_history(self, composition, results=[], times=[], priors=None, weights=[], obs=[]): + def add_history( + self, composition, results=[], times=[], priors=None, weights=[], obs=[] + ): """ Add new games to an existing History. - + Args: composition: List of games to add results: List of outcomes for new games (default: empty) @@ -945,10 +1058,10 @@ def add_history(self, composition, results=[], times=[], priors=None, weights=[] obs: Observation models (default: all "Ordinal") """ self.check_data(composition, results, times, priors, weights, obs) - + # Merge priors dict (keeping first definition) self.priors = (priors if priors else {}) | self.priors - + last_t = self.btimes[-1] if self.btimes else 0 times = list(range(last_t, len(composition) + last_t)) if not times else times for i in range(len(composition)): @@ -957,9 +1070,12 @@ def add_history(self, composition, results=[], times=[], priors=None, weights=[] i_b = self.batches.index(times[i]) else: i_b = len(self.batches) - self.btimes.append(t); self.batches.append([]) - self.bresults.append([]); self.bweights.append([]) - self.bskills.append({}); self.bobs.append([]) + self.btimes.append(t) + self.batches.append([]) + self.bresults.append([]) + self.bweights.append([]) + self.bskills.append({}) + self.bobs.append([]) self.bevidence.append([]) self.add_to_batch(i_b, i, composition, results, times, weights, obs) @@ -968,9 +1084,11 @@ def add_to_batch(self, i_b, i, composition, results, times, weights, obs): self.batches[i_b].append(composition[i]) self.bresults[i_b].append(results[i] if results else []) self.bweights[i_b].append(weights[i] if weights else []) - self.bobs[i_b].append(GameType[obs[i]].value if obs else GameType["Ordinal"].value) + self.bobs[i_b].append( + GameType[obs[i]].value if obs else GameType["Ordinal"].value + ) self.bevidence[i_b].append(None) - + e = len(self.batches[i_b]) - 1 for team in composition[i]: for name in team: @@ -988,7 +1106,8 @@ def _in_skills(self, b, forward): elapsed = abs(h.btimes[b] - old_t) if (old_t is not None) else 0 gamma = h.priors[name].gamma receive = h._last_message[name] + Gaussian( - 0, min(math.sqrt(elapsed * (gamma**2)), 1.67 * self.sigma)) + 0, min(math.sqrt(elapsed * (gamma**2)), 1.67 * self.sigma) + ) if forward: h.bskills[b][name].forward = receive if h.online and not h.bskills[b][name].online: @@ -999,8 +1118,13 @@ def _in_skills(self, b, forward): def _up_skills(self, b, e): """Update skills based on a single game within a batch.""" h = self - g = Game(h.within_priors(b, e), h.bresults[b][e], h.p_draw, h.bweights[b][e], - obs=GameType(self.bobs[b][e]).name) + g = Game( + h.within_priors(b, e), + h.bresults[b][e], + h.p_draw, + h.bweights[b][e], + obs=GameType(self.bobs[b][e]).name, + ) likelihoods = g.likelihoods if self.online and not self.bevidence[b][e]: self.bevidence[b][e] = g.evidence @@ -1010,7 +1134,9 @@ def _up_skills(self, b, e): for t in range(len(h.batches[b][e])): for i in range(len(h.batches[b][e][t])): name = h.batches[b][e][t][i] - mu_step, sigma_step = h.bskills[b][name].update_likelihood(e, likelihoods[t][i]) + mu_step, sigma_step = h.bskills[b][name].update_likelihood( + e, likelihoods[t][i] + ) mu_step_max = max(mu_step_max, mu_step) sigma_step_max = max(sigma_step_max, sigma_step) return (mu_step_max, sigma_step_max) @@ -1034,56 +1160,64 @@ def within_priors(self, b, e, online=False): for i in range(len(h.batches[b][e][t])): name = h.batches[b][e][t][i] if not online: - prior = h.bskills[b][name].posterior / h.bskills[b][name].likelihood(e) + prior = h.bskills[b][name].posterior / h.bskills[b][ + name + ].likelihood(e) else: prior = h.bskills[b][name].online - priors[-1].append(Player(prior, h.priors[name].beta, h.priors[name].gamma)) + priors[-1].append( + Player(prior, h.priors[name].beta, h.priors[name].gamma) + ) return priors def forward_propagation(self): """ Forward pass: propagate skill estimates from past to future. - + Processes games chronologically, updating beliefs about player skills as we move forward in time. - + Returns: - tuple: (max_mu_step, max_sigma_step) - maximum likelihood change + tuple: (max_mu_step, max_sigma_step) - maximum likelihood change """ h = self h._last_message = defaultdict( - lambda: Gaussian(h.mu, h.sigma), - {k: v.prior for k, v in h.priors.items()}) + lambda: Gaussian(h.mu, h.sigma), {k: v.prior for k, v in h.priors.items()} + ) h._last_time = defaultdict(lambda: None) - mu_step_max = 0; sigma_step_max = 0; + mu_step_max = 0 + sigma_step_max = 0 for b in range(h.b_until): h._in_skills(b, forward=True) for e in range(len(h.batches[b])): mu_step, sigma_step = h._up_skills(b, e) - mu_step_max = max(mu_step_max, mu_step); sigma_step_max = max(sigma_step_max, sigma_step) + mu_step_max = max(mu_step_max, mu_step) + sigma_step_max = max(sigma_step_max, sigma_step) h._out_skills(b, forward=True) return (mu_step_max, sigma_step_max) def backward_propagation(self): """ Backward pass: propagate skill estimates from future to past. - + Processes games in reverse chronological order, updating beliefs about player skills based on future information. - + Returns: tuple: (max_mu_step, max_sigma_step) - maximum likelihood change """ h = self h._last_message = defaultdict(lambda: Gaussian(0.0, math.inf)) h._last_time = defaultdict(lambda: None) - mu_step_max = 0; sigma_step_max = 0; - h._out_skills(h.b_until-1, forward=False) - for b in reversed(range(h.b_until-1)): + mu_step_max = 0 + sigma_step_max = 0 + h._out_skills(h.b_until - 1, forward=False) + for b in reversed(range(h.b_until - 1)): h._in_skills(b, forward=False) for e in range(len(h.batches[b])): mu_step, sigma_step = h._up_skills(b, e) - mu_step_max = max(mu_step_max, mu_step); sigma_step_max = max(sigma_step_max, sigma_step) + mu_step_max = max(mu_step_max, mu_step) + sigma_step_max = max(sigma_step_max, sigma_step) h._out_skills(b, forward=False) h._last_message = None h._last_time = None @@ -1092,7 +1226,7 @@ def backward_propagation(self): def iteration(self): """ Perform one complete iteration: forward pass followed by backward pass. - + Returns: tuple: (max_mu_step, max_sigma_step) - maximum likelihood change across both passes """ @@ -1103,30 +1237,35 @@ def iteration(self): def convergence(self, iterations=8, epsilon=0.00001, verbose=True): """ Run iterative message passing until convergence. - + Performs forward and backward passes, iterating until convergence or maximum iterations reached. In online mode, processes batches sequentially. - + Args: iterations: Maximum number of iterations per batch (default: 8) epsilon: Convergence threshold for message updates (default: 1e-5) verbose: Print iteration progress (default: True) - + Returns: tuple: (final_step, num_iterations) - convergence metrics """ i = 0 delta = math.inf self.unveil_batch() - for _ in range(1+len(self.batches)-self.b_until): - i = 0; delta = math.inf - if verbose: print("Batch = ", self.b_until) + for _ in range(1 + len(self.batches) - self.b_until): + i = 0 + delta = math.inf + if verbose: + print("Batch = ", self.b_until) while i < iterations and delta > epsilon: - if verbose: print("Iteration = ", i, end=" ") - step = self.iteration(); delta = max(step) + if verbose: + print("Iteration = ", i, end=" ") + step = self.iteration() + delta = max(step) i += 1 - if verbose: print(", step = ", step) + if verbose: + print(", step = ", step) self.unveil_batch() return step, i @@ -1138,14 +1277,14 @@ def unveil_batch(self): def learning_curves(self, who=None, online=False): """ Extract learning curves (skill over time) for players. - + Args: who: List of player names to extract (default: None, returns all) online: Use online estimates instead of batch posteriors (default: False) - + Returns: dict: Maps player names to lists of (time, skill) tuples - + Example: >>> lc = history.learning_curves(who=["alice", "bob"]) >>> for time, skill in lc["alice"]: @@ -1171,45 +1310,52 @@ def learning_curves(self, who=None, online=False): def log_evidence(self): """ Compute the log marginal likelihood of all observed game outcomes. - + This is the log probability of the observed results given the model. Useful for model comparison and hyperparameter tuning. - + Returns: float: Sum of log evidences across all games """ - return sum([math.log(evidence) for bevidence in self.bevidence - for evidence in bevidence if evidence]) + return sum( + [ + math.log(evidence) + for bevidence in self.bevidence + for evidence in bevidence + if evidence + ] + ) def geometric_mean(self): """ Compute the geometric mean of game outcome probabilities. - + Returns: float: exp(log_evidence / num_games) - average probability per game """ - unveil_size = sum([not (e is None) for bevidence in self.bevidence - for e in bevidence]) + unveil_size = sum( + [(e is not None) for bevidence in self.bevidence for e in bevidence] + ) return math.exp(self.log_evidence() / unveil_size) def __getstate__(self): """ Prepare History for pickling (serialization). - + Returns: dict: Serializable state dictionary """ state = copy.deepcopy(self.__dict__.copy()) # Convert defaultdict to regular dict - state['priors'] = dict(self.priors) + state["priors"] = dict(self.priors) # Remove unpickleable lambda function - del state['_last_message'] - del state['_last_time'] + del state["_last_message"] + del state["_last_time"] # Convert all Skill objects to their serializable form - for bskill in state['bskills']: + for bskill in state["bskills"]: for name in bskill: bskill[name] = bskill[name].serialize() return state @@ -1217,53 +1363,48 @@ def __getstate__(self): def __setstate__(self, state): """ Restore History from pickled state (deserialization). - + Args: state: Serialized state dictionary """ # Restore defaultdict with proper default factory - priors_dict = state['priors'] - state['priors'] = defaultdict( - lambda: Player(Gaussian(state['mu'], state['sigma']), - state['beta'], - state['gamma']), - priors_dict + priors_dict = state["priors"] + state["priors"] = defaultdict( + lambda: Player( + Gaussian(state["mu"], state["sigma"]), state["beta"], state["gamma"] + ), + priors_dict, ) # Restore Skill objects - for bskill in state['bskills']: + for bskill in state["bskills"]: for name in bskill: skill_dict = bskill[name] bskill[name] = Skill( - bevents=skill_dict['bevents'], - forward=Gaussian(*skill_dict['forward']), - backward=Gaussian(*skill_dict['backward']), - likelihoods=[Gaussian(*l) for l in skill_dict['likelihoods']] + bevents=skill_dict["bevents"], + forward=Gaussian(*skill_dict["forward"]), + backward=Gaussian(*skill_dict["backward"]), + likelihoods=[Gaussian(*llhd) for llhd in skill_dict["likelihoods"]], ) - if skill_dict['online'] is not None: - bskill[name].online = Gaussian(*skill_dict['online']) + if skill_dict["online"] is not None: + bskill[name].online = Gaussian(*skill_dict["online"]) # Restore None values for unpickleable attributes - state['_last_message'] = None - state['_last_time'] = None + state["_last_message"] = None + state["_last_time"] = None self.__dict__.update(state) + def orden(xs, reverse=True): """ Return indices that sort a list. - + Args: xs: List to sort reverse: Sort in descending order (default: True) - + Returns: list: Indices of sorted elements """ return [i[0] for i in sorted(enumerate(xs), key=lambda x: x[1], reverse=reverse)] - - - - - - From 8e70c263659ade411d2160cf9b069bb2332bfdfb Mon Sep 17 00:00:00 2001 From: apiss2 Date: Fri, 21 Nov 2025 23:49:42 +0900 Subject: [PATCH 02/10] add typing --- trueskillthroughtime/__init__.py | 715 +++++++++++++++++-------------- 1 file changed, 382 insertions(+), 333 deletions(-) diff --git a/trueskillthroughtime/__init__.py b/trueskillthroughtime/__init__.py index 2e54f61..bcbadf3 100644 --- a/trueskillthroughtime/__init__.py +++ b/trueskillthroughtime/__init__.py @@ -28,11 +28,11 @@ from collections import defaultdict from enum import Enum from math import erfc +from typing import Any, Iterator, Sequence import numpy as np -import scipy from scipy.special import erfcinv -from scipy.stats import norm +from scipy.stats import norm, poisson __all__ = [ "MU", @@ -45,6 +45,7 @@ "Player", "Game", "History", + "GameType", ] inf = math.inf @@ -53,14 +54,11 @@ MU = 0.0 SIGMA = 6 -PI = SIGMA**-2 -TAU = PI * MU - BETA = 1.0 # Performance noise std dev GAMMA = 0.03 # Skill drift rate -def cdf(x, mu=0, sigma=1): +def cdf(x: float, mu: float = 0.0, sigma: float = 1.0) -> float: """ Cumulative distribution function of a Gaussian distribution. @@ -76,7 +74,7 @@ def cdf(x, mu=0, sigma=1): return 0.5 * erfc(z) -def pdf(x, mu, sigma): +def pdf(x: float, mu: float, sigma: float) -> float: """ Probability density function of a Gaussian distribution. @@ -93,7 +91,7 @@ def pdf(x, mu, sigma): return normalizer * functional -def ppf(p, mu, sigma): +def ppf(p: float, mu: float, sigma: float) -> float: """ Percent point function (inverse CDF) of a Gaussian distribution. @@ -108,7 +106,7 @@ def ppf(p, mu, sigma): return mu - sigma * sqrt2 * erfcinv(2 * p) -def v_w(mu, sigma, margin, tie): +def v_w(mu: float, sigma: float, margin: float, tie: bool) -> tuple[float, float]: """ Compute correction factors v and w for truncated Gaussian moments. @@ -141,7 +139,7 @@ def v_w(mu, sigma, margin, tie): return v, w -def trunc(mu, sigma, margin, tie): +def trunc(mu: float, sigma: float, margin: float, tie: bool) -> tuple[float, float]: """ Compute parameters of a truncated Gaussian distribution. @@ -160,7 +158,7 @@ def trunc(mu, sigma, margin, tie): return mu_trunc, sigma_trunc -def approx(N, margin, tie): +def approx(N: "Gaussian", margin: float, tie: bool) -> "Gaussian": """ Create a Gaussian approximation of a truncated distribution. @@ -177,7 +175,7 @@ def approx(N, margin, tie): def fixed_point_approx( - r: int, mu: float, sigma: float, max_iter: int = 16, tol: float = 1e-6 + r: float, mu: float, sigma: float, max_iter: int = 16, tol: float = 1e-6 ) -> tuple[float, float]: """ Gaussian approximation via fixed-point iteration for Poisson observations. @@ -200,7 +198,7 @@ def fixed_point_approx( """ sigma2 = sigma**2 - def compute_kappa(k_prev): + def compute_kappa(k_prev: float) -> float: term = k_prev - mu - r * sigma2 - 1 sqrt_term = math.sqrt(term**2 + 2 * sigma2) numerator = mu + r * sigma2 - 1 - k_prev + sqrt_term @@ -208,7 +206,7 @@ def compute_kappa(k_prev): # # Initialize kappa - kappa = 1 + kappa = 1.0 # # Fixed-point iteration i = 0 @@ -228,7 +226,7 @@ def compute_kappa(k_prev): return mu_new, math.sqrt(sigma2_new) -def compute_margin(p_draw, sd): +def compute_margin(p_draw: float, sd: float) -> float: """ Compute the draw margin based on draw probability. @@ -259,7 +257,7 @@ class Gaussian(object): tau (float): Precision-adjusted mean = mu/sigma² """ - def __init__(self, mu=MU, sigma=SIGMA): + def __init__(self, mu: float = MU, sigma: float = SIGMA) -> None: """ Initialize a Gaussian distribution. @@ -275,16 +273,16 @@ def __init__(self, mu=MU, sigma=SIGMA): else: raise ValueError("sigma should be greater than 0") - def __iter__(self): + def __iter__(self) -> Iterator[float]: """Allow unpacking: mu, sigma = gaussian_obj""" return iter((self.mu, self.sigma)) - def __repr__(self): + def __repr__(self) -> str: """String representation of the Gaussian.""" return "N(mu={:.6f}, sigma={:.6f})".format(self.mu, self.sigma) @property - def pi(self): + def pi(self) -> float: """Precision: 1/sigma² (returns inf if sigma=0)""" if self.sigma > 0.0: return 1 / self.sigma**2 @@ -292,14 +290,14 @@ def pi(self): return inf @property - def tau(self): + def tau(self) -> float: """Precision-adjusted mean: mu/sigma² (returns 0 if sigma=0)""" if self.sigma > 0.0: return self.mu * self.pi else: - return 0 + return 0.0 - def __add__(self, M): + def __add__(self, M: "Gaussian") -> "Gaussian": """ Addition of independent Gaussians (convolution). @@ -308,7 +306,7 @@ def __add__(self, M): """ return Gaussian(self.mu + M.mu, math.sqrt(self.sigma**2 + M.sigma**2)) - def __sub__(self, M): + def __sub__(self, M: "Gaussian") -> "Gaussian": """ Subtraction of independent Gaussians. @@ -317,7 +315,7 @@ def __sub__(self, M): """ return Gaussian(self.mu - M.mu, math.sqrt(self.sigma**2 + M.sigma**2)) - def __mul__(self, M): + def __mul__(self, M: "Gaussian" | float | int) -> "Gaussian": """ Multiplication of Gaussians or scalar multiplication. @@ -338,10 +336,10 @@ def __mul__(self, M): _pi = self.pi + M.pi return Gaussian((self.tau + M.tau) / _pi, _pi ** (-1 / 2)) - def __rmul__(self, other): + def __rmul__(self, other: "Gaussian") -> "Gaussian": return self.__mul__(other) - def __truediv__(self, M): + def __truediv__(self, M: "Gaussian") -> "Gaussian": """ Division of Gaussians (removes the contribution of M from self). @@ -353,7 +351,7 @@ def __truediv__(self, M): _mu = 0.0 if _pi == 0.0 else (self.tau - M.tau) / _pi return Gaussian(_mu, _sigma) - def delta(self, M): + def delta(self, M: "Gaussian") -> tuple[float, float]: """ Compute the difference between two Gaussians. @@ -362,11 +360,11 @@ def delta(self, M): """ return abs(self.mu - M.mu), abs(self.sigma - M.sigma) - def cdf(self, x): + def cdf(self, x: float) -> float: """Evaluate CDF at x using scipy.stats.norm""" return norm(*self).cdf(x) - def isapprox(self, M, tol=1e-5): + def isapprox(self, M: "Gaussian", tol: float = 1e-5) -> bool: """ Check if two Gaussians are approximately equal. @@ -380,7 +378,7 @@ def isapprox(self, M, tol=1e-5): return (abs(self.mu - M.mu) < tol) and (abs(self.sigma - M.sigma) < tol) -def suma(Ns): +def suma(Ns: list[Gaussian]) -> Gaussian: """ Sum a list of independent Gaussian random variables. @@ -396,7 +394,7 @@ def suma(Ns): return res -def producto(Ns): +def producto(Ns: list[Gaussian]) -> Gaussian: """ Product of Gaussian PDFs (normalized). @@ -429,7 +427,11 @@ class Player(object): """ def __init__( - self, prior=Gaussian(MU, SIGMA), beta=BETA, gamma=GAMMA, prior_draw=Ninf + self, + prior: Gaussian = Gaussian(MU, SIGMA), + beta: float = BETA, + gamma: float = GAMMA, + prior_draw: Gaussian = Ninf, ): """ Initialize a Player. @@ -444,7 +446,7 @@ def __init__( self.beta = beta self.gamma = gamma - def performance(self): + def performance(self) -> Gaussian: """ Generate a performance distribution: skill + noise. @@ -453,7 +455,7 @@ def performance(self): """ return self.prior + Gaussian(0, self.beta) - def __repr__(self): + def __repr__(self) -> str: """String representation of the Player.""" return "Player(Gaussian(mu=%.3f, sigma=%.3f), beta=%.3f, gamma=%.3f)" % ( self.prior.mu, @@ -473,33 +475,38 @@ class team_variable(object): likelihood_win: Likelihood message from winning constraint """ - def __init__(self, prior, likelihood_lose=Ninf, likelihood_win=Ninf): + def __init__( + self, + prior: Gaussian, + likelihood_lose: Gaussian = Ninf, + likelihood_win: Gaussian = Ninf, + ) -> None: """Initialize team variable with prior and likelihood messages.""" self.prior = prior self.likelihood_lose = likelihood_lose self.likelihood_win = likelihood_win - def __repr__(self): + def __repr__(self) -> str: """String representation of the team variable.""" return f"Team(prior={self.prior}, likelihood_lose={self.likelihood_lose}, likelihood_win={self.likelihood_win}" @property - def p(self): + def p(self) -> Gaussian: """Full posterior: prior * all likelihoods""" return self.prior * self.likelihood_lose * self.likelihood_win @property - def posterior_win(self): + def posterior_win(self) -> Gaussian: """Posterior after incorporating lose constraint""" return self.prior * self.likelihood_lose @property - def posterior_lose(self): + def posterior_lose(self) -> Gaussian: """Posterior after incorporating win constraint""" return self.prior * self.likelihood_win @property - def likelihood(self): + def likelihood(self) -> Gaussian: """Combined likelihood from both constraints""" return self.likelihood_win * self.likelihood_lose @@ -513,17 +520,31 @@ class diff_messages(object): likelihood: Likelihood message from outcome observation """ - def __init__(self, prior, likelihood=Ninf): + def __init__(self, prior: Gaussian, likelihood: Gaussian = Ninf) -> None: """Initialize difference message with prior and likelihood.""" self.prior = prior self.likelihood = likelihood @property - def p(self): + def p(self) -> Gaussian: """Posterior: prior * likelihood""" return self.prior * self.likelihood +class GameType(Enum): + """ + Enumeration of observation models for game outcomes. + + - Ordinal: Ranking/placement (win/loss/draw) + - Continuous: Continuous scores (e.g., time, distance) + - Discrete: Discrete counts (e.g., goals, points scored) + """ + + Ordinal = 0 + Continuous = 1 + Discrete = 2 + + class Game(object): """ Represents a single game/match with multiple teams and an outcome. @@ -550,7 +571,14 @@ class Game(object): >>> posteriors = game.posteriors() """ - def __init__(self, teams, result=[], p_draw=0.0, weights=[], obs="Ordinal"): + def __init__( + self, + teams: list[list[Player]], + result: list[int | float] = [], + p_draw: float = 0.0, + weights: list[list[float]] = [], + obs: GameType = GameType.Ordinal, + ): """ Initialize a Game. @@ -562,112 +590,95 @@ def __init__(self, teams, result=[], p_draw=0.0, weights=[], obs="Ordinal"): weights: Player contribution weights (default: all 1.0) obs: Observation model - "Ordinal", "Continuous", or "Discrete" (default: "Ordinal") """ - g = self - g.teams = teams - g.result = ( + self.teams = teams + self.result = ( result if len(result) == len(teams) else list(range(len(teams) - 1, -1, -1)) ) if not weights: weights = [[1.0 for p in t] for t in teams] - g.weights = weights - g.p_draw = p_draw - g.o = g.orden() - g.t = g.performance_teams() - g.d = g.difference_teams() - if obs == "Ordinal": - g.tie = [g.result[g.o[e]] == g.result[g.o[e + 1]] for e in range(len(g.d))] - g.margin = g.margin() - else: - g.tie = None - g.margin = None - g.obs = obs - g.evidence = 1.0 - g.likelihoods = self.likelihoods() + self.weights = weights + self.p_draw = p_draw + self.o = orden(self.result, reverse=True) + self.t = self.performance_teams() + self.d = self.difference_teams() + self.tie: list[bool] | None = None + self.margin: list[float] | None = None + if obs == GameType.Ordinal: + self.tie = [ + self.result[self.o[e]] == self.result[self.o[e + 1]] + for e in range(len(self.d)) + ] + self.margin = self.get_margin() + self.obs = obs + self.evidence = 1.0 + self.likelihoods = self.get_likelihoods() - def __repr__(self): + def __repr__(self) -> str: """String representation of the Game.""" return f"{self.teams}" - def __len__(self): + def __len__(self) -> int: """Return number of teams in the game.""" return len(self.teams) - def orden(self): - """ - Compute the ordering of teams by result (best to worst). - - Returns: - list: Indices of teams sorted by result (descending order) - """ - return [ - i[0] - for i in sorted(enumerate(self.result), key=lambda x: x[1], reverse=True) - ] - - def margin(self): - g = self + def get_margin(self) -> list[float]: res = [] - for e in range(len(g.d)): + for e in range(len(self.d)): sd = math.sqrt( - sum([a.beta**2 for a in g.teams[g.o[e]]]) - + sum([a.beta**2 for a in g.teams[g.o[e + 1]]]) + sum([a.beta**2 for a in self.teams[self.o[e]]]) + + sum([a.beta**2 for a in self.teams[self.o[e + 1]]]) ) - compute_margin(g.p_draw, sd) - res.append(0.0 if g.p_draw == 0.0 else compute_margin(g.p_draw, sd)) + compute_margin(self.p_draw, sd) + res.append(0.0 if self.p_draw == 0.0 else compute_margin(self.p_draw, sd)) return res - def performance_individuals(self): + def performance_individuals(self) -> list[list[Gaussian]]: # Generate individual performances by adding noise to skills - res = [] + res: list[list[Gaussian]] = [] for t in range(len(self.teams)): res.append([]) # Team container for i in range(len(self.teams[t])): - res[-1].append(self.teams[t][i].performance() * (self.weights[t][i])) + res[-1].append(self.teams[t][i].performance() * self.weights[t][i]) return res - def performance_teams(self): + def performance_teams(self) -> list[team_variable]: # Sum of individual performances res = [] for team in self.performance_individuals(): res.append(team_variable(suma(team))) return res - def difference_teams(self): - g = self - res = [] - for e in range(len(g) - 1): - res.append(diff_messages(g.t[g.o[e]].prior - g.t[g.o[e + 1]].prior)) + def difference_teams(self) -> list[diff_messages]: + res: list[diff_messages] = [] + for e in range(len(self) - 1): + res.append( + diff_messages(self.t[self.o[e]].prior - self.t[self.o[e + 1]].prior) + ) return res - def partial_evidence(self, i_d): + def partial_evidence(self, i_d: int) -> None: """Compute partial evidence for a difference variable.""" - g = self - mu, sigma = g.d[i_d].prior - if self.obs == "Ordinal": - if g.tie[i_d]: - self.evidence *= cdf(g.margin[i_d], mu, sigma) - cdf( - -g.margin[i_d], mu, sigma + mu, sigma = self.d[i_d].prior + if self.obs == GameType.Ordinal: + assert self.tie is not None + assert self.margin is not None + if self.tie[i_d]: + self.evidence *= cdf(self.margin[i_d], mu, sigma) - cdf( + -self.margin[i_d], mu, sigma ) else: - self.evidence *= 1 - cdf(g.margin[i_d], mu, sigma) - elif self.obs == "Continuous": + self.evidence *= 1 - cdf(self.margin[i_d], mu, sigma) + elif self.obs == GameType.Continuous: self.evidence *= pdf( - self.result[g.o[i_d]] - self.result[g.o[i_d + 1]], mu, sigma + self.result[self.o[i_d]] - self.result[self.o[i_d + 1]], mu, sigma ) - elif self.obs == "Discrete": - r = self.result[g.o[i_d]] - self.result[g.o[i_d + 1]] + elif self.obs == GameType.Discrete: + r = self.result[self.o[i_d]] - self.result[self.o[i_d + 1]] # Monte Carlo Solution N = 5000 hardcoded_lower_bound = 1 / (2 * N) - evidence = ( - sum( - r - == scipy.stats.poisson.rvs( - mu=np.exp(scipy.stats.norm.rvs(size=N, loc=mu, scale=sigma)) - ) - ) - / N - ) + poisson_rvs = poisson.rvs(mu=np.exp(norm.rvs(size=N, loc=mu, scale=sigma))) + evidence = np.sum(r == poisson_rvs) / N self.evidence *= hardcoded_lower_bound + evidence # # Version Guo et al: @@ -677,73 +688,87 @@ def partial_evidence(self, i_d): # evidence = sum([math.exp(-(lmbda) * (lmbda)**(k/2) * np.i0(2*math.sqrt(lmbda)) for k in range(1,101)]) # - def likelihood_difference(self, i_d): - g = self - if g.obs == "Ordinal": - return approx(g.d[i_d].prior, g.margin[i_d], g.tie[i_d]) / g.d[i_d].prior - elif g.obs == "Continuous": - return Gaussian(self.result[g.o[i_d]] - self.result[g.o[i_d + 1]], 0.0) - elif g.obs == "Discrete": - r = self.result[g.o[i_d]] - self.result[g.o[i_d + 1]] - mu, sigma = g.d[i_d].prior - return Gaussian(*fixed_point_approx(r, mu, sigma)) / g.d[i_d].prior - - def likelihood_convergence(self): + def likelihood_difference(self, i_d: int) -> Gaussian: + if self.obs == GameType.Ordinal: + assert self.tie is not None + assert self.margin is not None + return ( + approx(self.d[i_d].prior, self.margin[i_d], self.tie[i_d]) + / self.d[i_d].prior + ) + elif self.obs == GameType.Continuous: + return Gaussian( + self.result[self.o[i_d]] - self.result[self.o[i_d + 1]], 0.0 + ) + elif self.obs == GameType.Discrete: + r = self.result[self.o[i_d]] - self.result[self.o[i_d + 1]] + mu, sigma = self.d[i_d].prior + return Gaussian(*fixed_point_approx(r, mu, sigma)) / self.d[i_d].prior + + def likelihood_convergence(self) -> list[Gaussian]: """Iterate likelihood messages until convergence.""" - g = self for i in range(5): # Convergence iterations - for e in range(len(g.d) - 1): - g.d[e].prior = ( - g.t[g.o[e]].posterior_win - g.t[g.o[e + 1]].posterior_lose + for e in range(len(self.d) - 1): + self.d[e].prior = ( + self.t[self.o[e]].posterior_win + - self.t[self.o[e + 1]].posterior_lose ) if i == 0: - g.partial_evidence(e) - g.d[e].likelihood = g.likelihood_difference(e) - likelihood_lose = g.t[g.o[e]].posterior_win - g.d[e].likelihood - g.t[g.o[e + 1]].likelihood_lose = likelihood_lose - for e in range(len(g.d) - 1, 0, -1): - g.d[e].prior = ( - g.t[g.o[e]].posterior_win - g.t[g.o[e + 1]].posterior_lose + self.partial_evidence(e) + self.d[e].likelihood = self.likelihood_difference(e) + likelihood_lose = self.t[self.o[e]].posterior_win - self.d[e].likelihood + self.t[self.o[e + 1]].likelihood_lose = likelihood_lose + for e in range(len(self.d) - 1, 0, -1): + self.d[e].prior = ( + self.t[self.o[e]].posterior_win + - self.t[self.o[e + 1]].posterior_lose + ) + if i == 0 and e == len(self.d) - 1: + self.partial_evidence(e) + self.d[e].likelihood = self.likelihood_difference(e) + likelihood_win = ( + self.t[self.o[e + 1]].posterior_lose + self.d[e].likelihood ) - if i == 0 and e == len(g.d) - 1: - g.partial_evidence(e) - g.d[e].likelihood = g.likelihood_difference(e) - likelihood_win = g.t[g.o[e + 1]].posterior_lose + g.d[e].likelihood - g.t[g.o[e]].likelihood_win = likelihood_win - if len(g.d) == 1: - g.partial_evidence(0) - g.d[0].prior = g.t[g.o[0]].posterior_win - g.t[g.o[1]].posterior_lose - g.d[0].likelihood = g.likelihood_difference(0) - g.t[g.o[0]].likelihood_win = g.t[g.o[1]].posterior_lose + g.d[0].likelihood - g.t[g.o[-1]].likelihood_lose = g.t[g.o[-2]].posterior_win - g.d[-1].likelihood - return [g.t[e].likelihood for e in range(len(g.t))] - - def likelihood_performance(self): + self.t[self.o[e]].likelihood_win = likelihood_win + if len(self.d) == 1: + self.partial_evidence(0) + self.d[0].prior = ( + self.t[self.o[0]].posterior_win - self.t[self.o[1]].posterior_lose + ) + self.d[0].likelihood = self.likelihood_difference(0) + self.t[self.o[0]].likelihood_win = ( + self.t[self.o[1]].posterior_lose + self.d[0].likelihood + ) + self.t[self.o[-1]].likelihood_lose = ( + self.t[self.o[-2]].posterior_win - self.d[-1].likelihood + ) + return [self.t[e].likelihood for e in range(len(self.t))] + + def likelihood_performance(self) -> list[list[Gaussian]]: """Compute likelihood messages for individual performances.""" - g = self - performance_individuals = g.performance_individuals() - likelihood_teams = g.likelihood_convergence() + performance_individuals = self.performance_individuals() + likelihood_teams = self.likelihood_convergence() # te = p1 + p2 + p3 <=> p1 = te - (p2 + p3) = te - te_without_i - res = [] - for e in range(len(g.teams)): + res: list[list[Gaussian]] = [] + for e in range(len(self.teams)): res.append([]) - for i in range(len(g.teams[e])): + for i in range(len(self.teams[e])): te = likelihood_teams[e] te_without_i = Gaussian( - g.t[e].prior.mu - performance_individuals[e][i].mu, + self.t[e].prior.mu - performance_individuals[e][i].mu, math.sqrt( - g.t[e].prior.sigma ** 2 + self.t[e].prior.sigma ** 2 - performance_individuals[e][i].sigma ** 2 ), ) - w_i = g.weights[e][i] + w_i = self.weights[e][i] inv_w_i = inf if w_i == 0 else 1 / w_i - res[-1].append(inv_w_i * (te - te_without_i)) + res[-1].append((te - te_without_i) * inv_w_i) return res - def likelihood_skill(self): + def likelihood_skill(self) -> list[list[Gaussian]]: """Compute likelihood messages for player skills.""" - res = [] + res: list[list[Gaussian]] = [] lh_p = self.likelihood_performance() for e in range(len(lh_p)): res.append([]) @@ -752,19 +777,18 @@ def likelihood_skill(self): res[-1].append(lh_p[e][i] - noise) return res - def likelihood_analytic(self): + def likelihood_analytic(self) -> list[list[Gaussian]]: """Compute likelihoods analytically for 2-team games.""" - g = self - g.partial_evidence(0) - psi, vartheta = g.d[0].prior - psi_div, vartheta_div = g.likelihood_difference(0) - res = [] - for t in range(len(g.t)): + self.partial_evidence(0) + psi, vartheta = self.d[0].prior + psi_div, vartheta_div = self.likelihood_difference(0) + res: list[list[Gaussian]] = [] + for t in range(len(self.t)): res.append([]) lose_case = t == 1 - for i in range(len(g.teams[g.o[t]])): - mu_i, sigma_i = g.teams[g.o[t]][i].prior - w_i = g.weights[g.o[t]][i] + for i in range(len(self.teams[self.o[t]])): + mu_i, sigma_i = self.teams[self.o[t]][i].prior + w_i = self.weights[self.o[t]][i] inv_w_i = inf if w_i == 0 else 1 / w_i mu_analytic = mu_i + inv_w_i * (-psi + psi_div) * (-1) ** (lose_case) sigma_analytic = math.sqrt( @@ -773,16 +797,16 @@ def likelihood_analytic(self): - sigma_i**2 ) res[-1].append(Gaussian(mu_analytic, sigma_analytic)) - return [res[0], res[1]] if g.o[0] < g.o[1] else [res[1], res[0]] + return [res[0], res[1]] if self.o[0] < self.o[1] else [res[1], res[0]] - def likelihoods(self): + def get_likelihoods(self) -> list[list[Gaussian]]: """Compute likelihood messages using appropriate method.""" if len(self.teams) == 2: return self.likelihood_analytic() else: return self.likelihood_skill() - def posteriors(self): + def posteriors(self) -> list[list[Gaussian]]: """ Compute posterior skill distributions for all players. @@ -791,12 +815,11 @@ def posteriors(self): Returns: list: Nested list of Gaussian posteriors, structured as [team][player] """ - g = self - res = [] - for e in range(len(g.teams)): + res: list[list[Gaussian]] = [] + for e in range(len(self.teams)): res.append([]) - for i in range(len(g.teams[e])): - res[-1].append(g.teams[e][i].prior * g.likelihoods[e][i]) + for i in range(len(self.teams[e])): + res[-1].append(self.teams[e][i].prior * self.likelihoods[e][i]) return res @@ -815,46 +838,54 @@ class Skill(object): online: Online estimate (for online learning mode) """ - def __init__(self, bevents=[], forward=Ninf, backward=Ninf, likelihoods=[]): + def __init__( + self, + bevents: list[int] = [], + forward: Gaussian = Ninf, + backward: Gaussian = Ninf, + likelihoods: list[Gaussian] = [], + ) -> None: """Initialize a Skill variable.""" self.bevents = bevents self.forward = forward self.backward = backward self.likelihoods = likelihoods - self.online = None + self.online: Gaussian | None = None - def __repr__(self): + def __repr__(self) -> str: """String representation of the Skill.""" return f"Skill(events={self.bevents})" @property - def posterior(self): + def posterior(self) -> Gaussian: """Full posterior: forward * backward * all likelihoods""" return self.forward * self.backward * producto(self.likelihoods) @property - def forward_posterior(self): + def forward_posterior(self) -> Gaussian: """Forward posterior: forward message * likelihoods""" return self.forward * producto(self.likelihoods) @property - def backward_posterior(self): + def backward_posterior(self) -> Gaussian: """Backward posterior: backward message * likelihoods""" return self.backward * producto(self.likelihoods) - def likelihood(self, e): + def likelihood(self, e: int) -> Gaussian: """Get likelihood for event e.""" i = self.bevents.index(e) return self.likelihoods[i] - def update_likelihood(self, e, likelihood): + def update_likelihood(self, e: int, likelihood: Gaussian) -> tuple[float, float]: """Update likelihood for event e and return step size.""" i = self.bevents.index(e) step = likelihood.delta(self.likelihoods[i]) self.likelihoods[i] = likelihood return step - def serialize(self): + def serialize( + self, + ) -> dict[str, list[int] | tuple[float, float] | list[tuple[float, float]] | None]: """Serialize skill object to dictionary.""" return { "bevents": self.bevents, @@ -865,20 +896,6 @@ def serialize(self): } -class GameType(Enum): - """ - Enumeration of observation models for game outcomes. - - - Ordinal: Ranking/placement (win/loss/draw) - - Continuous: Continuous scores (e.g., time, distance) - - Discrete: Discrete counts (e.g., goals, points scored) - """ - - Ordinal = 0 - Continuous = 1 - Discrete = 2 - - class History(object): """ Manages a sequence of games and performs temporal Bayesian skill inference. @@ -911,18 +928,18 @@ class History(object): def __init__( self, - composition, - results=[], - times=[], - priors=None, - mu=0, - sigma=3, - beta=1, - gamma=0.15, - p_draw=0.0, - online=False, - weights=[], - obs=[], + composition: list[list[list[str]]], + results: list[list[float]] = [], + times: list[int] = [], + priors: dict[str, Player] = dict(), + mu: float = 0.0, + sigma: float = 3.0, + beta: float = 1.0, + gamma: float = 0.15, + p_draw: float = 0.0, + online: bool = False, + weights: list[list[list[float]]] = [], + obs: list[GameType] = [], ): """ Initialize a History of games. @@ -959,46 +976,48 @@ def __init__( obs, ) - self.size = 0 - self.batches = [] - self.bresults = [] - self.btimes = [] - self.bskills = [] - self.bweights = [] - self.bobs = [] - self.bevidence = [] + self.size: int = 0 + self.batches: list[list[list[str]]] = [] + self.bresults: list[list[list[float]]] = [] + self.btimes: list[int] = [] + self.bskills: list[dict[str, Skill]] = [] + self.bweights: list[list[list[list[float]]]] = [] + self.bobs: list[list[GameType]] = [] + self.bevidence: list[list[float | None]] = [] self.init_batches(composition, results, times, weights, obs) self.mu = mu self.sigma = sigma self.beta = beta self.gamma = gamma self.p_draw = p_draw - self.priors = defaultdict( - lambda: Player(Gaussian(mu, sigma), beta, gamma), priors if priors else {} + default_player = Player(Gaussian(mu, sigma), beta, gamma) + self.priors = defaultdict(lambda: default_player, priors) + + self._last_message: defaultdict[str, Gaussian] = defaultdict( + lambda: Gaussian(0.0, math.inf) ) - self._last_message = None - self._last_time = None + self._last_time: defaultdict[str, int | None] = defaultdict(lambda: None) self.online = online self.b_until = 0 if online else len(self.batches) - def __repr__(self): + def __repr__(self) -> str: """String representation of the History.""" return f"History(Events={self.size})" def check_input( self, - composition, - results, - times, - priors, - mu, - sigma, - beta, - gamma, - p_draw, - weights, - obs, - ): + composition: list[list[list[str]]], + results: list[list[float]], + times: list[int], + priors: dict[str, Player], + mu: float, + sigma: float, + beta: float, + gamma: float, + p_draw: float, + weights: list[list[list[float]]] = [], + obs: list[GameType] = [], + ) -> None: """Validate input parameters.""" self.check_data(composition, results, times, priors, weights, obs) if sigma < 0.0: @@ -1011,12 +1030,20 @@ def check_input( raise ValueError("p_draw < 0.0 or p_draw > 1.0") # - def check_data(self, composition, results, times, priors, weights, obs): + def check_data( + self, + composition: list[list[list[str]]], + results: list[list[float]], + times: list[int], + priors: dict[str, Player], + weights: list[list[list[float]]] = [], + obs: list[GameType] = [], + ) -> None: if results and (len(composition) != len(results)): raise ValueError("len(composition) != len(results)") if times and (len(composition) != len(times)): raise ValueError("len(composition) != len(times)") - if (priors is not None) and (not isinstance(priors, dict)): + if not isinstance(priors, dict): raise ValueError("not isinstance(priors, dict)") if weights and (len(composition) != len(weights)): raise ValueError("len(composition) != len(weights)") @@ -1024,7 +1051,14 @@ def check_data(self, composition, results, times, priors, weights, obs): raise ValueError("len(composition) != len(obs)") # - def init_batches(self, composition, results, times, weights, obs): + def init_batches( + self, + composition: list[list[list[str]]], + results: list[list[float]], + times: list[int], + weights: list[list[list[float]]] = [], + obs: list[GameType] = [], + ) -> None: """Initialize batches from composition data.""" times = list(range(len(composition))) if not times else times last_time = -inf @@ -1044,8 +1078,14 @@ def init_batches(self, composition, results, times, weights, obs): self.add_to_batch(i_b, i_e, composition, results, times, weights, obs) def add_history( - self, composition, results=[], times=[], priors=None, weights=[], obs=[] - ): + self, + composition: list[list[list[str]]], + results: list[list[float]], + times: list[int], + priors: dict[str, Player] = dict(), + weights: list[list[list[float]]] = [], + obs: list[GameType] = [], + ) -> None: """ Add new games to an existing History. @@ -1079,14 +1119,21 @@ def add_history( self.bevidence.append([]) self.add_to_batch(i_b, i, composition, results, times, weights, obs) - def add_to_batch(self, i_b, i, composition, results, times, weights, obs): + def add_to_batch( + self, + i_b: int, + i: int, + composition: list[list[list[str]]], + results: list[list[float]], + times: list[int], + weights: list[list[list[float]]] = [], + obs: list[GameType] = [], + ) -> None: """Add a single game to a batch.""" self.batches[i_b].append(composition[i]) self.bresults[i_b].append(results[i] if results else []) self.bweights[i_b].append(weights[i] if weights else []) - self.bobs[i_b].append( - GameType[obs[i]].value if obs else GameType["Ordinal"].value - ) + self.bobs[i_b].append(obs[i] if obs else GameType.Ordinal) self.bevidence[i_b].append(None) e = len(self.batches[i_b]) - 1 @@ -1098,79 +1145,77 @@ def add_to_batch(self, i_b, i, composition, results, times, weights, obs): else: self.bskills[i_b][name] = Skill(bevents=[e], likelihoods=[Ninf]) - def _in_skills(self, b, forward): + def _in_skills(self, b: int, forward: bool) -> None: """Propagate messages into a batch (with skill drift).""" - h = self - for name in h.bskills[b]: - old_t = h._last_time[name] - elapsed = abs(h.btimes[b] - old_t) if (old_t is not None) else 0 - gamma = h.priors[name].gamma - receive = h._last_message[name] + Gaussian( + for name in self.bskills[b]: + old_t = self._last_time[name] + elapsed = abs(self.btimes[b] - old_t) if (old_t is not None) else 0 + gamma = self.priors[name].gamma + receive = self._last_message[name] + Gaussian( 0, min(math.sqrt(elapsed * (gamma**2)), 1.67 * self.sigma) ) if forward: - h.bskills[b][name].forward = receive - if h.online and not h.bskills[b][name].online: - h.bskills[b][name].online = receive + self.bskills[b][name].forward = receive + if self.online and not self.bskills[b][name].online: + self.bskills[b][name].online = receive else: - h.bskills[b][name].backward = receive + self.bskills[b][name].backward = receive - def _up_skills(self, b, e): + def _up_skills(self, b: int, e: int) -> tuple[float, float]: """Update skills based on a single game within a batch.""" - h = self - g = Game( - h.within_priors(b, e), - h.bresults[b][e], - h.p_draw, - h.bweights[b][e], - obs=GameType(self.bobs[b][e]).name, + game = Game( + self.within_priors(b, e), + self.bresults[b][e], + self.p_draw, + self.bweights[b][e], + obs=self.bobs[b][e], ) - likelihoods = g.likelihoods + likelihoods = game.likelihoods if self.online and not self.bevidence[b][e]: - self.bevidence[b][e] = g.evidence + self.bevidence[b][e] = game.evidence if not self.online: - self.bevidence[b][e] = g.evidence - mu_step_max, sigma_step_max = (0, 0) - for t in range(len(h.batches[b][e])): - for i in range(len(h.batches[b][e][t])): - name = h.batches[b][e][t][i] - mu_step, sigma_step = h.bskills[b][name].update_likelihood( + self.bevidence[b][e] = game.evidence + mu_step_max, sigma_step_max = (0.0, 0.0) + for t in range(len(self.batches[b][e])): + for i in range(len(self.batches[b][e][t])): + name = self.batches[b][e][t][i] + mu_step, sigma_step = self.bskills[b][name].update_likelihood( e, likelihoods[t][i] ) mu_step_max = max(mu_step_max, mu_step) sigma_step_max = max(sigma_step_max, sigma_step) return (mu_step_max, sigma_step_max) - def _out_skills(self, b, forward): + def _out_skills(self, b: int, forward: bool) -> None: """Propagate messages out of a batch.""" - h = self - for name in h.bskills[b]: - h._last_time[name] = h.btimes[b] + for name in self.bskills[b]: + self._last_time[name] = self.btimes[b] if forward: - h._last_message[name] = h.bskills[b][name].forward_posterior + self._last_message[name] = self.bskills[b][name].forward_posterior else: - h._last_message[name] = h.bskills[b][name].backward_posterior + self._last_message[name] = self.bskills[b][name].backward_posterior - def within_priors(self, b, e, online=False): + def within_priors(self, b: int, e: int, online: bool = False) -> list[list[Player]]: """Get player priors for a game (excluding its own likelihood).""" - h = self - priors = [] - for t in range(len(h.batches[b][e])): + priors: list[list[Player]] = [] + for t in range(len(self.batches[b][e])): priors.append([]) - for i in range(len(h.batches[b][e][t])): - name = h.batches[b][e][t][i] + for i in range(len(self.batches[b][e][t])): + name = self.batches[b][e][t][i] if not online: - prior = h.bskills[b][name].posterior / h.bskills[b][ + prior = self.bskills[b][name].posterior / self.bskills[b][ name ].likelihood(e) else: - prior = h.bskills[b][name].online + _online = self.bskills[b][name].online + assert _online is not None + prior = _online priors[-1].append( - Player(prior, h.priors[name].beta, h.priors[name].gamma) + Player(prior, self.priors[name].beta, self.priors[name].gamma) ) return priors - def forward_propagation(self): + def forward_propagation(self) -> tuple[float, float]: """ Forward pass: propagate skill estimates from past to future. @@ -1180,23 +1225,23 @@ def forward_propagation(self): Returns: tuple: (max_mu_step, max_sigma_step) - maximum likelihood change """ - h = self - h._last_message = defaultdict( - lambda: Gaussian(h.mu, h.sigma), {k: v.prior for k, v in h.priors.items()} + self._last_message = defaultdict( + lambda: Gaussian(self.mu, self.sigma), + {k: v.prior for k, v in self.priors.items()}, ) - h._last_time = defaultdict(lambda: None) - mu_step_max = 0 - sigma_step_max = 0 - for b in range(h.b_until): - h._in_skills(b, forward=True) - for e in range(len(h.batches[b])): - mu_step, sigma_step = h._up_skills(b, e) + self._last_time = defaultdict(lambda: None) + mu_step_max = 0.0 + sigma_step_max = 0.0 + for b in range(self.b_until): + self._in_skills(b, forward=True) + for e in range(len(self.batches[b])): + mu_step, sigma_step = self._up_skills(b, e) mu_step_max = max(mu_step_max, mu_step) sigma_step_max = max(sigma_step_max, sigma_step) - h._out_skills(b, forward=True) + self._out_skills(b, forward=True) return (mu_step_max, sigma_step_max) - def backward_propagation(self): + def backward_propagation(self) -> tuple[float, float]: """ Backward pass: propagate skill estimates from future to past. @@ -1206,24 +1251,23 @@ def backward_propagation(self): Returns: tuple: (max_mu_step, max_sigma_step) - maximum likelihood change """ - h = self - h._last_message = defaultdict(lambda: Gaussian(0.0, math.inf)) - h._last_time = defaultdict(lambda: None) - mu_step_max = 0 - sigma_step_max = 0 - h._out_skills(h.b_until - 1, forward=False) - for b in reversed(range(h.b_until - 1)): - h._in_skills(b, forward=False) - for e in range(len(h.batches[b])): - mu_step, sigma_step = h._up_skills(b, e) + self._last_message = defaultdict(lambda: Gaussian(0.0, math.inf)) + self._last_time = defaultdict(lambda: None) + mu_step_max = 0.0 + sigma_step_max = 0.0 + self._out_skills(self.b_until - 1, forward=False) + for b in reversed(range(self.b_until - 1)): + self._in_skills(b, forward=False) + for e in range(len(self.batches[b])): + mu_step, sigma_step = self._up_skills(b, e) mu_step_max = max(mu_step_max, mu_step) sigma_step_max = max(sigma_step_max, sigma_step) - h._out_skills(b, forward=False) - h._last_message = None - h._last_time = None + self._out_skills(b, forward=False) + self._last_message = defaultdict(lambda: Gaussian(0.0, math.inf)) + self._last_time = defaultdict(lambda: None) return (mu_step_max, sigma_step_max) - def iteration(self): + def iteration(self) -> tuple[float, float]: """ Perform one complete iteration: forward pass followed by backward pass. @@ -1234,7 +1278,9 @@ def iteration(self): mu_backward, sigma_backward = self.backward_propagation() return (max(mu_forward, mu_backward), max(sigma_forward, sigma_backward)) - def convergence(self, iterations=8, epsilon=0.00001, verbose=True): + def convergence( + self, iterations: int = 8, epsilon: float = 0.00001, verbose: bool = True + ) -> tuple[tuple[float, float], int]: """ Run iterative message passing until convergence. @@ -1253,6 +1299,7 @@ def convergence(self, iterations=8, epsilon=0.00001, verbose=True): i = 0 delta = math.inf self.unveil_batch() + step = (0.0, 0.0) for _ in range(1 + len(self.batches) - self.b_until): i = 0 delta = math.inf @@ -1269,12 +1316,14 @@ def convergence(self, iterations=8, epsilon=0.00001, verbose=True): self.unveil_batch() return step, i - def unveil_batch(self): + def unveil_batch(self) -> None: """Unveil the next batch for online processing.""" if self.b_until < len(self.batches): self.b_until += 1 - def learning_curves(self, who=None, online=False): + def learning_curves( + self, who: list[str] = [], online: bool = False + ) -> dict[str, list[tuple[int, Gaussian | None]]]: """ Extract learning curves (skill over time) for players. @@ -1290,16 +1339,15 @@ def learning_curves(self, who=None, online=False): >>> for time, skill in lc["alice"]: ... print(f"Time {time}: mu={skill.mu:.2f}, sigma={skill.sigma:.2f}") """ - h = self - res = dict() - for b in range(len(h.bskills)): - time = h.btimes[b] - for name in h.bskills[b]: - if (who is None) or (name in who): + res: dict[str, list[tuple[int, Gaussian | None]]] = dict() + for b in range(len(self.bskills)): + time = self.btimes[b] + for name in self.bskills[b]: + if (len(who) == 0) or (name in who): if self.online and online: - skill = h.bskills[b][name].online + skill = self.bskills[b][name].online else: - skill = h.bskills[b][name].posterior + skill = self.bskills[b][name].posterior t_p = (time, skill) if name in res: res[name].append(t_p) @@ -1307,7 +1355,7 @@ def learning_curves(self, who=None, online=False): res[name] = [t_p] return res - def log_evidence(self): + def log_evidence(self) -> float: """ Compute the log marginal likelihood of all observed game outcomes. @@ -1326,7 +1374,7 @@ def log_evidence(self): ] ) - def geometric_mean(self): + def geometric_mean(self) -> float: """ Compute the geometric mean of game outcome probabilities. @@ -1338,7 +1386,7 @@ def geometric_mean(self): ) return math.exp(self.log_evidence() / unveil_size) - def __getstate__(self): + def __getstate__(self) -> dict[str, Any]: """ Prepare History for pickling (serialization). @@ -1355,12 +1403,13 @@ def __getstate__(self): del state["_last_time"] # Convert all Skill objects to their serializable form - for bskill in state["bskills"]: + bskills: list[dict[str, Any]] = state["bskills"] + for bskill in bskills: for name in bskill: bskill[name] = bskill[name].serialize() return state - def __setstate__(self, state): + def __setstate__(self, state: dict[str, Any]) -> None: """ Restore History from pickled state (deserialization). @@ -1396,7 +1445,7 @@ def __setstate__(self, state): self.__dict__.update(state) -def orden(xs, reverse=True): +def orden(xs: Sequence[float | int], reverse: bool = True) -> list[int]: """ Return indices that sort a list. From f6ff9d900dfd357bf4f98ef8a822ea38a0e1d73b Mon Sep 17 00:00:00 2001 From: apiss2 Date: Sat, 22 Nov 2025 00:50:27 +0900 Subject: [PATCH 03/10] mypy check passed --- trueskillthroughtime/__init__.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/trueskillthroughtime/__init__.py b/trueskillthroughtime/__init__.py index bcbadf3..199b858 100644 --- a/trueskillthroughtime/__init__.py +++ b/trueskillthroughtime/__init__.py @@ -928,9 +928,9 @@ class History(object): def __init__( self, - composition: list[list[list[str]]], - results: list[list[float]] = [], - times: list[int] = [], + composition: list[list[list[str]]], # Game[Team[Player[str]]] + results: list[list[float]] = [], # Game[TeamResult[float]] + times: list[int] = [], # GameDate[int] priors: dict[str, Player] = dict(), mu: float = 0.0, sigma: float = 3.0, @@ -938,7 +938,7 @@ def __init__( gamma: float = 0.15, p_draw: float = 0.0, online: bool = False, - weights: list[list[list[float]]] = [], + weights: list[list[list[float]]] = [], # Game[Team[PlayerWeight[float]]] obs: list[GameType] = [], ): """ @@ -977,8 +977,8 @@ def __init__( ) self.size: int = 0 - self.batches: list[list[list[str]]] = [] - self.bresults: list[list[list[float]]] = [] + self.batches: list[list[list[list[str]]]] = [] # Day[Game[Team[Player[str]]]] + self.bresults: list[list[list[float]]] = [] # Day[Game[TeamResult[float]]] self.btimes: list[int] = [] self.bskills: list[dict[str, Skill]] = [] self.bweights: list[list[list[list[float]]]] = [] @@ -1106,10 +1106,10 @@ def add_history( times = list(range(last_t, len(composition) + last_t)) if not times else times for i in range(len(composition)): t = times[i] - if t in self.batches: - i_b = self.batches.index(times[i]) + if t in self.btimes: + i_b = self.btimes.index(times[i]) else: - i_b = len(self.batches) + i_b = len(self.btimes) self.btimes.append(t) self.batches.append([]) self.bresults.append([]) From bf9da5e2fa58a284625a0a804a603c4c46e3b662 Mon Sep 17 00:00:00 2001 From: apiss2 Date: Sat, 22 Nov 2025 01:19:59 +0900 Subject: [PATCH 04/10] Rename classes to follow PEP 8 naming conventions --- trueskillthroughtime/__init__.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/trueskillthroughtime/__init__.py b/trueskillthroughtime/__init__.py index 199b858..1c0f926 100644 --- a/trueskillthroughtime/__init__.py +++ b/trueskillthroughtime/__init__.py @@ -465,7 +465,7 @@ def __repr__(self) -> str: ) -class team_variable(object): +class TeamVariable(object): """ Internal class representing a team's performance variable in message passing. @@ -511,7 +511,7 @@ def likelihood(self) -> Gaussian: return self.likelihood_win * self.likelihood_lose -class diff_messages(object): +class DiffMessages(object): """ Internal class for performance difference variables in message passing. @@ -641,18 +641,18 @@ def performance_individuals(self) -> list[list[Gaussian]]: res[-1].append(self.teams[t][i].performance() * self.weights[t][i]) return res - def performance_teams(self) -> list[team_variable]: + def performance_teams(self) -> list[TeamVariable]: # Sum of individual performances res = [] for team in self.performance_individuals(): - res.append(team_variable(suma(team))) + res.append(TeamVariable(suma(team))) return res - def difference_teams(self) -> list[diff_messages]: - res: list[diff_messages] = [] + def difference_teams(self) -> list[DiffMessages]: + res: list[DiffMessages] = [] for e in range(len(self) - 1): res.append( - diff_messages(self.t[self.o[e]].prior - self.t[self.o[e + 1]].prior) + DiffMessages(self.t[self.o[e]].prior - self.t[self.o[e + 1]].prior) ) return res From 7e63e658ca9176c21704e8aba020c29ec62331e6 Mon Sep 17 00:00:00 2001 From: apiss2 Date: Thu, 27 Nov 2025 01:34:01 +0900 Subject: [PATCH 05/10] Update tests to use GameType instead of string for Game input --- test/runtest.py | 86 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 60 insertions(+), 26 deletions(-) diff --git a/test/runtest.py b/test/runtest.py index 5c710a2..51983df 100644 --- a/test/runtest.py +++ b/test/runtest.py @@ -602,18 +602,28 @@ def test_game_continuous_1vs1(self): # result_ta = 45.24 result_tb = 44.24 - g = ttt.Game([ta,tb], [result_ta,result_tb], obs="Continuous") + g = ttt.Game([ta, tb], [result_ta, result_tb], obs=ttt.GameType.Continuous) post = g.posteriors() self.assertTrue(post[0][0].isapprox(ttt.Gaussian(2,1.549193))) self.assertTrue(post[1][0].isapprox(ttt.Gaussian(1,1.549193))) - g = ttt.Game([ta,tb], [result_ta,result_tb], obs="Continuous", weights=[[1.0],[1.0]]) + g = ttt.Game( + [ta, tb], + [result_ta, result_tb], + obs=ttt.GameType.Continuous, + weights=[[1.0], [1.0]], + ) post = g.posteriors() self.assertTrue(post[0][0].isapprox(ttt.Gaussian(2,1.549193))) self.assertTrue(post[1][0].isapprox(ttt.Gaussian(1,1.549193))) # Los pesos tienen un compartamiento raro, porque si la media es positiva un peso alto aumenta la habilidad, pero si la media es negativa, un peso bajo la disminuye. Esto no parece ser un comportamiento razonable teniendo en cuenta que el valor absoluto de las medias no tiene ningún significado. TODO: profundizar esta idea en el caso en el que el observable es "orden", pues esto mismo ocurre también ahí. - g = ttt.Game([ta,tb], [result_ta,result_tb], obs="Continuous", weights=[[1.0],[2.0]]) + g = ttt.Game( + [ta, tb], + [result_ta, result_tb], + obs=ttt.GameType.Continuous, + weights=[[1.0], [2.0]], + ) post = g.posteriors() self.assertTrue(post[0][0].isapprox(ttt.Gaussian(2.160000,1.833030))) self.assertTrue(post[1][0].isapprox(ttt.Gaussian(0.680000,1.200000))) @@ -624,7 +634,12 @@ def test_game_continuous_1vs1(self): w_ta = [1.0] w_tb = [5.0] # - g = ttt.Game([ta,tb], [result_ta,result_tb], obs="Continuous", weights=[w_ta,w_tb]) + g = ttt.Game( + [ta, tb], + [result_ta, result_tb], + obs=ttt.GameType.Continuous, + weights=[w_ta, w_tb], + ) post = g.posteriors() self.assertTrue(post[0][0].isapprox(ttt.Gaussian(-0.123077,1.968990), 1e-5)) self.assertTrue(post[1][0].isapprox(ttt.Gaussian(-0.384615,0.960769), 1e-5)) @@ -635,7 +650,12 @@ def test_game_continuous_1vs1(self): w_ta = [1.0] w_tb = [5.0] # - g = ttt.Game([ta,tb], [result_ta,result_tb], obs="Continuous", weights=[w_ta,w_tb]) + g = ttt.Game( + [ta, tb], + [result_ta, result_tb], + obs=ttt.GameType.Continuous, + weights=[w_ta, w_tb], + ) post = g.posteriors() self.assertTrue(post[0][0].isapprox(ttt.Gaussian(2.123077,1.968990) , 1e-5)) self.assertTrue(post[1][0].isapprox(ttt.Gaussian(0.384615,0.960769), 1e-5)) @@ -646,7 +666,7 @@ def test_game_continuous_NvsM(self): tb = [ttt.Player(ttt.Gaussian(4,2),1,0)] tc = [ttt.Player(ttt.Gaussian(3,2),1,0)] result = [4.2, 0.2, 2.1] - g = ttt.Game([ta,tc,tb], result, obs="Continuous") + g = ttt.Game([ta, tc, tb], result, obs=ttt.GameType.Continuous) post = g.posteriors() self.assertTrue(post[0][0].isapprox(ttt.Gaussian(2.816000,1.649242) , 1e-5)) self.assertTrue(post[0][1].isapprox(ttt.Gaussian(2.816000,1.649242) , 1e-5)) @@ -660,8 +680,13 @@ def test_history_continuous_NvsMvsL(self): priors["b"] = ttt.Player(ttt.Gaussian(4,2),1,0) priors["c"] = ttt.Player(ttt.Gaussian(3,2),1,0) results = [[4.2, 0.2, 2.1]] - obs = ["Continuous"] - h = ttt.History(composition=[ [ ["a1", "a2"], ["c"], ["b"] ] ], results = results, priors=priors, obs=obs ) + obs = [ttt.GameType.Continuous] + h = ttt.History( + composition=[[["a1", "a2"], ["c"], ["b"]]], + results=results, + priors=priors, + obs=obs, + ) h.forward_propagation() lc = h.learning_curves() self.assertTrue(lc["a1"][0][1].isapprox(ttt.Gaussian(2.816000,1.649242) , 1e-5)) @@ -682,23 +707,27 @@ def test_game_discrete_1vs1(self): wa = [1.0] tb = [ttt.Player(ttt.Gaussian(0,6),1,0)] wb = [1.0] - result = [0,54] - g = ttt.Game([ta,tb], result = result, weights=[wa,wb], obs="Discrete") - post= g.posteriors() - #print(post) - self.assertTrue(post[0][0].isapprox(ttt.Gaussian(0.118952,4.300102))) - self.assertTrue(post[1][0].isapprox(ttt.Gaussian(3.881048,4.300102))) - - ta = [ttt.Player(ttt.Gaussian(4,1),1,0)] + result = [0, 54] + g = ttt.Game( + [ta, tb], result=result, weights=[wa, wb], obs=ttt.GameType.Discrete + ) + post = g.posteriors() + # print(post) + self.assertTrue(post[0][0].isapprox(ttt.Gaussian(0.118952, 4.300102))) + self.assertTrue(post[1][0].isapprox(ttt.Gaussian(3.881048, 4.300102))) + + ta = [ttt.Player(ttt.Gaussian(4, 1), 1, 0)] wa = [1.0] - tb = [ttt.Player(ttt.Gaussian(0,1),1,0)] + tb = [ttt.Player(ttt.Gaussian(0, 1), 1, 0)] wb = [1.0] - result = [math.exp(4),0] - g = ttt.Game([ta,tb], result = result, weights=[wa,wb], obs="Discrete") - post= g.posteriors() - #print(post) - self.assertTrue(post[0][0].isapprox(ttt.Gaussian(3.997732,0.866683))) - self.assertTrue(post[1][0].isapprox(ttt.Gaussian(0.002268,0.866683))) + result = [math.exp(4), 0] + g = ttt.Game( + [ta, tb], result=result, weights=[wa, wb], obs=ttt.GameType.Discrete + ) + post = g.posteriors() + # print(post) + self.assertTrue(post[0][0].isapprox(ttt.Gaussian(3.997732, 0.866683))) + self.assertTrue(post[1][0].isapprox(ttt.Gaussian(0.002268, 0.866683))) def test_history_discrete_NvsMvsL(self): priors = dict() @@ -707,8 +736,13 @@ def test_history_discrete_NvsMvsL(self): priors["b"] = ttt.Player(ttt.Gaussian(4,2),1,0) priors["c"] = ttt.Player(ttt.Gaussian(3,2),1,0) results = [[4, 0, 2]] - obs = ["Discrete"] - h = ttt.History(composition=[ [ ["a1", "a2"], ["c"], ["b"] ] ], results = results, priors=priors, obs=obs ) + obs = [ttt.GameType.Discrete] + h = ttt.History( + composition=[[["a1", "a2"], ["c"], ["b"]]], + results=results, + priors=priors, + obs=obs, + ) h.forward_propagation() lc = h.learning_curves() #print(lc) @@ -725,7 +759,7 @@ def test_history_mixed_type_of_game_NvsMvsL(self): priors["c"] = ttt.Player(ttt.Gaussian(3,2),1,0.1) results = [[4,0,2],[4.0, 0.0, 2.1],[4, 0, 2]] times = [0, 1, 2] - obs = ["Ordinal", "Continuous", "Discrete"] + obs = [ttt.GameType.Ordinal, ttt.GameType.Continuous, ttt.GameType.Discrete] h = ttt.History(composition=[ [ ["a1", "a2"], ["c"], ["b"] ] ]*3, results = results, times=times, priors=priors, obs=obs ) h.forward_propagation() From 052a1ddefccb1c6b2924c019d1b2d5ad651a82a9 Mon Sep 17 00:00:00 2001 From: apiss2 Date: Thu, 27 Nov 2025 01:34:45 +0900 Subject: [PATCH 06/10] fix add_history default args --- trueskillthroughtime/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/trueskillthroughtime/__init__.py b/trueskillthroughtime/__init__.py index 1c0f926..09b515c 100644 --- a/trueskillthroughtime/__init__.py +++ b/trueskillthroughtime/__init__.py @@ -1080,8 +1080,8 @@ def init_batches( def add_history( self, composition: list[list[list[str]]], - results: list[list[float]], - times: list[int], + results: list[list[float]] = [], + times: list[int] = [], priors: dict[str, Player] = dict(), weights: list[list[list[float]]] = [], obs: list[GameType] = [], From 9ee8e0c0bf997c39c8e2950921f045245b589dfe Mon Sep 17 00:00:00 2001 From: apiss2 Date: Thu, 27 Nov 2025 01:36:35 +0900 Subject: [PATCH 07/10] Add future annotations import to support type hints --- trueskillthroughtime/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/trueskillthroughtime/__init__.py b/trueskillthroughtime/__init__.py index 09b515c..30b2654 100644 --- a/trueskillthroughtime/__init__.py +++ b/trueskillthroughtime/__init__.py @@ -23,6 +23,8 @@ :license: BSD, see LICENSE for more details. """ +from __future__ import annotations + import copy import math from collections import defaultdict From ed3af5f4bc8695ea9b347780dfed517f19e9d92b Mon Sep 17 00:00:00 2001 From: apiss2 Date: Thu, 27 Nov 2025 01:50:19 +0900 Subject: [PATCH 08/10] fix import module name --- test/runtest.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/runtest.py b/test/runtest.py index 51983df..6c49559 100644 --- a/test/runtest.py +++ b/test/runtest.py @@ -17,9 +17,10 @@ import unittest import sys -sys.path.append('..') -import trueskillthroughtime2 as ttt -#import old +sys.path.append("..") +import trueskillthroughtime as ttt + +# import old from importlib import reload # Python 3.4+ only. reload(ttt) #reload(old) From ed4a5c820d600c36b965290d16eb94b9dd434c6e Mon Sep 17 00:00:00 2001 From: apiss2 Date: Thu, 27 Nov 2025 01:51:01 +0900 Subject: [PATCH 09/10] formatting by ruff --- test/runtest.py | 1149 +++++++++++++++++++++++++++++------------------ 1 file changed, 713 insertions(+), 436 deletions(-) diff --git a/test/runtest.py b/test/runtest.py index 6c49559..bcc83b8 100644 --- a/test/runtest.py +++ b/test/runtest.py @@ -17,18 +17,20 @@ import unittest import sys + sys.path.append("..") import trueskillthroughtime as ttt # import old from importlib import reload # Python 3.4+ only. + reload(ttt) -#reload(old) +# reload(old) import math import numpy as np -#import trueskill as ts -#env = ts.TrueSkill(draw_probability=0.0, beta=1.0, tau=0.0) +# import trueskill as ts +# env = ts.TrueSkill(draw_probability=0.0, beta=1.0, tau=0.0) import time @@ -37,226 +39,293 @@ class tests(unittest.TestCase): def test_gaussian_init(self): """Test Gaussian distribution initialization with various parameters.""" - N01 = ttt.Gaussian(mu=0,sigma=1) - self.assertAlmostEqual(N01.mu,0) - self.assertAlmostEqual(N01.sigma,1.0) - Ninf = ttt.Gaussian(0,math.inf) - self.assertAlmostEqual(Ninf.mu,0) - self.assertAlmostEqual(Ninf.sigma,math.inf) - N00 = ttt.Gaussian(mu=0,sigma=0) - self.assertAlmostEqual(N00.mu,0) - self.assertAlmostEqual(N00.sigma,0) + N01 = ttt.Gaussian(mu=0, sigma=1) + self.assertAlmostEqual(N01.mu, 0) + self.assertAlmostEqual(N01.sigma, 1.0) + Ninf = ttt.Gaussian(0, math.inf) + self.assertAlmostEqual(Ninf.mu, 0) + self.assertAlmostEqual(Ninf.sigma, math.inf) + N00 = ttt.Gaussian(mu=0, sigma=0) + self.assertAlmostEqual(N00.mu, 0) + self.assertAlmostEqual(N00.sigma, 0) def test_ppf(self): """Test percent point function (inverse CDF) for Gaussian distributions.""" - self.assertAlmostEqual(ttt.ppf(0.3,ttt.N01.mu, ttt.N01.sigma),-0.5244005127080409) - N23 = ttt.Gaussian(2.,3.) - self.assertAlmostEqual(ttt.ppf(0.3,N23.mu, N23.sigma),0.4267984618758771) + self.assertAlmostEqual( + ttt.ppf(0.3, ttt.N01.mu, ttt.N01.sigma), -0.5244005127080409 + ) + N23 = ttt.Gaussian(2.0, 3.0) + self.assertAlmostEqual(ttt.ppf(0.3, N23.mu, N23.sigma), 0.4267984618758771) def test_cdf(self): """Test cumulative distribution function for Gaussian distributions.""" - self.assertAlmostEqual(ttt.cdf(0.3,ttt.N01.mu,ttt.N01.sigma),0.617911409) - N23 = ttt.Gaussian(2.,3.) - self.assertAlmostEqual(ttt.cdf(0.3,N23.mu,N23.sigma),0.28547031) + self.assertAlmostEqual(ttt.cdf(0.3, ttt.N01.mu, ttt.N01.sigma), 0.617911409) + N23 = ttt.Gaussian(2.0, 3.0) + self.assertAlmostEqual(ttt.cdf(0.3, N23.mu, N23.sigma), 0.28547031) def test_pdf(self): - """Test probability density function for Gaussian distributions.""" - self.assertAlmostEqual(ttt.pdf(0.3,ttt.N01.mu,ttt.N01.sigma),0.38138781) - N23 = ttt.Gaussian(2.,3.) - self.assertAlmostEqual(ttt.pdf(0.3,N23.mu,N23.sigma),0.11325579) + """Test probability density function for Gaussian distributions.""" + self.assertAlmostEqual(ttt.pdf(0.3, ttt.N01.mu, ttt.N01.sigma), 0.38138781) + N23 = ttt.Gaussian(2.0, 3.0) + self.assertAlmostEqual(ttt.pdf(0.3, N23.mu, N23.sigma), 0.11325579) def test_compute_margin(self): """Test draw margin computation from draw probability.""" - self.assertAlmostEqual(ttt.compute_margin(0.25,math.sqrt(2)*25.0/6),1.8776004584348176) - self.assertAlmostEqual(ttt.compute_margin(0.25,math.sqrt(3)*25.0/6),2.2995815319905395) - self.assertAlmostEqual(ttt.compute_margin(0.0,math.sqrt(3)*25.0/6),0.0) - self.assertAlmostEqual(ttt.compute_margin(1.0,math.sqrt(3)*25.0/6),math.inf) + self.assertAlmostEqual( + ttt.compute_margin(0.25, math.sqrt(2) * 25.0 / 6), 1.8776004584348176 + ) + self.assertAlmostEqual( + ttt.compute_margin(0.25, math.sqrt(3) * 25.0 / 6), 2.2995815319905395 + ) + self.assertAlmostEqual(ttt.compute_margin(0.0, math.sqrt(3) * 25.0 / 6), 0.0) + self.assertAlmostEqual( + ttt.compute_margin(1.0, math.sqrt(3) * 25.0 / 6), math.inf + ) def test_trunc(self): """Test truncated Gaussian distribution parameters for win/draw outcomes.""" - mu, sigma = ttt.trunc(*ttt.Gaussian(0,1),0.,False) + mu, sigma = ttt.trunc(*ttt.Gaussian(0, 1), 0.0, False) self.assertAlmostEqual(mu, 0.7978845368663289) self.assertAlmostEqual(sigma, 0.6028103066716792) - mu, sigma = ttt.trunc(*ttt.Gaussian(0.,math.sqrt(2)*(25/6) ),1.8776005988,True) - self.assertAlmostEqual(mu,0.0) - self.assertAlmostEqual(sigma,1.0767055, places=4) - mu, sigma = ttt.trunc(*ttt.Gaussian(12.,math.sqrt(2)*(25/6)),1.8776005988,True) - self.assertAlmostEqual(mu,0.3900995, places=5) - self.assertAlmostEqual(sigma,1.0343979, places=5) + mu, sigma = ttt.trunc( + *ttt.Gaussian(0.0, math.sqrt(2) * (25 / 6)), 1.8776005988, True + ) + self.assertAlmostEqual(mu, 0.0) + self.assertAlmostEqual(sigma, 1.0767055, places=4) + mu, sigma = ttt.trunc( + *ttt.Gaussian(12.0, math.sqrt(2) * (25 / 6)), 1.8776005988, True + ) + self.assertAlmostEqual(mu, 0.3900995, places=5) + self.assertAlmostEqual(sigma, 1.0343979, places=5) def gaussian(self): """Test Gaussian algebraic operations (add, subtract, multiply, divide).""" - N, M = ttt.Gaussian(25.0, 25.0/3), ttt.Gaussian(0.0, 1.0) - mu, sigma = M/N - self.assertAlmostEqual(mu,-0.365, places=3) - self.assertAlmostEqual(sigma, 1.007, places=3) - mu, sigma = N*M - self.assertAlmostEqual(mu,0.355, places=3) - self.assertAlmostEqual(sigma,0.993, places=3) - mu, sigma = N+M - self.assertAlmostEqual(mu,25.000, places=3) - self.assertAlmostEqual(sigma,8.393, places=3) + N, M = ttt.Gaussian(25.0, 25.0 / 3), ttt.Gaussian(0.0, 1.0) + mu, sigma = M / N + self.assertAlmostEqual(mu, -0.365, places=3) + self.assertAlmostEqual(sigma, 1.007, places=3) + mu, sigma = N * M + self.assertAlmostEqual(mu, 0.355, places=3) + self.assertAlmostEqual(sigma, 0.993, places=3) + mu, sigma = N + M + self.assertAlmostEqual(mu, 25.000, places=3) + self.assertAlmostEqual(sigma, 8.393, places=3) mu, sigma = N - ttt.Gaussian(1.0, 1.0) - self.assertAlmostEqual(mu,24.000, places=3) - self.assertAlmostEqual(sigma,8.393, places=3) + self.assertAlmostEqual(mu, 24.000, places=3) + self.assertAlmostEqual(sigma, 8.393, places=3) def test_1vs1(self): """Test 1v1 games with various skill distributions and parameters.""" - ta = [ttt.Player(ttt.Gaussian(25.0,25.0/3),25.0/6,25.0/300)] - tb = [ttt.Player(ttt.Gaussian(25.0,25.0/3),25.0/6,25.0/300)] - g = ttt.Game([ta,tb],[0,1], 0.0) + ta = [ttt.Player(ttt.Gaussian(25.0, 25.0 / 3), 25.0 / 6, 25.0 / 300)] + tb = [ttt.Player(ttt.Gaussian(25.0, 25.0 / 3), 25.0 / 6, 25.0 / 300)] + g = ttt.Game([ta, tb], [0, 1], 0.0) [a], [b] = g.posteriors() - self.assertAlmostEqual(a.mu,20.79477925612302,4) - self.assertAlmostEqual(b.mu,29.20522074387697,4) - self.assertAlmostEqual(a.sigma,7.194481422570443 ,places=4) - - g = ttt.Game([[ttt.Player(ttt.Gaussian(29.,1.),25.0/6)] ,[ttt.Player(ttt.Gaussian(25.0,25.0/3),25.0/6)]], [0,1]) + self.assertAlmostEqual(a.mu, 20.79477925612302, 4) + self.assertAlmostEqual(b.mu, 29.20522074387697, 4) + self.assertAlmostEqual(a.sigma, 7.194481422570443, places=4) + + g = ttt.Game( + [ + [ttt.Player(ttt.Gaussian(29.0, 1.0), 25.0 / 6)], + [ttt.Player(ttt.Gaussian(25.0, 25.0 / 3), 25.0 / 6)], + ], + [0, 1], + ) [a], [b] = g.posteriors() - self.assertAlmostEqual(a.mu,28.89648, places=4) - self.assertAlmostEqual(a.sigma,0.9966043, places=4) - self.assertAlmostEqual(b.mu,32.18921, places=4) - self.assertAlmostEqual(b.sigma,6.062064, places=4) + self.assertAlmostEqual(a.mu, 28.89648, places=4) + self.assertAlmostEqual(a.sigma, 0.9966043, places=4) + self.assertAlmostEqual(b.mu, 32.18921, places=4) + self.assertAlmostEqual(b.sigma, 6.062064, places=4) - ta = [ttt.Player(ttt.Gaussian(1.139,0.531),1.0,0.2125)] - tb = [ttt.Player(ttt.Gaussian(15.568,0.51),1.0,0.2125)] - g = ttt.Game([ta,tb], [0,1], 0.0) - self.assertAlmostEqual(g.likelihoods[0][0].sigma,ttt.inf) - self.assertAlmostEqual(g.likelihoods[1][0].sigma,ttt.inf) + ta = [ttt.Player(ttt.Gaussian(1.139, 0.531), 1.0, 0.2125)] + tb = [ttt.Player(ttt.Gaussian(15.568, 0.51), 1.0, 0.2125)] + g = ttt.Game([ta, tb], [0, 1], 0.0) + self.assertAlmostEqual(g.likelihoods[0][0].sigma, ttt.inf) + self.assertAlmostEqual(g.likelihoods[1][0].sigma, ttt.inf) def test_1vs1vs1(self): """Test three-player free-for-all games with various outcomes.""" - [a], [b], [c] = ttt.Game([[ttt.Player(ttt.Gaussian(25.0,25.0/3),25.0/6,25.0/300)],[ttt.Player(ttt.Gaussian(25.0,25.0/3),25.0/6,25.0/300)],[ttt.Player(ttt.Gaussian(25.0,25.0/3),25.0/6,25.0/300)]], [1,2,0]).posteriors() - self.assertAlmostEqual(a.mu,25.000000,5) - self.assertAlmostEqual(a.sigma,6.238469796,5) - self.assertAlmostEqual(b.mu,31.3113582213,5) - self.assertAlmostEqual(b.sigma,6.69881865,5) - self.assertAlmostEqual(c.mu,18.6886417787,5) - - [a], [b], [c] = ttt.Game([[ttt.Player(ttt.Gaussian(25.0,25.0/3),25.0/6,25.0/300)],[ttt.Player(ttt.Gaussian(25.0,25.0/3),25.0/6,25.0/300)],[ttt.Player(ttt.Gaussian(25.0,25.0/3),25.0/6,25.0/300)]]).posteriors() - self.assertAlmostEqual(b.mu,25.000000,5) - self.assertAlmostEqual(b.sigma,6.238469796,5) - self.assertAlmostEqual(a.mu,31.3113582213,5) - self.assertAlmostEqual(a.sigma,6.69881865,5) - self.assertAlmostEqual(c.mu,18.6886417787,5) - - [a], [b], [c] = ttt.Game([[ttt.Player(ttt.Gaussian(25.0,25.0/3),25.0/6,25.0/300)],[ttt.Player(ttt.Gaussian(25.0,25.0/3),25.0/6,25.0/300)],[ttt.Player(ttt.Gaussian(25.0,25.0/3),25.0/6,25.0/300)]], [1,2,0],0.5).posteriors() - self.assertAlmostEqual(a.mu,25.000,3) - self.assertAlmostEqual(a.sigma,6.093,3) - self.assertAlmostEqual(b.mu,33.379,3) - self.assertAlmostEqual(b.sigma,6.484,3) - self.assertAlmostEqual(c.mu,16.621,3) + [a], [b], [c] = ttt.Game( + [ + [ttt.Player(ttt.Gaussian(25.0, 25.0 / 3), 25.0 / 6, 25.0 / 300)], + [ttt.Player(ttt.Gaussian(25.0, 25.0 / 3), 25.0 / 6, 25.0 / 300)], + [ttt.Player(ttt.Gaussian(25.0, 25.0 / 3), 25.0 / 6, 25.0 / 300)], + ], + [1, 2, 0], + ).posteriors() + self.assertAlmostEqual(a.mu, 25.000000, 5) + self.assertAlmostEqual(a.sigma, 6.238469796, 5) + self.assertAlmostEqual(b.mu, 31.3113582213, 5) + self.assertAlmostEqual(b.sigma, 6.69881865, 5) + self.assertAlmostEqual(c.mu, 18.6886417787, 5) + + [a], [b], [c] = ttt.Game( + [ + [ttt.Player(ttt.Gaussian(25.0, 25.0 / 3), 25.0 / 6, 25.0 / 300)], + [ttt.Player(ttt.Gaussian(25.0, 25.0 / 3), 25.0 / 6, 25.0 / 300)], + [ttt.Player(ttt.Gaussian(25.0, 25.0 / 3), 25.0 / 6, 25.0 / 300)], + ] + ).posteriors() + self.assertAlmostEqual(b.mu, 25.000000, 5) + self.assertAlmostEqual(b.sigma, 6.238469796, 5) + self.assertAlmostEqual(a.mu, 31.3113582213, 5) + self.assertAlmostEqual(a.sigma, 6.69881865, 5) + self.assertAlmostEqual(c.mu, 18.6886417787, 5) + + [a], [b], [c] = ttt.Game( + [ + [ttt.Player(ttt.Gaussian(25.0, 25.0 / 3), 25.0 / 6, 25.0 / 300)], + [ttt.Player(ttt.Gaussian(25.0, 25.0 / 3), 25.0 / 6, 25.0 / 300)], + [ttt.Player(ttt.Gaussian(25.0, 25.0 / 3), 25.0 / 6, 25.0 / 300)], + ], + [1, 2, 0], + 0.5, + ).posteriors() + self.assertAlmostEqual(a.mu, 25.000, 3) + self.assertAlmostEqual(a.sigma, 6.093, 3) + self.assertAlmostEqual(b.mu, 33.379, 3) + self.assertAlmostEqual(b.sigma, 6.484, 3) + self.assertAlmostEqual(c.mu, 16.621, 3) def test_1vs1_draw(self): """Test 1v1 games ending in a draw with various draw probabilities.""" - [a], [b] = ttt.Game([[ttt.Player(ttt.Gaussian(25.0,25.0/3),25.0/6,25.0/300)],[ttt.Player(ttt.Gaussian(25.0,25.0/3),25.0/6,25.0/300)]], [0,0], 0.25).posteriors() - self.assertAlmostEqual(a.mu,25.000,2) - self.assertAlmostEqual(a.sigma,6.469,2) - self.assertAlmostEqual(b.mu,25.000,2) - self.assertAlmostEqual(b.sigma,6.469,2) - - ta = [ttt.Player(ttt.Gaussian(25.,3.),25.0/6,25.0/300)] - tb = [ttt.Player(ttt.Gaussian(29.,2.),25.0/6,25.0/300)] - [a], [b] = ttt.Game([ta,tb], [0,0], 0.25).posteriors() - self.assertAlmostEqual(a.mu,25.736,4) - self.assertAlmostEqual(a.sigma,2.709956,4) - self.assertAlmostEqual(b.mu,28.67289,4) - self.assertAlmostEqual(b.sigma,1.916471,4) + [a], [b] = ttt.Game( + [ + [ttt.Player(ttt.Gaussian(25.0, 25.0 / 3), 25.0 / 6, 25.0 / 300)], + [ttt.Player(ttt.Gaussian(25.0, 25.0 / 3), 25.0 / 6, 25.0 / 300)], + ], + [0, 0], + 0.25, + ).posteriors() + self.assertAlmostEqual(a.mu, 25.000, 2) + self.assertAlmostEqual(a.sigma, 6.469, 2) + self.assertAlmostEqual(b.mu, 25.000, 2) + self.assertAlmostEqual(b.sigma, 6.469, 2) + + ta = [ttt.Player(ttt.Gaussian(25.0, 3.0), 25.0 / 6, 25.0 / 300)] + tb = [ttt.Player(ttt.Gaussian(29.0, 2.0), 25.0 / 6, 25.0 / 300)] + [a], [b] = ttt.Game([ta, tb], [0, 0], 0.25).posteriors() + self.assertAlmostEqual(a.mu, 25.736, 4) + self.assertAlmostEqual(a.sigma, 2.709956, 4) + self.assertAlmostEqual(b.mu, 28.67289, 4) + self.assertAlmostEqual(b.sigma, 1.916471, 4) def draw_evidence_game(self): """Test evidence computation for draw games.""" - home = ttt.Player(ttt.Gaussian(0,0.001)) - away = ttt.Player(ttt.Gaussian(0,0.001)) + home = ttt.Player(ttt.Gaussian(0, 0.001)) + away = ttt.Player(ttt.Gaussian(0, 0.001)) teams = [[home], [away]] result = [0, 0] g = ttt.Game(teams, result, p_draw=0.25) lhs = g.likelihoods[0][0] ev = g.evidence - self.assertAlmostEqual(ev,0.25) + self.assertAlmostEqual(ev, 0.25) def test_1vs1vs1_draw(self): """Test three-player games with draws and mixed outcomes.""" - [a], [b], [c] = ttt.Game([[ttt.Player(ttt.Gaussian(25.0,25.0/3),25.0/6,25.0/300)],[ttt.Player(ttt.Gaussian(25.0,25.0/3),25.0/6,25.0/300)],[ttt.Player(ttt.Gaussian(25.0,25.0/3),25.0/6,25.0/300)]], [0,0,0],0.25).posteriors() - self.assertAlmostEqual(a.mu,25.000,3) - self.assertAlmostEqual(a.sigma,5.729,3) - self.assertAlmostEqual(b.mu,25.000,3) - self.assertAlmostEqual(b.sigma,5.707,3) - - ta = [ttt.Player(ttt.Gaussian(25.,3.),25.0/6,25.0/300)] - tb = [ttt.Player(ttt.Gaussian(25.,3.),25.0/6,25.0/300)] - tc = [ttt.Player(ttt.Gaussian(29.,2.),25.0/6,25.0/300)] - [a], [b], [c] = ttt.Game([ta,tb,tc], [0,0,0],0.25).posteriors() - self.assertAlmostEqual(a.mu,25.489,3) - self.assertAlmostEqual(a.sigma,2.638,3) - self.assertAlmostEqual(b.mu,25.511,3) - self.assertAlmostEqual(b.sigma,2.629,3) - self.assertAlmostEqual(c.mu,28.556,3) - self.assertAlmostEqual(c.sigma,1.886,3) + [a], [b], [c] = ttt.Game( + [ + [ttt.Player(ttt.Gaussian(25.0, 25.0 / 3), 25.0 / 6, 25.0 / 300)], + [ttt.Player(ttt.Gaussian(25.0, 25.0 / 3), 25.0 / 6, 25.0 / 300)], + [ttt.Player(ttt.Gaussian(25.0, 25.0 / 3), 25.0 / 6, 25.0 / 300)], + ], + [0, 0, 0], + 0.25, + ).posteriors() + self.assertAlmostEqual(a.mu, 25.000, 3) + self.assertAlmostEqual(a.sigma, 5.729, 3) + self.assertAlmostEqual(b.mu, 25.000, 3) + self.assertAlmostEqual(b.sigma, 5.707, 3) + + ta = [ttt.Player(ttt.Gaussian(25.0, 3.0), 25.0 / 6, 25.0 / 300)] + tb = [ttt.Player(ttt.Gaussian(25.0, 3.0), 25.0 / 6, 25.0 / 300)] + tc = [ttt.Player(ttt.Gaussian(29.0, 2.0), 25.0 / 6, 25.0 / 300)] + [a], [b], [c] = ttt.Game([ta, tb, tc], [0, 0, 0], 0.25).posteriors() + self.assertAlmostEqual(a.mu, 25.489, 3) + self.assertAlmostEqual(a.sigma, 2.638, 3) + self.assertAlmostEqual(b.mu, 25.511, 3) + self.assertAlmostEqual(b.sigma, 2.629, 3) + self.assertAlmostEqual(c.mu, 28.556, 3) + self.assertAlmostEqual(c.sigma, 1.886, 3) def test_NvsN_Draw(self): """Test team vs team games with draws and various team sizes.""" - ta = [ttt.Player(ttt.Gaussian(15.,1.),25.0/6,25.0/300),ttt.Player(ttt.Gaussian(15.,1.),25.0/6,25.0/300)] - tb = [ttt.Player(ttt.Gaussian(30.,2.),25.0/6,25.0/300)] - [a,b], [c] = ttt.Game([ta,tb], [0,0], 0.25).posteriors() - self.assertAlmostEqual(a.mu,15.000,3) - self.assertAlmostEqual(a.sigma,0.9916,3) - self.assertAlmostEqual(b.mu,15.000,3) - self.assertAlmostEqual(b.sigma,0.9916,3) - self.assertAlmostEqual(c.mu,30.000,3) - self.assertAlmostEqual(c.sigma,1.9320,3) - - [a,b], [c] = ttt.Game([ta,tb], [1,0], 0.0).posteriors() - self.assertAlmostEqual(a.mu,15.105,3) - self.assertAlmostEqual(a.sigma,0.995,3) - - ta = [ttt.Player(ttt.Gaussian(15.,1.),25.0/6,25.0/300),ttt.Player(ttt.Gaussian(15.,1.),25.0/6,25.0/300)] - tb = [ttt.Player(ttt.Gaussian(15.,1.),25.0/6,25.0/300),ttt.Player(ttt.Gaussian(15.,1.),25.0/6,25.0/300)] - [a,b], [c,d] = ttt.Game([ta,tb], [1,0], 0.0).posteriors() - self.assertAlmostEqual(a.mu,15.093,3) - self.assertAlmostEqual(a.sigma,0.996,3) - self.assertAlmostEqual(c.mu,14.907,3) - self.assertAlmostEqual(c.sigma,0.996,3) + ta = [ + ttt.Player(ttt.Gaussian(15.0, 1.0), 25.0 / 6, 25.0 / 300), + ttt.Player(ttt.Gaussian(15.0, 1.0), 25.0 / 6, 25.0 / 300), + ] + tb = [ttt.Player(ttt.Gaussian(30.0, 2.0), 25.0 / 6, 25.0 / 300)] + [a, b], [c] = ttt.Game([ta, tb], [0, 0], 0.25).posteriors() + self.assertAlmostEqual(a.mu, 15.000, 3) + self.assertAlmostEqual(a.sigma, 0.9916, 3) + self.assertAlmostEqual(b.mu, 15.000, 3) + self.assertAlmostEqual(b.sigma, 0.9916, 3) + self.assertAlmostEqual(c.mu, 30.000, 3) + self.assertAlmostEqual(c.sigma, 1.9320, 3) + + [a, b], [c] = ttt.Game([ta, tb], [1, 0], 0.0).posteriors() + self.assertAlmostEqual(a.mu, 15.105, 3) + self.assertAlmostEqual(a.sigma, 0.995, 3) + + ta = [ + ttt.Player(ttt.Gaussian(15.0, 1.0), 25.0 / 6, 25.0 / 300), + ttt.Player(ttt.Gaussian(15.0, 1.0), 25.0 / 6, 25.0 / 300), + ] + tb = [ + ttt.Player(ttt.Gaussian(15.0, 1.0), 25.0 / 6, 25.0 / 300), + ttt.Player(ttt.Gaussian(15.0, 1.0), 25.0 / 6, 25.0 / 300), + ] + [a, b], [c, d] = ttt.Game([ta, tb], [1, 0], 0.0).posteriors() + self.assertAlmostEqual(a.mu, 15.093, 3) + self.assertAlmostEqual(a.sigma, 0.996, 3) + self.assertAlmostEqual(c.mu, 14.907, 3) + self.assertAlmostEqual(c.sigma, 0.996, 3) def test_NvsNvsN_mixt(self): """Test complex multi-team games with mixed team sizes and outcomes.""" - ta = [ttt.Player(ttt.Gaussian(12.,3.),25.0/6,25.0/300) - ,ttt.Player(ttt.Gaussian(18.,3.),25.0/6,25.0/300)] - tb = [ttt.Player(ttt.Gaussian(30.,3.),25.0/6,25.0/300)] - tc = [ttt.Player(ttt.Gaussian(14.,3.),25.0/6,25.0/300) - ,ttt.Player(ttt.Gaussian(16.,3.),25.0/6,25.0/300)] - [a,b], [c], [d,e] = ttt.Game([ta,tb, tc], [1,0,0], 0.25).posteriors() - self.assertAlmostEqual(a.mu,13.051,3) - self.assertAlmostEqual(a.sigma,2.864,3) - self.assertAlmostEqual(b.mu,19.051,3) - self.assertAlmostEqual(b.sigma,2.864,3) - self.assertAlmostEqual(c.mu,29.292,3) - self.assertAlmostEqual(c.sigma,2.764,3) - self.assertAlmostEqual(d.mu,13.658,3) - self.assertAlmostEqual(d.sigma,2.813,3) - self.assertAlmostEqual(e.mu,15.658,3) - self.assertAlmostEqual(e.sigma,2.813,3) + ta = [ + ttt.Player(ttt.Gaussian(12.0, 3.0), 25.0 / 6, 25.0 / 300), + ttt.Player(ttt.Gaussian(18.0, 3.0), 25.0 / 6, 25.0 / 300), + ] + tb = [ttt.Player(ttt.Gaussian(30.0, 3.0), 25.0 / 6, 25.0 / 300)] + tc = [ + ttt.Player(ttt.Gaussian(14.0, 3.0), 25.0 / 6, 25.0 / 300), + ttt.Player(ttt.Gaussian(16.0, 3.0), 25.0 / 6, 25.0 / 300), + ] + [a, b], [c], [d, e] = ttt.Game([ta, tb, tc], [1, 0, 0], 0.25).posteriors() + self.assertAlmostEqual(a.mu, 13.051, 3) + self.assertAlmostEqual(a.sigma, 2.864, 3) + self.assertAlmostEqual(b.mu, 19.051, 3) + self.assertAlmostEqual(b.sigma, 2.864, 3) + self.assertAlmostEqual(c.mu, 29.292, 3) + self.assertAlmostEqual(c.sigma, 2.764, 3) + self.assertAlmostEqual(d.mu, 13.658, 3) + self.assertAlmostEqual(d.sigma, 2.813, 3) + self.assertAlmostEqual(e.mu, 15.658, 3) + self.assertAlmostEqual(e.sigma, 2.813, 3) def test_evidence_1vs1(self): """Test evidence (marginal likelihood) computation for 1v1 games.""" - ta = [ttt.Player(ttt.Gaussian(25.,1e-7),25.0/6,25.0/300)] - tb = [ttt.Player(ttt.Gaussian(25.,1e-7),25.0/6,25.0/300)] - g = ttt.Game([ta,tb], [0,0], 0.25) - self.assertAlmostEqual(g.evidence,0.25,3) - g = ttt.Game([ta,tb], [1,0], 0.25) - self.assertAlmostEqual(g.evidence,0.375,3) + ta = [ttt.Player(ttt.Gaussian(25.0, 1e-7), 25.0 / 6, 25.0 / 300)] + tb = [ttt.Player(ttt.Gaussian(25.0, 1e-7), 25.0 / 6, 25.0 / 300)] + g = ttt.Game([ta, tb], [0, 0], 0.25) + self.assertAlmostEqual(g.evidence, 0.25, 3) + g = ttt.Game([ta, tb], [1, 0], 0.25) + self.assertAlmostEqual(g.evidence, 0.375, 3) def test_1vs1vs1_margin_0(self): """Test that evidence sums to ~1 for all possible three-player outcomes.""" - ta = [ttt.Player(ttt.Gaussian(25.,1e-7),25.0/6,25.0/300)] - tb = [ttt.Player(ttt.Gaussian(25.,1e-7),25.0/6,25.0/300)] - tc = [ttt.Player(ttt.Gaussian(25.,1e-7),25.0/6,25.0/300)] + ta = [ttt.Player(ttt.Gaussian(25.0, 1e-7), 25.0 / 6, 25.0 / 300)] + tb = [ttt.Player(ttt.Gaussian(25.0, 1e-7), 25.0 / 6, 25.0 / 300)] + tc = [ttt.Player(ttt.Gaussian(25.0, 1e-7), 25.0 / 6, 25.0 / 300)] proba = 0 - proba += ttt.Game([ta,tb,tc], [3,2,1], 0.).evidence - proba += ttt.Game([ta,tb,tc], [3,1,2], 0.).evidence - proba += ttt.Game([ta,tb,tc], [2,3,1], 0.).evidence - proba += ttt.Game([ta,tb,tc], [1,3,2], 0.).evidence - proba += ttt.Game([ta,tb,tc], [2,1,3], 0.).evidence - proba += ttt.Game([ta,tb,tc], [1,2,3], 0.).evidence + proba += ttt.Game([ta, tb, tc], [3, 2, 1], 0.0).evidence + proba += ttt.Game([ta, tb, tc], [3, 1, 2], 0.0).evidence + proba += ttt.Game([ta, tb, tc], [2, 3, 1], 0.0).evidence + proba += ttt.Game([ta, tb, tc], [1, 3, 2], 0.0).evidence + proba += ttt.Game([ta, tb, tc], [2, 1, 3], 0.0).evidence + proba += ttt.Game([ta, tb, tc], [1, 2, 3], 0.0).evidence self.assertAlmostEqual(proba, 0.9952751273757627) @@ -264,172 +333,235 @@ def test_12_factorial_combinations(self): """Test evidence for 12-player game approaches uniform distribution.""" teams = [] for i in range(12): - teams.append([ttt.Player(ttt.Gaussian(0,0),1,0)]) + teams.append([ttt.Player(ttt.Gaussian(0, 0), 1, 0)]) - evidence = ttt.Game(teams, list(range(12)), 0.).evidence + evidence = ttt.Game(teams, list(range(12)), 0.0).evidence self.assertAlmostEqual(evidence, 1.2180818904254274e-08) - self.assertAlmostEqual(1/479001600, 2.08767569878681e-09) - self.assertAlmostEqual(evidence*479001600, 5.83463174444807) + self.assertAlmostEqual(1 / 479001600, 2.08767569878681e-09) + self.assertAlmostEqual(evidence * 479001600, 5.83463174444807) def test_history_init(self): """Test History initialization and forward propagation (TrueSkill mode).""" - composition = [ [["aa"],["b"]], [["aa"],["c"]] , [["b"],["c"]] ] - results = [[1,0],[0,1],[1,0]] + composition = [[["aa"], ["b"]], [["aa"], ["c"]], [["b"], ["c"]]] + results = [[1, 0], [0, 1], [1, 0]] priors = dict() for k in ["aa", "b", "c"]: - priors[k] = ttt.Player(ttt.Gaussian(25., 25.0/3), 25.0/6, 0.15*25.0/3) + priors[k] = ttt.Player( + ttt.Gaussian(25.0, 25.0 / 3), 25.0 / 6, 0.15 * 25.0 / 3 + ) - h = ttt.History(composition, results, [1,2,3],priors) - h.forward_propagation() # TrueSkill + h = ttt.History(composition, results, [1, 2, 3], priors) + h.forward_propagation() # TrueSkill p0 = h.learning_curves() - self.assertAlmostEqual(p0["aa"][0][1].mu,29.205,3) - self.assertAlmostEqual(p0["aa"][0][1].sigma,7.19448,3) + self.assertAlmostEqual(p0["aa"][0][1].mu, 29.205, 3) + self.assertAlmostEqual(p0["aa"][0][1].sigma, 7.19448, 3) observed = p0["aa"][1][1] - [expected], [c] = ttt.Game(h.within_priors(b=1,e=0),[0,1]).posteriors() + [expected], [c] = ttt.Game(h.within_priors(b=1, e=0), [0, 1]).posteriors() self.assertAlmostEqual(observed.mu, expected.mu, 3) self.assertAlmostEqual(observed.sigma, expected.sigma, 3) def test_one_batch_history(self): """Test single batch history with convergence iterations.""" - composition = [ [['aj'],['bj']],[['bj'],['cj']], [['cj'],['aj']] ] - results = [[1,0],[1,0],[1,0]] - times = [1,1,1] + composition = [[["aj"], ["bj"]], [["bj"], ["cj"]], [["cj"], ["aj"]]] + results = [[1, 0], [1, 0], [1, 0]] + times = [1, 1, 1] priors = dict() for k in ["aj", "bj", "cj"]: - priors[k] = ttt.Player(ttt.Gaussian(25., 25.0/3), 25.0/6, 0.15*25.0/3) - h1 = ttt.History(composition,results, times,priors) + priors[k] = ttt.Player( + ttt.Gaussian(25.0, 25.0 / 3), 25.0 / 6, 0.15 * 25.0 / 3 + ) + h1 = ttt.History(composition, results, times, priors) h1.forward_propagation() - self.assertAlmostEqual(h1.bskills[0]["aj"].posterior.mu,22.904,3) - self.assertAlmostEqual(h1.bskills[0]["aj"].posterior.sigma,6.010,3) - self.assertAlmostEqual(h1.bskills[0]["cj"].posterior.mu,25.110,3) - self.assertAlmostEqual(h1.bskills[0]["cj"].posterior.sigma,5.866,3) - step , i = h1.convergence(iterations=10,epsilon=0.0001, verbose=False) - self.assertAlmostEqual(h1.bskills[0]["aj"].posterior.mu,25.000,3) - self.assertAlmostEqual(h1.bskills[0]["aj"].posterior.sigma,5.419,3) - self.assertAlmostEqual(h1.bskills[0]["cj"].posterior.mu,25.000,3) - self.assertAlmostEqual(h1.bskills[0]["cj"].posterior.sigma,5.419,3) + self.assertAlmostEqual(h1.bskills[0]["aj"].posterior.mu, 22.904, 3) + self.assertAlmostEqual(h1.bskills[0]["aj"].posterior.sigma, 6.010, 3) + self.assertAlmostEqual(h1.bskills[0]["cj"].posterior.mu, 25.110, 3) + self.assertAlmostEqual(h1.bskills[0]["cj"].posterior.sigma, 5.866, 3) + step, i = h1.convergence(iterations=10, epsilon=0.0001, verbose=False) + self.assertAlmostEqual(h1.bskills[0]["aj"].posterior.mu, 25.000, 3) + self.assertAlmostEqual(h1.bskills[0]["aj"].posterior.sigma, 5.419, 3) + self.assertAlmostEqual(h1.bskills[0]["cj"].posterior.mu, 25.000, 3) + self.assertAlmostEqual(h1.bskills[0]["cj"].posterior.sigma, 5.419, 3) priors = dict() for k in ["aj", "bj", "cj"]: - priors[k] = ttt.Player(ttt.Gaussian(25., 25.0/3), 25.0/6, 25.0/300) - h2 = ttt.History(composition,results, [1,2,3], priors) + priors[k] = ttt.Player(ttt.Gaussian(25.0, 25.0 / 3), 25.0 / 6, 25.0 / 300) + h2 = ttt.History(composition, results, [1, 2, 3], priors) h2.forward_propagation() - self.assertAlmostEqual(h2.bskills[2]["aj"].posterior.mu,22.904,3) - self.assertAlmostEqual(h2.bskills[2]["aj"].posterior.sigma,6.011,3) - self.assertAlmostEqual(h2.bskills[2]["cj"].posterior.mu,25.111,3) - self.assertAlmostEqual(h2.bskills[2]["cj"].posterior.sigma,5.867,3) - #h1.backward_propagation() - step , i = h2.convergence(iterations=10,epsilon=0.0001, verbose=False) - self.assertAlmostEqual(h2.bskills[2]["aj"].posterior.mu,24.999,3) - self.assertAlmostEqual(h2.bskills[2]["aj"].posterior.sigma,5.420,3) - self.assertAlmostEqual(h2.bskills[2]["cj"].posterior.mu,25.001,3) - self.assertAlmostEqual(h2.bskills[2]["cj"].posterior.sigma,5.420,3) + self.assertAlmostEqual(h2.bskills[2]["aj"].posterior.mu, 22.904, 3) + self.assertAlmostEqual(h2.bskills[2]["aj"].posterior.sigma, 6.011, 3) + self.assertAlmostEqual(h2.bskills[2]["cj"].posterior.mu, 25.111, 3) + self.assertAlmostEqual(h2.bskills[2]["cj"].posterior.sigma, 5.867, 3) + # h1.backward_propagation() + step, i = h2.convergence(iterations=10, epsilon=0.0001, verbose=False) + self.assertAlmostEqual(h2.bskills[2]["aj"].posterior.mu, 24.999, 3) + self.assertAlmostEqual(h2.bskills[2]["aj"].posterior.sigma, 5.420, 3) + self.assertAlmostEqual(h2.bskills[2]["cj"].posterior.mu, 25.001, 3) + self.assertAlmostEqual(h2.bskills[2]["cj"].posterior.sigma, 5.420, 3) def test_trueSkill_Through_Time(self): """Test TrueSkill Through Time algorithm with multiple batches.""" - composition = [ [["a"],["b"]], [["a"],["c"]] , [["b"],["c"]] ] - results = [[1,0],[0,1],[1,0]] - priors = {k: ttt.Player(ttt.Gaussian(25., 25.0/3), 25.0/6, 25.0/300) for k in ["a", "b", "c"]} - h = ttt.History(composition , results, [], priors) + composition = [[["a"], ["b"]], [["a"], ["c"]], [["b"], ["c"]]] + results = [[1, 0], [0, 1], [1, 0]] + priors = { + k: ttt.Player(ttt.Gaussian(25.0, 25.0 / 3), 25.0 / 6, 25.0 / 300) + for k in ["a", "b", "c"] + } + h = ttt.History(composition, results, [], priors) # BIG CHANGE. # Version 0 tenía elapsed 1 entre dos skills si no se le pasaban los tiempos # Version 1 Si no se le pasa tiempo, # Decidir: # - todo ocurre en el mismo momento. # - los tiempos son los indices de la posición (* la que se está usando) - step , i = h.convergence(iterations=10,epsilon=0, verbose=False) - #print(h.learning_curves()) - self.assertAlmostEqual(h.bskills[0]["a"].posterior.mu,25.000267,5) - self.assertAlmostEqual(h.bskills[0]["a"].posterior.sigma,5.419423,5) - self.assertAlmostEqual(h.bskills[0]["b"].posterior.mu,24.999198,5) - self.assertAlmostEqual(h.bskills[0]["b"].posterior.sigma,5.419511,5) - self.assertAlmostEqual(h.bskills[2]["b"].posterior.mu,25.001332,5) - self.assertAlmostEqual(h.bskills[2]["b"].posterior.sigma,5.420053,5) + step, i = h.convergence(iterations=10, epsilon=0, verbose=False) + # print(h.learning_curves()) + self.assertAlmostEqual(h.bskills[0]["a"].posterior.mu, 25.000267, 5) + self.assertAlmostEqual(h.bskills[0]["a"].posterior.sigma, 5.419423, 5) + self.assertAlmostEqual(h.bskills[0]["b"].posterior.mu, 24.999198, 5) + self.assertAlmostEqual(h.bskills[0]["b"].posterior.sigma, 5.419511, 5) + self.assertAlmostEqual(h.bskills[2]["b"].posterior.mu, 25.001332, 5) + self.assertAlmostEqual(h.bskills[2]["b"].posterior.sigma, 5.420053, 5) def test_env_TTT(self): """Test TTT with custom environment parameters.""" - composition = [ [["a"],["b"]], [["a"],["c"]] , [["b"],["c"]] ] - results = [[1,0],[0,1],[1,0]] - - h = ttt.History(composition=composition, results=results, mu=25., sigma=25.0/3, beta=25.0/6, gamma=25.0/300) - step , i = h.convergence(verbose=False) - #print(h.learning_curves()) - self.assertAlmostEqual(h.bskills[0]["a"].posterior.mu,25.000268,5) - self.assertAlmostEqual(h.bskills[0]["a"].posterior.sigma,5.419423,5) - self.assertAlmostEqual(h.bskills[0]["b"].posterior.mu,24.999198,5) - self.assertAlmostEqual(h.bskills[0]["b"].posterior.sigma,5.419511,5) - self.assertAlmostEqual(h.bskills[2]["b"].posterior.mu,25.001332,5) - self.assertAlmostEqual(h.bskills[2]["b"].posterior.sigma,5.420053,5) + composition = [[["a"], ["b"]], [["a"], ["c"]], [["b"], ["c"]]] + results = [[1, 0], [0, 1], [1, 0]] + + h = ttt.History( + composition=composition, + results=results, + mu=25.0, + sigma=25.0 / 3, + beta=25.0 / 6, + gamma=25.0 / 300, + ) + step, i = h.convergence(verbose=False) + # print(h.learning_curves()) + self.assertAlmostEqual(h.bskills[0]["a"].posterior.mu, 25.000268, 5) + self.assertAlmostEqual(h.bskills[0]["a"].posterior.sigma, 5.419423, 5) + self.assertAlmostEqual(h.bskills[0]["b"].posterior.mu, 24.999198, 5) + self.assertAlmostEqual(h.bskills[0]["b"].posterior.sigma, 5.419511, 5) + self.assertAlmostEqual(h.bskills[2]["b"].posterior.mu, 25.001332, 5) + self.assertAlmostEqual(h.bskills[2]["b"].posterior.sigma, 5.420053, 5) def test_env_0_TTT(self): """Test TTT with zero-centered priors and custom parameters.""" - composition = [ [["a"],["b"]], [["a"],["c"]] , [["b"],["c"]] ] - results = [[1,0],[0,1],[1,0]] - h = ttt.History(composition=composition, results=results, mu=0.0,sigma=6.0, beta=1.0, gamma=0.05) - step , i = h.convergence(iterations=14, verbose=False, epsilon=0) - #print(h.learning_curves()) - self.assertAlmostEqual(h.bskills[0]["a"].posterior.mu,0.000548,5) - self.assertAlmostEqual(h.bskills[0]["a"].posterior.sigma,2.395715,5) - self.assertAlmostEqual(h.bskills[0]["b"].posterior.mu,-0.001641,5) - self.assertAlmostEqual(h.bskills[0]["b"].posterior.sigma,2.395765,5) - self.assertAlmostEqual(h.bskills[2]["b"].posterior.mu,0.001762,5) - self.assertAlmostEqual(h.bskills[2]["b"].posterior.sigma,2.395930,5) - - composition = [ [["a"],["b"]], [["c"],["a"]] , [["b"],["c"]] ] - h = ttt.History(composition=composition, mu=0.0,sigma=6.0, beta=1.0, gamma=0.05) - step , i = h.convergence(iterations=14, epsilon=0, verbose=False) - self.assertAlmostEqual(h.bskills[0]["a"].posterior.mu,0.000548,5) - self.assertAlmostEqual(h.bskills[0]["a"].posterior.sigma,2.395715,5) - self.assertAlmostEqual(h.bskills[0]["b"].posterior.mu,-0.001641,5) - self.assertAlmostEqual(h.bskills[0]["b"].posterior.sigma,2.395765,5) - self.assertAlmostEqual(h.bskills[2]["b"].posterior.mu,0.001762,5) - self.assertAlmostEqual(h.bskills[2]["b"].posterior.sigma,2.395930,5) + composition = [[["a"], ["b"]], [["a"], ["c"]], [["b"], ["c"]]] + results = [[1, 0], [0, 1], [1, 0]] + h = ttt.History( + composition=composition, + results=results, + mu=0.0, + sigma=6.0, + beta=1.0, + gamma=0.05, + ) + step, i = h.convergence(iterations=14, verbose=False, epsilon=0) + # print(h.learning_curves()) + self.assertAlmostEqual(h.bskills[0]["a"].posterior.mu, 0.000548, 5) + self.assertAlmostEqual(h.bskills[0]["a"].posterior.sigma, 2.395715, 5) + self.assertAlmostEqual(h.bskills[0]["b"].posterior.mu, -0.001641, 5) + self.assertAlmostEqual(h.bskills[0]["b"].posterior.sigma, 2.395765, 5) + self.assertAlmostEqual(h.bskills[2]["b"].posterior.mu, 0.001762, 5) + self.assertAlmostEqual(h.bskills[2]["b"].posterior.sigma, 2.395930, 5) + + composition = [[["a"], ["b"]], [["c"], ["a"]], [["b"], ["c"]]] + h = ttt.History( + composition=composition, mu=0.0, sigma=6.0, beta=1.0, gamma=0.05 + ) + step, i = h.convergence(iterations=14, epsilon=0, verbose=False) + self.assertAlmostEqual(h.bskills[0]["a"].posterior.mu, 0.000548, 5) + self.assertAlmostEqual(h.bskills[0]["a"].posterior.sigma, 2.395715, 5) + self.assertAlmostEqual(h.bskills[0]["b"].posterior.mu, -0.001641, 5) + self.assertAlmostEqual(h.bskills[0]["b"].posterior.sigma, 2.395765, 5) + self.assertAlmostEqual(h.bskills[2]["b"].posterior.mu, 0.001762, 5) + self.assertAlmostEqual(h.bskills[2]["b"].posterior.sigma, 2.395930, 5) def test_teams(self): """Test team games through history tracking.""" - composition = [ [["a","b"],["c","d"]], [["e","f"] , ["b","c"]], [["a","d"], ["e","f"]] ] - results = [[1,0],[0,1],[1,0]] - h = ttt.History(composition=composition, results=results, mu=0.0,sigma=6.0, beta=1.0, gamma=0.0) + composition = [ + [["a", "b"], ["c", "d"]], + [["e", "f"], ["b", "c"]], + [["a", "d"], ["e", "f"]], + ] + results = [[1, 0], [0, 1], [1, 0]] + h = ttt.History( + composition=composition, + results=results, + mu=0.0, + sigma=6.0, + beta=1.0, + gamma=0.0, + ) step, i = h.convergence(verbose=False) - #print(h.learning_curves()) - self.assertAlmostEqual(h.bskills[0]["a"].posterior.mu,h.bskills[0]["b"].posterior.mu,5) - self.assertAlmostEqual(h.bskills[0]["a"].posterior.sigma, h.bskills[0]["b"].posterior.sigma,5) - self.assertAlmostEqual(h.bskills[0]["c"].posterior.mu,h.bskills[0]["d"].posterior.mu,3) - self.assertAlmostEqual(h.bskills[0]["c"].posterior.sigma,h.bskills[0]["d"].posterior.sigma,3) - self.assertAlmostEqual(h.bskills[1]["e"].posterior.mu,h.bskills[1]["f"].posterior.mu,3) - self.assertAlmostEqual(h.bskills[1]["e"].posterior.sigma,h.bskills[1]["f"].posterior.sigma,3) - - self.assertAlmostEqual(h.bskills[0]["a"].posterior.mu,4.0849024,5) - self.assertAlmostEqual(h.bskills[0]["a"].posterior.sigma,5.106919056,5) - self.assertAlmostEqual(h.bskills[0]["c"].posterior.mu,-0.53302949,5) - self.assertAlmostEqual(h.bskills[0]["c"].posterior.sigma,5.1069190,5) - self.assertAlmostEqual(h.bskills[2]["e"].posterior.mu,-3.551872939,5) - self.assertAlmostEqual(h.bskills[2]["e"].posterior.sigma,5.15456970,5) + # print(h.learning_curves()) + self.assertAlmostEqual( + h.bskills[0]["a"].posterior.mu, h.bskills[0]["b"].posterior.mu, 5 + ) + self.assertAlmostEqual( + h.bskills[0]["a"].posterior.sigma, h.bskills[0]["b"].posterior.sigma, 5 + ) + self.assertAlmostEqual( + h.bskills[0]["c"].posterior.mu, h.bskills[0]["d"].posterior.mu, 3 + ) + self.assertAlmostEqual( + h.bskills[0]["c"].posterior.sigma, h.bskills[0]["d"].posterior.sigma, 3 + ) + self.assertAlmostEqual( + h.bskills[1]["e"].posterior.mu, h.bskills[1]["f"].posterior.mu, 3 + ) + self.assertAlmostEqual( + h.bskills[1]["e"].posterior.sigma, h.bskills[1]["f"].posterior.sigma, 3 + ) + + self.assertAlmostEqual(h.bskills[0]["a"].posterior.mu, 4.0849024, 5) + self.assertAlmostEqual(h.bskills[0]["a"].posterior.sigma, 5.106919056, 5) + self.assertAlmostEqual(h.bskills[0]["c"].posterior.mu, -0.53302949, 5) + self.assertAlmostEqual(h.bskills[0]["c"].posterior.sigma, 5.1069190, 5) + self.assertAlmostEqual(h.bskills[2]["e"].posterior.mu, -3.551872939, 5) + self.assertAlmostEqual(h.bskills[2]["e"].posterior.sigma, 5.15456970, 5) + def test_sigma_beta_0(self): - composition = [ [["a","a_b","b"],["c","c_d","d"]] - , [["e","e_f","f"],["b","b_c","c"]] - , [["a","a_d","d"],["e","e_f","f"]] ] - results = [[1,0],[0,1],[1,0]] + composition = [ + [["a", "a_b", "b"], ["c", "c_d", "d"]], + [["e", "e_f", "f"], ["b", "b_c", "c"]], + [["a", "a_d", "d"], ["e", "e_f", "f"]], + ] + results = [[1, 0], [0, 1], [1, 0]] priors = dict() for k in ["a_b", "c_d", "e_f", "b_c", "a_d", "e_f"]: - priors[k] = ttt.Player(ttt.Gaussian(mu=0.0, sigma=1e-7), beta=0.0, gamma=0.2) - h = ttt.History(composition=composition, results=results, priors=priors, mu=0.0,sigma=6.0, beta=1.0, gamma=0.0) - step , i = h.convergence(verbose=False) - self.assertAlmostEqual(h.bskills[0]["a_b"].posterior.mu,0.0,5) - self.assertAlmostEqual(h.bskills[0]["a_b"].posterior.sigma,0.0,5) - self.assertAlmostEqual(h.bskills[2]["e_f"].posterior.mu,-0.0019730,5) - self.assertAlmostEqual(h.bskills[2]["e_f"].posterior.sigma,0.19998286,5) + priors[k] = ttt.Player( + ttt.Gaussian(mu=0.0, sigma=1e-7), beta=0.0, gamma=0.2 + ) + h = ttt.History( + composition=composition, + results=results, + priors=priors, + mu=0.0, + sigma=6.0, + beta=1.0, + gamma=0.0, + ) + step, i = h.convergence(verbose=False) + self.assertAlmostEqual(h.bskills[0]["a_b"].posterior.mu, 0.0, 5) + self.assertAlmostEqual(h.bskills[0]["a_b"].posterior.sigma, 0.0, 5) + self.assertAlmostEqual(h.bskills[2]["e_f"].posterior.mu, -0.0019730, 5) + self.assertAlmostEqual(h.bskills[2]["e_f"].posterior.sigma, 0.19998286, 5) + def test_memory_Size(self): def summarysize(obj): import sys - from types import ModuleType, FunctionType from gc import get_referents + from types import FunctionType, ModuleType + BLACKLIST = type, ModuleType, FunctionType if isinstance(obj, BLACKLIST): - raise TypeError('getsize() does not take argument of type: '+ str(type(obj))) + raise TypeError( + "getsize() does not take argument of type: " + str(type(obj)) + ) seen_ids = set() size = 0 objects = [obj] @@ -443,170 +575,241 @@ def summarysize(obj): objects = get_referents(*need_referents) return size - composition = [ [["a"],["b"]], [["a"],["c"]] , [["b"],["c"]] ] - results = [[1,0],[0,1],[1,0]] - h = ttt.History(composition =composition, results=results, times = [0, 10, 20], mu=0.0,sigma=6.0, beta=1.0, gamma=0.05) - #print(summarysize(h)) - #print(summarysize(h.bskills)) - self.assertEqual( summarysize(h) < 9375 , True) # Antes 13000 - self.assertEqual( summarysize(h.bskills) < 5081 , True) - self.assertEqual( summarysize(h.batches) < 1103 , True) + composition = [[["a"], ["b"]], [["a"], ["c"]], [["b"], ["c"]]] + results = [[1, 0], [0, 1], [1, 0]] + h = ttt.History( + composition=composition, + results=results, + times=[0, 10, 20], + mu=0.0, + sigma=6.0, + beta=1.0, + gamma=0.05, + ) + # print(summarysize(h)) + # print(summarysize(h.bskills)) + self.assertEqual(summarysize(h) < 9375, True) # Antes 13000 + self.assertEqual(summarysize(h.bskills) < 5081, True) + self.assertEqual(summarysize(h.batches) < 1103, True) + def test_learning_curve(self): - composition = [ [["aj"],["bj"]],[["bj"],["cj"]], [["cj"],["aj"]] ] - results = [[1,0],[1,0],[1,0]] + composition = [[["aj"], ["bj"]], [["bj"], ["cj"]], [["cj"], ["aj"]]] + results = [[1, 0], [1, 0], [1, 0]] priors = dict() for k in ["aj", "bj", "cj"]: - priors[k] = ttt.Player(ttt.Gaussian(25., 25.0/3), 25.0/6, 25.0/300) - h = ttt.History(composition,results, [5,6,7], priors) + priors[k] = ttt.Player(ttt.Gaussian(25.0, 25.0 / 3), 25.0 / 6, 25.0 / 300) + h = ttt.History(composition, results, [5, 6, 7], priors) h.convergence(verbose=False) lc = h.learning_curves() - self.assertEqual(lc["aj"][0][0],5) - self.assertEqual(lc["aj"][-1][0],7) - self.assertAlmostEqual(lc["aj"][-1][1].mu,24.999,3) - self.assertAlmostEqual(lc["aj"][-1][1].sigma,5.420,3) - self.assertAlmostEqual(lc["cj"][-1][1].mu,25.001,3) - self.assertAlmostEqual(lc["cj"][-1][1].sigma,5.420,3) + self.assertEqual(lc["aj"][0][0], 5) + self.assertEqual(lc["aj"][-1][0], 7) + self.assertAlmostEqual(lc["aj"][-1][1].mu, 24.999, 3) + self.assertAlmostEqual(lc["aj"][-1][1].sigma, 5.420, 3) + self.assertAlmostEqual(lc["cj"][-1][1].mu, 25.001, 3) + self.assertAlmostEqual(lc["cj"][-1][1].sigma, 5.420, 3) + def test_1vs1_with_weights(self): - ta = [ttt.Player(ttt.Gaussian(25.0,25.0/3),25.0/6,0.0)] + ta = [ttt.Player(ttt.Gaussian(25.0, 25.0 / 3), 25.0 / 6, 0.0)] wa = [1.0] - tb = [ttt.Player(ttt.Gaussian(25.0,25.0/3),25.0/6,0.0)] + tb = [ttt.Player(ttt.Gaussian(25.0, 25.0 / 3), 25.0 / 6, 0.0)] wb = [2.0] - g = ttt.Game([ta,tb], weights=[wa,wb]) + g = ttt.Game([ta, tb], weights=[wa, wb]) post = g.posteriors() - self.assertTrue(post[0][0].isapprox( ttt.Gaussian(30.625173, 7.765472))) - self.assertTrue(post[1][0].isapprox( ttt.Gaussian(13.749653, 5.733839))) + self.assertTrue(post[0][0].isapprox(ttt.Gaussian(30.625173, 7.765472))) + self.assertTrue(post[1][0].isapprox(ttt.Gaussian(13.749653, 5.733839))) wa = [1.0] wb = [0.7] - g = ttt.Game([ta,tb], weights=[wa,wb]) + g = ttt.Game([ta, tb], weights=[wa, wb]) post = g.posteriors() - self.assertTrue(post[0][0].isapprox( ttt.Gaussian(27.630081, 7.206677))) - self.assertTrue(post[1][0].isapprox( ttt.Gaussian(23.158943, 7.801628))) + self.assertTrue(post[0][0].isapprox(ttt.Gaussian(27.630081, 7.206677))) + self.assertTrue(post[1][0].isapprox(ttt.Gaussian(23.158943, 7.801628))) wa = [1.6] wb = [0.7] - g = ttt.Game([ta,tb], weights=[wa,wb]) + g = ttt.Game([ta, tb], weights=[wa, wb]) post = g.posteriors() - self.assertTrue(post[0][0].isapprox( ttt.Gaussian(26.142438, 7.573088), 1e-4)) - self.assertTrue(post[1][0].isapprox( ttt.Gaussian(24.500183, 8.193278), 1e-4)) + self.assertTrue(post[0][0].isapprox(ttt.Gaussian(26.142438, 7.573088), 1e-4)) + self.assertTrue(post[1][0].isapprox(ttt.Gaussian(24.500183, 8.193278), 1e-4)) - wa = [1.0]; wb = [0.0] - ta = [ttt.Player(ttt.Gaussian(2.0,6.0),1.0,0.0)] - tb = [ttt.Player(ttt.Gaussian(2.0,6.0),1.0,0.0)] - g = ttt.Game([ta,tb], weights=[wa,wb]) + wa = [1.0] + wb = [0.0] + ta = [ttt.Player(ttt.Gaussian(2.0, 6.0), 1.0, 0.0)] + tb = [ttt.Player(ttt.Gaussian(2.0, 6.0), 1.0, 0.0)] + g = ttt.Game([ta, tb], weights=[wa, wb]) post = g.posteriors() - self.assertTrue(post[0][0].isapprox( ttt.Gaussian(5.557176746, 4.0527906913), 1e-3)) - self.assertTrue(post[1][0].isapprox( ttt.Gaussian(2.0, 6.0), 1e-4)) + self.assertTrue( + post[0][0].isapprox(ttt.Gaussian(5.557176746, 4.0527906913), 1e-3) + ) + self.assertTrue(post[1][0].isapprox(ttt.Gaussian(2.0, 6.0), 1e-4)) # NOTA: trueskill original tiene probelmas en la aproximación: post[1][0].mu = 1.999644 - - wa = [1.0]; wb = [-1.0] - ta = [ttt.Player(ttt.Gaussian(2.0,6.0),1.0,0.0)] - tb = [ttt.Player(ttt.Gaussian(2.0,6.0),1.0,0.0)] - g = ttt.Game([ta,tb], weights=[wa,wb]) + wa = [1.0] + wb = [-1.0] + ta = [ttt.Player(ttt.Gaussian(2.0, 6.0), 1.0, 0.0)] + tb = [ttt.Player(ttt.Gaussian(2.0, 6.0), 1.0, 0.0)] + g = ttt.Game([ta, tb], weights=[wa, wb]) post = g.posteriors() - self.assertTrue(post[0][0].isapprox( post[1][0], 1e-4)) + self.assertTrue(post[0][0].isapprox(post[1][0], 1e-4)) + def test_NvsN_with_weights(self): - ta = [ttt.Player(ttt.Gaussian(25.0,25.0/3),25.0/6,0.0), ttt.Player(ttt.Gaussian(25.0,25.0/3),25.0/6,0.0)] + ta = [ + ttt.Player(ttt.Gaussian(25.0, 25.0 / 3), 25.0 / 6, 0.0), + ttt.Player(ttt.Gaussian(25.0, 25.0 / 3), 25.0 / 6, 0.0), + ] wa = [0.4, 0.8] - tb = [ttt.Player(ttt.Gaussian(25.0,25.0/3),25.0/6,0.0), ttt.Player(ttt.Gaussian(25.0,25.0/3),25.0/6,0.0)] + tb = [ + ttt.Player(ttt.Gaussian(25.0, 25.0 / 3), 25.0 / 6, 0.0), + ttt.Player(ttt.Gaussian(25.0, 25.0 / 3), 25.0 / 6, 0.0), + ] wb = [0.9, 0.6] - g = ttt.Game([ta,tb], weights=[wa,wb]) + g = ttt.Game([ta, tb], weights=[wa, wb]) post = g.posteriors() - self.assertTrue(post[0][0].isapprox( ttt.Gaussian(27.539023, 8.129639), 1e-4)) - self.assertTrue(post[0][1].isapprox( ttt.Gaussian(30.078046, 7.485372), 1e-4)) - self.assertTrue(post[1][0].isapprox( ttt.Gaussian(19.287197, 7.243465), 1e-4)) - self.assertTrue(post[1][1].isapprox( ttt.Gaussian(21.191465, 7.867608), 1e-4)) + self.assertTrue(post[0][0].isapprox(ttt.Gaussian(27.539023, 8.129639), 1e-4)) + self.assertTrue(post[0][1].isapprox(ttt.Gaussian(30.078046, 7.485372), 1e-4)) + self.assertTrue(post[1][0].isapprox(ttt.Gaussian(19.287197, 7.243465), 1e-4)) + self.assertTrue(post[1][1].isapprox(ttt.Gaussian(21.191465, 7.867608), 1e-4)) wa = [1.3, 1.5] wb = [0.7, 0.4] - g = ttt.Game([ta,tb], weights=[wa,wb]) + g = ttt.Game([ta, tb], weights=[wa, wb]) post = g.posteriors() - self.assertTrue(post[0][0].isapprox( ttt.Gaussian(25.190190, 8.220511), 1e-4)) - self.assertTrue(post[0][1].isapprox( ttt.Gaussian(25.219450, 8.182783), 1e-4)) - self.assertTrue(post[1][0].isapprox( ttt.Gaussian(24.897589, 8.300779), 1e-4)) - self.assertTrue(post[1][1].isapprox( ttt.Gaussian(24.941479, 8.322717), 1e-4)) + self.assertTrue(post[0][0].isapprox(ttt.Gaussian(25.190190, 8.220511), 1e-4)) + self.assertTrue(post[0][1].isapprox(ttt.Gaussian(25.219450, 8.182783), 1e-4)) + self.assertTrue(post[1][0].isapprox(ttt.Gaussian(24.897589, 8.300779), 1e-4)) + self.assertTrue(post[1][1].isapprox(ttt.Gaussian(24.941479, 8.322717), 1e-4)) wa = [1.6, 0.2] wb = [0.7, 2.4] - g = ttt.Game([ta,tb], weights=[wa,wb]) + g = ttt.Game([ta, tb], weights=[wa, wb]) post = g.posteriors() - self.assertTrue(post[0][0].isapprox( ttt.Gaussian(31.674697, 7.501180), 1e-4)) - self.assertTrue(post[0][1].isapprox( ttt.Gaussian(25.834337, 8.320970), 1e-4)) - self.assertTrue(post[1][0].isapprox( ttt.Gaussian(22.079819, 8.180607), 1e-4)) - self.assertTrue(post[1][1].isapprox( ttt.Gaussian(14.987953, 6.308469), 1e-4)) + self.assertTrue(post[0][0].isapprox(ttt.Gaussian(31.674697, 7.501180), 1e-4)) + self.assertTrue(post[0][1].isapprox(ttt.Gaussian(25.834337, 8.320970), 1e-4)) + self.assertTrue(post[1][0].isapprox(ttt.Gaussian(22.079819, 8.180607), 1e-4)) + self.assertTrue(post[1][1].isapprox(ttt.Gaussian(14.987953, 6.308469), 1e-4)) - - tc = [ttt.Player(ttt.Gaussian(25.0,25.0/3),25.0/6,0.0)] - g = ttt.Game([ta,tc]) + tc = [ttt.Player(ttt.Gaussian(25.0, 25.0 / 3), 25.0 / 6, 0.0)] + g = ttt.Game([ta, tc]) post_2vs1 = g.posteriors() wa = [1.0, 1.0] wb = [1.0, 0.0] - g = ttt.Game([ta,tb], weights=[wa,wb]) + g = ttt.Game([ta, tb], weights=[wa, wb]) post = g.posteriors() self.assertTrue(post[0][0].isapprox(post_2vs1[0][0], 1e-4)) self.assertTrue(post[0][1].isapprox(post_2vs1[0][1], 1e-4)) self.assertTrue(post[1][0].isapprox(post_2vs1[1][0], 1e-4)) self.assertTrue(post[1][1].isapprox(tb[1].prior, 1e-4)) + def test_1vs1_TTT_with_weights(self): - composition = [[["a"],["b"]], [["b"],["a"]]] - weights = [[[5.0],[4.0]],[[5.0],[4.0]]] - h = ttt.History(composition, mu=2.0, beta=1.0, sigma=6.0, gamma=0.0, weights=weights) + composition = [[["a"], ["b"]], [["b"], ["a"]]] + weights = [[[5.0], [4.0]], [[5.0], [4.0]]] + h = ttt.History( + composition, mu=2.0, beta=1.0, sigma=6.0, gamma=0.0, weights=weights + ) h.forward_propagation() lc = h.learning_curves() - self.assertTrue(lc["a"][0][1].isapprox( ttt.Gaussian(5.53765944, 4.758722), 1e-4)) - self.assertTrue(lc["b"][0][1].isapprox( ttt.Gaussian(-0.83012755, 5.2395689), 1e-4)) - self.assertTrue(lc["a"][1][1].isapprox( ttt.Gaussian(1.7922776, 4.099566689), 1e-4)) - self.assertTrue(lc["b"][1][1].isapprox( ttt.Gaussian(4.8455331752, 3.7476161), 1e-4)) + self.assertTrue( + lc["a"][0][1].isapprox(ttt.Gaussian(5.53765944, 4.758722), 1e-4) + ) + self.assertTrue( + lc["b"][0][1].isapprox(ttt.Gaussian(-0.83012755, 5.2395689), 1e-4) + ) + self.assertTrue( + lc["a"][1][1].isapprox(ttt.Gaussian(1.7922776, 4.099566689), 1e-4) + ) + self.assertTrue( + lc["b"][1][1].isapprox(ttt.Gaussian(4.8455331752, 3.7476161), 1e-4) + ) h.convergence(verbose=False, iterations=16, epsilon=0) lc = h.learning_curves() - self.assertTrue(lc["a"][0][1].isapprox( ttt.Gaussian(lc["a"][0][1].mu, lc["a"][0][1].sigma), 1e-4)) - self.assertTrue(lc["b"][0][1].isapprox( ttt.Gaussian(lc["a"][0][1].mu, lc["a"][0][1].sigma), 1e-4)) - self.assertTrue(lc["a"][1][1].isapprox( ttt.Gaussian(lc["a"][0][1].mu, lc["a"][0][1].sigma), 1e-4)) - self.assertTrue(lc["b"][1][1].isapprox( ttt.Gaussian(lc["a"][0][1].mu, lc["a"][0][1].sigma), 1e-4)) + self.assertTrue( + lc["a"][0][1].isapprox( + ttt.Gaussian(lc["a"][0][1].mu, lc["a"][0][1].sigma), 1e-4 + ) + ) + self.assertTrue( + lc["b"][0][1].isapprox( + ttt.Gaussian(lc["a"][0][1].mu, lc["a"][0][1].sigma), 1e-4 + ) + ) + self.assertTrue( + lc["a"][1][1].isapprox( + ttt.Gaussian(lc["a"][0][1].mu, lc["a"][0][1].sigma), 1e-4 + ) + ) + self.assertTrue( + lc["b"][1][1].isapprox( + ttt.Gaussian(lc["a"][0][1].mu, lc["a"][0][1].sigma), 1e-4 + ) + ) # In the julia tests but is this really doing anything? - composition = [[["a"],["b"]], [["b"],["a"]]] - weights = [[[1.0],[4.0]],[[5.0],[4.0]]] - h = ttt.History(composition, mu=2.0, beta=1.0, sigma=6.0, gamma=0.0, weights=weights) + composition = [[["a"], ["b"]], [["b"], ["a"]]] + weights = [[[1.0], [4.0]], [[5.0], [4.0]]] + h = ttt.History( + composition, mu=2.0, beta=1.0, sigma=6.0, gamma=0.0, weights=weights + ) lc = h.learning_curves() + def test_gamma(self): - composition = [ [["a"],["b"]], [["a"],["b"]]] - results = [[1,0],[1,0]] + composition = [[["a"], ["b"]], [["a"], ["b"]]] + results = [[1, 0], [1, 0]] - h = ttt.History(composition=composition,results=results, mu=0.0,sigma=6.0, beta=1.0, gamma=0.0) + h = ttt.History( + composition=composition, + results=results, + mu=0.0, + sigma=6.0, + beta=1.0, + gamma=0.0, + ) h.forward_propagation() - mu0, sigma0 = h.bskills[1]['a'].forward + mu0, sigma0 = h.bskills[1]["a"].forward self.assertAlmostEqual(mu0, 3.33907906) self.assertAlmostEqual(sigma0, 4.985032699) - h = ttt.History(composition=composition,results=results, mu=0.0,sigma=6.0, beta=1.0, gamma=10.0) + h = ttt.History( + composition=composition, + results=results, + mu=0.0, + sigma=6.0, + beta=1.0, + gamma=10.0, + ) h.forward_propagation() - mu10, sigma10 = h.bskills[1]['a'].forward + mu10, sigma10 = h.bskills[1]["a"].forward self.assertAlmostEqual(mu10, 3.33907906260) self.assertAlmostEqual(sigma10, 11.1736543) - #Observaci'on: + # Observaci'on: # El paquete trueskill python agrega gamma antes de la partida # devuelve, trueskill.Player(mu=6.555, sigma=9.645) - h = ttt.History(composition=composition,results=results, mu=0.0,sigma=math.sqrt(6.0**2+10**2), beta=1.0, gamma=10.0) + h = ttt.History( + composition=composition, + results=results, + mu=0.0, + sigma=math.sqrt(6.0**2 + 10**2), + beta=1.0, + gamma=10.0, + ) h.forward_propagation() - mu100, sigma100 = h.bskills[0]['a'].posterior + mu100, sigma100 = h.bskills[0]["a"].posterior self.assertAlmostEqual(mu100, 6.555467799) self.assertAlmostEqual(sigma100, 9.6449905098) + def test_game_continuous_1vs1(self): - ta = [ttt.Player(ttt.Gaussian(2,2),1,0)] - tb = [ttt.Player(ttt.Gaussian(1,2),1,0)] + ta = [ttt.Player(ttt.Gaussian(2, 2), 1, 0)] + tb = [ttt.Player(ttt.Gaussian(1, 2), 1, 0)] # result_ta = 45.24 result_tb = 44.24 g = ttt.Game([ta, tb], [result_ta, result_tb], obs=ttt.GameType.Continuous) post = g.posteriors() - self.assertTrue(post[0][0].isapprox(ttt.Gaussian(2,1.549193))) - self.assertTrue(post[1][0].isapprox(ttt.Gaussian(1,1.549193))) + self.assertTrue(post[0][0].isapprox(ttt.Gaussian(2, 1.549193))) + self.assertTrue(post[1][0].isapprox(ttt.Gaussian(1, 1.549193))) g = ttt.Game( [ta, tb], @@ -615,8 +818,8 @@ def test_game_continuous_1vs1(self): weights=[[1.0], [1.0]], ) post = g.posteriors() - self.assertTrue(post[0][0].isapprox(ttt.Gaussian(2,1.549193))) - self.assertTrue(post[1][0].isapprox(ttt.Gaussian(1,1.549193))) + self.assertTrue(post[0][0].isapprox(ttt.Gaussian(2, 1.549193))) + self.assertTrue(post[1][0].isapprox(ttt.Gaussian(1, 1.549193))) # Los pesos tienen un compartamiento raro, porque si la media es positiva un peso alto aumenta la habilidad, pero si la media es negativa, un peso bajo la disminuye. Esto no parece ser un comportamiento razonable teniendo en cuenta que el valor absoluto de las medias no tiene ningún significado. TODO: profundizar esta idea en el caso en el que el observable es "orden", pues esto mismo ocurre también ahí. g = ttt.Game( @@ -626,11 +829,11 @@ def test_game_continuous_1vs1(self): weights=[[1.0], [2.0]], ) post = g.posteriors() - self.assertTrue(post[0][0].isapprox(ttt.Gaussian(2.160000,1.833030))) - self.assertTrue(post[1][0].isapprox(ttt.Gaussian(0.680000,1.200000))) + self.assertTrue(post[0][0].isapprox(ttt.Gaussian(2.160000, 1.833030))) + self.assertTrue(post[1][0].isapprox(ttt.Gaussian(0.680000, 1.200000))) - ta = [ttt.Player(ttt.Gaussian(0,2),1,0)] - tb = [ttt.Player(ttt.Gaussian(-1,2),1,0)] + ta = [ttt.Player(ttt.Gaussian(0, 2), 1, 0)] + tb = [ttt.Player(ttt.Gaussian(-1, 2), 1, 0)] # w_ta = [1.0] w_tb = [5.0] @@ -642,11 +845,11 @@ def test_game_continuous_1vs1(self): weights=[w_ta, w_tb], ) post = g.posteriors() - self.assertTrue(post[0][0].isapprox(ttt.Gaussian(-0.123077,1.968990), 1e-5)) - self.assertTrue(post[1][0].isapprox(ttt.Gaussian(-0.384615,0.960769), 1e-5)) + self.assertTrue(post[0][0].isapprox(ttt.Gaussian(-0.123077, 1.968990), 1e-5)) + self.assertTrue(post[1][0].isapprox(ttt.Gaussian(-0.384615, 0.960769), 1e-5)) - ta = [ttt.Player(ttt.Gaussian(2,2),1,0)] - tb = [ttt.Player(ttt.Gaussian(1,2),1,0)] + ta = [ttt.Player(ttt.Gaussian(2, 2), 1, 0)] + tb = [ttt.Player(ttt.Gaussian(1, 2), 1, 0)] # w_ta = [1.0] w_tb = [5.0] @@ -658,28 +861,30 @@ def test_game_continuous_1vs1(self): weights=[w_ta, w_tb], ) post = g.posteriors() - self.assertTrue(post[0][0].isapprox(ttt.Gaussian(2.123077,1.968990) , 1e-5)) - self.assertTrue(post[1][0].isapprox(ttt.Gaussian(0.384615,0.960769), 1e-5)) + self.assertTrue(post[0][0].isapprox(ttt.Gaussian(2.123077, 1.968990), 1e-5)) + self.assertTrue(post[1][0].isapprox(ttt.Gaussian(0.384615, 0.960769), 1e-5)) def test_game_continuous_NvsM(self): - ta = [ttt.Player(ttt.Gaussian(2,2),1,0), - ttt.Player(ttt.Gaussian(2,2),1,0)] - tb = [ttt.Player(ttt.Gaussian(4,2),1,0)] - tc = [ttt.Player(ttt.Gaussian(3,2),1,0)] + ta = [ + ttt.Player(ttt.Gaussian(2, 2), 1, 0), + ttt.Player(ttt.Gaussian(2, 2), 1, 0), + ] + tb = [ttt.Player(ttt.Gaussian(4, 2), 1, 0)] + tc = [ttt.Player(ttt.Gaussian(3, 2), 1, 0)] result = [4.2, 0.2, 2.1] g = ttt.Game([ta, tc, tb], result, obs=ttt.GameType.Continuous) post = g.posteriors() - self.assertTrue(post[0][0].isapprox(ttt.Gaussian(2.816000,1.649242) , 1e-5)) - self.assertTrue(post[0][1].isapprox(ttt.Gaussian(2.816000,1.649242) , 1e-5)) - self.assertTrue(post[1][0].isapprox(ttt.Gaussian(2.232000,1.442221) , 1e-5)) - self.assertTrue(post[2][0].isapprox(ttt.Gaussian(3.952000,1.442221) , 1e-5)) + self.assertTrue(post[0][0].isapprox(ttt.Gaussian(2.816000, 1.649242), 1e-5)) + self.assertTrue(post[0][1].isapprox(ttt.Gaussian(2.816000, 1.649242), 1e-5)) + self.assertTrue(post[1][0].isapprox(ttt.Gaussian(2.232000, 1.442221), 1e-5)) + self.assertTrue(post[2][0].isapprox(ttt.Gaussian(3.952000, 1.442221), 1e-5)) def test_history_continuous_NvsMvsL(self): priors = dict() - priors["a1"] = ttt.Player(ttt.Gaussian(2,2),1,0) - priors["a2"] = ttt.Player(ttt.Gaussian(2,2),1,0) - priors["b"] = ttt.Player(ttt.Gaussian(4,2),1,0) - priors["c"] = ttt.Player(ttt.Gaussian(3,2),1,0) + priors["a1"] = ttt.Player(ttt.Gaussian(2, 2), 1, 0) + priors["a2"] = ttt.Player(ttt.Gaussian(2, 2), 1, 0) + priors["b"] = ttt.Player(ttt.Gaussian(4, 2), 1, 0) + priors["c"] = ttt.Player(ttt.Gaussian(3, 2), 1, 0) results = [[4.2, 0.2, 2.1]] obs = [ttt.GameType.Continuous] h = ttt.History( @@ -690,23 +895,24 @@ def test_history_continuous_NvsMvsL(self): ) h.forward_propagation() lc = h.learning_curves() - self.assertTrue(lc["a1"][0][1].isapprox(ttt.Gaussian(2.816000,1.649242) , 1e-5)) - self.assertTrue(lc["a2"][0][1].isapprox(ttt.Gaussian(2.816000,1.649242) , 1e-5)) - self.assertTrue(lc["c"][0][1].isapprox(ttt.Gaussian(2.232000,1.442221) , 1e-5)) - self.assertTrue(lc["b"][0][1].isapprox(ttt.Gaussian(3.952000,1.442221) , 1e-5)) - + self.assertTrue(lc["a1"][0][1].isapprox(ttt.Gaussian(2.816000, 1.649242), 1e-5)) + self.assertTrue(lc["a2"][0][1].isapprox(ttt.Gaussian(2.816000, 1.649242), 1e-5)) + self.assertTrue(lc["c"][0][1].isapprox(ttt.Gaussian(2.232000, 1.442221), 1e-5)) + self.assertTrue(lc["b"][0][1].isapprox(ttt.Gaussian(3.952000, 1.442221), 1e-5)) def test_fixed_point_approx(self): - r = 2 # score - mu = 2 # mean difference - sigma = 2 # sigma difference - self.assertAlmostEqual(ttt.fixed_point_approx(r, mu, sigma), - (0.655192942490574, 0.6218258871945438)) + r = 2 # score + mu = 2 # mean difference + sigma = 2 # sigma difference + self.assertAlmostEqual( + ttt.fixed_point_approx(r, mu, sigma), + (0.655192942490574, 0.6218258871945438), + ) def test_game_discrete_1vs1(self): - ta = [ttt.Player(ttt.Gaussian(4,6),1,0)] + ta = [ttt.Player(ttt.Gaussian(4, 6), 1, 0)] wa = [1.0] - tb = [ttt.Player(ttt.Gaussian(0,6),1,0)] + tb = [ttt.Player(ttt.Gaussian(0, 6), 1, 0)] wb = [1.0] result = [0, 54] g = ttt.Game( @@ -732,10 +938,10 @@ def test_game_discrete_1vs1(self): def test_history_discrete_NvsMvsL(self): priors = dict() - priors["a1"] = ttt.Player(ttt.Gaussian(2,2),1,0) - priors["a2"] = ttt.Player(ttt.Gaussian(2,2),1,0) - priors["b"] = ttt.Player(ttt.Gaussian(4,2),1,0) - priors["c"] = ttt.Player(ttt.Gaussian(3,2),1,0) + priors["a1"] = ttt.Player(ttt.Gaussian(2, 2), 1, 0) + priors["a2"] = ttt.Player(ttt.Gaussian(2, 2), 1, 0) + priors["b"] = ttt.Player(ttt.Gaussian(4, 2), 1, 0) + priors["c"] = ttt.Player(ttt.Gaussian(3, 2), 1, 0) results = [[4, 0, 2]] obs = [ttt.GameType.Discrete] h = ttt.History( @@ -746,38 +952,61 @@ def test_history_discrete_NvsMvsL(self): ) h.forward_propagation() lc = h.learning_curves() - #print(lc) - self.assertTrue(lc["a1"][0][1].isapprox(ttt.Gaussian(2.059343,1.667489) , 1e-5)) - self.assertTrue(lc["a2"][0][1].isapprox(ttt.Gaussian(2.059343,1.667489) , 1e-5)) - self.assertTrue(lc["c"][0][1].isapprox(ttt.Gaussian(3.176773,1.482405) , 1e-5)) - self.assertTrue(lc["b"][0][1].isapprox(ttt.Gaussian(3.763883,1.463096) , 1e-5)) + # print(lc) + self.assertTrue(lc["a1"][0][1].isapprox(ttt.Gaussian(2.059343, 1.667489), 1e-5)) + self.assertTrue(lc["a2"][0][1].isapprox(ttt.Gaussian(2.059343, 1.667489), 1e-5)) + self.assertTrue(lc["c"][0][1].isapprox(ttt.Gaussian(3.176773, 1.482405), 1e-5)) + self.assertTrue(lc["b"][0][1].isapprox(ttt.Gaussian(3.763883, 1.463096), 1e-5)) def test_history_mixed_type_of_game_NvsMvsL(self): priors = dict() - priors["a1"] = ttt.Player(ttt.Gaussian(2,2),1,0.1) - priors["a2"] = ttt.Player(ttt.Gaussian(2,2),1,0.1) - priors["b"] = ttt.Player(ttt.Gaussian(4,2),1,0.1) - priors["c"] = ttt.Player(ttt.Gaussian(3,2),1,0.1) - results = [[4,0,2],[4.0, 0.0, 2.1],[4, 0, 2]] + priors["a1"] = ttt.Player(ttt.Gaussian(2, 2), 1, 0.1) + priors["a2"] = ttt.Player(ttt.Gaussian(2, 2), 1, 0.1) + priors["b"] = ttt.Player(ttt.Gaussian(4, 2), 1, 0.1) + priors["c"] = ttt.Player(ttt.Gaussian(3, 2), 1, 0.1) + results = [[4, 0, 2], [4.0, 0.0, 2.1], [4, 0, 2]] times = [0, 1, 2] obs = [ttt.GameType.Ordinal, ttt.GameType.Continuous, ttt.GameType.Discrete] - h = ttt.History(composition=[ [ ["a1", "a2"], ["c"], ["b"] ] ]*3, results = results, times=times, priors=priors, obs=obs ) + h = ttt.History( + composition=[[["a1", "a2"], ["c"], ["b"]]] * 3, + results=results, + times=times, + priors=priors, + obs=obs, + ) h.forward_propagation() lc = h.learning_curves() - #print(lc) - self.assertTrue(lc["a1"][0][1].isapprox(ttt.Gaussian(3.053757,1.782038) , 1e-5)) - self.assertTrue(lc["a1"][1][1].isapprox(ttt.Gaussian(2.996293,1.480994) , 1e-5)) - self.assertTrue(lc["a1"][2][1].isapprox(ttt.Gaussian(2.413075,1.269865) , 1e-5)) + # print(lc) + self.assertTrue(lc["a1"][0][1].isapprox(ttt.Gaussian(3.053757, 1.782038), 1e-5)) + self.assertTrue(lc["a1"][1][1].isapprox(ttt.Gaussian(2.996293, 1.480994), 1e-5)) + self.assertTrue(lc["a1"][2][1].isapprox(ttt.Gaussian(2.413075, 1.269865), 1e-5)) - self.assertTrue(lc["a2"][0][1].isapprox(ttt.Gaussian(3.053757,1.782038) , 1e-5)) - self.assertTrue(lc["c"][0][1].isapprox(ttt.Gaussian(1.925404,1.686565) , 1e-5)) + self.assertTrue(lc["a2"][0][1].isapprox(ttt.Gaussian(3.053757, 1.782038), 1e-5)) + self.assertTrue(lc["c"][0][1].isapprox(ttt.Gaussian(1.925404, 1.686565), 1e-5)) def test_add_history(self): - primer_parte = [[['c'], ['e']], [['h'], ['e']], [['a'], ['f']], [['a'], ['b']], [['c'], ['f']], [['b'], ['e']], [['c'], ['b']], [['f'], ['e']], [['b'], ['h']], [['c'], ['e']], [['f'], ['h']], [['b'], ['h']], [['a'], ['e']], [['h'], ['d']], [['d'], ['h']], [['f'], ['a']]] + primer_parte = [ + [["c"], ["e"]], + [["h"], ["e"]], + [["a"], ["f"]], + [["a"], ["b"]], + [["c"], ["f"]], + [["b"], ["e"]], + [["c"], ["b"]], + [["f"], ["e"]], + [["b"], ["h"]], + [["c"], ["e"]], + [["f"], ["h"]], + [["b"], ["h"]], + [["a"], ["e"]], + [["h"], ["d"]], + [["d"], ["h"]], + [["f"], ["a"]], + ] - h = ttt.History(composition=primer_parte, online = True) - step, i = h.convergence(epsilon=0.0, iterations=16,verbose=False) + h = ttt.History(composition=primer_parte, online=True) + step, i = h.convergence(epsilon=0.0, iterations=16, verbose=False) h2 = ttt.History(composition=[], online=True) h2.add_history(primer_parte) @@ -794,18 +1023,52 @@ def test_add_history(self): def test_pickle(self): import pickle - primer_parte = [[['c'], ['e']], [['h'], ['e']], [['a'], ['f']], [['a'], ['b']], [['c'], ['f']], [['b'], ['e']], [['c'], ['b']], [['f'], ['e']], [['b'], ['h']], [['c'], ['e']], [['f'], ['h']], [['b'], ['h']], [['a'], ['e']], [['h'], ['d']], [['d'], ['h']], [['f'], ['a']]] + primer_parte = [ + [["c"], ["e"]], + [["h"], ["e"]], + [["a"], ["f"]], + [["a"], ["b"]], + [["c"], ["f"]], + [["b"], ["e"]], + [["c"], ["b"]], + [["f"], ["e"]], + [["b"], ["h"]], + [["c"], ["e"]], + [["f"], ["h"]], + [["b"], ["h"]], + [["a"], ["e"]], + [["h"], ["d"]], + [["d"], ["h"]], + [["f"], ["a"]], + ] h = ttt.History(composition=primer_parte, online=True) step, i = h.convergence(epsilon=0.0, iterations=16, verbose=False) - with open('History.pickle', 'wb') as handle: + with open("History.pickle", "wb") as handle: pickle.dump(h, handle, protocol=pickle.HIGHEST_PROTOCOL) - with open('History.pickle', 'rb') as handle: + with open("History.pickle", "rb") as handle: h2 = pickle.load(handle) - segunda_parte = [[['d'], ['b']], [['c'], ['f']], [['e'], ['a']], [['d'], ['g']], [['h'], ['c']], [['a'], ['g']], [['h'], ['c']], [['f'], ['d']], [['e'], ['d']], [['c'], ['b']], [['c'], ['g']], [['a'], ['d']], [['c'], ['a']], [['h'], ['b']], [['c'], ['b']], [['b'], ['c']]] + segunda_parte = [ + [["d"], ["b"]], + [["c"], ["f"]], + [["e"], ["a"]], + [["d"], ["g"]], + [["h"], ["c"]], + [["a"], ["g"]], + [["h"], ["c"]], + [["f"], ["d"]], + [["e"], ["d"]], + [["c"], ["b"]], + [["c"], ["g"]], + [["a"], ["d"]], + [["c"], ["a"]], + [["h"], ["b"]], + [["c"], ["b"]], + [["b"], ["c"]], + ] h.add_history(composition=segunda_parte) step, i = h.convergence(epsilon=0.0, iterations=16, verbose=False) @@ -819,8 +1082,24 @@ def test_pickle(self): self.assertTrue(lc2["c"][i][1].isapprox(lc["c"][i][1])) def test_history_log_evidence(self): - - primer_parte = [[['c'], ['e']], [['h'], ['e']], [['a'], ['f']], [['a'], ['b']], [['c'], ['f']], [['b'], ['e']], [['c'], ['b']], [['f'], ['e']], [['b'], ['h']], [['c'], ['e']], [['f'], ['h']], [['b'], ['h']], [['a'], ['e']], [['h'], ['d']], [['d'], ['h']], [['f'], ['a']]] + primer_parte = [ + [["c"], ["e"]], + [["h"], ["e"]], + [["a"], ["f"]], + [["a"], ["b"]], + [["c"], ["f"]], + [["b"], ["e"]], + [["c"], ["b"]], + [["f"], ["e"]], + [["b"], ["h"]], + [["c"], ["e"]], + [["f"], ["h"]], + [["b"], ["h"]], + [["a"], ["e"]], + [["h"], ["d"]], + [["d"], ["h"]], + [["f"], ["a"]], + ] h = ttt.History(composition=primer_parte) ho = ttt.History(composition=primer_parte, online=True) @@ -834,5 +1113,3 @@ def test_history_log_evidence(self): if __name__ == "__main__": """Run all tests when script is executed directly.""" unittest.main() - - From fa62c443da7a0d8e09893137ce1aed5c4fcc49b1 Mon Sep 17 00:00:00 2001 From: apiss2 Date: Fri, 28 Nov 2025 00:01:55 +0900 Subject: [PATCH 10/10] fix Discrete case of Game.partial_evidence --- trueskillthroughtime/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/trueskillthroughtime/__init__.py b/trueskillthroughtime/__init__.py index 30b2654..9b17336 100644 --- a/trueskillthroughtime/__init__.py +++ b/trueskillthroughtime/__init__.py @@ -679,8 +679,11 @@ def partial_evidence(self, i_d: int) -> None: # Monte Carlo Solution N = 5000 hardcoded_lower_bound = 1 / (2 * N) - poisson_rvs = poisson.rvs(mu=np.exp(norm.rvs(size=N, loc=mu, scale=sigma))) - evidence = np.sum(r == poisson_rvs) / N + + latent_diffs = norm.rvs(size=N, loc=mu, scale=sigma) + lambdas = np.exp(latent_diffs) + probs = poisson.pmf(r, lambdas) + evidence = np.mean(probs) self.evidence *= hardcoded_lower_bound + evidence # # Version Guo et al: