mirror of
https://github.com/avitex/elixir-glicko
synced 2024-11-01 01:29:57 +00:00
e22c34f2ff
Add formatter configuration and run `mix format`. Additionally, manually replace leading tabs in documentation examples with four spaces.
312 lines
9.4 KiB
Elixir
312 lines
9.4 KiB
Elixir
defmodule Glicko do
|
|
@moduledoc """
|
|
Provides the implementation of the Glicko rating system.
|
|
|
|
See the [specification](http://www.glicko.net/glicko/glicko2.pdf) for implementation details.
|
|
|
|
## Usage
|
|
|
|
Get a player's new rating after a series of matches in a rating period.
|
|
|
|
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)]
|
|
iex> player = Player.new_v1([rating: 1500, rating_deviation: 200])
|
|
iex> Glicko.new_rating(player, results, [system_constant: 0.5])
|
|
{1464.0506705393013, 151.51652412385727}
|
|
|
|
Get a player's new rating when they haven't played within a rating period.
|
|
|
|
iex> player = Player.new_v1([rating: 1500, rating_deviation: 200])
|
|
iex> Glicko.new_rating(player, [], [system_constant: 0.5])
|
|
{1.5e3, 200.27141669877065}
|
|
|
|
Calculate the probability of a player winning against an opponent.
|
|
|
|
iex> player = Player.new_v1
|
|
iex> opponent = Player.new_v1
|
|
iex> Glicko.win_probability(player, opponent)
|
|
0.5
|
|
|
|
Calculate the probability of a player drawing against an opponent.
|
|
|
|
iex> player = Player.new_v1
|
|
iex> opponent = Player.new_v1
|
|
iex> Glicko.draw_probability(player, opponent)
|
|
1.0
|
|
|
|
"""
|
|
|
|
alias __MODULE__.{
|
|
Player,
|
|
Result
|
|
}
|
|
|
|
@default_system_constant 0.8
|
|
@default_convergence_tolerance 1.0e-7
|
|
|
|
@type new_rating_opts :: [system_constant: float, convergence_tolerance: float]
|
|
|
|
@doc """
|
|
Calculates the probability of a player winning against an opponent.
|
|
|
|
Returns a value between `0.0` and `1.0`.
|
|
"""
|
|
@spec win_probability(player :: Player.t(), opponent :: Player.t()) :: float
|
|
def win_probability(player, opponent) do
|
|
win_probability(
|
|
player |> Player.rating(:v2),
|
|
opponent |> Player.rating(:v2),
|
|
opponent |> Player.rating_deviation(:v2)
|
|
)
|
|
end
|
|
|
|
@doc """
|
|
Calculates the probability of a player winning against an opponent from a player rating, opponent rating and opponent rating deviation.
|
|
|
|
Values provided for the player rating, opponent rating and opponent rating deviation must be *v2* based.
|
|
|
|
Returns a value between `0.0` and `1.0`.
|
|
"""
|
|
@spec win_probability(
|
|
player_rating :: Player.rating(),
|
|
opponent_rating :: Player.rating(),
|
|
opponent_rating_deviation :: Player.rating_deviation()
|
|
) :: float
|
|
def win_probability(player_rating, opponent_rating, opponent_rating_deviation) do
|
|
calc_e(player_rating, opponent_rating, calc_g(opponent_rating_deviation))
|
|
end
|
|
|
|
@doc """
|
|
Calculates the probability of a player drawing against an opponent.
|
|
|
|
Returns a value between `0.0` and `1.0`.
|
|
"""
|
|
@spec draw_probability(player :: Player.t(), opponent :: Player.t()) :: float
|
|
def draw_probability(player, opponent) do
|
|
draw_probability(
|
|
player |> Player.rating(:v2),
|
|
opponent |> Player.rating(:v2),
|
|
opponent |> Player.rating_deviation(:v2)
|
|
)
|
|
end
|
|
|
|
@doc """
|
|
Calculates the probability of a player drawing against an opponent from a player rating, opponent rating and opponent rating deviation.
|
|
|
|
Values provided for the player rating, opponent rating and opponent rating deviation must be *v2* based.
|
|
|
|
Returns a value between `0.0` and `1.0`.
|
|
"""
|
|
@spec draw_probability(
|
|
player_rating :: Player.rating(),
|
|
opponent_rating :: Player.rating(),
|
|
opponent_rating_deviation :: Player.rating_deviation()
|
|
) :: float
|
|
def draw_probability(player_rating, opponent_rating, opponent_rating_deviation) do
|
|
1 -
|
|
abs(win_probability(player_rating, opponent_rating, opponent_rating_deviation) - 0.5) / 0.5
|
|
end
|
|
|
|
@doc """
|
|
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.
|
|
"""
|
|
@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()
|
|
end
|
|
|
|
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)
|
|
|
|
{player_r, player_post_rd, player_v}
|
|
end
|
|
|
|
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)
|
|
|
|
# Initialization (skips steps 1, 2 and 3)
|
|
player_pre_rd_sq = :math.pow(player_pre_rd, 2)
|
|
{variance_est, results_effect} = result_calculations(results, player_pre_r)
|
|
# Step 4
|
|
delta = calc_delta(results_effect, variance_est)
|
|
# Step 5.1
|
|
alpha = calc_alpha(player_pre_v)
|
|
# Step 5.2
|
|
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
|
|
)
|
|
|
|
# Step 5.3
|
|
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)
|
|
# Step 5.4
|
|
a =
|
|
iterative_algorithm_body(
|
|
alpha,
|
|
delta,
|
|
player_pre_rd_sq,
|
|
variance_est,
|
|
sys_const,
|
|
conv_tol,
|
|
initial_a,
|
|
initial_b,
|
|
initial_fa,
|
|
initial_fb
|
|
)
|
|
|
|
# Step 5.5
|
|
player_post_v = calc_new_player_volatility(a)
|
|
# Step 6
|
|
player_post_base_rd = calc_player_post_base_rd(player_pre_rd_sq, player_post_v)
|
|
# Step 7
|
|
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)
|
|
|
|
{player_post_r, player_post_rd, player_post_v}
|
|
end
|
|
|
|
defp result_calculations(results, player_pre_r) do
|
|
{variance_estimate_acc, result_effect_acc} =
|
|
Enum.reduce(results, {0.0, 0.0}, fn result, {variance_estimate_acc, result_effect_acc} ->
|
|
opponent_rd_g =
|
|
result
|
|
|> Result.opponent_rating_deviation()
|
|
|> calc_g
|
|
|
|
win_probability = calc_e(player_pre_r, Result.opponent_rating(result), opponent_rd_g)
|
|
|
|
{
|
|
variance_estimate_acc +
|
|
:math.pow(opponent_rd_g, 2) * win_probability * (1 - win_probability),
|
|
result_effect_acc + opponent_rd_g * (Result.score(result) - win_probability)
|
|
}
|
|
end)
|
|
|
|
{:math.pow(variance_estimate_acc, -1), result_effect_acc}
|
|
end
|
|
|
|
defp calc_delta(results_effect, variance_est) do
|
|
results_effect * variance_est
|
|
end
|
|
|
|
defp calc_f(alpha, delta, player_pre_rd_sq, variance_est, sys_const, x) do
|
|
:math.exp(x) *
|
|
(: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)
|
|
end
|
|
|
|
defp calc_alpha(player_pre_v) do
|
|
:math.log(:math.pow(player_pre_v, 2))
|
|
end
|
|
|
|
defp calc_new_player_volatility(a) do
|
|
:math.exp(a / 2)
|
|
end
|
|
|
|
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
|
|
end
|
|
|
|
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)
|
|
end
|
|
|
|
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)
|
|
end
|
|
|
|
defp iterative_algorithm_initial(alpha, delta, player_pre_rd_sq, variance_est, sys_const, k) do
|
|
initial_a = alpha
|
|
|
|
initial_b =
|
|
if :math.pow(delta, 2) > player_pre_rd_sq + variance_est do
|
|
:math.log(:math.pow(delta, 2) - player_pre_rd_sq - variance_est)
|
|
else
|
|
alpha - k * sys_const
|
|
end
|
|
|
|
{initial_a, initial_b}
|
|
end
|
|
|
|
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
|
|
c = a + (a - b) * fa / (fb - fa)
|
|
fc = calc_f(alpha, delta, player_pre_rd_sq, variance_est, sys_const, c)
|
|
|
|
{a, fa} =
|
|
if fc * fb < 0 do
|
|
{b, fb}
|
|
else
|
|
{a, fa / 2}
|
|
end
|
|
|
|
iterative_algorithm_body(
|
|
alpha,
|
|
delta,
|
|
player_pre_rd_sq,
|
|
variance_est,
|
|
sys_const,
|
|
conv_tol,
|
|
a,
|
|
c,
|
|
fa,
|
|
fc
|
|
)
|
|
else
|
|
a
|
|
end
|
|
end
|
|
|
|
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)
|
|
else
|
|
k
|
|
end
|
|
end
|
|
|
|
# g function
|
|
defp calc_g(rd) do
|
|
1 / :math.sqrt(1 + 3 * :math.pow(rd, 2) / :math.pow(:math.pi(), 2))
|
|
end
|
|
|
|
# E function
|
|
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)))
|
|
end
|
|
end
|