| // Copyright 2015 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package stats |
| |
| import ( |
| "math" |
| "sort" |
| ) |
| |
| // A LocationHypothesis specifies the alternative hypothesis of a |
| // location test such as a t-test or a Mann-Whitney U-test. The |
| // default (zero) value is to test against the alternative hypothesis |
| // that they differ. |
| type LocationHypothesis int |
| |
| //go:generate stringer -type LocationHypothesis |
| |
| const ( |
| // LocationLess specifies the alternative hypothesis that the |
| // location of the first sample is less than the second. This |
| // is a one-tailed test. |
| LocationLess LocationHypothesis = -1 |
| |
| // LocationDiffers specifies the alternative hypothesis that |
| // the locations of the two samples are not equal. This is a |
| // two-tailed test. |
| LocationDiffers LocationHypothesis = 0 |
| |
| // LocationGreater specifies the alternative hypothesis that |
| // the location of the first sample is greater than the |
| // second. This is a one-tailed test. |
| LocationGreater LocationHypothesis = 1 |
| ) |
| |
| // A MannWhitneyUTestResult is the result of a Mann-Whitney U-test. |
| type MannWhitneyUTestResult struct { |
| // N1 and N2 are the sizes of the input samples. |
| N1, N2 int |
| |
| // U is the value of the Mann-Whitney U statistic for this |
| // test, generalized by counting ties as 0.5. |
| // |
| // Given the Cartesian product of the two samples, this is the |
| // number of pairs in which the value from the first sample is |
| // greater than the value of the second, plus 0.5 times the |
| // number of pairs where the values from the two samples are |
| // equal. Hence, U is always an integer multiple of 0.5 (it is |
| // a whole integer if there are no ties) in the range [0, N1*N2]. |
| // |
| // U statistics always come in pairs, depending on which |
| // sample is "first". The mirror U for the other sample can be |
| // calculated as N1*N2 - U. |
| // |
| // There are many equivalent statistics with slightly |
| // different definitions. The Wilcoxon (1945) W statistic |
| // (generalized for ties) is U + (N1(N1+1))/2. It is also |
| // common to use 2U to eliminate the half steps and Smid |
| // (1956) uses N1*N2 - 2U to additionally center the |
| // distribution. |
| U float64 |
| |
| // AltHypothesis specifies the alternative hypothesis tested |
| // by this test against the null hypothesis that there is no |
| // difference in the locations of the samples. |
| AltHypothesis LocationHypothesis |
| |
| // P is the p-value of the Mann-Whitney test for the given |
| // null hypothesis. |
| P float64 |
| } |
| |
| // MannWhitneyExactLimit gives the largest sample size for which the |
| // exact U distribution will be used for the Mann-Whitney U-test. |
| // |
| // Using the exact distribution is necessary for small sample sizes |
| // because the distribution is highly irregular. However, computing |
| // the distribution for large sample sizes is both computationally |
| // expensive and unnecessary because it quickly approaches a normal |
| // approximation. Computing the distribution for two 50 value samples |
| // takes a few milliseconds on a 2014 laptop. |
| var MannWhitneyExactLimit = 50 |
| |
| // MannWhitneyTiesExactLimit gives the largest sample size for which |
| // the exact U distribution will be used for the Mann-Whitney U-test |
| // in the presence of ties. |
| // |
| // Computing this distribution is more expensive than computing the |
| // distribution without ties, so this is set lower. Computing this |
| // distribution for two 25 value samples takes about ten milliseconds |
| // on a 2014 laptop. |
| var MannWhitneyTiesExactLimit = 25 |
| |
| // MannWhitneyUTest performs a Mann-Whitney U-test [1,2] of the null |
| // hypothesis that two samples come from the same population against |
| // the alternative hypothesis that one sample tends to have larger or |
| // smaller values than the other. |
| // |
| // This is similar to a t-test, but unlike the t-test, the |
| // Mann-Whitney U-test is non-parametric (it does not assume a normal |
| // distribution). It has very slightly lower efficiency than the |
| // t-test on normal distributions. |
| // |
| // Computing the exact U distribution is expensive for large sample |
| // sizes, so this uses a normal approximation for sample sizes larger |
| // than MannWhitneyExactLimit if there are no ties or |
| // MannWhitneyTiesExactLimit if there are ties. This normal |
| // approximation uses both the tie correction and the continuity |
| // correction. |
| // |
| // This can fail with ErrSampleSize if either sample is empty or |
| // ErrSamplesEqual if all sample values are equal. |
| // |
| // This is also known as a Mann-Whitney-Wilcoxon test and is |
| // equivalent to the Wilcoxon rank-sum test, though the Wilcoxon |
| // rank-sum test differs in nomenclature. |
| // |
| // [1] Mann, Henry B.; Whitney, Donald R. (1947). "On a Test of |
| // Whether one of Two Random Variables is Stochastically Larger than |
| // the Other". Annals of Mathematical Statistics 18 (1): 50–60. |
| // |
| // [2] Klotz, J. H. (1966). "The Wilcoxon, Ties, and the Computer". |
| // Journal of the American Statistical Association 61 (315): 772-787. |
| func MannWhitneyUTest(x1, x2 []float64, alt LocationHypothesis) (*MannWhitneyUTestResult, error) { |
| n1, n2 := len(x1), len(x2) |
| if n1 == 0 || n2 == 0 { |
| return nil, ErrSampleSize |
| } |
| |
| // Compute the U statistic and tie vector T. |
| x1 = append([]float64(nil), x1...) |
| x2 = append([]float64(nil), x2...) |
| sort.Float64s(x1) |
| sort.Float64s(x2) |
| merged, labels := labeledMerge(x1, x2) |
| |
| R1 := 0.0 |
| T, hasTies := []int{}, false |
| for i := 0; i < len(merged); { |
| rank1, nx1, v1 := i+1, 0, merged[i] |
| // Consume samples that tie this sample (including itself). |
| for ; i < len(merged) && merged[i] == v1; i++ { |
| if labels[i] == 1 { |
| nx1++ |
| } |
| } |
| // Assign all tied samples the average rank of the |
| // samples, where merged[0] has rank 1. |
| if nx1 != 0 { |
| rank := float64(i+rank1) / 2 |
| R1 += rank * float64(nx1) |
| } |
| T = append(T, i-rank1+1) |
| if i > rank1 { |
| hasTies = true |
| } |
| } |
| U1 := R1 - float64(n1*(n1+1))/2 |
| |
| // Compute the smaller of U1 and U2 |
| U2 := float64(n1*n2) - U1 |
| Usmall := math.Min(U1, U2) |
| |
| var p float64 |
| if !hasTies && n1 <= MannWhitneyExactLimit && n2 <= MannWhitneyExactLimit || |
| hasTies && n1 <= MannWhitneyTiesExactLimit && n2 <= MannWhitneyTiesExactLimit { |
| // Use exact U distribution. U1 will be an integer. |
| if len(T) == 1 { |
| // All values are equal. Test is meaningless. |
| return nil, ErrSamplesEqual |
| } |
| |
| dist := UDist{N1: n1, N2: n2, T: T} |
| switch alt { |
| case LocationDiffers: |
| if U1 == U2 { |
| // The distribution is symmetric about |
| // Usmall. Since the distribution is |
| // discrete, the CDF is discontinuous |
| // and if simply double CDF(Usmall), |
| // we'll double count the |
| // (non-infinitesimal) probability |
| // mass at Usmall. What we want is |
| // just the integral of the whole CDF, |
| // which is 1. |
| p = 1 |
| } else { |
| p = dist.CDF(Usmall) * 2 |
| } |
| |
| case LocationLess: |
| p = dist.CDF(U1) |
| |
| case LocationGreater: |
| p = 1 - dist.CDF(U1-1) |
| } |
| } else { |
| // Use normal approximation (with tie and continuity |
| // correction). |
| t := tieCorrection(T) |
| N := float64(n1 + n2) |
| μ_U := float64(n1*n2) / 2 |
| σ_U := math.Sqrt(float64(n1*n2) * ((N + 1) - t/(N*(N-1))) / 12) |
| if σ_U == 0 { |
| return nil, ErrSamplesEqual |
| } |
| numer := U1 - μ_U |
| // Perform continuity correction. |
| switch alt { |
| case LocationDiffers: |
| numer -= mathSign(numer) * 0.5 |
| case LocationLess: |
| numer += 0.5 |
| case LocationGreater: |
| numer -= 0.5 |
| } |
| z := numer / σ_U |
| switch alt { |
| case LocationDiffers: |
| p = 2 * math.Min(StdNormal.CDF(z), 1-StdNormal.CDF(z)) |
| case LocationLess: |
| p = StdNormal.CDF(z) |
| case LocationGreater: |
| p = 1 - StdNormal.CDF(z) |
| } |
| } |
| |
| return &MannWhitneyUTestResult{N1: n1, N2: n2, U: U1, |
| AltHypothesis: alt, P: p}, nil |
| } |
| |
| // labeledMerge merges sorted lists x1 and x2 into sorted list merged. |
| // labels[i] is 1 or 2 depending on whether merged[i] is a value from |
| // x1 or x2, respectively. |
| func labeledMerge(x1, x2 []float64) (merged []float64, labels []byte) { |
| merged = make([]float64, len(x1)+len(x2)) |
| labels = make([]byte, len(x1)+len(x2)) |
| |
| i, j, o := 0, 0, 0 |
| for i < len(x1) && j < len(x2) { |
| if x1[i] < x2[j] { |
| merged[o] = x1[i] |
| labels[o] = 1 |
| i++ |
| } else { |
| merged[o] = x2[j] |
| labels[o] = 2 |
| j++ |
| } |
| o++ |
| } |
| for ; i < len(x1); i++ { |
| merged[o] = x1[i] |
| labels[o] = 1 |
| o++ |
| } |
| for ; j < len(x2); j++ { |
| merged[o] = x2[j] |
| labels[o] = 2 |
| o++ |
| } |
| return |
| } |
| |
| // tieCorrection computes the tie correction factor Σ_j (t_j³ - t_j) |
| // where t_j is the number of ties in the j'th rank. |
| func tieCorrection(ties []int) float64 { |
| t := 0 |
| for _, tie := range ties { |
| t += tie*tie*tie - tie |
| } |
| return float64(t) |
| } |