Home | History | Annotate | Download | only in dashboard
      1 # Copyright 2015 The Chromium Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 import unittest
      6 
      7 import mock
      8 
      9 from dashboard import ttest
     10 
     11 
     12 class TTestTest(unittest.TestCase):
     13   """Tests for the t-test functions."""
     14 
     15   def setUp(self):
     16     """Sets the t-table values for the tests below."""
     17     table_patch = mock.patch.object(
     18         ttest, '_TABLE',
     19         [
     20             (1, [0, 6.314, 12.71, 31.82, 63.66, 318.31]),
     21             (2, [0, 2.920, 4.303, 6.965, 9.925, 22.327]),
     22             (3, [0, 2.353, 3.182, 4.541, 5.841, 10.215]),
     23             (4, [0, 2.132, 2.776, 3.747, 4.604, 7.173]),
     24             (10, [0, 1.372, 1.812, 2.228, 2.764, 3.169]),
     25             (100, [0, 1.290, 1.660, 1.984, 2.364, 2.626]),
     26         ])
     27     table_patch.start()
     28     self.addCleanup(table_patch.stop)
     29     two_tail_patch = mock.patch.object(
     30         ttest, '_TWO_TAIL',
     31         [1, 0.2, 0.1, 0.05, 0.02, 0.01])
     32     two_tail_patch.start()
     33     self.addCleanup(two_tail_patch.stop)
     34 
     35   def testWelchsTTest(self):
     36     """Tests the t value and degrees of freedom output of Welch's t-test."""
     37     # The t-value can be checked with scipy.stats.ttest_ind(equal_var=False).
     38     # However the t-value output by scipy.stats.ttest_ind is -6.32455532034.
     39     # This implementation produces slightly different results.
     40     result = ttest.WelchsTTest([2, 3, 2, 3, 2, 3], [4, 5, 4, 5, 4, 5])
     41     self.assertAlmostEqual(10.0, result.df)
     42     self.assertAlmostEqual(-6.325, result.t, delta=1.0)
     43 
     44   def testWelchsTTest_EmptySample_RaisesError(self):
     45     """An error should be raised when an empty sample is passed in."""
     46     with self.assertRaises(RuntimeError):
     47       ttest.WelchsTTest([], [])
     48     with self.assertRaises(RuntimeError):
     49       ttest.WelchsTTest([], [1, 2, 3])
     50     with self.assertRaises(RuntimeError):
     51       ttest.WelchsTTest([1, 2, 3], [])
     52 
     53   def testTTest_EqualSamples_PValueIsOne(self):
     54     """Checks that t = 0 and p = 1 when the samples are the same."""
     55     result = ttest.WelchsTTest([1, 2, 3], [1, 2, 3])
     56     self.assertEqual(0, result.t)
     57     self.assertEqual(1, result.p)
     58 
     59   def testTTest_VeryDifferentSamples_PValueIsLow(self):
     60     """Checks that p is very low when the samples are clearly different."""
     61     result = ttest.WelchsTTest([100, 101, 100, 101, 100],
     62                                [1, 2, 1, 2, 1, 2, 1, 2])
     63     self.assertLessEqual(250, result.t)
     64     self.assertLessEqual(0.01, result.p)
     65 
     66   def testTTest_DifferentVariance(self):
     67     """Verifies that higher variance -> higher p value."""
     68     result_low_var = ttest.WelchsTTest([2, 3, 2, 3], [4, 5, 4, 5])
     69     result_high_var = ttest.WelchsTTest([1, 4, 1, 4], [3, 6, 3, 6])
     70     self.assertLess(result_low_var.p, result_high_var.p)
     71 
     72   def testTTest_DifferentSampleSize(self):
     73     """Verifies that smaller sample size -> higher p value."""
     74     result_larger_sample = ttest.WelchsTTest([2, 3, 2, 3], [4, 5, 4, 5])
     75     result_smaller_sample = ttest.WelchsTTest([2, 3, 2, 3], [4, 5])
     76     self.assertLess(result_larger_sample.p, result_smaller_sample.p)
     77 
     78   def testTTest_DifferentMeanDifference(self):
     79     """Verifies that smaller difference between means -> higher p value."""
     80     result_far_means = ttest.WelchsTTest([2, 3, 2, 3], [5, 6, 5, 6])
     81     result_near_means = ttest.WelchsTTest([2, 3, 2, 3], [3, 4, 3, 4])
     82     self.assertLess(result_far_means.p, result_near_means.p)
     83 
     84   def testTValue(self):
     85     """Tests calculation of the t-value using Welch's formula."""
     86     # Results can be verified by directly plugging variables into Welch's
     87     # equation (e.g. using a calculator or the Python interpreter).
     88     stats1 = ttest.SampleStats(mean=0.299, var=0.05, size=150)
     89     stats2 = ttest.SampleStats(mean=0.307, var=0.08, size=165)
     90     # Note that a negative t-value is obtained when the first sample has a
     91     # smaller mean than the second, otherwise a positive value is returned.
     92     self.assertAlmostEqual(-0.27968236, ttest._TValue(stats1, stats2))
     93     self.assertAlmostEqual(0.27968236, ttest._TValue(stats2, stats1))
     94 
     95   def testTValue_ConstantSamples_ResultIsInfinity(self):
     96     """If there is no variation, infinity is used as the t-statistic value."""
     97     stats = ttest.SampleStats(mean=1.0, var=0, size=10)
     98     self.assertEqual(float('inf'), ttest._TValue(stats, stats))
     99 
    100   def testDegreesOfFreedom(self):
    101     """Tests calculation of estimated degrees of freedom."""
    102     # The formula used to estimate degrees of freedom for independent-samples
    103     # t-test is called the Welch-Satterthwaite equation. Note that since the
    104     # Welch-Satterthwaite equation gives an estimate of degrees of freedom,
    105     # the result is a floating-point number and not an integer.
    106     stats1 = ttest.SampleStats(mean=0.299, var=0.05, size=150)
    107     stats2 = ttest.SampleStats(mean=0.307, var=0.08, size=165)
    108     self.assertAlmostEqual(
    109         307.19879975, ttest._DegreesOfFreedom(stats1, stats2))
    110 
    111   def testDegreesOfFreedom_ZeroVariance_ResultIsOne(self):
    112     """The lowest possible value is returned for df if variance is zero."""
    113     stats = ttest.SampleStats(mean=1.0, var=0, size=10)
    114     self.assertEqual(1.0, ttest._DegreesOfFreedom(stats, stats))
    115 
    116   def testDegreesOfFreedom_SmallSample_RaisesError(self):
    117     """Degrees of freedom can't be calculated if sample size is too small."""
    118     size_0 = ttest.SampleStats(mean=0, var=0, size=0)
    119     size_1 = ttest.SampleStats(mean=1.0, var=0, size=1)
    120     size_5 = ttest.SampleStats(mean=2.0, var=0.5, size=5)
    121 
    122     # An error is raised if the size of one of the samples is too small.
    123     with self.assertRaises(RuntimeError):
    124       ttest._DegreesOfFreedom(size_0, size_5)
    125     with self.assertRaises(RuntimeError):
    126       ttest._DegreesOfFreedom(size_1, size_5)
    127     with self.assertRaises(RuntimeError):
    128       ttest._DegreesOfFreedom(size_5, size_0)
    129     with self.assertRaises(RuntimeError):
    130       ttest._DegreesOfFreedom(size_5, size_1)
    131 
    132     # If both of the samples have a variance of 0, no error is raised.
    133     self.assertEqual(1.0, ttest._DegreesOfFreedom(size_1, size_1))
    134 
    135 
    136 class LookupPValueTest(unittest.TestCase):
    137 
    138   def setUp(self):
    139     """Sets the t-table values for the tests below."""
    140     table_patch = mock.patch.object(
    141         ttest, '_TABLE',
    142         [
    143             (1, [0, 6.314, 12.71, 31.82, 63.66, 318.31]),
    144             (2, [0, 2.920, 4.303, 6.965, 9.925, 22.327]),
    145             (3, [0, 2.353, 3.182, 4.541, 5.841, 10.215]),
    146             (4, [0, 2.132, 2.776, 3.747, 4.604, 7.173]),
    147             (10, [0, 1.372, 1.812, 2.228, 2.764, 3.169]),
    148             (100, [0, 1.290, 1.660, 1.984, 2.364, 2.626]),
    149         ])
    150     table_patch.start()
    151     self.addCleanup(table_patch.stop)
    152     two_tail_patch = mock.patch.object(
    153         ttest, '_TWO_TAIL',
    154         [1, 0.2, 0.1, 0.05, 0.02, 0.01])
    155     two_tail_patch.start()
    156     self.addCleanup(two_tail_patch.stop)
    157 
    158   def testLookupPValue_ExactMatchInTable(self):
    159     """Tests looking up an entry that is in the table."""
    160     self.assertEqual(0.1, ttest._LookupPValue(3.182, 3.0))
    161     self.assertEqual(0.1, ttest._LookupPValue(-3.182, 3.0))
    162 
    163   def testLookupPValue_TValueBetweenTwoValues_SmallerColumnIsUsed(self):
    164     # The second column is used because 3.1 is below 4.303,
    165     # so the next-lowest t-value, 2.920, is used.
    166     self.assertEqual(0.2, ttest._LookupPValue(3.1, 2.0))
    167     self.assertEqual(0.2, ttest._LookupPValue(-3.1, 2.0))
    168 
    169   def testLookup_DFBetweenTwoValues_SmallerRowIsUsed(self):
    170     self.assertEqual(0.05, ttest._LookupPValue(2.228, 45.0))
    171     self.assertEqual(0.05, ttest._LookupPValue(-2.228, 45.0))
    172 
    173   def testLookup_DFAndTValueBetweenTwoValues_SmallerRowAndColumnIsUsed(self):
    174     self.assertEqual(0.1, ttest._LookupPValue(2.0, 45.0))
    175     self.assertEqual(0.1, ttest._LookupPValue(-2.0, 45.0))
    176 
    177   def testLookupPValue_LargeTValue_LastColumnIsUsed(self):
    178     # The smallest possible p-value will be used when t is large.
    179     self.assertEqual(0.01, ttest._LookupPValue(500.0, 1.0))
    180     self.assertEqual(0.01, ttest._LookupPValue(-500.0, 1.0))
    181 
    182   def testLookupPValue_ZeroTValue_FirstColumnIsUsed(self):
    183     # The largest possible p-value will be used when t is zero.
    184     self.assertEqual(1.0, ttest._LookupPValue(0.0, 1.0))
    185     self.assertEqual(1.0, ttest._LookupPValue(0.0, 2.0))
    186 
    187   def testLookupPValue_SmallTValue_FirstColumnIsUsed(self):
    188     # The largest possible p-value will be used when t is almost zero.
    189     self.assertEqual(1.0, ttest._LookupPValue(0.1, 2.0))
    190     self.assertEqual(1.0, ttest._LookupPValue(-0.1, 2.0))
    191 
    192   def testLookupPValue_LargeDegreesOfFreedom_LastRowIsUsed(self):
    193     # The last row of the table should be used.
    194     self.assertEqual(0.02, ttest._LookupPValue(2.365, 100.0))
    195 
    196 
    197 if __name__ == '__main__':
    198   unittest.main()
    199