Repository: statsmodels/statsmodels

Issue:
sign_test   check
there is a sign_test in  statsmodels.sandbox.descstats and a copy in statsmodels.stats.descriptivestats as method with missing self.

This should be added to http://statsmodels.sourceforge.net/devel/stats.html#non-parametric-tests

looks similar to statsmodels.sandbox.stats.runs.median_test_ksample which uses chisquare_test instead of binom_test



Discussion:
results looks wrong

```
>>> x = [7.8, 6.6, 6.5, 7.4, 7.3, 7., 6.4, 7.1, 6.7, 7.6, 6.8]
>>> from statsmodels.sandbox.descstats import sign_test
>>> sign_test(x, 6.5)
(0.5, 1.0)
```

instead of 

```
>>> xs = (7.8, 6.6, 6.5, 7.4, 7.3, 7., 6.4, 7.1, 6.7, 7.6, 6.8) 
>>> xs = np.asarray(xs) 
>>> med=6.5; stats.binom_test([(xs<med).sum(),(xs>med).sum()], p=0.5) 
0.021484374999999997 
same p-value as in R 
```

see http://scipy-user.10969.n7.nabble.com/One-Sample-Sign-Test-for-a-Median-in-SciPy-td2009.html

link to R function: http://rss.acs.unt.edu/Rdoc/library/BSDA/html/sign.test.html
note misspelling should be SIGN.test

If it's the one I wrote long ago, the comparison for this should be the sign test from SAS proc univariate, though I haven't looked at this in some time and it could very well be incorrect.

http://support.sas.com/documentation/cdl/en/procstat/63104/HTML/default/viewer.htm#procstat_univariate_sect068.htm

Yes, it's from early in your first GSOC.
from a quick look the second argument in the call to binom_test is wrong, or has changed. I didn't look closely yet.

This just needed an asarray call on samp. x > 6.5 return True or False for a list.

That's a funny bug, I didn't think it would be this simple, and I never tried an array.


Resolving patch:
diff --git a/statsmodels/sandbox/descstats.py b/statsmodels/sandbox/descstats.py
--- a/statsmodels/sandbox/descstats.py
+++ b/statsmodels/sandbox/descstats.py
@@ -4,6 +4,7 @@
 import numpy as np
 from scipy import stats
 import os
+from statsmodels.stats.descriptivestats import sign_test
 
 #############################################
 #
@@ -12,40 +13,6 @@
 #============================================
 #
 
-def sign_test(samp,mu0=0):
-        '''
-        Signs test with mu0=0 by default (though
-        the median is often used in practice)
-
-        Parameters
-        ----------
-        samp
-
-        mu0
-
-        Returns
-        ---------
-        M, p-value
-
-        where
-
-        M=(N(+) - N(-))/2, N(+) is the number of values above Mu0,
-        N(-) is the number of values below.  Values equal to Mu0
-        are discarded.
-
-        The p-value for M is calculated using the binomial distrubution
-        and can be intrepreted the same as for a t-test.
-
-        See Also
-        ---------
-        scipy.stats.wilcoxon
-        '''
-        pos=np.sum(samp>mu0)
-        neg=np.sum(samp<mu0)
-        M=(pos-neg)/2.
-        p=stats.binom_test(min(pos,neg),pos+neg,.5)
-        return M, p
-
 def descstats(data, cols=None, axis=0):
     '''
     Prints descriptive statistics for one or multiple variables.
diff --git a/statsmodels/stats/descriptivestats.py b/statsmodels/stats/descriptivestats.py
--- a/statsmodels/stats/descriptivestats.py
+++ b/statsmodels/stats/descriptivestats.py
@@ -3,7 +3,7 @@
 from scipy import stats
 #from statsmodels.iolib.table import SimpleTable
 from statsmodels.iolib.table import SimpleTable
-
+from statsmodels.tools.decorators import nottest
 
 def _kurtosis(a):
     '''wrapper for scipy.stats.kurtosis that returns nan instead of raising Error
@@ -27,22 +27,68 @@ def _skew(a):
         res = np.nan
     return res
 
