Пару недель назад я писал о системе рейтингов Elo и, узнав о ней больше, я узнал, что одна из ее слабых сторон заключается в том, что она не учитывает надежность рейтинга игроков.
Например, игрок, возможно, не играл в течение длительного времени. Когда они в следующий раз играют матч, мы не должны предполагать, что точность этого рейтинга такая же, как и у другого игрока с таким же рейтингом, но который играет регулярно.
Марк Гликман написал систему рейтинга Glicko, чтобы учесть неопределенность, введя «отклонение рейтингов» (RD). Низкий RD указывает, что игрок часто участвует в соревнованиях, а более высокий RD указывает, что они этого не делают
Еще одно различие между Глико и Эло заключается в следующем:
Интересно отметить, что в системе Glicko изменения рейтинга не сбалансированы, как это обычно бывает в системе Elo.
Если рейтинг одного игрока увеличивается на x, рейтинг противника обычно не уменьшается на x, как в системе Эло.
Фактически, в системе Glicko величина, на которую уменьшается рейтинг противника, определяется RD обоих игроков.
Значение RD фактически указывает нам диапазон, в котором, вероятно, существует фактический рейтинг игрока. то есть 95% доверительный интервал.
Например, если игрок имеет рейтинг 1850 и RD 50, то интервал 1750 — 1950 или (Рейтинг — 2 * RD) — (Рейтинг + 2 * RD)
Алгоритм имеет 2 шага:
- Определите рейтинг и RD для каждого игрока в начале периода рейтинга. Если игрок без рейтинга, используйте значение 1500 и RD 350. Если у него есть рейтинг, мы рассчитаем новое RD из старого RD по следующей формуле:
где:
- t — количество рейтинговых периодов с момента последнего соревнования (например, если игрок
соревновался в самый последний рейтинговый период, t = 1) - с является константой, которая определяет увеличение неопределенности с течением времени.
- t — количество рейтинговых периодов с момента последнего соревнования (например, если игрок
- Обновите рейтинг каждого игрока и RD отдельно, используя следующую формулу:
где:
- r — предварительный рейтинг игрока
- RD — отклонение рейтингов игрока до периода
- r 1 , r 2 ,…, r m — рейтинги своих оппонентов перед периодом
- RD 1 , RD 2 ,…, RD m — предопериодные рейтинги отклонений их оппонентов
- s 1 , s 2 ,…, 2 m — очки против оппонентов. 1 — победа, 1/2 — ничья, 0 — поражение.
- r ‘- рейтинг игрока после периода
- RD ‘- отклонение рейтингов игрока после периода
В документе приведен пример для подражания, и включены промежуточные этапы, которые упростили построение алгоритма по одной функции за раз.
Д функция была проще реализовать , так что я создал , что и г функции одновременно:
(ns ranking-algorithms.glicko (:require [clojure.math.numeric-tower :as math])) (def q (/ (java.lang.Math/log 10) 400)) (defn g [rd] (/ 1 (java.lang.Math/sqrt (+ 1 (/ (* 3 (math/expt q 2) (math/expt rd 2)) (math/expt ( . Math PI) 2))))))
Мы можем использовать следующую таблицу, чтобы проверить, что мы получаем правильные результаты, когда мы ее называем:
> (g 30) 0.9954980060779481 > (g 100) 0.953148974234587 > (g 300) 0.7242354637384434
Следующей простой функцией для записи была функция E :
(defn e [rating opponent-rating opponent-rd] (/ 1 (+ 1 (math/expt 10 (/ (* (- (g opponent-rd)) (- rating opponent-rating)) 400)))))
И если мы проверим это, предполагая, что у нас рейтинг 1500 с RD 200:
> (e 1500 1400 30) 0.639467736007921 > (e 1500 1550 100) 0.43184235355955686 > (e 1500 1700 300) 0.30284072524764
Наконец, нам нужно написать вспомогательную функцию d 2 :
(defn d2 [opponents] (/ 1 (* (math/expt q 2) (reduce process-opponent 0 opponents)))) (defn process-opponent [total opponent] (let [{:keys [g e]} opponent] (+ total (* (math/expt g 2) e (- 1 e)))))
В этой функции нам нужно суммировать комбинацию значений g и e, которые мы рассчитали ранее для каждого оппонента, чтобы мы могли использовать уменьшение по совокупности этих значений для каждого оппонента, чтобы сделать это:
> (d2 [{:g (g 30) :e (e 1500 1400 30)} {:g (g 100) :e (e 1500 1550 100)} {:g (g 300) :e (e 1500 1700 300)}]) 53685.74290197874
Я получаю немного другое значение для этой функции, которое я считаю, потому что я не округлил промежуточные значения до 2 десятичных знаков, как в примере.
Теперь мы можем ввести функцию r ‘, которая возвращает наш рейтинг после учета матчей с этими противниками:
(defn update-ranking [ranking-delta opponent] (let [{:keys [ranking opponent-ranking opponent-ranking-rd score]} opponent] (+ ranking-delta (* (g opponent-ranking-rd) (- score (e ranking opponent-ranking opponent-ranking-rd)))))) (defn g-and-e [ranking {o-rd :opponent-ranking-rd o-ranking :opponent-ranking}] {:g (g o-rd) :e (e ranking o-ranking o-rd)}) (defn ranking-after-round [{ ranking :ranking rd :ranking-rd opponents :opponents}] (+ ranking (* (/ q (+ (/ 1 (math/expt rd 2)) (/ 1 (d2 (map (partial g-and-e ranking) opponents))))) (reduce update-ranking 0 (map #(assoc-in % [:ranking] ranking) opponents)))))
Одна вещь, в которой я не был уверен, это использование частичного, что немного напоминает Хаскель. Я еще не уверен, что такое одобренный подход на земле Clojure.
Если мы выполним эту функцию, мы получим ожидаемый результат:
> (ranking-after-round { :ranking 1500 :ranking-rd 200 :opponents[{:opponent-ranking 1400 :opponent-ranking-rd 30 :score 1} {:opponent-ranking 1550 :opponent-ranking-rd 100 :score 0} {:opponent-ranking 1700 :opponent-ranking-rd 300 :score 0}]}) 1464.1064627569112
Единственной отсутствующей функцией сейчас является RD ‘, которая возвращает наше RD после учета этих совпадений:
(defn rd-after-round [{ ranking :ranking rd :ranking-rd opponents :opponents}] (java.lang.Math/sqrt (/ 1 (+ (/ 1 (math/expt rd 2) ) (/ 1 (d2 (map (partial g-and-e ranking) opponents)))))))
Если мы выполним эту функцию, мы получим ожидаемый результат, и все готово!
> (rd-after-round { :ranking 1500 :ranking-rd 200 :opponents[{:opponent-ranking 1400 :opponent-ranking-rd 30 :score 1} {:opponent-ranking 1550 :opponent-ranking-rd 100 :score 0} {:opponent-ranking 1700 :opponent-ranking-rd 300 :score 0}]}) 151.39890244796933
Следующий шаг — запустить этот алгоритм для футбольных данных и посмотреть, отличаются ли его результаты от тех, которые я получил с помощью алгоритма Эло.
Я все еще не совсем уверен, что я должен установить период оценки. Сначала я думал, что рейтинговый период может быть сезоном, но это будет означать, что рейтинг команды имеет смысл только после нескольких сезонов матчей.
Код на GitHub , если вы хотите играть с ним , и если у вас есть какие — либо предложения о том , как сделать код более идиоматическим Я хотел бы услышать их.