mirror of
https://github.com/avitex/elixir-glicko
synced 2024-11-21 18:59:57 +00:00
Initial rating implementation
This commit is contained in:
parent
6bafa2be0b
commit
c96d357812
167
lib/glicko.ex
Normal file
167
lib/glicko.ex
Normal file
@ -0,0 +1,167 @@
|
||||
defmodule Glicko do
|
||||
|
||||
alias __MODULE__.{
|
||||
Player,
|
||||
GameResult,
|
||||
}
|
||||
|
||||
@epsilon 0.0000001
|
||||
@default_system_constant 0.8
|
||||
|
||||
@doc """
|
||||
Generate a new Rating from an existing rating and a series of results.
|
||||
"""
|
||||
@spec new_rating(player :: Player.t, results :: list(GameResult.t), sys_constant :: float) :: Player.t
|
||||
def new_rating(player, results, sys_constant \\ @default_system_constant)
|
||||
def new_rating(player = %Player{version: :v1}, results, sys_constant) do
|
||||
player
|
||||
|> Player.to_v2
|
||||
|> do_new_rating(results, sys_constant)
|
||||
|> Player.to_v1
|
||||
end
|
||||
|
||||
def new_rating(player = %Player{version: :v2}, results, sys_constant) do
|
||||
do_new_rating(player, results, sys_constant)
|
||||
end
|
||||
|
||||
defp do_new_rating(player = %Player{version: :v2}, results, sys_constant) do
|
||||
results = Enum.map(results, fn result ->
|
||||
opponent = Player.to_v2(result.opponent)
|
||||
%{
|
||||
score: result.score,
|
||||
opponent: opponent,
|
||||
opponent_rating_deviation_g: calc_g(opponent.rating_deviation),
|
||||
}
|
||||
end)
|
||||
|
||||
ctx =
|
||||
Map.new
|
||||
|> Map.put(:system_constant, sys_constant)
|
||||
|> Map.put(:results, results)
|
||||
|> Map.put(:player, player)
|
||||
|> Map.put(:player_rating_deviation_squared, :math.pow(player.rating_deviation, 2))
|
||||
|
||||
# Step 3
|
||||
ctx = Map.put(ctx, :variance_estimate, calc_variance_estimate(ctx))
|
||||
# Step 4
|
||||
ctx = Map.put(ctx, :delta, calc_delta(ctx))
|
||||
# Step 5.1
|
||||
ctx = Map.put(ctx, :alpha, calc_alpha(ctx))
|
||||
# Step 5.2
|
||||
{initial_a, initial_b} = iterative_algorithm_initial(ctx)
|
||||
ctx = Map.put(ctx, :initial_a, initial_a)
|
||||
ctx = Map.put(ctx, :initial_b, initial_b)
|
||||
# Step 5.3
|
||||
ctx = Map.put(ctx, :initial_fa, calc_f(ctx, ctx.initial_a))
|
||||
ctx = Map.put(ctx, :initial_fb, calc_f(ctx, ctx.initial_b))
|
||||
# Step 5.4
|
||||
ctx = Map.put(ctx, :a, iterative_algorithm_body(
|
||||
ctx, ctx.initial_a, ctx.initial_b, ctx.initial_fa, ctx.initial_fb
|
||||
))
|
||||
# Step 5.5
|
||||
ctx = Map.put(ctx, :new_player_volatility, calc_new_player_volatility(ctx))
|
||||
# Step 6
|
||||
ctx = Map.put(ctx, :prerating_period, calc_prerating_period(ctx))
|
||||
# Step 7
|
||||
ctx = Map.put(ctx, :new_player_rating_deviation, calc_new_player_rating_deviation(ctx))
|
||||
ctx = Map.put(ctx, :new_player_rating, calc_new_player_rating(ctx))
|
||||
|
||||
Player.new_v2([
|
||||
rating: ctx.new_player_rating,
|
||||
rating_deviation: ctx.new_player_rating_deviation,
|
||||
volatility: ctx.new_player_volatility,
|
||||
])
|
||||
end
|
||||
|
||||
# Calculation of the estimated variance of the player's rating based on game outcomes
|
||||
defp calc_variance_estimate(%{player: player, results: results}) do
|
||||
Enum.reduce(results, 0.0, fn result, acc ->
|
||||
tmp_e = calc_e(player, result)
|
||||
acc + :math.pow(result.opponent_rating_deviation_g, 2) * tmp_e * (1 - tmp_e)
|
||||
end)
|
||||
|> :math.pow(-1)
|
||||
end
|
||||
|
||||
defp calc_delta(ctx) do
|
||||
calc_results_effect(ctx) * ctx.variance_estimate
|
||||
end
|
||||
|
||||
defp calc_f(ctx, x) do
|
||||
:math.exp(x) *
|
||||
(:math.pow(ctx.delta, 2) - :math.exp(x) - ctx.player_rating_deviation_squared - ctx.variance_estimate) /
|
||||
(2 * :math.pow(ctx.player_rating_deviation_squared + ctx.variance_estimate + :math.exp(x), 2)) -
|
||||
(x - ctx.alpha) / :math.pow(ctx.system_constant, 2)
|
||||
end
|
||||
|
||||
defp calc_alpha(%{player: player}) do
|
||||
:math.log(:math.pow(player.volatility, 2))
|
||||
end
|
||||
|
||||
defp calc_new_player_volatility(%{a: a}) do
|
||||
:math.exp(a/2)
|
||||
end
|
||||
|
||||
defp calc_results_effect(%{player: player, results: results}) do
|
||||
Enum.reduce(results, 0.0, fn result, acc ->
|
||||
acc + result.opponent_rating_deviation_g * (result.score - calc_e(player, result))
|
||||
end)
|
||||
end
|
||||
|
||||
defp calc_new_player_rating(ctx) do
|
||||
ctx.player.rating + :math.pow(ctx.new_player_rating_deviation, 2) * calc_results_effect(ctx)
|
||||
end
|
||||
|
||||
defp calc_new_player_rating_deviation(ctx) do
|
||||
1/:math.sqrt(1/:math.pow(ctx.prerating_period, 2) + 1/ctx.variance_estimate)
|
||||
end
|
||||
|
||||
defp calc_prerating_period(ctx) do
|
||||
:math.sqrt((:math.pow(ctx.new_player_volatility, 2) + ctx.player_rating_deviation_squared))
|
||||
end
|
||||
|
||||
defp iterative_algorithm_initial(ctx) do
|
||||
initial_a = ctx.alpha
|
||||
initial_b =
|
||||
if :math.pow(ctx.delta, 2) > ctx.player_rating_deviation_squared + ctx.variance_estimate do
|
||||
:math.log(:math.pow(ctx.delta, 2) - ctx.player_rating_deviation_squared - ctx.variance_estimate)
|
||||
else
|
||||
ctx.alpha - calc_k(ctx, 1) * ctx.system_constant
|
||||
end
|
||||
|
||||
{initial_a, initial_b}
|
||||
end
|
||||
|
||||
defp iterative_algorithm_body(ctx, a, b, fa, fb) do
|
||||
if abs(b - a) > @epsilon do
|
||||
c = a + (a - b) * fa / (fb - fa)
|
||||
fc = calc_f(ctx, c)
|
||||
{a, fa} =
|
||||
if fc * fb < 0 do
|
||||
{b, fb}
|
||||
else
|
||||
{a, fa / 2}
|
||||
end
|
||||
iterative_algorithm_body(ctx, a, c, fa, fc)
|
||||
else
|
||||
a
|
||||
end
|
||||
end
|
||||
|
||||
defp calc_k(ctx, k) do
|
||||
if calc_f(ctx, (ctx.alpha - k * ctx.system_constant)) < 0 do
|
||||
calc_k(ctx, k + 1)
|
||||
else
|
||||
k
|
||||
end
|
||||
end
|
||||
|
||||
# g function
|
||||
defp calc_g(rating_deviation) do
|
||||
1 / :math.sqrt(1 + 3 * :math.pow(rating_deviation, 2) / :math.pow(:math.pi, 2))
|
||||
end
|
||||
|
||||
# E function
|
||||
defp calc_e(player, result) do
|
||||
1 / (1 + :math.exp(-1 * result.opponent_rating_deviation_g * (player.rating - result.opponent.rating)))
|
||||
end
|
||||
end
|
8
mix.lock
8
mix.lock
@ -1,4 +1,4 @@
|
||||
%{"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [], [], "hexpm"},
|
||||
"credo": {:hex, :credo, "0.8.8", "990e7844a8d06ebacd88744a55853a83b74270b8a8461c55a4d0334b8e1736c9", [], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"earmark": {:hex, :earmark, "1.2.3", "206eb2e2ac1a794aa5256f3982de7a76bf4579ff91cb28d0e17ea2c9491e46a4", [], [], "hexpm"},
|
||||
"ex_doc": {:hex, :ex_doc, "0.18.1", "37c69d2ef62f24928c1f4fdc7c724ea04aecfdf500c4329185f8e3649c915baf", [], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}}
|
||||
%{"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], []},
|
||||
"credo": {:hex, :credo, "0.8.8", "990e7844a8d06ebacd88744a55853a83b74270b8a8461c55a4d0334b8e1736c9", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, optional: false]}]},
|
||||
"earmark": {:hex, :earmark, "1.2.3", "206eb2e2ac1a794aa5256f3982de7a76bf4579ff91cb28d0e17ea2c9491e46a4", [:mix], []},
|
||||
"ex_doc": {:hex, :ex_doc, "0.18.1", "37c69d2ef62f24928c1f4fdc7c724ea04aecfdf500c4329185f8e3649c915baf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, optional: false]}]}}
|
||||
|
29
test/glicko_test.exs
Normal file
29
test/glicko_test.exs
Normal file
@ -0,0 +1,29 @@
|
||||
defmodule GlickoTest do
|
||||
use ExUnit.Case
|
||||
|
||||
alias Glicko.{
|
||||
Player,
|
||||
GameResult,
|
||||
}
|
||||
|
||||
doctest Glicko
|
||||
|
||||
@player Player.new_v1([rating: 1500, rating_deviation: 200])
|
||||
|
||||
@results [
|
||||
GameResult.new(Player.new_v1([rating: 1400, rating_deviation: 30]), :win),
|
||||
GameResult.new(Player.new_v1([rating: 1550, rating_deviation: 100]), :loss),
|
||||
GameResult.new(Player.new_v1([rating: 1700, rating_deviation: 300]), :loss),
|
||||
]
|
||||
|
||||
@valid_player_rating_after_results 1464.06
|
||||
@valid_player_rating_deviation_after_results 151.52
|
||||
|
||||
test "new rating" do
|
||||
%Player{rating: new_rating, rating_deviation: new_rating_deviation} =
|
||||
Glicko.new_rating(@player, @results, 0.5)
|
||||
|
||||
assert_in_delta new_rating, @valid_player_rating_after_results, 0.1
|
||||
assert_in_delta new_rating_deviation, @valid_player_rating_deviation_after_results, 0.1
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue
Block a user