+_sign_test_doc = '''
+    Signs test.
+
+    Parameters
+    ----------
+    samp : array-like
+        1d array. The sample for which you want to perform the signs
+        test.
+    mu0 : float
+        See Notes for the definition of the sign test. mu0 is 0 by
+        default, but it is common to set it to the median.
+
+    Returns
+    ---------
+    M, p-value
+
+    Notes
+    -----
+    The signs test returns
+
+    M = (N(+) - N(-))/2
+
+    where N(+) is the number of values above `mu0`, N(-) is the number of
+    values below.  Values equal to `mu0` are discarded.
+
+    The p-value for M is calculated using the binomial distrubution
+    and can be intrepreted the same as for a t-test. The test-statistic
+    is distributed Binom(min(N(+), N(-)), n_trials, .5) where n_trials
+    equals N(+) + N(-).
+
+    See Also
+    ---------
+    scipy.stats.wilcoxon
+    '''
+
+@nottest
+def sign_test(samp, mu0=0):
+    samp = np.asarray(samp)
+    pos = np.sum(samp > mu0)
+    neg = np.sum(samp < mu0)
+    M = (pos-neg)/2.
+    p = stats.binom_test(min(pos,neg), pos+neg, .5)
+    return M, p
+sign_test.__doc__ = _sign_test_doc
+
 class Describe(object):
     '''
     Calculates descriptive statistics for data.
-    Defaults to a basic set of statistics, "all" can be specified, or a list can
-    be given.
-
-    dataset : can be either a structured or ndarray (Larry?), observations in
-              rows, variables in columns.
 
+    Defaults to a basic set of statistics, "all" can be specified, or a list
+    can be given.
 
+    Parameters
+    ----------
+    dataset : array-like
+        2D dataset for descriptive statistics.
     '''
     def __init__(self, dataset):
         self.dataset = dataset
 
-        #better if this is initially a list to define order, or use an ordered dict
-        # First position is the function
+        #better if this is initially a list to define order, or use an
+        # ordered dict. First position is the function
         # Second position is the tuple/list of column names/numbers
         # third is are the results in order of the columns
         self.univariate = dict(
@@ -59,18 +105,20 @@ def __init__(self, dataset):
             skew = [stats.skew, None, None],
             uss = [stats.ss, None, None],
             kurtosis = [stats.kurtosis, None, None],
-            percentiles = [self._percentiles, None, None], #BUG: not single value
+            percentiles = [self._percentiles, None, None],
+            #BUG: not single value
             #sign_test_M = [self.sign_test_m, None, None],
             #sign_test_P = [self.sign_test_p, None, None]
         )
-#TODO: Basic stats for strings
+
+        #TODO: Basic stats for strings
         #self.strings = dict(
             #unique = [np.unique, None, None],
             #number_uniq = [len(
             #most = [
             #least = [
 
-#TODO: Multivariate
+        #TODO: Multivariate
         #self.multivariate = dict(
             #corrcoef(x[, y, rowvar, bias]),
             #cov(m[, y, rowvar, bias]),
@@ -101,11 +149,12 @@ def _array_typer(self):
 
     def _is_dtype_like(self, col):
         """
-        Check whether self.dataset.[col][0] behaves like a string, numbern unknown.
-        `numpy.lib._iotools._is_string_like`
+        Check whether self.dataset.[col][0] behaves like a string, numbern
+        unknown. `numpy.lib._iotools._is_string_like`
         """
         def string_like():
-#TODO: not sure what the result is if the first item is some type of missing value
+        #TODO: not sure what the result is if the first item is some type of
+        #      missing value
             try:
                 self.dataset[col][0] + ''
             except (TypeError, ValueError):
