mvn.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. import numpy as np
  2. import scipy
  3. from typing import Tuple
  4. # * * * STATISTICAL TESTS * * *
  5. def mardia_test(data: np.ndarray, cov: bool = True) -> Tuple[float, float, float, float]:
  6. """
  7. https://rdrr.io/cran/MVN/src/R/mvn.R
  8. https://stats.stackexchange.com/questions/317147/how-to-get-a-single-p-value-from-the-two-p-values-of-a-mardias-multinormality-t
  9. Mardia's multivariate skewness and kurtosis.
  10. Calculates the Mardia's multivariate skewness and kurtosis coefficients
  11. as well as their corresponding statistical test. For large sample size
  12. the multivariate skewness is asymptotically distributed as a Chi-square
  13. random variable; here it is corrected for small sample size. However,
  14. both uncorrected and corrected skewness statistic are presented. Likewise,
  15. the multivariate kurtosis it is distributed as a unit-normal.
  16. Syntax: function [Mskekur] = Mskekur(X,c,alpha)
  17. Inputs:
  18. X - multivariate data matrix [Size of matrix must be n(data)-by-p(variables)].
  19. cov - boolean to whether to normalize the covariance matrix by n (c=1[default]) or by n-1 (c~=1)
  20. Outputs:
  21. - skewness test statistic
  22. - kurtosis test statistic
  23. - significance value for skewness
  24. - significance value for kurtosis
  25. """
  26. n, p = data.shape
  27. # correct for small sample size
  28. small: bool = True if n < 20 else False
  29. if cov:
  30. S = ((n - 1)/n) * np.cov(data.T)
  31. else:
  32. S = np.cov(data.T)
  33. # calculate mean
  34. data_mean = data.mean(axis=0)
  35. # inverse - check if singular matrix
  36. try:
  37. iS = np.linalg.inv(S)
  38. except Exception as e:
  39. # print for now
  40. print(e)
  41. return 0.0, 0.0, 0.0, 0.0
  42. # squared-Mahalanobis' distances matrix
  43. D: np.ndarray = (data - data_mean) @ iS @ (data - data_mean).T
  44. # multivariate skewness coefficient
  45. g1p: float = np.sum(D**3)/n**2
  46. # multivariate kurtosis coefficient
  47. g2p: float = np.trace(D**2)/n
  48. # small sample correction
  49. k: float = ((p + 1)*(n + 1)*(n + 3))/(n*(((n + 1)*(p + 1)) - 6))
  50. # degrees of freedom
  51. df: float = (p * (p + 1) * (p + 2))/6
  52. if small:
  53. # skewness test statistic corrected for small sample: it approximates to a chi-square distribution
  54. g_skew = (n * g1p * k)/6
  55. else:
  56. # skewness test statistic:it approximates to a chi-square distribution
  57. g_skew = (n * g1p)/6
  58. # significance value associated to the skewness corrected for small sample
  59. p_skew: float = 1.0 - scipy.stats.chi2.cdf(g_skew, df)
  60. # kurtosis test statistic: it approximates to a unit-normal distribution
  61. g_kurt = (g2p - (p*(p + 2)))/(np.sqrt((8 * p * (p + 2))/n))
  62. # significance value associated to the kurtosis
  63. p_kurt: float = 2 * (1.0 - scipy.stats.norm.cdf(np.abs(g_kurt)))
  64. return g_skew, g_kurt, p_skew, p_kurt
  65. def hz_test(data: np.ndarray, cov: bool = True) -> Tuple[float, float]:
  66. """
  67. Henze-Zirkler method for goodness of fit of data to a multivariate normal distribution.
  68. Researchers tend to use this MVN test for larger samples (N > 100).
  69. https://www.tandfonline.com/doi/abs/10.1080/03610929008830400
  70. :param data: multivariate data matrix [Size of matrix must be n(data)-by-p(variables)].
  71. :param cov: boolean to whether to normalize the covariance matrix by n (c=1[default]) or by n-1 (c~=1)
  72. :return:
  73. HZ - Henze-Zirkler test statistic
  74. p_value - significance value
  75. """
  76. n, p = data.shape
  77. if cov:
  78. S = ((n - 1)/n) * np.cov(data.T)
  79. else:
  80. S = np.cov(data.T)
  81. # calculate mean
  82. data_mean = data.mean(axis=0)
  83. try:
  84. iS = np.linalg.inv(S)
  85. except Exception as e:
  86. print(e)
  87. return 0.0, 0.0
  88. Y = data @ iS @ data.T
  89. Dj = np.diag((data - data_mean) @ iS @ (data - data_mean).T)
  90. Djk = - 2 * Y.T + np.tensordot(np.diag(Y.T), np.ones(n), axes=0) + np.tensordot(np.ones(n), np.diag(Y.T), axes=0)
  91. b: float = 1 / (np.sqrt(2)) * ((2 * p + 1) / 4) ** (1 / (p + 4)) * (n ** (1 / (p + 4)))
  92. # calculate rank of matrix
  93. S_rank = np.linalg.matrix_rank(S)
  94. if S_rank == p:
  95. HZ = n * (1 / (n ** 2) * np.sum(np.sum(np.exp(- (b ** 2) / 2 * Djk))) - 2 * ((1 + (b ** 2)) ** (- p / 2)) * (1 / n) * (np.sum(np.exp(- ((b ** 2) / (2 * (1 + (b ** 2)))) * Dj))) + ((1 + (2 * (b ** 2))) ** (- p / 2)))
  96. else:
  97. HZ = n * 4
  98. wb = (1 + b ** 2) * (1 + 3 * b ** 2)
  99. a = 1 + 2 * b ** 2
  100. # HZ mean
  101. mu = 1 - a ** (- p / 2) * (1 + p * b ** 2 / a + (p * (p + 2) * (b ** 4)) / (2 * a ** 2)) # HZ mean
  102. # HZ variance
  103. si2 = 2 * (1 + 4 * b ** 2) ** (- p / 2) + 2 * a ** (- p) * (1 + (2 * p * b ** 4) / a ** 2 + (3 * p * (p + 2) * b ** 8) / (4 * a ** 4)) - 4 * wb ** (- p / 2) * (1 + (3 * p * b ** 4) / (2 * wb) + (p * (p + 2) * b ** 8) / (2 * wb ** 2))
  104. pmu = np.log(np.sqrt(mu ** 4 / (si2 + mu ** 2))) # lognormal HZ mean
  105. psi = np.sqrt(np.log((si2 + mu ** 2) / mu ** 2)) # lognormal HZ standard deviation
  106. # calculate p-value
  107. p_value = 1.0 - scipy.stats.lognorm.cdf(HZ, psi, scale=np.exp(pmu))
  108. return HZ, p_value
  109. import numpy as np
  110. from scipy.stats import shapiro, chi2
  111. def royston_test(X):
  112. """
  113. Royston's Multivariate Normality Test using Fisher's method on Shapiro-Wilk p-values.
  114. Parameters:
  115. X (ndarray): 2D array (n_samples x n_variables)
  116. Returns:
  117. stat (float): Fisher's combined test statistic
  118. p_value (float): p-value for overall multivariate normality
  119. """
  120. X = np.asarray(X)
  121. n, p = X.shape
  122. if n < 3:
  123. raise ValueError("At least 3 observations are required.")
  124. if p < 2:
  125. raise ValueError("At least 2 variables required.")
  126. p_values = []
  127. for i in range(p):
  128. _, pval = shapiro(X[:, i])
  129. p_values.append(pval)
  130. p_values = np.clip(p_values, 1e-16, 1.0) # avoid log(0)
  131. stat = -2 * np.sum(np.log(p_values))
  132. df = 2 * p
  133. p_combined = 1 - chi2.cdf(stat, df)
  134. return stat, p_combined