diff --git a/test/runtest.py b/test/runtest.py index 5c710a2..bcc83b8 100644 --- a/test/runtest.py +++ b/test/runtest.py @@ -17,17 +17,20 @@ 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) +# 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 @@ -36,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) @@ -263,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] @@ -442,307 +575,438 @@ 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]] - h = ttt.History(composition=composition,results=results, mu=0.0,sigma=6.0, beta=1.0, gamma=0.0) + def test_gamma(self): + 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.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="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]]) + 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=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))) + 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))) + 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] # - 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)) + 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] # - 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)) + 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="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)) - 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 = ["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)) - 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([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() - 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 = ["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) - 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 = ["Ordinal", "Continuous", "Discrete"] - - h = ttt.History(composition=[ [ ["a1", "a2"], ["c"], ["b"] ] ]*3, results = results, times=times, priors=priors, obs=obs ) + 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() 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) @@ -759,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) @@ -784,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) @@ -799,5 +1113,3 @@ def test_history_log_evidence(self): if __name__ == "__main__": """Run all tests when script is executed directly.""" unittest.main() - - diff --git a/trueskillthroughtime/__init__.py b/trueskillthroughtime/__init__.py index 15d255e..9b17336 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,140 +23,152 @@ :license: BSD, see LICENSE for more details. """ +from __future__ import annotations + +import copy import math +from collections import defaultdict +from enum import Enum +from math import erfc +from typing import Any, Iterator, Sequence + +import numpy as np +from scipy.special import erfcinv +from scipy.stats import norm, poisson + +__all__ = [ + "MU", + "SIGMA", + "Gaussian", + "N01", + "N00", + "Ninf", + "Nms", + "Player", + "Game", + "History", + "GameType", +] 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 -import numpy as np -from scipy.optimize import minimize -import copy - - -__all__ = ['MU', 'SIGMA', 'Gaussian', 'N01', 'N00', 'Ninf', 'Nms', 'Player', 'Game', 'History' ] 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 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): +def cdf(x: float, mu: float = 0.0, sigma: float = 1.0) -> float: """ 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): +def pdf(x: float, mu: float, sigma: float) -> float: """ 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): + +def ppf(p: float, mu: float, sigma: float) -> float: """ 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): +def v_w(mu: float, sigma: float, margin: float, tie: bool) -> tuple[float, float]: """ 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): + +def trunc(mu: float, sigma: float, margin: float, tie: bool) -> tuple[float, float]: """ 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): + +def approx(N: "Gaussian", margin: float, tie: bool) -> "Gaussian": """ 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,35 +176,39 @@ 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: 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. - - 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): + + 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 - 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 + kappa = 1.0 # # Fixed-point iteration i = 0 @@ -201,54 +217,56 @@ 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): + +def compute_margin(p_draw: float, sd: float) -> float: """ 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² """ - def __init__(self, mu=MU, sigma=SIGMA): + def __init__(self, mu: float = MU, sigma: float = SIGMA) -> None: """ 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 """ @@ -257,16 +275,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) + 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 @@ -274,101 +292,101 @@ 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). - + If X ~ N(mu1, sigma1²) and Y ~ N(mu2, sigma2²) are independent, then X + Y ~ N(mu1+mu2, sigma1²+sigma2²) """ 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. - + If X ~ N(mu1, sigma1²) and Y ~ N(mu2, sigma2²) are independent, then X - Y ~ N(mu1-mu2, sigma1²+sigma2²) """ 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. - + - 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): + 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). - + 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): + def delta(self, M: "Gaussian") -> tuple[float, float]: """ Compute the difference between two Gaussians. - + Returns: tuple: (|mu1 - mu2|, |sigma1 - sigma2|) """ 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. - + Args: M: Another Gaussian object tol: Tolerance for comparison (default: 1e-5) - + Returns: bool: True if both mu and sigma are within tolerance """ 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. - + Args: Ns: List of Gaussian objects - + Returns: Gaussian: Sum of all Gaussians in the list """ @@ -378,13 +396,13 @@ def suma(Ns): return res -def producto(Ns): +def producto(Ns: list[Gaussian]) -> Gaussian: """ Product of Gaussian PDFs (normalized). - + Args: Ns: List of Gaussian objects - + Returns: Gaussian: Product of all Gaussians in the list """ @@ -394,26 +412,32 @@ 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 = Gaussian(MU, SIGMA), + beta: float = BETA, + gamma: float = GAMMA, + prior_draw: Gaussian = 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,89 +447,113 @@ 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): + + def performance(self) -> Gaussian: """ Generate a performance distribution: skill + noise. - + Returns: Gaussian: Performance ~ N(skill_mu, skill_sigma² + beta²) """ 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, 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): +class TeamVariable(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 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}' + 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 -class diff_messages(object): +class DiffMessages(object): """ Internal class for performance difference variables in message passing. - + Attributes: prior: Prior distribution of performance difference 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. - + 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 +562,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() @@ -525,174 +573,207 @@ 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. - + 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) obs: Observation model - "Ordinal", "Continuous", or "Discrete" (default: "Ordinal") """ - g = self - g.teams = teams - g.result = result if len(result)==len(teams) else list(range(len(teams)-1,-1,-1)) + 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() - - def __repr__(self): + 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) -> str: """String representation of the Game.""" - return f'{self.teams}' + 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)): - 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)) + for e in range(len(self.d)): + sd = math.sqrt( + 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(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[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): - 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[DiffMessages]: + res: list[DiffMessages] = [] + for e in range(len(self) - 1): + res.append( + DiffMessages(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 *= 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]] + self.evidence *= 1 - cdf(self.margin[i_d], mu, sigma) + elif self.obs == GameType.Continuous: + self.evidence *= pdf( + self.result[self.o[i_d]] - self.result[self.o[i_d + 1]], mu, sigma + ) + 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 + hardcoded_lower_bound = 1 / (2 * 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: - #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): - 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 - 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.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 + ) + 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, - math.sqrt(g.t[e].prior.sigma**2 - performance_individuals[e][i].sigma**2)) - w_i = g.weights[e][i] + self.t[e].prior.mu - performance_individuals[e][i].mu, + math.sqrt( + self.t[e].prior.sigma ** 2 + - performance_individuals[e][i].sigma ** 2 + ), + ) + 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([]) @@ -701,58 +782,59 @@ 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] + lose_case = t == 1 + 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) + 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]] + 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. - + Combines each player's prior skill with the likelihood from the game outcome. - + 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 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,79 +842,73 @@ 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=[]): + + 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})' + 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, - '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 - 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 +922,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 +931,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: 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, + beta: float = 1.0, + gamma: float = 0.15, + p_draw: float = 0.0, + online: bool = False, + weights: list[list[list[float]]] = [], # Game[Team[PlayerWeight[float]]] + obs: list[GameType] = [], + ): """ 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,48 +963,107 @@ 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.size = 0 - self.batches = []; self.bresults = []; self.btimes = [] - self.bskills = []; self.bweights = []; self.bobs = [] - self.bevidence = [] + self.check_input( + composition, + results, + times, + priors, + mu, + sigma, + beta, + gamma, + p_draw, + weights, + obs, + ) + + self.size: int = 0 + 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]]]] = [] + 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.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 {}) - self._last_message = None - self._last_time = None + 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_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): + return f"History(Events={self.size})" + + def check_input( + self, + 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: 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)") + 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 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): + 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 @@ -924,18 +1072,28 @@ 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: 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. - + Args: composition: List of games to add results: List of outcomes for new games (default: empty) @@ -945,32 +1103,44 @@ 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)): 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) - self.btimes.append(t); self.batches.append([]) - self.bresults.append([]); self.bweights.append([]) - self.bskills.append({}); self.bobs.append([]) + i_b = len(self.btimes) + 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) - 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 for team in composition[i]: for name in team: @@ -980,119 +1150,132 @@ 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( - 0, min(math.sqrt(elapsed * (gamma**2)), 1.67 * self.sigma)) + 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) - likelihoods = g.likelihoods + game = Game( + self.within_priors(b, e), + self.bresults[b][e], + self.p_draw, + self.bweights[b][e], + obs=self.bobs[b][e], + ) + 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(e, likelihoods[t][i]) + 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][name].likelihood(e) + prior = self.bskills[b][name].posterior / self.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)) + _online = self.bskills[b][name].online + assert _online is not None + prior = _online + priors[-1].append( + 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. - + 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()}) - 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) - 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._last_message = defaultdict( + lambda: Gaussian(self.mu, self.sigma), + {k: v.prior for k, v in self.priors.items()}, + ) + 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) + 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. - + 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)): - 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) - h._out_skills(b, forward=False) - h._last_message = None - h._last_time = None + 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) + 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. - + Returns: tuple: (max_mu_step, max_sigma_step) - maximum likelihood change across both passes """ @@ -1100,67 +1283,76 @@ 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. - + 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) + step = (0.0, 0.0) + 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 - 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. - + 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"]: ... 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) @@ -1168,102 +1360,105 @@ 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. - + 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): + def geometric_mean(self) -> float: """ 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): + def __getstate__(self) -> dict[str, Any]: """ 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']: + 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). - + 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): + +def orden(xs: Sequence[float | int], reverse: bool = True) -> list[int]: """ 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)] - - - - - -