github.com/jgbaldwinbrown/perf@v0.1.1/pkg/stats/ttest.go (about) 1 // Copyright 2015 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package stats 6 7 import ( 8 "errors" 9 "math" 10 ) 11 12 // A TTestResult is the result of a t-test. 13 type TTestResult struct { 14 // N1 and N2 are the sizes of the input samples. For a 15 // one-sample t-test, N2 is 0. 16 N1, N2 int 17 18 // T is the value of the t-statistic for this t-test. 19 T float64 20 21 // DoF is the degrees of freedom for this t-test. 22 DoF float64 23 24 // AltHypothesis specifies the alternative hypothesis tested 25 // by this test against the null hypothesis that there is no 26 // difference in the means of the samples. 27 AltHypothesis LocationHypothesis 28 29 // P is p-value for this t-test for the given null hypothesis. 30 P float64 31 } 32 33 func newTTestResult(n1, n2 int, t, dof float64, alt LocationHypothesis) *TTestResult { 34 dist := TDist{dof} 35 var p float64 36 switch alt { 37 case LocationDiffers: 38 p = 2 * (1 - dist.CDF(math.Abs(t))) 39 case LocationLess: 40 p = dist.CDF(t) 41 case LocationGreater: 42 p = 1 - dist.CDF(t) 43 } 44 return &TTestResult{N1: n1, N2: n2, T: t, DoF: dof, AltHypothesis: alt, P: p} 45 } 46 47 // A TTestSample is a sample that can be used for a one or two sample 48 // t-test. 49 type TTestSample interface { 50 Weight() float64 51 Mean() float64 52 Variance() float64 53 } 54 55 var ( 56 ErrSampleSize = errors.New("sample is too small") 57 ErrZeroVariance = errors.New("sample has zero variance") 58 ErrMismatchedSamples = errors.New("samples have different lengths") 59 ) 60 61 // TwoSampleTTest performs a two-sample (unpaired) Student's t-test on 62 // samples x1 and x2. This is a test of the null hypothesis that x1 63 // and x2 are drawn from populations with equal means. It assumes x1 64 // and x2 are independent samples, that the distributions have equal 65 // variance, and that the populations are normally distributed. 66 func TwoSampleTTest(x1, x2 TTestSample, alt LocationHypothesis) (*TTestResult, error) { 67 n1, n2 := x1.Weight(), x2.Weight() 68 if n1 == 0 || n2 == 0 { 69 return nil, ErrSampleSize 70 } 71 v1, v2 := x1.Variance(), x2.Variance() 72 if v1 == 0 && v2 == 0 { 73 return nil, ErrZeroVariance 74 } 75 76 dof := n1 + n2 - 2 77 v12 := ((n1-1)*v1 + (n2-1)*v2) / dof 78 t := (x1.Mean() - x2.Mean()) / math.Sqrt(v12*(1/n1+1/n2)) 79 return newTTestResult(int(n1), int(n2), t, dof, alt), nil 80 } 81 82 // TwoSampleWelchTTest performs a two-sample (unpaired) Welch's t-test 83 // on samples x1 and x2. This is like TwoSampleTTest, but does not 84 // assume the distributions have equal variance. 85 func TwoSampleWelchTTest(x1, x2 TTestSample, alt LocationHypothesis) (*TTestResult, error) { 86 n1, n2 := x1.Weight(), x2.Weight() 87 if n1 <= 1 || n2 <= 1 { 88 // TODO: Can we still do this with n == 1? 89 return nil, ErrSampleSize 90 } 91 v1, v2 := x1.Variance(), x2.Variance() 92 if v1 == 0 && v2 == 0 { 93 return nil, ErrZeroVariance 94 } 95 96 dof := math.Pow(v1/n1+v2/n2, 2) / 97 (math.Pow(v1/n1, 2)/(n1-1) + math.Pow(v2/n2, 2)/(n2-1)) 98 s := math.Sqrt(v1/n1 + v2/n2) 99 t := (x1.Mean() - x2.Mean()) / s 100 return newTTestResult(int(n1), int(n2), t, dof, alt), nil 101 } 102 103 // PairedTTest performs a two-sample paired t-test on samples x1 and 104 // x2. If μ0 is non-zero, this tests if the average of the difference 105 // is significantly different from μ0. If x1 and x2 are identical, 106 // this returns nil. 107 func PairedTTest(x1, x2 []float64, μ0 float64, alt LocationHypothesis) (*TTestResult, error) { 108 if len(x1) != len(x2) { 109 return nil, ErrMismatchedSamples 110 } 111 if len(x1) <= 1 { 112 // TODO: Can we still do this with n == 1? 113 return nil, ErrSampleSize 114 } 115 116 dof := float64(len(x1) - 1) 117 118 diff := make([]float64, len(x1)) 119 for i := range x1 { 120 diff[i] = x1[i] - x2[i] 121 } 122 sd := StdDev(diff) 123 if sd == 0 { 124 // TODO: Can we still do the test? 125 return nil, ErrZeroVariance 126 } 127 t := (Mean(diff) - μ0) * math.Sqrt(float64(len(x1))) / sd 128 return newTTestResult(len(x1), len(x2), t, dof, alt), nil 129 } 130 131 // OneSampleTTest performs a one-sample t-test on sample x. This tests 132 // the null hypothesis that the population mean is equal to μ0. This 133 // assumes the distribution of the population of sample means is 134 // normal. 135 func OneSampleTTest(x TTestSample, μ0 float64, alt LocationHypothesis) (*TTestResult, error) { 136 n, v := x.Weight(), x.Variance() 137 if n == 0 { 138 return nil, ErrSampleSize 139 } 140 if v == 0 { 141 // TODO: Can we still do the test? 142 return nil, ErrZeroVariance 143 } 144 dof := n - 1 145 t := (x.Mean() - μ0) * math.Sqrt(n) / math.Sqrt(v) 146 return newTTestResult(int(n), 0, t, dof, alt), nil 147 }