import numpy as np from numpy import poly1d from scipy.special import beta # The following code was used to generate the Pade coefficients for the # Tukey Lambda variance function. Version 0.17 of mpmath was used. #--------------------------------------------------------------------------- # import mpmath as mp # # mp.mp.dps = 60 # # one = mp.mpf(1) # two = mp.mpf(2) # # def mpvar(lam): # if lam == 0: # v = mp.pi**2 / three # else: # v = (two / lam**2) * (one / (one + two*lam) - # mp.beta(lam + one, lam + one)) # return v # # t = mp.taylor(mpvar, 0, 8) # p, q = mp.pade(t, 4, 4) # print("p =", [mp.fp.mpf(c) for c in p]) # print("q =", [mp.fp.mpf(c) for c in q]) #--------------------------------------------------------------------------- # Pade coefficients for the Tukey Lambda variance function. _tukeylambda_var_pc = [3.289868133696453, 0.7306125098871127, -0.5370742306855439, 0.17292046290190008, -0.02371146284628187] _tukeylambda_var_qc = [1.0, 3.683605511659861, 4.184152498888124, 1.7660926747377275, 0.2643989311168465] # numpy.poly1d instances for the numerator and denominator of the # Pade approximation to the Tukey Lambda variance. _tukeylambda_var_p = poly1d(_tukeylambda_var_pc[::-1]) _tukeylambda_var_q = poly1d(_tukeylambda_var_qc[::-1]) def tukeylambda_variance(lam): """Variance of the Tukey Lambda distribution. Parameters ---------- lam : array_like The lambda values at which to compute the variance. Returns ------- v : ndarray The variance. For lam < -0.5, the variance is not defined, so np.nan is returned. For lam = 0.5, np.inf is returned. Notes ----- In an interval around lambda=0, this function uses the [4,4] Pade approximation to compute the variance. Otherwise it uses the standard formula (https://en.wikipedia.org/wiki/Tukey_lambda_distribution). The Pade approximation is used because the standard formula has a removable discontinuity at lambda = 0, and does not produce accurate numerical results near lambda = 0. """ lam = np.asarray(lam) shp = lam.shape lam = np.atleast_1d(lam).astype(np.float64) # For absolute values of lam less than threshold, use the Pade # approximation. threshold = 0.075 # Play games with masks to implement the conditional evaluation of # the distribution. # lambda < -0.5: var = nan low_mask = lam < -0.5 # lambda == -0.5: var = inf neghalf_mask = lam == -0.5 # abs(lambda) < threshold: use Pade approximation small_mask = np.abs(lam) < threshold # else the "regular" case: use the explicit formula. reg_mask = ~(low_mask | neghalf_mask | small_mask) # Get the 'lam' values for the cases where they are needed. small = lam[small_mask] reg = lam[reg_mask] # Compute the function for each case. v = np.empty_like(lam) v[low_mask] = np.nan v[neghalf_mask] = np.inf if small.size > 0: # Use the Pade approximation near lambda = 0. v[small_mask] = _tukeylambda_var_p(small) / _tukeylambda_var_q(small) if reg.size > 0: v[reg_mask] = (2.0 / reg**2) * (1.0 / (1.0 + 2 * reg) - beta(reg + 1, reg + 1)) v.shape = shp return v # The following code was used to generate the Pade coefficients for the # Tukey Lambda kurtosis function. Version 0.17 of mpmath was used. #--------------------------------------------------------------------------- # import mpmath as mp # # mp.mp.dps = 60 # # one = mp.mpf(1) # two = mp.mpf(2) # three = mp.mpf(3) # four = mp.mpf(4) # # def mpkurt(lam): # if lam == 0: # k = mp.mpf(6)/5 # else: # numer = (one/(four*lam+one) - four*mp.beta(three*lam+one, lam+one) + # three*mp.beta(two*lam+one, two*lam+one)) # denom = two*(one/(two*lam+one) - mp.beta(lam+one,lam+one))**2 # k = numer / denom - three # return k # # # There is a bug in mpmath 0.17: when we use the 'method' keyword of the # # taylor function and we request a degree 9 Taylor polynomial, we actually # # get degree 8. # t = mp.taylor(mpkurt, 0, 9, method='quad', radius=0.01) # t = [mp.chop(c, tol=1e-15) for c in t] # p, q = mp.pade(t, 4, 4) # print("p =", [mp.fp.mpf(c) for c in p]) # print("q =", [mp.fp.mpf(c) for c in q]) #--------------------------------------------------------------------------- # Pade coefficients for the Tukey Lambda kurtosis function. _tukeylambda_kurt_pc = [1.2, -5.853465139719495, -22.653447381131077, 0.20601184383406815, 4.59796302262789] _tukeylambda_kurt_qc = [1.0, 7.171149192233599, 12.96663094361842, 0.43075235247853005, -2.789746758009912] # numpy.poly1d instances for the numerator and denominator of the # Pade approximation to the Tukey Lambda kurtosis. _tukeylambda_kurt_p = poly1d(_tukeylambda_kurt_pc[::-1]) _tukeylambda_kurt_q = poly1d(_tukeylambda_kurt_qc[::-1]) def tukeylambda_kurtosis(lam): """Kurtosis of the Tukey Lambda distribution. Parameters ---------- lam : array_like The lambda values at which to compute the variance. Returns ------- v : ndarray The variance. For lam < -0.25, the variance is not defined, so np.nan is returned. For lam = 0.25, np.inf is returned. """ lam = np.asarray(lam) shp = lam.shape lam = np.atleast_1d(lam).astype(np.float64) # For absolute values of lam less than threshold, use the Pade # approximation. threshold = 0.055 # Use masks to implement the conditional evaluation of the kurtosis. # lambda < -0.25: kurtosis = nan low_mask = lam < -0.25 # lambda == -0.25: kurtosis = inf negqrtr_mask = lam == -0.25 # lambda near 0: use Pade approximation small_mask = np.abs(lam) < threshold # else the "regular" case: use the explicit formula. reg_mask = ~(low_mask | negqrtr_mask | small_mask) # Get the 'lam' values for the cases where they are needed. small = lam[small_mask] reg = lam[reg_mask] # Compute the function for each case. k = np.empty_like(lam) k[low_mask] = np.nan k[negqrtr_mask] = np.inf if small.size > 0: k[small_mask] = _tukeylambda_kurt_p(small) / _tukeylambda_kurt_q(small) if reg.size > 0: numer = (1.0 / (4 * reg + 1) - 4 * beta(3 * reg + 1, reg + 1) + 3 * beta(2 * reg + 1, 2 * reg + 1)) denom = 2 * (1.0/(2 * reg + 1) - beta(reg + 1, reg + 1))**2 k[reg_mask] = numer / denom - 3 # The return value will be a numpy array; resetting the shape ensures that # if `lam` was a scalar, the return value is a 0-d array. k.shape = shp return k