Module: TrueskillThroughTime

Defined in:
lib/TrueskillThroughTime.rb,
lib/TrueskillThroughTime/version.rb

Defined Under Namespace

Classes: Agent, Batch, DiffMessages, DrawMessages, Error, Event, Game, Gaussian, History, Item, Player, Skill, Team, TeamVariable

Constant Summary collapse

BETA =

Configuration Constants

1.0
MU =
0.0
SIGMA =
BETA * 6
GAMMA =
BETA * 0.03
P_DRAW =
0.0
EPSILON =
1e-6
ITERATIONS =
30
SQRT2 =

Derived Math Constants

Math.sqrt(2)
SQRT2PI =
Math.sqrt(2 * Math::PI)
INF =
Float::INFINITY
N01 =
Gaussian.new(0.0, 1.0)
N00 =
Gaussian.new(0.0, 0.0)
NINF =
Gaussian.new(0.0, INF)
Nms =
Gaussian.new(MU, SIGMA)
VERSION =
"1.0.0"

Instance Method Summary collapse

Instance Method Details

#approx(n, margin, tie) ⇒ Object

TODO: It seems like this should be a Guassian function



126
127
128
129
130
131
132
# File 'lib/TrueskillThroughTime.rb', line 126

def approx(n, margin, tie) # TODO: It seems like this should be a Guassian function
  mu, sigma = trunc(n.mu, n.sigma, margin, tie)
  # if $should_debug
  #   puts "approx(#{[n, margin, tie]}) (n, margin, tie) = mu #{mu} sigma #{sigma}"
  # end
  Gaussian.new(mu, sigma)
end

#cdf(x, mu = 0, sigma = 1) ⇒ Object



90
91
92
# File 'lib/TrueskillThroughTime.rb', line 90

def cdf(x, mu=0, sigma=1)
  0.5 * erfc(-(x - mu) / (sigma * SQRT2))
end

#clean!(agents, last_time = false) ⇒ Object

Agents is a dict in python and their implementation is whack.



670
671
672
673
674
675
# File 'lib/TrueskillThroughTime.rb', line 670

def clean!(agents, last_time=false)
  agents.each_pair do |agent_label, agent|
    agent.message = NINF
    agent.last_time = -INF if last_time
  end
end

#compute_elapsed(last_time, actual_time) ⇒ Object



736
737
738
739
740
741
742
743
744
# File 'lib/TrueskillThroughTime.rb', line 736

def compute_elapsed(last_time, actual_time)
  if last_time == INF
    1
  elsif last_time == -INF
    0
  else
    actual_time - last_time
  end
end

#compute_margin(p_draw, sd) ⇒ Object



134
135
136
# File 'lib/TrueskillThroughTime.rb', line 134

def compute_margin(p_draw, sd)
  (ppf(0.5-p_draw/2.0, 0.0, sd)).abs
end

#dict_diff(old, new) ⇒ Object



170
171
172
173
174
175
176
# File 'lib/TrueskillThroughTime.rb', line 170

def dict_diff(old, new)
  step = [0.0, 0.0]
  old.each_key do |a|
    step = max_tuple(step, old[a].delta(new[a])) # .delta is Guassian method....
  end
  step
end

#erfc(x) ⇒ Object



31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/TrueskillThroughTime.rb', line 31

def erfc(x)
  # """(http://bit.ly/zOLqbc)"""
  z = x.abs
  t = 1.0 / (1.0 + z / 2.0)
  a = -0.82215223 + t * 0.17087277; b = 1.48851587 + t * a
  c = -1.13520398 + t * b; d = 0.27886807 + t * c; e = -0.18628806 + t * d
  f = 0.09678418 + t * e; g = 0.37409196 + t * f; h = 1.00002368 + t * g
  r = t * Math.exp(-z * z - 1.26551223 + t * h)
  if x.positive?
    r
  else
    2.0 - r
  end
end

#erfcinv(y) ⇒ Object

Raises:

  • (ArgumentError)


46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/TrueskillThroughTime.rb', line 46

def erfcinv(y)
  raise(ArgumentError, "Argument must be non-negative numbers") if y.negative?
  return -INF if y > 2
  return INF if y.zero?

  y = 2 - y if y >= 1

  t = Math.sqrt(-2 * Math.log(y / 2.0))
  x = -0.70711 * ((2.30753 + t * 0.27061) / (1.0 + t * (0.99229 + t * 0.04481)) - t)

  3.times do
    err = erfc(x) - y
    x += err / (1.12837916709551257 * Math.exp(-(x**2)) - x * err)
  end

  y < 1 ? x : -x