@@ -128,30 +177,36 @@ def number_like():
     #@property
     def summary(self, stats='basic', columns='all', orientation='auto'):
         """
-        prints a table of summary statistics and stores the stats.
-        stats: The desired statistics, A list[] or 'basic' or 'all' are options
+        Return a summary of descriptive statistics.
+
+        Parameters
+        -----------
+        stats: list or str
+            The desired statistics, Accepts 'basic' or 'all' or a list.
                'basic' = ('obs', 'mean', 'std', 'min', 'max')
-               'all' = ('obs', 'mean', 'std', 'min', 'max', 'ptp', 'var', 'mode',
-                        'meadian', 'skew', 'uss', 'kurtosis', 'percentiles')
-        Columns: The columns/variables to report the statistics, default is 'all'
-                 structured array: specify the column names
-                                summary(stats='basic', columns=['alpha', 'beta'])
-                standard array: Specifiy column numbers (NEED TO TEST)
-
-        percentiles currently broken
-        mode requires mode_val and mode_bin separately
+               'all' = ('obs', 'mean', 'std', 'min', 'max', 'ptp', 'var',
+                        'mode', 'meadian', 'skew', 'uss', 'kurtosis',
+                        'percentiles')
+        columns : list or str
+          The columns/variables to report the statistics, default is 'all'
+          If an object with named columns is given, you may specify the
+          column names. For example
         """
+        #NOTE
+        # standard array: Specifiy column numbers (NEED TO TEST)
+        # percentiles currently broken
+        # mode requires mode_val and mode_bin separately
         if self._arraytype == None:
             self._array_typer()
 
-
         if stats == 'basic':
             stats = ('obs', 'mean', 'std', 'min', 'max')
         elif stats == 'all':
             #stats = self.univariate.keys()
             #dict doesn't keep an order, use full list instead
-            stats = ['obs', 'mean', 'std', 'min', 'max', 'ptp', 'var', 'mode_val', 'mode_bin',
-                        'median', 'uss', 'skew', 'kurtosis', 'percentiles']
+            stats = ['obs', 'mean', 'std', 'min', 'max', 'ptp', 'var',
+                     'mode_val', 'mode_bin', 'median', 'uss', 'skew',
+                     'kurtosis', 'percentiles']
         else:
             for astat in stats:
                 pass
@@ -162,9 +217,9 @@ def summary(self, stats='basic', columns='all', orientation='auto'):
         #bad naming
         import scipy.stats
         #BUG: the following has all per the same per=99
-##        perdict = dict(('perc_%2d'%per, [lambda x: scipy.stats.scoreatpercentile(x, per),
-##                                         None, None])
-##                        for per in (1,5,10,25,50,75,90,95,99))
+        ##perdict = dict(('perc_%2d'%per, [lambda x:
+         #      scipy.stats.scoreatpercentile(x, per), None, None])
+        ##          for per in (1,5,10,25,50,75,90,95,99))
 
         def _fun(per):
             return lambda x: scipy.stats.scoreatpercentile(x, per)
@@ -179,14 +234,17 @@ def _fun(per):
 
 
 
-        #JP: this doesn't allow a change in sequence, sequence in stats is ignored
+        #JP: this doesn't allow a change in sequence, sequence in stats is
+        #ignored
         #this is just an if condition
-        if any([aitem[1] for aitem in self.univariate.items() if aitem[0] in stats]):
+        if any([aitem[1] for aitem in self.univariate.items() if aitem[0] in
+                stats]):
             if columns == 'all':
                 self._columns_list = []
                 if self._arraytype == 'sctruct':
                     self._columns_list = self.dataset.dtype.names
-                    #self._columns_list = [col for col in self.dataset.dtype.names if
+                    #self._columns_list = [col for col in
+                    #                      self.dataset.dtype.names if
                             #(self._is_dtype_like(col)=='number')]
                 else:
                     self._columns_list = range(self.dataset.shape[1])
@@ -199,26 +257,27 @@ def _fun(per):
                     assert self._is_dtype_like(self.dataset) == 'number'
 
             columstypes = self.dataset.dtype
