1
0
mirror of https://github.com/avitex/elixir-glicko synced 2024-09-21 15:19:57 +00:00
glicko-elixir/lib/glicko.ex

199 lines
6.5 KiB
Elixir
Raw Normal View History

2017-11-16 01:39:07 +00:00
defmodule Glicko do
2017-11-16 04:18:57 +00:00
@moduledoc """
Provides the implementation of the Glicko rating system.
See the [specification](http://www.glicko.net/glicko/glicko2.pdf) for implementation details.
## Usage
2017-11-16 11:52:55 +00:00
Get a player's new rating after a series of matches in a rating period.
2017-11-16 04:18:57 +00:00
2017-11-16 11:44:01 +00:00
iex> results = [Result.new(Player.new_v1([rating: 1400, rating_deviation: 30]), :win),
...> Result.new(Player.new_v1([rating: 1550, rating_deviation: 100]), :loss),
...> Result.new(Player.new_v1([rating: 1700, rating_deviation: 300]), :loss)]
2017-11-16 04:18:57 +00:00
iex> player = Player.new_v1([rating: 1500, rating_deviation: 200])
iex> Glicko.new_rating(player, results, [system_constant: 0.5])
{1464.0506705393013, 151.51652412385727}
2017-11-16 04:18:57 +00:00
2017-11-16 11:52:55 +00:00
Get a player's new rating when they haven't played within a rating period.
2017-11-16 04:18:57 +00:00
iex> player = Player.new_v1([rating: 1500, rating_deviation: 200])
iex> Glicko.new_rating(player, [], [system_constant: 0.5])
{1.5e3, 200.27141669877065}
2017-11-16 04:18:57 +00:00
"""
2017-11-16 01:39:07 +00:00
alias __MODULE__.{
Player,
2017-11-16 11:44:01 +00:00
Result,
2017-11-16 01:39:07 +00:00
}
@default_system_constant 0.8
2017-11-16 02:37:56 +00:00
@default_convergence_tolerance 1.0e-7
@type new_rating_opts :: [system_constant: float, convergence_tolerance: float]
2017-11-16 01:39:07 +00:00
@doc """
2017-11-16 04:18:57 +00:00
Generate a new rating from an existing rating and a series (or lack) of results.
Returns the updated player with the same version given to the function.
2017-11-16 01:39:07 +00:00
"""
@spec new_rating(player :: Player.t, results :: list(Result.t), opts :: new_rating_opts) :: Player.t
def new_rating(player, results, opts \\ [])
def new_rating(player, results, opts) when tuple_size(player) == 3 do
do_new_rating(player, results, opts)
end
def new_rating(player, results, opts) when tuple_size(player) == 2 do
player
|> Player.to_v2
|> do_new_rating(results, opts)
|> Player.to_v1
2017-11-16 01:39:07 +00:00
end
2017-11-29 01:10:37 +00:00
defp do_new_rating({player_r, player_pre_rd, player_v}, [], _) do
player_post_rd = calc_player_post_base_rd(:math.pow(player_pre_rd, 2), player_v)
2017-11-29 01:10:37 +00:00
{player_r, player_post_rd, player_v}
end
2017-11-29 01:10:37 +00:00
defp do_new_rating({player_pre_r, player_pre_rd, player_pre_v}, results, opts) do
sys_const = Keyword.get(opts, :system_constant, @default_system_constant)
conv_tol = Keyword.get(opts, :convergence_tolerance, @default_convergence_tolerance)
2017-11-16 11:44:01 +00:00
# Init
2017-11-29 01:10:37 +00:00
player_pre_rd_sq = :math.pow(player_pre_rd, 2)
results = Enum.map(results, &build_internal_result(player_pre_r, &1))
results_effect = calc_results_effect(results)
2017-11-16 01:39:07 +00:00
# Step 3
2017-11-29 01:10:37 +00:00
variance_est = calc_variance_estimate(results)
2017-11-16 01:39:07 +00:00
# Step 4
2017-11-29 01:10:37 +00:00
delta = calc_delta(results_effect, variance_est)
2017-11-16 01:39:07 +00:00
# Step 5.1
2017-11-29 01:10:37 +00:00
alpha = calc_alpha(player_pre_v)
2017-11-16 01:39:07 +00:00
# Step 5.2
2017-11-29 01:10:37 +00:00
k = calc_k(alpha, delta, player_pre_rd_sq, variance_est, sys_const, 1)
{initial_a, initial_b} = iterative_algorithm_initial(
alpha, delta, player_pre_rd_sq, variance_est, sys_const, k
)
2017-11-16 01:39:07 +00:00
# Step 5.3
2017-11-29 01:10:37 +00:00
initial_fa = calc_f(alpha, delta, player_pre_rd_sq, variance_est, sys_const, initial_a)
initial_fb = calc_f(alpha, delta, player_pre_rd_sq, variance_est, sys_const, initial_b)
2017-11-16 01:39:07 +00:00
# Step 5.4
2017-11-29 01:10:37 +00:00
a = iterative_algorithm_body(
alpha, delta, player_pre_rd_sq, variance_est, sys_const, conv_tol,
initial_a, initial_b, initial_fa, initial_fb
)
2017-11-16 01:39:07 +00:00
# Step 5.5
2017-11-29 01:10:37 +00:00
player_post_v = calc_new_player_volatility(a)
2017-11-16 01:39:07 +00:00
# Step 6
2017-11-29 01:10:37 +00:00
player_post_base_rd = calc_player_post_base_rd(player_pre_rd_sq, player_post_v)
2017-11-16 01:39:07 +00:00
# Step 7
2017-11-29 01:10:37 +00:00
player_post_rd = calc_new_player_rating_deviation(player_post_base_rd, variance_est)
player_post_r = calc_new_player_rating(results_effect, player_pre_r, player_post_rd)
2017-11-16 01:39:07 +00:00
2017-11-29 01:10:37 +00:00
{player_post_r, player_post_rd, player_post_v}
2017-11-16 11:44:01 +00:00
end
2017-11-29 01:10:37 +00:00
defp build_internal_result(player_pre_r, result) do
2017-11-16 11:44:01 +00:00
result =
Map.new
|> Map.put(:score, Result.score(result))
2017-11-29 01:10:37 +00:00
|> Map.put(:opponent_r, Result.opponent_rating(result))
|> Map.put(:opponent_rd, Result.opponent_rating_deviation(result))
|> Map.put(:opponent_rd_g, calc_g(Result.opponent_rating_deviation(result)))
2017-11-16 11:44:01 +00:00
2017-11-29 01:10:37 +00:00
Map.put(result, :e, calc_e(player_pre_r, result.opponent_r, result.opponent_rd_g))
2017-11-16 11:44:01 +00:00
end
2017-11-16 01:39:07 +00:00
# Calculation of the estimated variance of the player's rating based on game outcomes
2017-11-29 01:10:37 +00:00
defp calc_variance_estimate(results) do
results
2017-11-16 01:45:12 +00:00
|> Enum.reduce(0.0, fn result, acc ->
2017-11-29 01:10:37 +00:00
acc + :math.pow(result.opponent_rd_g, 2) * result.e * (1 - result.e)
2017-11-16 01:39:07 +00:00
end)
|> :math.pow(-1)
end
2017-11-29 01:10:37 +00:00
defp calc_delta(results_effect, variance_est) do
results_effect * variance_est
2017-11-16 01:39:07 +00:00
end
2017-11-29 01:10:37 +00:00
defp calc_f(alpha, delta, player_pre_rd_sq, variance_est, sys_const, x) do
2017-11-16 01:39:07 +00:00
:math.exp(x) *
2017-11-29 01:10:37 +00:00
(:math.pow(delta, 2) - :math.exp(x) - player_pre_rd_sq - variance_est) /
(2 * :math.pow(player_pre_rd_sq + variance_est + :math.exp(x), 2)) -
(x - alpha) / :math.pow(sys_const, 2)
2017-11-16 01:39:07 +00:00
end
2017-11-29 01:10:37 +00:00
defp calc_alpha(player_pre_v) do
:math.log(:math.pow(player_pre_v, 2))
2017-11-16 01:39:07 +00:00
end
2017-11-29 01:10:37 +00:00
defp calc_new_player_volatility(a) do
2017-11-16 01:45:12 +00:00
:math.exp(a / 2)
2017-11-16 01:39:07 +00:00
end
2017-11-29 01:10:37 +00:00
defp calc_results_effect(results) do
Enum.reduce(results, 0.0, fn result, acc ->
acc + result.opponent_rd_g * (result.score - result.e)
2017-11-16 01:39:07 +00:00
end)
end
2017-11-29 01:10:37 +00:00
defp calc_new_player_rating(results_effect, player_pre_r, player_post_rd) do
player_pre_r + :math.pow(player_post_rd, 2) * results_effect
2017-11-16 01:39:07 +00:00
end
2017-11-29 01:10:37 +00:00
defp calc_new_player_rating_deviation(player_post_base_rd, variance_est) do
1 / :math.sqrt(1 / :math.pow(player_post_base_rd, 2) + 1 / variance_est)
2017-11-16 01:39:07 +00:00
end
2017-11-29 01:10:37 +00:00
defp calc_player_post_base_rd(player_pre_rd_sq, player_pre_v) do
:math.sqrt((:math.pow(player_pre_v, 2) + player_pre_rd_sq))
2017-11-16 01:39:07 +00:00
end
2017-11-29 01:10:37 +00:00
defp iterative_algorithm_initial(alpha, delta, player_pre_rd_sq, variance_est, sys_const, k) do
initial_a = alpha
2017-11-16 01:39:07 +00:00
initial_b =
2017-11-29 01:10:37 +00:00
if :math.pow(delta, 2) > player_pre_rd_sq + variance_est do
:math.log(:math.pow(delta, 2) - player_pre_rd_sq - variance_est)
2017-11-16 01:39:07 +00:00
else
2017-11-29 01:10:37 +00:00
alpha - k * sys_const
2017-11-16 01:39:07 +00:00
end
{initial_a, initial_b}
end
2017-11-29 01:10:37 +00:00
defp iterative_algorithm_body(alpha, delta, player_pre_rd_sq, variance_est, sys_const, conv_tol, a, b, fa, fb) do
if abs(b - a) > conv_tol do
2017-11-16 01:39:07 +00:00
c = a + (a - b) * fa / (fb - fa)
2017-11-29 01:10:37 +00:00
fc = calc_f(alpha, delta, player_pre_rd_sq, variance_est, sys_const, c)
2017-11-16 01:39:07 +00:00
{a, fa} =
if fc * fb < 0 do
2017-11-16 01:45:12 +00:00
{b, fb}
2017-11-16 01:39:07 +00:00
else
2017-11-16 01:45:12 +00:00
{a, fa / 2}
2017-11-16 01:39:07 +00:00
end
2017-11-29 01:10:37 +00:00
iterative_algorithm_body(alpha, delta, player_pre_rd_sq, variance_est, sys_const, conv_tol, a, c, fa, fc)
2017-11-16 01:39:07 +00:00
else
a
end
end
2017-11-29 01:10:37 +00:00
defp calc_k(alpha, delta, player_pre_rd_sq, variance_est, sys_const, k) do
if calc_f(alpha, delta, player_pre_rd_sq, variance_est, sys_const, alpha - k * sys_const) < 0 do
calc_k(alpha, delta, player_pre_rd_sq, variance_est, sys_const, k + 1)
2017-11-16 01:39:07 +00:00
else
k
end
end
# g function
2017-11-29 01:10:37 +00:00
defp calc_g(rd) do
1 / :math.sqrt(1 + 3 * :math.pow(rd, 2) / :math.pow(:math.pi, 2))
2017-11-16 01:39:07 +00:00
end
# E function
2017-11-29 01:10:37 +00:00
defp calc_e(player_pre_r, opponent_r, opponent_rd_g) do
1 / (1 + :math.exp(-1 * opponent_rd_g * (player_pre_r - opponent_r)))
2017-11-16 01:39:07 +00:00
end
end