riix.utils.math_utils

math utility functions for rating systems

  1"""math utility functions for rating systems"""
  2import math
  3import statistics
  4import numpy as np
  5from scipy.special import expit
  6from scipy.stats import norm
  7
  8
  9def sigmoid(x):
 10    """a little faster than implementing it in numpy for d < 100000"""
 11    return expit(x)
 12
 13
 14def sigmoid_scalar(x):
 15    """no need to use numpy on scalars"""
 16    return 1.0 / (1.0 + math.exp(-x))
 17
 18
 19def base_10_sigmoid(x):
 20    """some methods prefer base 10 unfortunately"""
 21    return 1.0 / (1.0 + (10.0**-x))
 22
 23
 24INV_SQRT_2 = 1.0 / math.sqrt(2.0)
 25
 26
 27def norm_cdf(x):
 28    """cdf of standard normal"""
 29    return 0.5 * (1.0 + math.erf(x * INV_SQRT_2))
 30
 31
 32STANDARD_NORMAL = statistics.NormalDist()
 33
 34
 35def norm_pdf(x):
 36    """pdf of standard normal"""
 37    return STANDARD_NORMAL.pdf(x)
 38
 39
 40def v_and_w_win_vector(t, eps):
 41    """calculate v and w for a win in a vectorized fashion"""
 42    diff = t - eps
 43    v = np.empty_like(diff)
 44    denom = norm.cdf(diff)
 45    bad_mask = denom < 1e-50
 46    v[bad_mask] = -1.0 * diff[bad_mask]
 47    v[~bad_mask] = norm.pdf(diff[~bad_mask]) / denom[~bad_mask]
 48    w = v * (v + diff)
 49    return v, w
 50
 51
 52def v_and_w_draw_vector(t, eps):
 53    """calculate v and w for a draw in a vectorized fashion"""
 54    abs_t = np.abs(t)  # the papers do NOT do this but ALL open source implementations DO...
 55    diff_a = eps - abs_t
 56    diff_b = -eps - abs_t
 57
 58    # TODO maybe this would be faster if I concatenated and put through pdf/cdf together?
 59    pdf_a = norm.pdf(diff_a)
 60    pdf_b = norm.pdf(diff_b)
 61    cdf_a = norm.cdf(diff_a)
 62    cdf_b = norm.cdf(diff_b)
 63
 64    v_num = pdf_b - pdf_a
 65    shared_denom = cdf_a - cdf_b
 66    bad_mask = shared_denom < 1e-5
 67    good_mask = ~bad_mask
 68
 69    v = np.empty_like(t)
 70    if eps.shape != t.shape:
 71        eps = np.repeat(eps, repeats=2, axis=1)
 72    v[bad_mask] = -t[bad_mask] + (np.sign(t[bad_mask]) * eps[bad_mask])
 73    v[good_mask] = v_num[good_mask] / shared_denom[good_mask]
 74
 75    w = np.empty_like(t)
 76    w_bad_mask = np.isnan(shared_denom) | np.isinf(shared_denom) | (shared_denom < 1e-50)
 77    w_good_mask = ~w_bad_mask
 78    w[w_bad_mask] = 1.0
 79
 80    w_num = (diff_a[w_good_mask] * pdf_a[w_good_mask]) - (diff_b[w_good_mask] * pdf_b[w_good_mask])
 81    w[w_good_mask] = (w_num / shared_denom[w_good_mask]) + np.square(v[w_good_mask])
 82    return v, w
 83
 84
 85def v_and_w_win_scalar(t, eps):
 86    """calculate v and w for a win in a scalar fashion"""
 87    diff = t - eps
 88    cdf = norm_cdf(diff)
 89    if cdf > 2.222758749e-162:
 90        v = norm_pdf(diff) / cdf
 91    else:
 92        v = -diff
 93    w = v * (v + diff)
 94    return v, w
 95
 96
 97def v_and_w_draw_scalar(t, eps):
 98    """calculate v and w for a draw in a scalar fashion"""
 99    abs_t = math.fabs(t)  # the papers do NOT do this but ALL open source implementations DO...
100    diff_a = eps - abs_t
101    diff_b = -eps - abs_t
102
103    cdf_a = norm_cdf(diff_a)
104    cdf_b = norm_cdf(diff_b)
105
106    pdf_a = norm_pdf(diff_a)
107    pdf_b = norm_pdf(diff_b)
108    v_num = pdf_a - pdf_b
109    shared_denom = cdf_a - cdf_b
110    sign = math.copysign(1.0, t)
111    if shared_denom < 1e-5:
112        v = -t + (sign * eps)
113    else:
114        v = sign * v_num / shared_denom
115    if shared_denom < 1e-50:
116        w = 1.0
117    else:
118        w_num = (diff_a * pdf_a) - (diff_b * pdf_b)
119        w = math.copysign(1.0, t) * ((w_num / shared_denom) + (v**2.0))
120    return v, w
def sigmoid(x):
10def sigmoid(x):
11    """a little faster than implementing it in numpy for d < 100000"""
12    return expit(x)

