Repository: statsmodels/statsmodels

Issue:
Split `grangercausalitytests()`` into function that runs on only one `lag` and one `type` of test.
The current grangercausalitytests() runs a for-loop over all lag values from 1 to maxlag. This is a new function that accepts a single lag value and only computes the granger causality for that lag-value. Additionally this function accepts a type of test that is to be run. This will allow a user to choose which lag values and types of tests that need to be run.



Discussion:
[![Coverage Status](https://coveralls.io/builds/5511195/badge)](https://coveralls.io/builds/5511195)

Coverage decreased (-0.007%) to 85.371% when pulling **857652c55d3d71c07597296a52bcc4916e6bf2d0 on lababidi:split_granger** into **eed4ae5a90a1d1ac312c24bbebf2bff88fe87da9 on statsmodels:master**.

[![Coverage Status](https://coveralls.io/builds/5539656/badge)](https://coveralls.io/builds/5539656)

Coverage increased (+0.001%) to 85.379% when pulling **250b495431abae918daf139e880349e7cbc8face on lababidi:split_granger** into **eed4ae5a90a1d1ac312c24bbebf2bff88fe87da9 on statsmodels:master**.

[![Coverage Status](https://coveralls.io/builds/5552362/badge)](https://coveralls.io/builds/5552362)

Coverage increased (+0.01%) to 85.389% when pulling **b742a2870b75e7ac145509eb0fd4a8ff19e7f299 on lababidi:split_granger** into **eed4ae5a90a1d1ac312c24bbebf2bff88fe87da9 on statsmodels:master**.

[![Coverage Status](https://coveralls.io/builds/5552515/badge)](https://coveralls.io/builds/5552515)

Coverage increased (+0.01%) to 85.389% when pulling **b742a2870b75e7ac145509eb0fd4a8ff19e7f299 on lababidi:split_granger** into **eed4ae5a90a1d1ac312c24bbebf2bff88fe87da9 on statsmodels:master**.

[![Coverage Status](https://coveralls.io/builds/5552731/badge)](https://coveralls.io/builds/5552731)

Coverage increased (+0.01%) to 85.389% when pulling **b742a2870b75e7ac145509eb0fd4a8ff19e7f299 on lababidi:split_granger** into **eed4ae5a90a1d1ac312c24bbebf2bff88fe87da9 on statsmodels:master**.

There are a number of issues:
1.  The docstring is poorly formatted.  Not quite your fault since the original one is also poorly formatted, but this should be cleaned up here. 
2. Logging code must be removed.  
3. The code is repetitve.  Why not just add one parameter to the existing GC code that would instruct which behavior?  Such as 

```
def grangercausalitytests(x, maxlag, addconst=True, verbose=True, all_lags=True):
        all_lags : bool
            Flag indicating whether to test all lags between 1 and maxlag (True) or only 
            maxlag (False)
