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):
cdf of standard normal
STANDARD_NORMAL =
NormalDist(mu=0.0, sigma=1.0)
def
norm_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