Статьи

Система рейтинга Glicko: простой пример использования Clojure

Пару недель назад я писал о системе  рейтингов Elo  и, узнав о ней больше, я узнал, что одна из ее слабых сторон заключается в том, что она не учитывает надежность рейтинга игроков.

Например, игрок, возможно, не играл в течение длительного времени. Когда они в следующий раз играют матч, мы не должны предполагать, что точность этого рейтинга такая же, как и у другого игрока с таким же рейтингом, но который играет регулярно.

Марк Гликман написал  систему рейтинга Glicko,  чтобы учесть неопределенность, введя «отклонение рейтингов» (RD). Низкий RD указывает, что игрок часто участвует в соревнованиях, а более высокий RD указывает, что они этого не делают

Еще одно различие между Глико и Эло заключается в следующем:

Интересно отметить, что в системе Glicko изменения рейтинга не сбалансированы, как это обычно бывает в системе Elo.

Если рейтинг одного игрока увеличивается на x, рейтинг противника обычно не уменьшается на x, как в системе Эло.

Фактически, в системе Glicko величина, на которую уменьшается рейтинг противника, определяется RD обоих игроков.

Значение RD фактически указывает нам диапазон, в котором, вероятно, существует фактический рейтинг игрока. то есть 95% доверительный интервал.

Например, если игрок имеет рейтинг 1850 и RD 50, то интервал 1750 — 1950 или  (Рейтинг — 2 * RD)  — (Рейтинг + 2 * RD)

Алгоритм имеет 2 шага:

  1. Определите рейтинг и RD для каждого игрока в начале периода рейтинга. Если игрок без рейтинга, используйте значение 1500 и RD 350. Если у него есть рейтинг, мы рассчитаем новое RD из старого RD по следующей формуле:

    Глико

    где:

    • t — количество рейтинговых периодов с момента последнего соревнования (например, если игрок
      соревновался в самый последний рейтинговый период, t = 1)
    • с является константой, которая определяет увеличение неопределенности с течением времени.
  2. Обновите рейтинг каждого игрока и 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  , если вы хотите играть с ним , и если у вас есть какие — либо предложения о том , как сделать код более идиоматическим Я хотел бы услышать их.