module Ranking.Glicko.Core
( compute
, computeP
, newToOld
, oldToNew
, glicko2Multiplier) where
import Prelude hiding ((^))
import qualified Prelude as P
import Data.Maybe
import Control.Lens
import Control.Parallel.Strategies
import Data.Map (Map)
import qualified Data.Map.Strict as Map
import Data.Set (Set)
import qualified Data.Set as Set
import Ranking.Glicko.Types
(^) :: Double -> Integer -> Double
(^) = (P.^)
pMap :: NFData b => Integer -> (a -> b) -> [a] -> [b]
pMap chunkSize f = withStrategy (parListChunk 100 rdeepseq) . map f
computeP :: Integer -> [Player] -> [Match] -> GlickoSettings -> [Player]
computeP chunkSize = compute' (pMap chunkSize)
compute :: [Player] -> [Match] -> GlickoSettings -> [Player]
compute = compute' map
compute' :: (((PlayerId, Player) -> Player) -> [(PlayerId, Player)] -> [Player])
-> [Player]
-> [Match]
-> GlickoSettings
-> [Player]
compute' map' ps ms settings = map' (newToOld . updater . snd) . Map.toList $ pmap'
where pmap = Map.fromList $ map (\p -> (_pid p, p)) ps
pmap' = preprocess pmap ms settings
matches = preprocessMatches pmap' ms
updater p = updatePlayer p matches settings
updatePlayer :: Player -> [RatedMatch] -> GlickoSettings -> Player
updatePlayer p ms GlickoSettings{ tau = tau, scoreFunction = scoreFun }
| null matches = (dev .~ sqrt (pφ^2 + pσ^2))
. (inactivity +~ 1)
. (age +~ 1) $ p
| otherwise = (dev .~ φ')
. (rating .~ µ')
. (vol .~ σ')
. (inactivity .~ 0)
. (age +~ 1) $ p
where
pµ = _rating p
pφ = _dev p
pσ = _vol p
µ (_, opp, _, _) = _rating opp
φ (_, opp, _, _) = _dev opp
s :: RatedMatch -> Double
s (_,_,sa,sb) = compareScores scoreFun sa sb
e m = _E pµ (µ m) (φ m)
v = 1 / summer (\m -> _g (φ m)^2 * e m * (1 e m))
delta = v * step4sum
σ' = calcSigma delta pφ pσ v tau
φstar = sqrt (pφ^2 + σ'^2)
φ' = 1 / sqrt (1 / φstar^2 + 1 / v)
µ' = pµ + φ'^2 * step4sum
step4sum = summer (\m -> _g (φ m) * (s m e m))
summer :: (RatedMatch -> Double) -> Double
summer f = sum . map f $ matches
matches :: [RatedMatch]
matches = map swap . filter (\(pla, plb, _, _) -> pla == p || plb == p) $ ms
swap :: RatedMatch -> RatedMatch
swap m@(pla, plb, sca, scb)
| pla == p = m
| otherwise = (plb, pla, scb, sca)
type RatedMatch = (Player, Player, Score, Score)
_g :: Double -> Double
_g φ = 1 / sqrt (1 + 3*φ^2/(pi^2))
_E :: Double -> Double -> Double -> Double
_E µ µj φj = 1 / (1 + exp ( _g φj * (µ µj)))
calcSigma :: Double -> Double -> Double -> Double -> Double -> Double
calcSigma delta φ σ v tau = step a b (f a) (f b)
where step a' b' fa fb
| abs (b' a') <= ε = exp (a'/2)
| fc*fb < 0 = step b' c fb fc
| otherwise = step a' c (fa/2) fc
where c = a' + (a' b') * fa/(fb fa)
fc = f c
a = log $ σ ^ 2
b = if delta^2 > φ^2 + v then log (delta^2 φ^2 v) else fixB 1
fixB k = if f (a k*tau) < 0 then fixB (k+1) else a k*tau
f x = (exp x * (delta^2 φ^2 v exp x)) / (2 * (φ^2 + v + exp x)^2) (x a) / tau^2
ε :: Double
ε = 0.000001
preprocess :: Map PlayerId Player -> [Match] -> GlickoSettings -> Map PlayerId Player
preprocess ps ms settings =
Map.map oldToNew
. Map.union ps
. Map.fromList
. map (\i -> (i, defaultPlayer {_pid=i}))
. Set.toList $ diff
where playersInMatches = Set.fromList $ (\m -> [_pla m, _plb m]) =<< ms
players = Map.keysSet ps
diff = Set.difference playersInMatches players
defaultPlayer = Player { _pid = 1
, _rating = initialRating settings
, _dev = initialDeviation settings
, _vol = initialVolatility settings
, _inactivity = 0
, _age = 0}
preprocessMatches :: Map PlayerId Player -> [Match] -> [RatedMatch]
preprocessMatches ps = mapMaybe (
\m -> (,,,)
<$> Map.lookup (_pla m) ps
<*> Map.lookup (_plb m) ps
<*> pure (_sca m)
<*> pure (_scb m)
)
oldToNew :: Player -> Player
oldToNew p@Player{ _rating = r, _dev = d} = p { _rating = (r 1500) / glicko2Multiplier
, _dev = d / glicko2Multiplier }
newToOld :: Player -> Player
newToOld p@Player{ _rating = r, _dev = d} = p { _rating = r*glicko2Multiplier + 1500
, _dev = d*glicko2Multiplier}
glicko2Multiplier :: Double
glicko2Multiplier = 173.7178
playersToMap :: [Player] -> Map PlayerId Player
playersToMap = Map.fromList . map (\p -> (_pid p, p))