TrueskillThroughTime 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.idea/TrueskillThroughTime.iml +72 -0
- data/.idea/modules.xml +8 -0
- data/.idea/vcs.xml +6 -0
- data/.idea/workspace.xml +71 -0
- data/.rubocop.yml +13 -0
- data/CHANGELOG.md +9 -0
- data/LICENCE +57 -0
- data/README.md +38 -0
- data/Rakefile +16 -0
- data/TrueskillThroughTime.gemspec +36 -0
- data/lib/TrueskillThroughTime/version.rb +5 -0
- data/lib/TrueskillThroughTime.rb +1158 -0
- data/lib/example_data/liquid_t2_tournament_results.json +1 -0
- data/lib/example_data/liquid_tournament_results.json +1 -0
- data/sig/TrueskillThroughTime.rbs +4 -0
- metadata +60 -0
|
@@ -0,0 +1,1158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "TrueskillThroughTime/version"
|
|
4
|
+
|
|
5
|
+
module TrueskillThroughTime
|
|
6
|
+
class Error < StandardError; end
|
|
7
|
+
# Notes:
|
|
8
|
+
# Ported from Python trueskillthroughtime, v1.1.0
|
|
9
|
+
# Tuples are replaced with arrays.
|
|
10
|
+
# Functions that receive tuples are looking for quacks on [0] and [1].
|
|
11
|
+
# They will not fail if [>1] exists. But behaviour is undefined.
|
|
12
|
+
# Methods named p() are replaced with prob(). p() is a kernel function in Ruby.
|
|
13
|
+
# __rmul__ methods are replaced with coerce
|
|
14
|
+
# truediv methods are replaced with / (fdiv vs div in ruby etc)
|
|
15
|
+
require 'json'
|
|
16
|
+
require 'date'
|
|
17
|
+
# Configuration Constants
|
|
18
|
+
BETA = 1.0
|
|
19
|
+
MU = 0.0
|
|
20
|
+
SIGMA = BETA * 6
|
|
21
|
+
GAMMA = BETA * 0.03
|
|
22
|
+
P_DRAW = 0.0
|
|
23
|
+
EPSILON = 1e-6
|
|
24
|
+
ITERATIONS = 30
|
|
25
|
+
|
|
26
|
+
# Derived Math Constants
|
|
27
|
+
SQRT2 = Math.sqrt(2)
|
|
28
|
+
SQRT2PI = Math.sqrt(2 * Math::PI)
|
|
29
|
+
INF = Float::INFINITY
|
|
30
|
+
|
|
31
|
+
def erfc(x)
|
|
32
|
+
# """(http://bit.ly/zOLqbc)"""
|
|
33
|
+
z = x.abs
|
|
34
|
+
t = 1.0 / (1.0 + z / 2.0)
|
|
35
|
+
a = -0.82215223 + t * 0.17087277; b = 1.48851587 + t * a
|
|
36
|
+
c = -1.13520398 + t * b; d = 0.27886807 + t * c; e = -0.18628806 + t * d
|
|
37
|
+
f = 0.09678418 + t * e; g = 0.37409196 + t * f; h = 1.00002368 + t * g
|
|
38
|
+
r = t * Math.exp(-z * z - 1.26551223 + t * h)
|
|
39
|
+
if x.positive?
|
|
40
|
+
r
|
|
41
|
+
else
|
|
42
|
+
2.0 - r
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def erfcinv(y)
|
|
47
|
+
raise(ArgumentError, "Argument must be non-negative numbers") if y.negative?
|
|
48
|
+
return -INF if y > 2
|
|
49
|
+
return INF if y.zero?
|
|
50
|
+
|
|
51
|
+
y = 2 - y if y >= 1
|
|
52
|
+
|
|
53
|
+
t = Math.sqrt(-2 * Math.log(y / 2.0))
|
|
54
|
+
x = -0.70711 * ((2.30753 + t * 0.27061) / (1.0 + t * (0.99229 + t * 0.04481)) - t)
|
|
55
|
+
|
|
56
|
+
3.times do
|
|
57
|
+
err = erfc(x) - y
|
|
58
|
+
x += err / (1.12837916709551257 * Math.exp(-(x**2)) - x * err)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
y < 1 ? x : -x
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def tau_pi(mu, sigma)
|
|
65
|
+
raise(ArgumentError, "Sigma must be greater than 0.") if (sigma + 1e-5) < 0.0
|
|
66
|
+
|
|
67
|
+
if sigma > 0.0
|
|
68
|
+
pi_ = sigma ** -2
|
|
69
|
+
tau_ = pi_ * mu
|
|
70
|
+
else
|
|
71
|
+
pi_ = INF
|
|
72
|
+
tau_ = INF
|
|
73
|
+
end
|
|
74
|
+
[tau_, pi_] # python returns tuple
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def mu_sigma(tau_, pi_)
|
|
78
|
+
raise(ArgumentError, "Sigma must be greater than 0.") if (pi_ + 1e-5) < 0.0
|
|
79
|
+
|
|
80
|
+
if pi_ > 0.0
|
|
81
|
+
sigma = Math.sqrt(1.0/pi_)
|
|
82
|
+
mu = tau_ / pi_.to_f
|
|
83
|
+
else
|
|
84
|
+
sigma = INF
|
|
85
|
+
mu = 0.0
|
|
86
|
+
end
|
|
87
|
+
[mu, sigma] # again tuple in original
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def cdf(x, mu=0, sigma=1)
|
|
91
|
+
0.5 * erfc(-(x - mu) / (sigma * SQRT2))
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def pdf(x, mu, sigma)
|
|
95
|
+
normaliser = (SQRT2PI * sigma)**-1
|
|
96
|
+
functional = Math.exp(-((x - mu)**2) / (2*sigma**2))
|
|
97
|
+
normaliser * functional
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def ppf(p, mu, sigma)
|
|
101
|
+
mu - sigma * SQRT2 * erfcinv(2 * p)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def v_w(mu, sigma, margin, tie)
|
|
105
|
+
if tie
|
|
106
|
+
_alpha = (-margin-mu)/sigma
|
|
107
|
+
_beta = ( margin-mu)/sigma
|
|
108
|
+
v = (pdf(_alpha, 0, 1)-pdf(_beta, 0, 1))/(cdf(_beta, 0, 1)-cdf(_alpha, 0, 1))
|
|
109
|
+
u = (_alpha*pdf(_alpha, 0, 1)-_beta*pdf(_beta, 0, 1))/(cdf(_beta, 0, 1)-cdf(_alpha, 0, 1))
|
|
110
|
+
w = - ( u - v**2 )
|
|
111
|
+
else
|
|
112
|
+
_alpha = (margin-mu)/sigma
|
|
113
|
+
v = pdf(-_alpha, 0, 1) / cdf(-_alpha, 0, 1)
|
|
114
|
+
w = v * (v + (-_alpha))
|
|
115
|
+
end
|
|
116
|
+
[v, w] # again, a tuple
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def trunc(mu, sigma, margin, tie)
|
|
120
|
+
v, w = v_w(mu, sigma, margin, tie)
|
|
121
|
+
mu_trunc = mu + sigma * v
|
|
122
|
+
sigma_trunc = sigma * Math.sqrt(1-w)
|
|
123
|
+
[mu_trunc, sigma_trunc]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def approx(n, margin, tie) # TODO: It seems like this should be a Guassian function
|
|
127
|
+
mu, sigma = trunc(n.mu, n.sigma, margin, tie)
|
|
128
|
+
# if $should_debug
|
|
129
|
+
# puts "approx(#{[n, margin, tie]}) (n, margin, tie) = mu #{mu} sigma #{sigma}"
|
|
130
|
+
# end
|
|
131
|
+
Gaussian.new(mu, sigma)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def compute_margin(p_draw, sd)
|
|
135
|
+
(ppf(0.5-p_draw/2.0, 0.0, sd)).abs
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def max_tuple(t1, t2)
|
|
139
|
+
# Let's emulate python nan sorting behaviour!
|
|
140
|
+
responses = [0.0, 0.0]
|
|
141
|
+
responses[0] = if t1[0].to_f.nan? || t2[0].to_f.nan?
|
|
142
|
+
t1[0]
|
|
143
|
+
else
|
|
144
|
+
[t1[0], t2[0]].max
|
|
145
|
+
end
|
|
146
|
+
responses[1] = if t1[1].to_f.nan? || t2[1].to_f.nan?
|
|
147
|
+
t1[1]
|
|
148
|
+
else
|
|
149
|
+
[t1[1], t2[1]].max
|
|
150
|
+
end
|
|
151
|
+
responses
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def gr_tuple(tup, threshold)
|
|
155
|
+
tup.max > threshold
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def podium(xs)
|
|
159
|
+
sortperm(xs)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def sortperm(xs, reverse=false)
|
|
163
|
+
sorted_indices = xs.each_with_index
|
|
164
|
+
.sort_by { |v, i| v }
|
|
165
|
+
.map { |v, i| i }
|
|
166
|
+
sorted_indices.reverse! if reverse
|
|
167
|
+
sorted_indices
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def dict_diff(old, new)
|
|
171
|
+
step = [0.0, 0.0]
|
|
172
|
+
old.each_key do |a|
|
|
173
|
+
step = max_tuple(step, old[a].delta(new[a])) # .delta is Guassian method....
|
|
174
|
+
end
|
|
175
|
+
step
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
class Gaussian
|
|
179
|
+
extend Enumerable
|
|
180
|
+
attr_accessor :mu, :sigma
|
|
181
|
+
|
|
182
|
+
def initialize(mu=MU, sigma=SIGMA)
|
|
183
|
+
raise(ArgumentError, "Sigma should be >= 0") unless sigma >= 0.0
|
|
184
|
+
|
|
185
|
+
@mu = mu.to_f; @sigma = sigma.to_f
|
|
186
|
+
@members = [@mu, @sigma]
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def tau
|
|
190
|
+
@sigma.positive? ? @mu * (@sigma**-2) : INF
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def pi
|
|
194
|
+
@sigma.positive? ? @sigma**-2 : INF
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def each(&block)
|
|
198
|
+
@members.each{|member| block.call(member)}
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def to_s
|
|
202
|
+
"N(mu= #{(@mu)}, sigma= #{(@sigma)})"
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def inspect
|
|
206
|
+
to_s
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def +(other)
|
|
210
|
+
unless other.is_a?(Gaussian)
|
|
211
|
+
raise(ArgumentError, "Can only add gaussians to other gaussians - tried #{other.class}")
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
Gaussian.new(@mu + other.mu, Math.sqrt(@sigma**2 + other.sigma**2) )
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def -(other)
|
|
218
|
+
unless other.is_a?(Gaussian)
|
|
219
|
+
raise(ArgumentError, "Can only subtract gaussians to other gaussians - tried #{other.class}")
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
Gaussian.new(@mu - other.mu, Math.sqrt(@sigma**2 + other.sigma**2) )
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def *(other)
|
|
226
|
+
if other.is_a?(Float)
|
|
227
|
+
return NINF if other == INF
|
|
228
|
+
|
|
229
|
+
# this is a gaussian instance, not -INF
|
|
230
|
+
|
|
231
|
+
return Gaussian.new(other*@mu, other.abs*@sigma)
|
|
232
|
+
|
|
233
|
+
elsif other.is_a?(Gaussian)
|
|
234
|
+
if @sigma.zero? || other.sigma.zero?
|
|
235
|
+
mu = other.mu / ((other.sigma**2/@sigma**2)+1)
|
|
236
|
+
mu = @mu / ((@sigma**2 / other.sigma**2)+1) if @sigma.zero?
|
|
237
|
+
sigma = 0.0
|
|
238
|
+
else
|
|
239
|
+
_tau = tau + other.tau
|
|
240
|
+
_pi = pi + other.pi
|
|
241
|
+
mu, sigma = mu_sigma(_tau, _pi)
|
|
242
|
+
end
|
|
243
|
+
return Gaussian.new(mu, sigma)
|
|
244
|
+
else
|
|
245
|
+
raise(ArgumentError, "Gaussian multiplication supports only floats or gaussian - tried #{other.class}")
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Unused. Use / instead.
|
|
250
|
+
def truediv(other)
|
|
251
|
+
# Python equivalent of float division...
|
|
252
|
+
# So for us just normal division:
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def /(other)
|
|
256
|
+
unless other.is_a?(Gaussian)
|
|
257
|
+
raise(ArgumentError, "Gaussian division supports only gaussian - tried #{other.class}")
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
_tau = tau - other.tau
|
|
261
|
+
_pi = pi - other.pi
|
|
262
|
+
_mu, _sigma = mu_sigma(_tau, _pi)
|
|
263
|
+
Gaussian.new(_mu, _sigma)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def rmul(other)
|
|
267
|
+
# pythonism for other * this where mul is this * other
|
|
268
|
+
# Ruby equivalent is to coerce. So:
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def coerce(other)
|
|
272
|
+
[self, other]
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def forget(gamma, t)
|
|
276
|
+
Gaussian.new(@mu, Math.sqrt(@sigma**2 + t*gamma**2))
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def delta(other)
|
|
280
|
+
[(@mu - other.mu).abs, (@sigma - other.sigma).abs]
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def exclude(other)
|
|
284
|
+
Gaussian.new(@mu - other.mu, Math.sqrt(@sigma**2 - other.sigma**2))
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def isapprox?(other, tol=1e-4)
|
|
288
|
+
((@mu - other.mu).abs < tol) && ((@sigma - other.sigma).abs < tol)
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
N01 = Gaussian.new(0.0, 1.0)
|
|
293
|
+
N00 = Gaussian.new(0.0, 0.0)
|
|
294
|
+
NINF = Gaussian.new(0.0, INF)
|
|
295
|
+
Nms = Gaussian.new(MU, SIGMA)
|
|
296
|
+
|
|
297
|
+
class Player
|
|
298
|
+
attr_accessor :prior, :beta, :gamma, :prior_draw
|
|
299
|
+
def initialize(prior=Gaussian.new(MU, SIGMA), beta=BETA, gamma=GAMMA, prior_draw=NINF)
|
|
300
|
+
@prior = prior
|
|
301
|
+
@beta = beta
|
|
302
|
+
@gamma = gamma
|
|
303
|
+
@prior_draw = prior_draw
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def performance
|
|
307
|
+
Gaussian.new(@prior.mu, Math.sqrt(@prior.sigma**2 + @beta**2))
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def to_s
|
|
311
|
+
"Player(Gaussian(mu=#{(@prior.mu)}, sigma=#{(@prior.sigma)}), beta=#{(@beta)}, gamma=#{(@gamma)})"
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def inspect
|
|
315
|
+
to_s
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
class TeamVariable
|
|
320
|
+
attr_accessor :prior, :likelihood_lose, :likelihood_win, :likelihood_draw
|
|
321
|
+
def initialize(prior=NINF, likelihood_lose=NINF, likelihood_win=NINF, likelihood_draw=NINF)
|
|
322
|
+
@prior = prior
|
|
323
|
+
@likelihood_lose = likelihood_lose
|
|
324
|
+
@likelihood_win = likelihood_win
|
|
325
|
+
@likelihood_draw = likelihood_draw
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Renamed from p to prob. p is kernel print...
|
|
329
|
+
def prob
|
|
330
|
+
@prior*@likelihood_lose*@likelihood_win*@likelihood_draw
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def posterior_win
|
|
334
|
+
@prior*@likelihood_lose*@likelihood_draw
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def posterior_lose
|
|
338
|
+
@prior*@likelihood_win*@likelihood_draw
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def likelihood
|
|
342
|
+
@likelihood_win*@likelihood_lose*@likelihood_draw
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def self.performance(team, weights) # I think this is used with weights as a single value
|
|
346
|
+
weights = Array.new(team.length, weights) unless ((weights.is_a?(Array)) && (weights.length == team.length))
|
|
347
|
+
res = N00
|
|
348
|
+
team.zip(weights).each do |player, w|
|
|
349
|
+
res += player.performance * w
|
|
350
|
+
end
|
|
351
|
+
res
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def to_str
|
|
355
|
+
to_s
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def to_s
|
|
359
|
+
inspect
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def inspect
|
|
363
|
+
"TTT::TeamVariable:#{self.object_id}:\t" + { prior: @prior, likelihood_lose: @likelihood_lose, likelihood_win: @likelihood_win, likelihood_draw: @likelihood_draw }.to_s
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
class DrawMessages
|
|
368
|
+
attr_accessor :prior, :prior_team, :likelihood_lose, :likelihood_win
|
|
369
|
+
def initialize(prior=NINF, prior_team=NINF, likelihood_lose=NINF, likelihood_win=NINF)
|
|
370
|
+
@prior = prior
|
|
371
|
+
@prior_team = prior_team
|
|
372
|
+
@likelihood_lose = likelihood_lose
|
|
373
|
+
@likelihood_win = likelihood_win
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# Renamed from p to prob. p() being a kernel method
|
|
377
|
+
def prob
|
|
378
|
+
@prior_team*@likelihood_lose*@likelihood_win
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def posterior_win
|
|
382
|
+
@prior_team*@likelihood_lose
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def posterior_lose
|
|
386
|
+
@prior_team*@likelihood_win
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def likelihood
|
|
390
|
+
@likelihood_win*@likelihood_lose
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
class DiffMessages
|
|
395
|
+
attr_accessor :prior, :likelihood
|
|
396
|
+
def initialize(prior=NINF, likelihood=NINF)
|
|
397
|
+
@prior = prior
|
|
398
|
+
@likelihood = likelihood
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def prob
|
|
402
|
+
@prior*@likelihood
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def to_str
|
|
406
|
+
to_s
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def to_s
|
|
410
|
+
inspect
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def inspect
|
|
414
|
+
"TTT::DiffMessages:#{self.object_id}: \t" + {prior: @prior, likelihood: @likelihood}.to_s
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
class Game
|
|
419
|
+
attr_accessor :likelihoods, :teams, :evidence
|
|
420
|
+
def initialize(teams, result=[], p_draw=0.0, weights=[])
|
|
421
|
+
if (p_draw.negative? || p_draw > 1.0)
|
|
422
|
+
raise(ArgumentError, "Draw probability #{p_draw} out of acceptable range: 1.0 > p_draw > 0.0.")
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
unless result.empty?
|
|
426
|
+
if (result.length != teams.length)
|
|
427
|
+
raise(ArgumentError, "Results provided but results for all teams not provided.")
|
|
428
|
+
end
|
|
429
|
+
if ((p_draw.zero?) && (result.length != result.uniq.length))
|
|
430
|
+
raise(ArgumentError, "Draws provided in results but p_draw is 0.0.")
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
unless weights.empty?
|
|
434
|
+
if (weights.length != teams.length)
|
|
435
|
+
raise(ArgumentError, "Weights provided but weights for all teams not provided.")
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
teams_without_weights = []
|
|
439
|
+
teams.each_with_index do |team, idx|
|
|
440
|
+
teams_without_weights << team if team.length != weights[idx].length
|
|
441
|
+
end
|
|
442
|
+
unless teams_without_weights.empty?
|
|
443
|
+
raise(ArgumentError, "No weight provided for teams #{teams_without_weights}")
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
@teams = teams
|
|
448
|
+
@result = result # I hate that this isn't pluralised, but either is fine.
|
|
449
|
+
@p_draw = p_draw
|
|
450
|
+
weights = @teams.map {|team| Array.new(team.length, 1.0) } if weights.empty?
|
|
451
|
+
@weights = weights
|
|
452
|
+
@likelihoods = []
|
|
453
|
+
@evidence = 0.0
|
|
454
|
+
compute_likelihoods
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
# The number of teams in the game
|
|
458
|
+
def length
|
|
459
|
+
@teams.length
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
# The number of players in the Game
|
|
463
|
+
def size
|
|
464
|
+
i = 0 # should probably use inject and oneline it
|
|
465
|
+
@teams.each do |team|
|
|
466
|
+
i += team.length
|
|
467
|
+
end
|
|
468
|
+
i
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def performance(i)
|
|
472
|
+
# I'm not 100% how this is really meant to be used.
|
|
473
|
+
# The semantics of Python zip() in python make it a little ambiguous.
|
|
474
|
+
TeamVariable.performance(@teams[i], @weights[i])
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
def partial_evidence(d, margin, tie, e)
|
|
478
|
+
mu = d[e].prior.mu
|
|
479
|
+
sigma = d[e].prior.sigma
|
|
480
|
+
@evidence *= if tie[e]
|
|
481
|
+
(cdf(margin[e], mu, sigma) - cdf(-margin[e], mu, sigma))
|
|
482
|
+
else
|
|
483
|
+
(1 - cdf(margin[e], mu, sigma))
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def graphical_model
|
|
488
|
+
if @result.empty?
|
|
489
|
+
r = (0...@teams.length).to_a.reverse if @result.empty?
|
|
490
|
+
else
|
|
491
|
+
r = @result
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
o = sortperm(r, true)
|
|
495
|
+
t = []
|
|
496
|
+
d = []
|
|
497
|
+
tie = []
|
|
498
|
+
margin = []
|
|
499
|
+
@teams.length.times do |i|
|
|
500
|
+
t << TeamVariable.new(performance(o[i]), NINF, NINF, NINF)
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
(@teams.length-1).times do |i|
|
|
504
|
+
d << DiffMessages.new(t[i].prior - t[i+1].prior, NINF)
|
|
505
|
+
tie << (r[o[i]] == r[o[i+1]])
|
|
506
|
+
if @p_draw.zero?
|
|
507
|
+
margin << 0.0
|
|
508
|
+
else
|
|
509
|
+
sum = 0.0
|
|
510
|
+
@teams[o[i]].each do |player|
|
|
511
|
+
sum += player.beta**2
|
|
512
|
+
end
|
|
513
|
+
@teams[o[i+1]].each do |player|
|
|
514
|
+
sum += player.beta**2
|
|
515
|
+
end
|
|
516
|
+
margin << compute_margin(@p_draw, Math.sqrt(sum))
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
@evidence = 1.0
|
|
520
|
+
[o, t, d, tie, margin]
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
def analytical_likelihood # "likelihood_analitico"
|
|
524
|
+
o, t, d, tie, margin = graphical_model
|
|
525
|
+
partial_evidence(d, margin, tie, 0)
|
|
526
|
+
d = d[0].prior
|
|
527
|
+
mu_trunc, sigma_trunc = trunc(d.mu, d.sigma, margin[0], tie[0])
|
|
528
|
+
if d.sigma == sigma_trunc
|
|
529
|
+
delta_div = d.sigma**2*mu_trunc - sigma_trunc**2*d.mu
|
|
530
|
+
theta_div_pow2 = INF
|
|
531
|
+
else
|
|
532
|
+
delta_div = (d.sigma**2*mu_trunc - sigma_trunc**2*d.mu)/(d.sigma**2-sigma_trunc**2)
|
|
533
|
+
delta_div_pow2 = (sigma_trunc**2*d.sigma**2)/(d.sigma**2 - sigma_trunc**2)
|
|
534
|
+
end
|
|
535
|
+
res = []
|
|
536
|
+
t.length.times do |i|
|
|
537
|
+
team = []
|
|
538
|
+
@teams[o[i]].length.times do |j|
|
|
539
|
+
mu == 0.0
|
|
540
|
+
mu = @teams[o[i]][j].prior.mu + ( delta_div - d.mu)*(-1)**(i==1) unless d.sigma == sigma_trunc
|
|
541
|
+
analytical_sigma = Math.sqrt(theta_div_pow2 + d.sigma**2 - @teams[o[i]][j].prior.sigma**2)
|
|
542
|
+
team << Gaussian.new(mu, analytical_sigma)
|
|
543
|
+
end
|
|
544
|
+
res << team
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
if o[0] < o[1]
|
|
548
|
+
[res[0], res[1]]
|
|
549
|
+
else
|
|
550
|
+
[res[1], res[0]]
|
|
551
|
+
end
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
def likelihood_teams
|
|
555
|
+
o, t, d, tie, margin = graphical_model
|
|
556
|
+
|
|
557
|
+
step = [INF, INF]
|
|
558
|
+
i = 0
|
|
559
|
+
|
|
560
|
+
while gr_tuple(step, 1e-6) && (i < 10)
|
|
561
|
+
step = [0.0, 0.0]
|
|
562
|
+
(0...d.length-1).each do |e|
|
|
563
|
+
d[e].prior = t[e].posterior_win - t[e+1].posterior_lose # prior giving wronnnggg
|
|
564
|
+
partial_evidence(d, margin, tie, e) if i.zero?
|
|
565
|
+
d[e].likelihood = approx(d[e].prior, margin[e], tie[e]) / d[e].prior
|
|
566
|
+
likelihood_lose = t[e].posterior_win - d[e].likelihood
|
|
567
|
+
step = max_tuple(step, t[e+1].likelihood_lose.delta(likelihood_lose))
|
|
568
|
+
t[e+1].likelihood_lose = likelihood_lose
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
(1...d.length).to_a.reverse.each do |e|
|
|
572
|
+
d[e].prior = t[e].posterior_win - t[e+1].posterior_lose
|
|
573
|
+
partial_evidence(d, margin, tie, e) if ((i.zero?) && (e == (d.length-1)))
|
|
574
|
+
d[e].likelihood = approx(d[e].prior, margin[e], tie[e]) / d[e].prior
|
|
575
|
+
likelihood_win = t[e+1].posterior_lose + d[e].likelihood
|
|
576
|
+
step = max_tuple(step, t[e].likelihood_win.delta(likelihood_win))
|
|
577
|
+
t[e].likelihood_win = likelihood_win
|
|
578
|
+
end
|
|
579
|
+
i += 1
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
if d.length == 1
|
|
583
|
+
partial_evidence(d, margin, tie, 0)
|
|
584
|
+
d[0].prior = t[0].posterior_win - t[1].posterior_lose
|
|
585
|
+
d[0].likelihood = approx(d[0].prior, margin[0], tie[0]) / d[0].prior
|
|
586
|
+
end
|
|
587
|
+
t[0].likelihood_win = t[1].posterior_lose + d[0].likelihood
|
|
588
|
+
t[-1].likelihood_lose = t[-2].posterior_win - d[-1].likelihood
|
|
589
|
+
(0...t.length).to_a.map { |e| t[o[e]].likelihood }
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
def compute_likelihoods
|
|
593
|
+
weighted = 0
|
|
594
|
+
@weights.each do |w|
|
|
595
|
+
weighted += 1 if w != 1.0
|
|
596
|
+
end
|
|
597
|
+
if @teams.length > 2 || weighted.positive?
|
|
598
|
+
m_t_ft = likelihood_teams
|
|
599
|
+
res = []
|
|
600
|
+
@teams.length.times do |e|
|
|
601
|
+
e_likelihoods = []
|
|
602
|
+
@teams[e].length.times do |i|
|
|
603
|
+
weight_factor = if @weights[e][i] != 0.0
|
|
604
|
+
1 / @weights[e][i]
|
|
605
|
+
else
|
|
606
|
+
INF
|
|
607
|
+
end
|
|
608
|
+
difference = m_t_ft[e] - performance(e).exclude(@teams[e][i].prior * @weights[e][i])
|
|
609
|
+
e_likelihoods << (weight_factor * difference)
|
|
610
|
+
end
|
|
611
|
+
res << e_likelihoods
|
|
612
|
+
end
|
|
613
|
+
@likelihoods = res
|
|
614
|
+
else
|
|
615
|
+
@likelihoods = analytical_likelihood # untested
|
|
616
|
+
end
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
def posteriors
|
|
620
|
+
res = []
|
|
621
|
+
@teams.each_with_index do |team, i|
|
|
622
|
+
il = []
|
|
623
|
+
team.each_with_index do |player, j|
|
|
624
|
+
product = @likelihoods[i][j] * player.prior
|
|
625
|
+
il << product
|
|
626
|
+
end
|
|
627
|
+
res << il
|
|
628
|
+
end
|
|
629
|
+
res
|
|
630
|
+
end
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
class Skill
|
|
634
|
+
attr_accessor :forward, :backward, :likelihood, :elapsed
|
|
635
|
+
def initialize(forward=NINF, backward=NINF, likelihood=NINF, elapsed=0)
|
|
636
|
+
@forward = forward
|
|
637
|
+
@backward = backward
|
|
638
|
+
@likelihood = likelihood
|
|
639
|
+
@elapsed = elapsed
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
def to_s
|
|
643
|
+
inspect
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
def inspect
|
|
647
|
+
{ 'forward'=>@forward, 'backward'=>@backward, 'likelihood'=>@likelihood, 'elapsed'=>@elapsed }.to_s
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
class Agent
|
|
653
|
+
attr_accessor :player, :message, :last_time
|
|
654
|
+
def initialize(player, message, last_time)
|
|
655
|
+
@player = player
|
|
656
|
+
@message = message
|
|
657
|
+
@last_time = last_time
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
def receive(elapsed)
|
|
661
|
+
if @message != NINF
|
|
662
|
+
@message.forget(@player.gamma, elapsed)
|
|
663
|
+
else
|
|
664
|
+
@player.prior
|
|
665
|
+
end
|
|
666
|
+
end
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
# Agents is a dict in python and their implementation is whack.
|
|
670
|
+
def clean!(agents, last_time=false)
|
|
671
|
+
agents.each_pair do |agent_label, agent|
|
|
672
|
+
agent.message = NINF
|
|
673
|
+
agent.last_time = -INF if last_time
|
|
674
|
+
end
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
class Item
|
|
678
|
+
attr_accessor :name, :likelihood
|
|
679
|
+
def initialize(name, likelihood)
|
|
680
|
+
@name = name
|
|
681
|
+
@likelihood = likelihood
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
def to_s
|
|
685
|
+
inspect
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
def inspect
|
|
689
|
+
{ 'name' => @name, 'likelihood' => @likelihood }.to_s
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
class Team
|
|
695
|
+
attr_accessor :items, :output
|
|
696
|
+
def initialize(items, output)
|
|
697
|
+
@items = items
|
|
698
|
+
@output = output
|
|
699
|
+
end
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
class Event
|
|
703
|
+
attr_accessor :teams, :evidence, :weights
|
|
704
|
+
def initialize(teams, evidence, weights)
|
|
705
|
+
@teams = teams
|
|
706
|
+
@evidence = evidence
|
|
707
|
+
@weights = weights
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
def to_s
|
|
711
|
+
inspect
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
def inspect
|
|
715
|
+
# { evidence: @evidence, teams: @teams, weights: @weights, result: result }.to_s
|
|
716
|
+
"Event(#{names}, #{result})"
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
def names
|
|
720
|
+
@teams.map { |team| team.items.map {|i| i.name} }
|
|
721
|
+
end
|
|
722
|
+
|
|
723
|
+
def result
|
|
724
|
+
@teams.map { | team | team.output }
|
|
725
|
+
end
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
def get_composition(events)
|
|
729
|
+
events.map {|event| event.teams.map {|team| team.items.map {|item| item.name} } }
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
def get_results(events)
|
|
733
|
+
events.map {|event| event.teams.map {|team| team.output } }
|
|
734
|
+
end
|
|
735
|
+
|
|
736
|
+
def compute_elapsed(last_time, actual_time)
|
|
737
|
+
if last_time == INF
|
|
738
|
+
1
|
|
739
|
+
elsif last_time == -INF
|
|
740
|
+
0
|
|
741
|
+
else
|
|
742
|
+
actual_time - last_time
|
|
743
|
+
end
|
|
744
|
+
end
|
|
745
|
+
|
|
746
|
+
class Batch
|
|
747
|
+
attr_accessor :skills, :events, :time
|
|
748
|
+
def initialize(composition, results=[], time=0, agents={}, p_draw=0.0, weights=[])
|
|
749
|
+
if (!results.empty? && (results.length != composition.length))
|
|
750
|
+
raise(ArgumentError, "Results provided but composition.length != results.length")
|
|
751
|
+
end
|
|
752
|
+
if (!weights.empty? &&(weights.length != composition.length))
|
|
753
|
+
raise(ArgumentError, "Weights provided but composition.length != weights.length")
|
|
754
|
+
end
|
|
755
|
+
|
|
756
|
+
this_agents = composition.flat_map { |teams| teams.flat_map { |team| team } }.to_set
|
|
757
|
+
elapsed = this_agents.map { |a| [a, compute_elapsed(agents[a].last_time, time)] }.to_h
|
|
758
|
+
@skills = this_agents.map { |a| [a, Skill.new(agents[a].receive(elapsed[a]), NINF, NINF, elapsed[a])] }.to_h
|
|
759
|
+
@events = composition.each_with_index.map { |event_composition, e|
|
|
760
|
+
Event.new(
|
|
761
|
+
event_composition.each_with_index.map { |team_composition, t|
|
|
762
|
+
Team.new(
|
|
763
|
+
team_composition.each_with_index.map { |_, a|
|
|
764
|
+
Item.new(composition[e][t][a], NINF)
|
|
765
|
+
},
|
|
766
|
+
results.empty? ? composition[e].length - t - 1 : results[e][t]
|
|
767
|
+
)
|
|
768
|
+
},
|
|
769
|
+
0.0,
|
|
770
|
+
weights.empty? ? weights : weights[e]
|
|
771
|
+
)
|
|
772
|
+
}
|
|
773
|
+
@time = time
|
|
774
|
+
@agents = agents
|
|
775
|
+
@p_draw = p_draw
|
|
776
|
+
iteration
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
def length
|
|
780
|
+
@events.length
|
|
781
|
+
end
|
|
782
|
+
|
|
783
|
+
def add_events(composition, results=[])
|
|
784
|
+
this_agents = composition.flat_map { |team| team.items }.to_set
|
|
785
|
+
this_agents.each_with_index do |agent, i|
|
|
786
|
+
elapsed = compute_elapsed(@agents[agent].last_time, @time)
|
|
787
|
+
if @skills.include?(agent)
|
|
788
|
+
@skills[agent] = Skill(@agents[agent].receive(elapsed), NINF, NINF, elapsed)
|
|
789
|
+
else
|
|
790
|
+
@skills[agent].elapsed = elapsed
|
|
791
|
+
@skills[agent].forward = @agents[agent].receive(elapsed)
|
|
792
|
+
end
|
|
793
|
+
end
|
|
794
|
+
|
|
795
|
+
_from = @events.length + 1
|
|
796
|
+
|
|
797
|
+
composition.length.times do |e|
|
|
798
|
+
@events << Event.new(
|
|
799
|
+
composition[e].each_with_index.map { |team_composition, t|
|
|
800
|
+
Team.new(
|
|
801
|
+
team_composition.each_with_index.map { |_, a|
|
|
802
|
+
Item.new(composition[e][t][a], NINF)
|
|
803
|
+
},
|
|
804
|
+
if results.empty?
|
|
805
|
+
composition[e].length - t - 1
|
|
806
|
+
else
|
|
807
|
+
results[e][t]
|
|
808
|
+
end
|
|
809
|
+
)
|
|
810
|
+
},
|
|
811
|
+
0.0,
|
|
812
|
+
if weights.nil? || weights.empty?
|
|
813
|
+
weights
|
|
814
|
+
else
|
|
815
|
+
weights[e]
|
|
816
|
+
end
|
|
817
|
+
)
|
|
818
|
+
end
|
|
819
|
+
iteration(_from)
|
|
820
|
+
end
|
|
821
|
+
|
|
822
|
+
def posterior(agent)
|
|
823
|
+
@skills[agent].likelihood * @skills[agent].backward * @skills[agent].forward
|
|
824
|
+
end
|
|
825
|
+
|
|
826
|
+
def posteriors
|
|
827
|
+
res = {}
|
|
828
|
+
@skills.each_key do |skill|
|
|
829
|
+
res[skill] = posterior(skill) # I think this is fucked because posterior doesn't index this way
|
|
830
|
+
end
|
|
831
|
+
res
|
|
832
|
+
end
|
|
833
|
+
|
|
834
|
+
def within_prior(item)
|
|
835
|
+
r = @agents[item.name].player
|
|
836
|
+
r_p = (posterior(item.name) / item.likelihood)
|
|
837
|
+
Player.new(Gaussian.new(r_p.mu, r_p.sigma), r.beta, r.gamma)
|
|
838
|
+
end
|
|
839
|
+
|
|
840
|
+
def within_priors(event)
|
|
841
|
+
@events[event].teams.map { |team| team.items.map { |item| within_prior(item) } }
|
|
842
|
+
end
|
|
843
|
+
|
|
844
|
+
def iteration(from=0)
|
|
845
|
+
(from...@events.length).each do |e|
|
|
846
|
+
teams = within_priors(e)
|
|
847
|
+
result = @events[e].result
|
|
848
|
+
weights = @events[e].weights
|
|
849
|
+
g = Game.new(teams, result, @p_draw, weights)
|
|
850
|
+
@events[e].teams.each_with_index do |team, t|
|
|
851
|
+
team.items.each_with_index do |item, i|
|
|
852
|
+
@skills[item.name].likelihood = (@skills[item.name].likelihood / item.likelihood) * g.likelihoods[t][i]
|
|
853
|
+
item.likelihood = g.likelihoods[t][i]
|
|
854
|
+
end
|
|
855
|
+
end
|
|
856
|
+
@events[e].evidence = g.evidence
|
|
857
|
+
end
|
|
858
|
+
end
|
|
859
|
+
|
|
860
|
+
def convergence(epsilon=1e-6, iterations = 20)
|
|
861
|
+
step = [INF, INF]
|
|
862
|
+
i = 0
|
|
863
|
+
while gr_tuple(step, epsilon) && (i < iterations)
|
|
864
|
+
old = posteriors # python makes a copy
|
|
865
|
+
iteration
|
|
866
|
+
step = dict_diff(old, posteriors)
|
|
867
|
+
i += 1
|
|
868
|
+
end
|
|
869
|
+
i
|
|
870
|
+
end
|
|
871
|
+
|
|
872
|
+
def forward_prior_out(agent)
|
|
873
|
+
@skills[agent].forward * @skills[agent].likelihood
|
|
874
|
+
end
|
|
875
|
+
|
|
876
|
+
def backward_prior_out(agent)
|
|
877
|
+
n = @skills[agent].likelihood * @skills[agent].backward
|
|
878
|
+
n.forget(@agents[agent].player.gamma, @skills[agent].elapsed)
|
|
879
|
+
end
|
|
880
|
+
|
|
881
|
+
def new_backward_info
|
|
882
|
+
@skills.each_key do |a|
|
|
883
|
+
@skills[a].backward = @agents[a].message
|
|
884
|
+
end
|
|
885
|
+
iteration # python explicitly: return self.iteration() -> and iteration() returns None???
|
|
886
|
+
nil
|
|
887
|
+
end
|
|
888
|
+
|
|
889
|
+
def new_forward_info
|
|
890
|
+
@skills.each_key do |a|
|
|
891
|
+
@skills[a].forward = @agents[a].receive(@skills[a].elapsed)
|
|
892
|
+
end
|
|
893
|
+
iteration # python explicitly: return self.iteration() -> and iteration() returns None?
|
|
894
|
+
nil
|
|
895
|
+
end
|
|
896
|
+
|
|
897
|
+
def to_s
|
|
898
|
+
"Batch(time=#{@time}, events=#{@events})"
|
|
899
|
+
end
|
|
900
|
+
|
|
901
|
+
def inspect
|
|
902
|
+
to_s
|
|
903
|
+
end
|
|
904
|
+
|
|
905
|
+
end
|
|
906
|
+
|
|
907
|
+
class History
|
|
908
|
+
attr_accessor :batches
|
|
909
|
+
|
|
910
|
+
def initialize(composition, results:[], times:[], priors:{}, mu:MU, sigma:SIGMA, beta:BETA, gamma:GAMMA, p_draw:P_DRAW, weights:[])
|
|
911
|
+
if results.length.positive? && results.length != composition.length
|
|
912
|
+
raise(ArgumentError, "Results provided but composition.length (#{composition.length}) != results.length} (#{results.length})")
|
|
913
|
+
end
|
|
914
|
+
if times.length.positive? && times.length != composition.length
|
|
915
|
+
raise(ArgumentError, "Times provided but composition.length (#{composition.length}) != times.length} (#{times.length})")
|
|
916
|
+
end
|
|
917
|
+
if weights.length.positive? && weights.length != composition.length
|
|
918
|
+
raise(ArgumentError, "Weights provided but composition.length (#{composition.length}) != weights.length} (#{weights.length})")
|
|
919
|
+
end
|
|
920
|
+
|
|
921
|
+
@size = composition.length
|
|
922
|
+
@batches = []
|
|
923
|
+
@agents = Hash[
|
|
924
|
+
composition.flatten(2).uniq.map do |a|
|
|
925
|
+
[
|
|
926
|
+
a,
|
|
927
|
+
if priors.key?(a)
|
|
928
|
+
Agent.new(priors[a], NINF, -INF)
|
|
929
|
+
else
|
|
930
|
+
Agent.new(Player.new(Gaussian.new(mu, sigma), beta, gamma), NINF, -INF)
|
|
931
|
+
end
|
|
932
|
+
]
|
|
933
|
+
end
|
|
934
|
+
]
|
|
935
|
+
@mu = mu
|
|
936
|
+
@sigma = sigma
|
|
937
|
+
@p_draw = p_draw
|
|
938
|
+
@time = times.length.positive?
|
|
939
|
+
trueskill(composition, results, times, weights)
|
|
940
|
+
|
|
941
|
+
end
|
|
942
|
+
|
|
943
|
+
def trueskill(composition, results, times, weights)
|
|
944
|
+
o = times.length.positive? ? sortperm(times) : (0...composition.length).to_a
|
|
945
|
+
i = 0
|
|
946
|
+
while i < @size
|
|
947
|
+
j = i + 1
|
|
948
|
+
t = times.length.zero? ? j : times[o[i]]
|
|
949
|
+
while (times.length.positive?) && (j < @size) && (times[o[j]] == t) # I don't even
|
|
950
|
+
j += 1
|
|
951
|
+
end
|
|
952
|
+
b = if results.length.positive?
|
|
953
|
+
Batch.new(
|
|
954
|
+
o[i...j].map { |k| composition[k] },
|
|
955
|
+
o[i...j].map { |k| results[k] },
|
|
956
|
+
t,
|
|
957
|
+
@agents,
|
|
958
|
+
@p_draw,
|
|
959
|
+
weights.length.positive? ? o[i...j].map { |k| weights[k] } : weights
|
|
960
|
+
)
|
|
961
|
+
else
|
|
962
|
+
Batch.new(
|
|
963
|
+
o[i...j].map { |k| composition[k] },
|
|
964
|
+
[],
|
|
965
|
+
t,
|
|
966
|
+
@agents,
|
|
967
|
+
@p_draw,
|
|
968
|
+
weights.length.positive? ? o[i...j].map { |k| weights[k] } : weights
|
|
969
|
+
)
|
|
970
|
+
end
|
|
971
|
+
@batches << b
|
|
972
|
+
b.skills.each_key do |a|
|
|
973
|
+
@agents[a].last_time = (@time ? t : INF)
|
|
974
|
+
@agents[a].message = b.forward_prior_out(a)
|
|
975
|
+
end
|
|
976
|
+
i = j
|
|
977
|
+
end
|
|
978
|
+
end
|
|
979
|
+
|
|
980
|
+
def iteration
|
|
981
|
+
step = [0.0, 0.0]
|
|
982
|
+
clean!(@agents)
|
|
983
|
+
|
|
984
|
+
(0...@batches.length-1).to_a.reverse.each do |j|
|
|
985
|
+
@batches[j+1].skills.each_key do |a|
|
|
986
|
+
@agents[a].message = @batches[j+1].backward_prior_out(a)
|
|
987
|
+
end
|
|
988
|
+
old = @batches[j].posteriors # python copies
|
|
989
|
+
@batches[j].new_backward_info
|
|
990
|
+
step = max_tuple(step, dict_diff(old, @batches[j].posteriors))
|
|
991
|
+
end
|
|
992
|
+
|
|
993
|
+
clean!(@agents)
|
|
994
|
+
|
|
995
|
+
(1...@batches.length).each do |j|
|
|
996
|
+
@batches[j-1].skills.each_key do |a|
|
|
997
|
+
@agents[a].message = @batches[j-1].forward_prior_out(a)
|
|
998
|
+
end
|
|
999
|
+
old = @batches[j].posteriors # again, original copies
|
|
1000
|
+
@batches[j].new_forward_info
|
|
1001
|
+
step = max_tuple(step, dict_diff(old, @batches[j].posteriors))
|
|
1002
|
+
end
|
|
1003
|
+
|
|
1004
|
+
if @batches.length == 1
|
|
1005
|
+
old = @batches[0].posteriors
|
|
1006
|
+
@batches[0].convergence
|
|
1007
|
+
step = max_tuple(step, dict_diff(old, @batches[0].posteriors))
|
|
1008
|
+
end
|
|
1009
|
+
|
|
1010
|
+
step
|
|
1011
|
+
end
|
|
1012
|
+
|
|
1013
|
+
def convergence(epsilon:EPSILON, iterations:ITERATIONS, verbose:true)
|
|
1014
|
+
step = [INF, INF]
|
|
1015
|
+
i = 0
|
|
1016
|
+
|
|
1017
|
+
while gr_tuple(step, epsilon) && (i < iterations)
|
|
1018
|
+
step = iteration
|
|
1019
|
+
puts "Iteration = #{i} , #{step}" if verbose
|
|
1020
|
+
i += 1
|
|
1021
|
+
end
|
|
1022
|
+
puts "End" if verbose
|
|
1023
|
+
|
|
1024
|
+
[step, i]
|
|
1025
|
+
end
|
|
1026
|
+
|
|
1027
|
+
def learning_curves
|
|
1028
|
+
res = {}
|
|
1029
|
+
@batches.each do |batch|
|
|
1030
|
+
batch.skills.each_key do |a|
|
|
1031
|
+
res[a] ||= []
|
|
1032
|
+
res[a] << [batch.time, batch.posterior(a)]
|
|
1033
|
+
end
|
|
1034
|
+
end
|
|
1035
|
+
res
|
|
1036
|
+
end
|
|
1037
|
+
|
|
1038
|
+
def evidence
|
|
1039
|
+
ev = []
|
|
1040
|
+
@batches.each do |batch|
|
|
1041
|
+
batch.events.each do |event|
|
|
1042
|
+
ev << event.evidence
|
|
1043
|
+
end
|
|
1044
|
+
end
|
|
1045
|
+
ev
|
|
1046
|
+
end
|
|
1047
|
+
|
|
1048
|
+
def log_evidence
|
|
1049
|
+
# Math.log(@batches.flat_map { |b| b.events }.map { |event| event.evidence }.reduce(1, :*))
|
|
1050
|
+
@batches.flat_map { |b| b.events }.map { |event| Math.log(event.evidence) }.sum
|
|
1051
|
+
end
|
|
1052
|
+
|
|
1053
|
+
def to_s
|
|
1054
|
+
"History(Events=#{@size}, Batches=#{@batches.length}, Agents=#{@agents})"
|
|
1055
|
+
end
|
|
1056
|
+
|
|
1057
|
+
def inspect
|
|
1058
|
+
to_s
|
|
1059
|
+
end
|
|
1060
|
+
|
|
1061
|
+
def length
|
|
1062
|
+
@size
|
|
1063
|
+
end
|
|
1064
|
+
end
|
|
1065
|
+
|
|
1066
|
+
def game_example(mu=MU, sigma=SIGMA, beta=BETA, p_draw=P_DRAW)
|
|
1067
|
+
a1 = Player.new(Gaussian.new(mu-500, sigma), beta=beta)
|
|
1068
|
+
a2 = Player.new(Gaussian.new(mu-500, sigma), beta=beta)
|
|
1069
|
+
a3 = Player.new(Gaussian.new(mu, sigma), beta=beta)
|
|
1070
|
+
a4 = Player.new(Gaussian.new(mu, sigma), beta=beta)
|
|
1071
|
+
a5 = Player.new(Gaussian.new(mu+5000, sigma), beta=beta)
|
|
1072
|
+
a6 = Player.new(Gaussian.new(mu+5000, sigma), beta=beta)
|
|
1073
|
+
team_a = [ a1, a2 ]
|
|
1074
|
+
team_b = [ a3, a4 ]
|
|
1075
|
+
team_c = [ a5, a6 ]
|
|
1076
|
+
teams = [team_a, team_b, team_c]
|
|
1077
|
+
result = [0.0, 1.0, 2.0]
|
|
1078
|
+
g = Game.new(teams, result, p_draw=p_draw)
|
|
1079
|
+
puts "Teams: #{g.teams}"
|
|
1080
|
+
puts "Likelihoods: #{g.likelihoods}"
|
|
1081
|
+
puts "Evidence: #{g.evidence}"
|
|
1082
|
+
puts "Posteriors: #{g.posteriors}"
|
|
1083
|
+
(0...teams.length).each do |i|
|
|
1084
|
+
puts "#{i}: #{g.performance(i)}"
|
|
1085
|
+
end
|
|
1086
|
+
end
|
|
1087
|
+
|
|
1088
|
+
def history_example(mu=MU, sigma=SIGMA, beta=BETA, p_draw=P_DRAW)
|
|
1089
|
+
c1 = [["a"], ["b"]]
|
|
1090
|
+
c2 = [["b"], ["c"]]
|
|
1091
|
+
c3 = [["c"], ["a"]]
|
|
1092
|
+
composition = [c1, c2, c3]
|
|
1093
|
+
h = History.new(composition, gamma: 0.0)
|
|
1094
|
+
puts "#{h.learning_curves["a"]}"
|
|
1095
|
+
# [(1, N(mu=3.339, sigma=4.985)), (3, N(mu=-2.688, sigma=3.779))]
|
|
1096
|
+
puts "#{h.learning_curves["b"]}"
|
|
1097
|
+
# [(1, N(mu=-3.339, sigma=4.985)), (2, N(mu=0.059, sigma=4.218))]
|
|
1098
|
+
h.convergence
|
|
1099
|
+
puts "#{h.learning_curves["a"]}"
|
|
1100
|
+
# [[1, N(mu= 1.1776221018565704e-07, sigma= 2.3948083841791528)], [3, N(mu= -9.875327098012653e-08, sigma= 2.3948083506699978)]]
|
|
1101
|
+
puts "#{h.learning_curves["b"]}"
|
|
1102
|
+
# [[1, N(mu= -6.743217353120669e-08, sigma= 2.3948083803990317)], [2, N(mu= -6.888059735564812e-08, sigma= 2.394808375074641)]]
|
|
1103
|
+
puts "#{h.log_evidence}"
|
|
1104
|
+
puts "Foo"
|
|
1105
|
+
end
|
|
1106
|
+
|
|
1107
|
+
def liquid_example(path:"./example_data/liquid_t2_tournament_results.json")
|
|
1108
|
+
json_data = JSON.load(File.open(path))
|
|
1109
|
+
tournaments = {}
|
|
1110
|
+
json_data["tournament_positions"].each_pair do |k, v| # Symbolise keys and convert to dates, remove fake tournaments, remove tournaments with draws
|
|
1111
|
+
next unless v["results"].length > 2 # fake tournaments have < 3 teams
|
|
1112
|
+
|
|
1113
|
+
results = {}
|
|
1114
|
+
had_ties = false
|
|
1115
|
+
v["results"].each_pair do |rk, rv| # numberise keys, then sort the results by them
|
|
1116
|
+
if rv.length > 1
|
|
1117
|
+
had_ties = true
|
|
1118
|
+
break
|
|
1119
|
+
else
|
|
1120
|
+
results[rk.to_f] = rv
|
|
1121
|
+
end
|
|
1122
|
+
end
|
|
1123
|
+
unless had_ties
|
|
1124
|
+
tournaments[k] = { :results => results.sort,
|
|
1125
|
+
:day => DateTime.strptime(v["date"], "%Y-%m-%d %H:%M:%S").to_time.to_i / (60 * 60 * 24) }
|
|
1126
|
+
end
|
|
1127
|
+
end
|
|
1128
|
+
tournaments = tournaments.sort_by {|k, v| v[:day]}.to_h
|
|
1129
|
+
# Need to get to compositions, results, times: results needs to be inverse of position
|
|
1130
|
+
compositions = []
|
|
1131
|
+
results = []
|
|
1132
|
+
times = []
|
|
1133
|
+
tournaments.each_pair do |tournament_name, t_data|
|
|
1134
|
+
times << t_data[:day]
|
|
1135
|
+
t_results = []
|
|
1136
|
+
t_compositions = []
|
|
1137
|
+
t_data[:results].each_with_index do |(position, composition), i|
|
|
1138
|
+
t_results << t_data[:results].length - i
|
|
1139
|
+
# t_compositions << composition.map {|player| [player]}
|
|
1140
|
+
t_compositions << composition.flatten
|
|
1141
|
+
end
|
|
1142
|
+
compositions << t_compositions
|
|
1143
|
+
results << t_results
|
|
1144
|
+
end
|
|
1145
|
+
|
|
1146
|
+
h = History.new(compositions, results:results, times:times, mu:1200, sigma:400, beta:1320, gamma:19, p_draw: 0.01375)
|
|
1147
|
+
|
|
1148
|
+
puts h.log_evidence
|
|
1149
|
+
h.convergence(epsilon: 0.01, iterations: 60, verbose: true)
|
|
1150
|
+
puts h.log_evidence
|
|
1151
|
+
puts h.learning_curves.first
|
|
1152
|
+
|
|
1153
|
+
h.evidence.each_with_index do |ev, idx|
|
|
1154
|
+
puts "#{idx}\t#{ev}\t#{tournaments.keys[idx]}"
|
|
1155
|
+
end
|
|
1156
|
+
|
|
1157
|
+
end
|
|
1158
|
+
end
|