end

#game_example(mu = MU, sigma = SIGMA, beta = BETA, p_draw = P_DRAW) ⇒ Object



1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
# File 'lib/TrueskillThroughTime.rb', line 1066

def game_example(mu=MU, sigma=SIGMA, beta=BETA, p_draw=P_DRAW)
  a1 = Player.new(Gaussian.new(mu-500, sigma), beta=beta)
  a2 = Player.new(Gaussian.new(mu-500, sigma), beta=beta)
  a3 = Player.new(Gaussian.new(mu, sigma), beta=beta)
  a4 = Player.new(Gaussian.new(mu, sigma), beta=beta)
  a5 = Player.new(Gaussian.new(mu+5000, sigma), beta=beta)
  a6 = Player.new(Gaussian.new(mu+5000, sigma), beta=beta)
  team_a = [ a1, a2 ]
  team_b = [ a3, a4 ]
  team_c = [ a5, a6 ]
  teams = [team_a, team_b, team_c]
  result = [0.0, 1.0, 2.0]
  g = Game.new(teams, result, p_draw=p_draw)
  puts "Teams: #{g.teams}"
  puts "Likelihoods: #{g.likelihoods}"
  puts "Evidence: #{g.evidence}"
  puts "Posteriors: #{g.posteriors}"
  (0...teams.length).each do |i|
    puts "#{i}: #{g.performance(i)}"
  end
end

#get_composition(events) ⇒ Object



728
729
730
# File 'lib/TrueskillThroughTime.rb', line 728

def get_composition(events)
  events.map {|event| event.teams.map {|team| team.items.map {|item| item.name} } }
end

#get_results(events) ⇒ Object



732
733
734
# File 'lib/TrueskillThroughTime.rb', line 732

def get_results(events)
  events.map {|event| event.teams.map {|team| team.output } }
end

#gr_tuple(tup, threshold) ⇒ Object



154
155
156
# File 'lib/TrueskillThroughTime.rb', line 154

def gr_tuple(tup, threshold)
  tup.max > threshold
end

#history_example(mu = MU, sigma = SIGMA, beta = BETA, p_draw = P_DRAW) ⇒ Object



1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
# File 'lib/TrueskillThroughTime.rb', line 1088

def history_example(mu=MU, sigma=SIGMA, beta=BETA, p_draw=P_DRAW)
  c1 = [["a"], ["b"]]
  c2 = [["b"], ["c"]]
  c3 = [["c"], ["a"]]
  composition = [c1, c2, c3]
  h = History.new(composition, gamma: 0.0)
  puts "#{h.learning_curves["a"]}"
  # [(1, N(mu=3.339, sigma=4.985)), (3, N(mu=-2.688, sigma=3.779))]
  puts "#{h.learning_curves["b"]}"
  # [(1, N(mu=-3.339, sigma=4.985)), (2, N(mu=0.059, sigma=4.218))]
  h.convergence
  puts "#{h.learning_curves["a"]}"
  # [[1, N(mu= 1.1776221018565704e-07, sigma= 2.3948083841791528)], [3, N(mu= -9.875327098012653e-08, sigma= 2.3948083506699978)]]
  puts "#{h.learning_curves["b"]}"
  # [[1, N(mu= -6.743217353120669e-08, sigma= 2.3948083803990317)], [2, N(mu= -6.888059735564812e-08, sigma= 2.394808375074641)]]
  puts "#{h.log_evidence}"
  puts "Foo"
end

#liquid_example(path: "./example_data/liquid_t2_tournament_results.json") ⇒ Object



1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
# File 'lib/TrueskillThroughTime.rb', line 1107