```

I also think adding a second function just to select some option is too much code duplication. It should be possible to add this through options to the existing function.

However, I think it would be useful to add a second function that runs many granger causality tests and avoids recreating the lag matrices for each one separately. For example, if we want to run all pairwise granger causality tests for a large number of time series with fixed number of lags.


Resolving patch:
diff --git a/statsmodels/tsa/stattools.py b/statsmodels/tsa/stattools.py
--- a/statsmodels/tsa/stattools.py
+++ b/statsmodels/tsa/stattools.py
@@ -1100,9 +1100,9 @@ def grangercausalitytests(x, maxlag, addconst=True, verbose=True):
     x : array, 2d
         data for test whether the time series in the second column Granger
         causes the time series in the first column
-    maxlag : integer
-        the Granger causality test results are calculated for all lags up to
-        maxlag
+    maxlag : integer, iterable[int]
+        If an integer, computes the test for all lags up to maxlag. If an
+        iterable, computes the tests only for the lags in maxlag.
     addconst : bool
         Include a constant in the model
     verbose : bool
@@ -1136,17 +1136,40 @@ def grangercausalitytests(x, maxlag, addconst=True, verbose=True):
 
     'ssr_chi2test', 'lrtest' are based on chi-square distribution
 
+    Examples
+    --------
+    >>> import statsmodels.api as sm
+    >>> from statsmodels.tsa.stattools import grangercausalitytests
+    >>> import numpy as np
+    >>> data = sm.datasets.macrodata.load_pandas()
+    >>> data = data.data[['realgdp', 'realcons']].pct_change().dropna()
+
+    # All lags up to 4
+    >>> gc_res = grangercausalitytests(data, 4)
+
+    # Only lag 4
+    >>> gc_res = grangercausalitytests(data, [4])
+
     References
     ----------
     https://en.wikipedia.org/wiki/Granger_causality
     Greene: Econometric Analysis
 
     """
-    maxlag = int_like(maxlag, 'maxlag')
+    x = array_like(x, 'x', ndim=2)
     addconst = bool_like(addconst, 'addconst')
     verbose = bool_like(verbose, 'verbose')
-
-    x = array_like(x, 'x', ndim=2)
+    try:
+        lags = np.array([int(lag) for lag in maxlag])
+        maxlag = lags.max()
+        if lags.min() <= 0 or lags.size == 0:
+            raise ValueError('maxlag must be a non-empty list containing only '
+                             'positive integers')
+    except Exception:
+        maxlag = int_like(maxlag, 'maxlag')
+        if maxlag <= 0:
+            raise ValueError('maxlag must a a positive integer')
+        lags = np.arange(1, maxlag + 1)
 
     if x.shape[0] <= 3 * maxlag + int(addconst):
         raise ValueError("Insufficient observations. Maximum allowable "
@@ -1155,7 +1178,7 @@ def grangercausalitytests(x, maxlag, addconst=True, verbose=True):
 
     resli = {}
 
-    for mlg in range(1, maxlag + 1):
+    for mlg in lags:
         result = {}
         if verbose:
             print('\nGranger Causality')
@@ -1165,33 +1188,33 @@ def grangercausalitytests(x, maxlag, addconst=True, verbose=True):
         # create lagmat of both time series
         dta = lagmat2ds(x, mxlg, trim='both', dropex=1)
 
-        #add constant
+        # add constant
         if addconst:
             dtaown = add_constant(dta[:, 1:(mxlg + 1)], prepend=False)
             dtajoint = add_constant(dta[:, 1:], prepend=False)
         else:
             raise NotImplementedError('Not Implemented')
-            #dtaown = dta[:, 1:mxlg]
-            #dtajoint = dta[:, 1:]
+            # dtaown = dta[:, 1:mxlg]
+            # dtajoint = dta[:, 1:]
 
         # Run ols on both models without and with lags of second variable
         res2down = OLS(dta[:, 0], dtaown).fit()
         res2djoint = OLS(dta[:, 0], dtajoint).fit()
 
-        #print results
-        #for ssr based tests see:
-        #http://support.sas.com/rnd/app/examples/ets/granger/index.htm
-        #the other tests are made-up
+        # print results
+        # for ssr based tests see:
+        # http://support.sas.com/rnd/app/examples/ets/granger/index.htm
+        # the other tests are made-up
 
         # Granger Causality test using ssr (F statistic)
         fgc1 = ((res2down.ssr - res2djoint.ssr) /
                 res2djoint.ssr / mxlg * res2djoint.df_resid)
         if verbose:
             print('ssr based F test:         F=%-8.4f, p=%-8.4f, df_denom=%d,'
-                   ' df_num=%d' % (fgc1,
-                                    stats.f.sf(fgc1, mxlg,
-                                               res2djoint.df_resid),
-                                    res2djoint.df_resid, mxlg))
+                  ' df_num=%d' % (fgc1,
+                                  stats.f.sf(fgc1, mxlg,
+                                             res2djoint.df_resid),
+                                  res2djoint.df_resid, mxlg))
         result['ssr_ftest'] = (fgc1,
                                stats.f.sf(fgc1, mxlg, res2djoint.df_resid),
                                res2djoint.df_resid, mxlg)
@@ -1200,14 +1223,14 @@ def grangercausalitytests(x, maxlag, addconst=True, verbose=True):
         fgc2 = res2down.nobs * (res2down.ssr - res2djoint.ssr) / res2djoint.ssr
         if verbose:
             print('ssr based chi2 test:   chi2=%-8.4f, p=%-8.4f, '
-                   'df=%d' % (fgc2, stats.chi2.sf(fgc2, mxlg), mxlg))
+                  'df=%d' % (fgc2, stats.chi2.sf(fgc2, mxlg), mxlg))
         result['ssr_chi2test'] = (fgc2, stats.chi2.sf(fgc2, mxlg), mxlg)
 
-        #likelihood ratio test pvalue:
+        # likelihood ratio test pvalue:
         lr = -2 * (res2down.llf - res2djoint.llf)
         if verbose:
             print('likelihood ratio test: chi2=%-8.4f, p=%-8.4f, df=%d' %
-                   (lr, stats.chi2.sf(lr, mxlg), mxlg))
+                  (lr, stats.chi2.sf(lr, mxlg), mxlg))
         result['lrtest'] = (lr, stats.chi2.sf(lr, mxlg), mxlg)
 
         # F test that all lag coefficients of exog are zero
@@ -1217,8 +1240,8 @@ def grangercausalitytests(x, maxlag, addconst=True, verbose=True):
         ftres = res2djoint.f_test(rconstr)
         if verbose:
             print('parameter F test:         F=%-8.4f, p=%-8.4f, df_denom=%d,'
-                   ' df_num=%d' % (ftres.fvalue, ftres.pvalue, ftres.df_denom,
-                                    ftres.df_num))
+                  ' df_num=%d' % (ftres.fvalue, ftres.pvalue, ftres.df_denom,
+                                  ftres.df_num))
         result['params_ftest'] = (np.squeeze(ftres.fvalue)[()],
                                   np.squeeze(ftres.pvalue)[()],
                                   ftres.df_denom, ftres.df_num)


Test patch:
diff --git a/statsmodels/tsa/tests/test_stattools.py b/statsmodels/tsa/tests/test_stattools.py
--- a/statsmodels/tsa/tests/test_stattools.py
+++ b/statsmodels/tsa/tests/test_stattools.py
@@ -442,13 +442,27 @@ def test_grangercausality(self):
         data = mdata.astype(float)
         data = np.diff(np.log(data), axis=0)
 
-        #R: lmtest:grangertest
+        # R: lmtest:grangertest
         r_result = [0.243097, 0.7844328, 195, 2]  # f_test
         gr = grangercausalitytests(data[:, 1::-1], 2, verbose=False)
         assert_almost_equal(r_result, gr[2][0]['ssr_ftest'], decimal=7)
         assert_almost_equal(gr[2][0]['params_ftest'], gr[2][0]['ssr_ftest'],
                             decimal=7)
 
+    def test_grangercausality_single(self):
+        mdata = macrodata.load_pandas().data
+        mdata = mdata[['realgdp', 'realcons']].values
+        data = mdata.astype(float)
+        data = np.diff(np.log(data), axis=0)
+        gr = grangercausalitytests(data[:, 1::-1], 2, verbose=False)
+        gr2 = grangercausalitytests(data[:, 1::-1], [2], verbose=False)
+        assert 1 in gr
+        assert 1 not in gr2
+        assert_almost_equal(gr[2][0]['ssr_ftest'], gr2[2][0]['ssr_ftest'],
+                            decimal=7)
+        assert_almost_equal(gr[2][0]['params_ftest'], gr2[2][0]['ssr_ftest'],
+                            decimal=7)
+
     def test_granger_fails_on_nobs_check(self, reset_randomstate):
         # Test that if maxlag is too large, Granger Test raises a clear error.
         X = np.random.rand(10, 2)