-#TODO: do we need to make sure they dtype is float64 ?
+            #TODO: do we need to make sure they dtype is float64 ?
             for  astat in stats:
                 calc = self.univariate[astat]
                 if self._arraytype == 'sctruct':
                     calc[1] =  self._columns_list
                     calc[2] = [calc[0](self.dataset[col]) for col in
-                               self._columns_list if (self._is_dtype_like(col) ==
+                            self._columns_list if (self._is_dtype_like(col) ==
                                                       'number')]
                     #calc[2].append([len(np.unique(self.dataset[col])) for col
                                    #in self._columns_list if
                                    #self._is_dtype_like(col)=='string']
                 else:
                     calc[1] = ['Col '+str(col) for col in self._columns_list]
-                    calc[2] = [calc[0](self.dataset[:,col]) for col in self._columns_list]
+                    calc[2] = [calc[0](self.dataset[:,col]) for col in
+                               self._columns_list]
             return self.print_summary(stats, orientation=orientation)
         else:
             return self.print_summary(stats, orientation=orientation)
 
     def print_summary(self, stats, orientation='auto'):
-#TODO: need to specify a table formating for the numbers, using defualt
+        #TODO: need to specify a table formating for the numbers, using defualt
         title = 'Summary Statistics'
         header = stats
         stubs = self.univariate['obs'][1]
@@ -241,114 +300,16 @@ def print_summary(self, stats, orientation='auto'):
         return table
 
 
-    def sign_test(samp,mu0=0):
-        '''
-        Signs test with mu0=0 by default (though
-        the median is often used in practice)
-
-        Parameters
-        ----------
-        samp
-
-        mu0
-
-        Returns
-        ---------
-        M, p-value
-
-        where
-
-        M=(N(+) - N(-))/2, N(+) is the number of values above Mu0,
-        N(-) is the number of values below.  Values equal to Mu0
-        are discarded.
-
-        The p-value for M is calculated using the binomial distrubution
-        and can be intrepreted the same as for a t-test.
-
-        See Also
-        ---------
-        scipy.stats.wilcoxon
-        '''
-        pos=np.sum(samp>mu0)
-        neg=np.sum(samp<mu0)
-        M=(pos-neg)/2.
-        p=stats.binom_test(min(pos,neg),pos+neg,.5)
-        return M, p
-#TODO: There must be a better way but formating the stats of a fuction that
-#      returns 2 values is a problem.
+    def sign_test(self, samp, mu0=0):
+        return sign_test(samp, mu0)
+    sign_test.__doc__ = _sign_test_doc
+    #TODO: There must be a better way but formating the stats of a fuction that
+    #      returns 2 values is a problem.
     #def sign_test_m(samp,mu0=0):
         #return self.sign_test(samp,mu0)[0]
     #def sign_test_p(samp,mu0=0):
         #return self.sign_test(samp,mu0)[1]
 
-########################################
-########################################
-import unittest
-data1 = np.array([(1,2,'a','aa'),
-                  (2,3,'b','bb'),
-                  (2,4,'b','cc')],
-                 dtype = [('alpha',float), ('beta', int),
-                          ('gamma', '|S1'), ('delta', '|S2')])
-data2 = np.array([(1,2),
-                  (2,3),
-                  (2,4)],
-                 dtype = [('alpha',float), ('beta', float)])
-
-data3 = np.array([[1,2,4,4],
-                  [2,3,3,3],
-                  [2,4,4,3]], dtype=float)
-
-data4 = np.array([[1,2,3,4,5,6],
-                  [6,5,4,3,2,1],
-                  [9,9,9,9,9,9]])
-
-class TestSimpleTable(unittest.TestCase):
-    #from statsmodels.iolib.table import SimpleTable, default_txt_fmt
-
-    def test_basic_1(self):
-        print('test_basic_1')
-        t1 = Describe(data1)
-        print(t1.summary())
-
-
-    def test_basic_2(self):
-        print('test_basic_2')
-        t2 = Describe(data2)
-        print(t2.summary())
-
-    def test_basic_3(self):
-        print('test_basic_3')
-        t1 = Describe(data3)
-        print(t1.summary())
-
-    def test_basic_4(self):
-        print('test_basic_4')
-        t1 = Describe(data4)
-        print(t1.summary())
-
-    def test_basic_1a(self):
-        print('test_basic_1a')
-        t1 = Describe(data1)
-        print(t1.summary(stats='basic', columns=['alpha']))
-
-    def test_basic_1b(self):
-        print('test_basic_1b')
-        t1 = Describe(data1)
-        print(t1.summary(stats='basic', columns='all'))
-
-    def test_basic_2a(self):
-        print('test_basic_2a')
-        t2 = Describe(data2)
-        print(t2.summary(stats='all'))
-
-    def test_basic_3(aself):
-        t1 = Describe(data3)
-        print(t1.summary(stats='all'))
-
-    def test_basic_4a(self):
-        t1 = Describe(data4)
-        print(t1.summary(stats='all'))
-
 if __name__ == "__main__":
     #unittest.main()
     t1 = Describe(data4)
@@ -363,3 +324,72 @@ def test_basic_4a(self):
     print(t1.summary(stats=['mean', 'median', 'min', 'max'], orientation=('varcols')))
     print(t1.summary(stats='all'))
 
+
+    import unittest
+    data1 = np.array([(1,2,'a','aa'),
+                      (2,3,'b','bb'),
+                      (2,4,'b','cc')],
+                     dtype = [('alpha',float), ('beta', int),
+                              ('gamma', '|S1'), ('delta', '|S2')])
+    data2 = np.array([(1,2),
+                      (2,3),
+                      (2,4)],
+                     dtype = [('alpha',float), ('beta', float)])
+
+    data3 = np.array([[1,2,4,4],
+                      [2,3,3,3],
+                      [2,4,4,3]], dtype=float)
+
+    data4 = np.array([[1,2,3,4,5,6],
+                      [6,5,4,3,2,1],
+                      [9,9,9,9,9,9]])
+
+    class TestSimpleTable(unittest.TestCase):
+        #from statsmodels.iolib.table import SimpleTable, default_txt_fmt
+
+        def test_basic_1(self):
+            print('test_basic_1')
+            t1 = Describe(data1)
+            print(t1.summary())
+
+
+        def test_basic_2(self):
+            print('test_basic_2')
+            t2 = Describe(data2)
+            print(t2.summary())
+
+        def test_basic_3(self):
+            print('test_basic_3')
+            t1 = Describe(data3)
+            print(t1.summary())
+
+        def test_basic_4(self):
+            print('test_basic_4')
+            t1 = Describe(data4)
+            print(t1.summary())
+
+        def test_basic_1a(self):
+            print('test_basic_1a')
+            t1 = Describe(data1)
+            print(t1.summary(stats='basic', columns=['alpha']))
+
+        def test_basic_1b(self):
+            print('test_basic_1b')
+            t1 = Describe(data1)
+            print(t1.summary(stats='basic', columns='all'))
+
+        def test_basic_2a(self):
+            print('test_basic_2a')
+            t2 = Describe(data2)
+            print(t2.summary(stats='all'))
+
+        def test_basic_3(aself):
+            t1 = Describe(data3)
+            print(t1.summary(stats='all'))
+
+        def test_basic_4a(self):
+            t1 = Describe(data4)
+            print(t1.summary(stats='all'))
+
+
+
diff --git a/statsmodels/tools/decorators.py b/statsmodels/tools/decorators.py
--- a/statsmodels/tools/decorators.py
+++ b/statsmodels/tools/decorators.py
@@ -190,6 +190,14 @@ def __get__(self,obj,type=None):
         setattr(obj, self.name, val)
         return val
 
+try:
+    from nose.tools import nottest
+except ImportError:
+    # make a dummy decorator so people that don't have nose installed
+    # don't get an error
+    def nottest(fn):
+        return fn
+
 
 if __name__ == "__main__":
 ### Tests resettable_cache ----------------------------------------------------


Test patch:
diff --git a/statsmodels/stats/tests/test_descriptivestats.py b/statsmodels/stats/tests/test_descriptivestats.py
new file mode 100644
--- /dev/null
+++ b/statsmodels/stats/tests/test_descriptivestats.py
@@ -0,0 +1,11 @@
+from statsmodels.stats.descriptivestats import sign_test
+from numpy.testing import assert_almost_equal, assert_equal
+
+def test_sign_test():
+    x = [7.8, 6.6, 6.5, 7.4, 7.3, 7., 6.4, 7.1, 6.7, 7.6, 6.8]
+    M, p = sign_test(x, mu0=6.5)
+    # from R SIGN.test(x, md=6.5)
+    # from R
+    assert_almost_equal(p, 0.02148, 5)
+    # not from R, we use a different convention
+    assert_equal(M, 4)