Repository: statsmodels/statsmodels

Issue:
plot_{,p}acf extra arguments
Currently, extra arguments to `plot_{,p}acf` are passed directly to the plotting functions, `plot` and `axhline`, as documented.  However, they are also passed to `vlines`, which accepts a different set of keyword arguments (`LineCollection` properties rather than `Line2D` properties), so e.g. something as simple `ls="-"` fails.
In fact, even with `use_vlines=False`, `ls="-"` can fail for a different and more subtle reason: if you start Python3 a couple of times until `list({"ls", "linestyle"}) == ['ls', 'linestyle']` (which depends on hash randomization) and try again setting `ls="-"` (with `use_vlines=False`), this will seemingly have no effect on the plot (i.e. it'll use dots instead of a connected line).  This is because `plot_{,p}acf` calls `kwargs.setdefault("linestyle", "None")` and in this case, the latter entry (as decided by hash ordering) wins.



Discussion:
@anntzer Thanks for the feedback.
I'll try to look at it later today.  Using the same keyword options in two different, incompatible plot functions sounds wrong to me.
Are there changes in matplotlib that might have changed the behavior? Or maybe it's just that nobody reported it yet.


Resolving patch:
diff --git a/statsmodels/graphics/tsaplots.py b/statsmodels/graphics/tsaplots.py
--- a/statsmodels/graphics/tsaplots.py
+++ b/statsmodels/graphics/tsaplots.py
@@ -23,19 +23,22 @@ def _prepare_data_corr_plot(x, lags, zero):
     return lags, nlags, irregular
 
 
-def _plot_corr(ax, title, acf_x, confint, lags, irregular, use_vlines, **kwargs):
+def _plot_corr(ax, title, acf_x, confint, lags, irregular, use_vlines,
+               vlines_kwargs, **kwargs):
     if irregular:
         acf_x = acf_x[lags]
         if confint is not None:
             confint = confint[lags]
 
     if use_vlines:
-        ax.vlines(lags, [0], acf_x, **kwargs)
+        ax.vlines(lags, [0], acf_x, **vlines_kwargs)
         ax.axhline(**kwargs)
 
     kwargs.setdefault('marker', 'o')
     kwargs.setdefault('markersize', 5)
-    kwargs.setdefault('linestyle', 'None')
+    if 'ls' not in kwargs:
+        # gh-2369
+        kwargs.setdefault('linestyle', 'None')
     ax.margins(.05)
     ax.plot(lags, acf_x, **kwargs)
     ax.set_title(title)
@@ -49,7 +52,8 @@ def _plot_corr(ax, title, acf_x, confint, lags, irregular, use_vlines, **kwargs)
 
 
 def plot_acf(x, ax=None, lags=None, alpha=.05, use_vlines=True, unbiased=False,
-             fft=False, title='Autocorrelation', zero=True, **kwargs):
+             fft=False, title='Autocorrelation', zero=True,
+             vlines_kwargs=None, **kwargs):
     """Plot the autocorrelation function
 
     Plots lags on the horizontal and the correlations on vertical axis.
@@ -83,6 +87,8 @@ def plot_acf(x, ax=None, lags=None, alpha=.05, use_vlines=True, unbiased=False,
     zero : bool, optional
         Flag indicating whether to include the 0-lag autocorrelation.
         Default is True.
+    vlines_kwargs : dict, optional
+        Optional dictionary of keyword arguments that are passed to vlines.
     **kwargs : kwargs, optional
         Optional keyword arguments that are directly passed on to the
         Matplotlib ``plot`` and ``axhline`` functions.
@@ -105,10 +111,18 @@ def plot_acf(x, ax=None, lags=None, alpha=.05, use_vlines=True, unbiased=False,
 
     Data are plotted as ``plot(lags, corr, **kwargs)``
 
+    kwargs is used to pass matplotlib optional arguments to both the line
+    tracing the autocorrelations and for the horizontal line at 0. These
+    options must be valid for a Line2D object.
+
+    vlines_kwargs is used to pass additional optional arguments to the
+    vertical lines connecting each autocorrelation to the axis.  These options
+    must be valid for a LineCollection object.
     """
     fig, ax = utils.create_mpl_ax(ax)
 
     lags, nlags, irregular = _prepare_data_corr_plot(x, lags, zero)
+    vlines_kwargs = {} if vlines_kwargs is None else vlines_kwargs
 
     confint = None
     # acf has different return type based on alpha
@@ -119,14 +133,15 @@ def plot_acf(x, ax=None, lags=None, alpha=.05, use_vlines=True, unbiased=False,
         acf_x, confint = acf(x, nlags=nlags, alpha=alpha, fft=fft,
                              unbiased=unbiased)
 
-    _plot_corr(ax, title, acf_x, confint, lags, irregular, use_vlines, **kwargs)
+    _plot_corr(ax, title, acf_x, confint, lags, irregular, use_vlines,
+               vlines_kwargs, **kwargs)
 
     return fig
 
 
 def plot_pacf(x, ax=None, lags=None, alpha=.05, method='ywunbiased',
               use_vlines=True, title='Partial Autocorrelation', zero=True,
-              **kwargs):
+              vlines_kwargs=None, **kwargs):
     """
     Plot the partial autocorrelation function
 
@@ -164,6 +179,8 @@ def plot_pacf(x, ax=None, lags=None, alpha=.05, method='ywunbiased',
     zero : bool, optional
         Flag indicating whether to include the 0-lag autocorrelation.
         Default is True.
+    vlines_kwargs : dict, optional
+        Optional dictionary of keyword arguments that are passed to vlines.
     **kwargs : kwargs, optional
         Optional keyword arguments that are directly passed on to the
         Matplotlib ``plot`` and ``axhline`` functions.
@@ -186,9 +203,17 @@ def plot_pacf(x, ax=None, lags=None, alpha=.05, method='ywunbiased',
     Adapted from matplotlib's `xcorr`.
 
     Data are plotted as ``plot(lags, corr, **kwargs)``
+
+    kwargs is used to pass matplotlib optional arguments to both the line
+    tracing the autocorrelations and for the horizontal line at 0. These
+    options must be valid for a Line2D object.
+
+    vlines_kwargs is used to pass additional optional arguments to the
+    vertical lines connecting each autocorrelation to the axis.  These options
+    must be valid for a LineCollection object.
     """
     fig, ax = utils.create_mpl_ax(ax)