def liquid_example(path:"./example_data/liquid_t2_tournament_results.json")
  json_data = JSON.load(File.open(path))
  tournaments = {}
  json_data["tournament_positions"].each_pair do |k, v| # Symbolise keys and convert to dates, remove fake tournaments, remove tournaments with draws
    next unless v["results"].length > 2 # fake tournaments have < 3 teams

    results = {}
    had_ties = false
    v["results"].each_pair do |rk, rv| # numberise keys, then sort the results by them
      if rv.length > 1
        had_ties = true
        break
      else
        results[rk.to_f] = rv
      end
    end
    unless had_ties
      tournaments[k] = { :results => results.sort,
                         :day => DateTime.strptime(v["date"], "%Y-%m-%d %H:%M:%S").to_time.to_i / (60 * 60 * 24) }
    end
  end
  tournaments = tournaments.sort_by {|k, v| v[:day]}.to_h
  # Need to get to compositions, results, times: results needs to be inverse of position
  compositions = []
  results = []
  times = []
  tournaments.each_pair do |tournament_name, t_data|
    times << t_data[:day]
    t_results = []
    t_compositions = []
    t_data[:results].each_with_index do |(position, composition), i|
      t_results << t_data[:results].length - i
      # t_compositions << composition.map {|player| [player]}
      t_compositions << composition.flatten
    end
    compositions << t_compositions
    results << t_results
  end

  h = History.new(compositions, results:results, times:times, mu:1200, sigma:400, beta:1320, gamma:19, p_draw: 0.01375)

  puts h.log_evidence
  h.convergence(epsilon: 0.01, iterations: 60, verbose: true)
  puts h.log_evidence
  puts h.learning_curves.first

  h.evidence.each_with_index do |ev, idx|
    puts "#{idx}\t#{ev}\t#{tournaments.keys[idx]}"
  end

end

#max_tuple(t1, t2) ⇒ Object



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/TrueskillThroughTime.rb', line 138

def max_tuple(t1, t2)
  # Let's emulate python nan sorting behaviour!
  responses = [0.0, 0.0]
  responses[0] = if t1[0].to_f.nan? || t2[0].to_f.nan?
                   t1[0]
                 else
                   [t1[0], t2[0]].max
                 end
  responses[1] = if t1[1].to_f.nan? || t2[1].to_f.nan?
                   t1[1]
                 else
                   [t1[1], t2[1]].max
                 end
  responses
end

#mu_sigma(tau_, pi_) ⇒ Object

Raises:

  • (ArgumentError)


77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/TrueskillThroughTime.rb', line 77

def mu_sigma(tau_, pi_)
  raise(ArgumentError, "Sigma must be greater than 0.") if (pi_ + 1e-5) < 0.0

  if pi_ > 0.0
    sigma = Math.sqrt(1.0/pi_)
    mu = tau_ / pi_.to_f
  else
    sigma = INF
    mu = 0.0
  end
  [mu, sigma] # again tuple in original
end

#pdf(x, mu, sigma) ⇒ Object



94
95
96
97
98
# File 'lib/TrueskillThroughTime.rb', line 94

def pdf(x, mu, sigma)
  normaliser = (SQRT2PI * sigma)**-1
  functional = Math.exp(-((x - mu)**2) / (2*sigma**2))
  normaliser * functional
end

#podium(xs) ⇒ Object



158
159
160
# File 'lib/TrueskillThroughTime.rb', line 158

def podium(xs)
  sortperm(xs)
end

#ppf(p, mu, sigma) ⇒ Object



100
101
102
# File 'lib/TrueskillThroughTime.rb', line 100

def ppf(p, mu, sigma)
  mu - sigma * SQRT2 * erfcinv(2 * p)
end

#sortperm(xs, reverse = false) ⇒ Object



162
163
164
165
166
167
168
# File 'lib/TrueskillThroughTime.rb', line 162

def sortperm(xs, reverse=false)
  sorted_indices = xs.each_with_index
                     .sort_by { |v, i| v }
                     .map { |v, i| i }
  sorted_indices.reverse! if reverse
  sorted_indices
end

#tau_pi(mu, sigma) ⇒ Object

Raises:

  • (ArgumentError)


64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/TrueskillThroughTime.rb', line 64

def tau_pi(mu, sigma)
  raise(ArgumentError, "Sigma must be greater than 0.") if (sigma + 1e-5) < 0.0

  if sigma > 0.0
    pi_ = sigma ** -2
    tau_ = pi_ * mu
  else
    pi_ = INF
    tau_ = INF
  end
  [tau_, pi_] # python returns tuple
end

#trunc(mu, sigma, margin, tie) ⇒ Object



119
120
121
122
123
124
# File 'lib/TrueskillThroughTime.rb', line 119

def trunc(mu, sigma, margin, tie)
  v, w = v_w(mu, sigma, margin, tie)
  mu_trunc = mu + sigma * v
  sigma_trunc = sigma * Math.sqrt(1-w)
  [mu_trunc, sigma_trunc]
end

#v_w(mu, sigma, margin, tie) ⇒ Object



104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/TrueskillThroughTime.rb', line 104

def v_w(mu, sigma, margin, tie)
  if tie
    _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 )
  else
    _alpha = (margin-mu)/sigma
    v = pdf(-_alpha, 0, 1) / cdf(-_alpha, 0, 1)
    w = v * (v + (-_alpha))
  end
  [v, w] # again, a tuple
end