a little faster than implementing it in numpy for d < 100000

def sigmoid_scalar(x):
15def sigmoid_scalar(x):
16    """no need to use numpy on scalars"""
17    return 1.0 / (1.0 + math.exp(-x))

no need to use numpy on scalars

def base_10_sigmoid(x):
20def base_10_sigmoid(x):
21    """some methods prefer base 10 unfortunately"""
22    return 1.0 / (1.0 + (10.0**-x))

some methods prefer base 10 unfortunately

INV_SQRT_2 = 0.7071067811865475
def norm_cdf(x):
28def norm_cdf(x):
29    """cdf of standard normal"""
30    return 0.5 * (1.0 + math.erf(x * INV_SQRT_2))

cdf of standard normal

STANDARD_NORMAL = NormalDist(mu=0.0, sigma=1.0)
def norm_pdf(x):
36def norm_pdf(x):
37    """pdf of standard normal"""
38    return STANDARD_NORMAL.pdf(x)

pdf of standard normal

def v_and_w_win_vector(t, eps):
41def v_and_w_win_vector(t, eps):
42    """calculate v and w for a win in a vectorized fashion"""
43    diff = t - eps
44    v = np.empty_like(diff)
45    denom = norm.cdf(diff)
46    bad_mask = denom < 1e-50
47    v[bad_mask] = -1.0 * diff[bad_mask]
48    v[~bad_mask] = norm.pdf(diff[~bad_mask]) / denom[~bad_mask]
49    w = v * (v + diff)
50    return v, w

calculate v and w for a win in a vectorized fashion

def v_and_w_draw_vector(t, eps):
53def v_and_w_draw_vector(t, eps):
54    """calculate v and w for a draw in a vectorized fashion"""
55    abs_t = np.abs(t)  # the papers do NOT do this but ALL open source implementations DO...
56    diff_a = eps - abs_t
57    diff_b = -eps - abs_t
58
59    # TODO maybe this would be faster if I concatenated and put through pdf/cdf together?
60    pdf_a = norm.pdf(diff_a)
61    pdf_b = norm.pdf(diff_b)
62    cdf_a = norm.cdf(diff_a)
63    cdf_b = norm.cdf(diff_b)
64
65    v_num = pdf_b - pdf_a
66    shared_denom = cdf_a - cdf_b
67    bad_mask = shared_denom < 1e-5
68    good_mask = ~bad_mask
69
70    v = np.empty_like(t)
71    if eps.shape != t.shape:
72        eps = np.repeat(eps, repeats=2, axis=1)
73    v[bad_mask] = -t[bad_mask] + (np.sign(t[bad_mask]) * eps[bad_mask])
74    v[good_mask] = v_num[good_mask] / shared_denom[good_mask]
75
76    w = np.empty_like(t)
77    w_bad_mask = np.isnan(shared_denom) | np.isinf(shared_denom) | (shared_denom < 1e-50)
78    w_good_mask = ~w_bad_mask
79    w[w_bad_mask] = 1.0
80
81    w_num = (diff_a[w_good_mask] * pdf_a[w_good_mask]) - (diff_b[w_good_mask] * pdf_b[w_good_mask])
82    w[w_good_mask] = (w_num / shared_denom[w_good_mask]) + np.square(v[w_good_mask])
83    return v, w

calculate v and w for a draw in a vectorized fashion

def v_and_w_win_scalar(t, eps):
86def v_and_w_win_scalar(t, eps):
87    """calculate v and w for a win in a scalar fashion"""
88    diff = t - eps
89    cdf = norm_cdf(diff)
90    if cdf > 2.222758749e-162:
91        v = norm_pdf(diff) / cdf
92    else:
93        v = -diff
94    w = v * (v + diff)
95    return v, w

calculate v and w for a win in a scalar fashion

def v_and_w_draw_scalar(t, eps):
 98def v_and_w_draw_scalar(t, eps):
 99    """calculate v and w for a draw in a scalar fashion"""
100    abs_t = math.fabs(t)  # the papers do NOT do this but ALL open source implementations DO...
101    diff_a = eps - abs_t
102    diff_b = -eps - abs_t
103
104    cdf_a = norm_cdf(diff_a)
105    cdf_b = norm_cdf(diff_b)
106
107    pdf_a = norm_pdf(diff_a)
108    pdf_b = norm_pdf(diff_b)
109    v_num = pdf_a - pdf_b
110    shared_denom = cdf_a - cdf_b
111    sign = math.copysign(1.0, t)
112    if shared_denom < 1e-5:
113        v = -t + (sign * eps)
114    else:
115        v = sign * v_num / shared_denom
116    if shared_denom < 1e-50:
117        w = 1.0
118    else:
119        w_num = (diff_a * pdf_a) - (diff_b * pdf_b)
120        w = math.copysign(1.0, t) * ((w_num / shared_denom) + (v**2.0))
121    return v, w

calculate v and w for a draw in a scalar fashion