-
+    vlines_kwargs = {} if vlines_kwargs is None else vlines_kwargs
     lags, nlags, irregular = _prepare_data_corr_plot(x, lags, zero)
 
     confint = None
@@ -197,7 +222,8 @@ def plot_pacf(x, ax=None, lags=None, alpha=.05, method='ywunbiased',
     else:
         acf_x, confint = pacf(x, nlags=nlags, alpha=alpha, method=method)
 
-    _plot_corr(ax, title, acf_x, confint, lags, irregular, use_vlines, **kwargs)
+    _plot_corr(ax, title, acf_x, confint, lags, irregular, use_vlines,
+               vlines_kwargs, **kwargs)
 
     return fig
 


Test patch:
diff --git a/statsmodels/graphics/tests/test_tsaplots.py b/statsmodels/graphics/tests/test_tsaplots.py
--- a/statsmodels/graphics/tests/test_tsaplots.py
+++ b/statsmodels/graphics/tests/test_tsaplots.py
@@ -1,10 +1,10 @@
-from statsmodels.compat.python import lmap
+from statsmodels.compat.python import lmap, BytesIO
 
 from distutils.version import LooseVersion
 
 import numpy as np
 import pandas as pd
-from numpy.testing import dec, assert_equal
+from numpy.testing import assert_equal, assert_
 from statsmodels.compat.testing import skipif
 
 import statsmodels.api as sm
@@ -75,6 +75,83 @@ def test_plot_pacf():
     plt.close(fig)
 
 
+@skipif(not have_matplotlib, reason='matplotlib not available')
+def test_plot_pacf_kwargs():
+    # Just test that it runs.
+    fig = plt.figure()
+    ax = fig.add_subplot(111)
+
+    ar = np.r_[1., -0.9]
+    ma = np.r_[1., 0.9]
+    armaprocess = tsp.ArmaProcess(ar, ma)
+    rs = np.random.RandomState(1234)
+    pacf = armaprocess.generate_sample(100, distrvs=rs.standard_normal)
+
+    buff = BytesIO()
+    plot_pacf(pacf, ax=ax)
+    fig.savefig(buff, format='rgba')
+    plt.close(fig)
+
+    buff_linestyle = BytesIO()
+    fig_linestyle = plt.figure()
+    ax = fig_linestyle.add_subplot(111)
+    plot_pacf(pacf, ax=ax, ls='-')
+    fig_linestyle.savefig(buff_linestyle, format='rgba')
+    plt.close(fig_linestyle)
+
+    buff_with_vlines = BytesIO()
+    fig_with_vlines = plt.figure()
+    ax = fig_with_vlines.add_subplot(111)
+    vlines_kwargs = {'linestyles': 'dashdot'}
+    plot_pacf(pacf, ax=ax, vlines_kwargs=vlines_kwargs)
+    fig_with_vlines.savefig(buff_with_vlines, format='rgba')
+    plt.close(fig_with_vlines)
+
+    buff.seek(0)
+    buff_linestyle.seek(0)
+    buff_with_vlines.seek(0)
+    plain = buff.read()
+    linestyle = buff_linestyle.read()
+    with_vlines = buff_with_vlines.read()
+
+    assert_(plain != linestyle)
+    assert_(with_vlines != plain)
+    assert_(linestyle != with_vlines)
+
+
+@skipif(not have_matplotlib, reason='matplotlib not available')
+def test_plot_acf_kwargs():
+    # Just test that it runs.
+    fig = plt.figure()
+    ax = fig.add_subplot(111)
+
+    ar = np.r_[1., -0.9]
+    ma = np.r_[1., 0.9]
+    armaprocess = tsp.ArmaProcess(ar, ma)
+    rs = np.random.RandomState(1234)
+    acf = armaprocess.generate_sample(100, distrvs=rs.standard_normal)
+
+    buff = BytesIO()
+    plot_acf(acf, ax=ax)
+    fig.savefig(buff, format='rgba')
+    plt.close(fig)
+
+    buff_with_vlines = BytesIO()
+    fig_with_vlines = plt.figure()
+    ax = fig_with_vlines.add_subplot(111)
+    vlines_kwargs = {'linestyles': 'dashdot'}
+    plot_acf(acf, ax=ax, vlines_kwargs=vlines_kwargs)
+    fig_with_vlines.savefig(buff_with_vlines, format='rgba')
+    plt.close(fig_with_vlines)
+
+    buff.seek(0)
+    buff_with_vlines.seek(0)
+    plain = buff.read()
+    with_vlines = buff_with_vlines.read()
+
+    assert_(with_vlines != plain)
+
+
 @skipif(not have_matplotlib, reason='matplotlib not available')
 def test_plot_pacf_irregular():
     # Just test that it runs.