From 89f3d984ca01100c039cece36aee20569fa60483 Mon Sep 17 00:00:00 2001 From: Johannes Dillmann Date: Fri, 15 Jan 2016 14:47:54 +0100 Subject: [PATCH] Fix asymmetric error bars for series (closes #9536) This fix is for handling asymmetric error bars for series. It adapts to the syntax for http://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.errorbar where a sequence of shape 2xN is expected in case of asymmetric error bars. If a single series is to be plotted and the error sequence is of shape 2xN it will be used as asymmetric error bars. Previously a 2xN error sequence was assumed to be 2 symmetric error sequences for 2 series. Thus in the end only the first error sequence was used. This commit improves the docstring of the `_parse_errorbars` method as well as the general pandas doc on error bars. --- doc/source/visualization.rst | 3 ++- doc/source/whatsnew/v0.18.1.txt | 9 ++++---- pandas/tests/test_graphics.py | 14 ++++++++++++ pandas/tools/plotting.py | 40 ++++++++++++++++++++++++--------- 4 files changed, 50 insertions(+), 16 deletions(-) diff --git a/doc/source/visualization.rst b/doc/source/visualization.rst index 7840ae29298b0..86fefa04e9619 100644 --- a/doc/source/visualization.rst +++ b/doc/source/visualization.rst @@ -1366,9 +1366,10 @@ Horizontal and vertical errorbars can be supplied to the ``xerr`` and ``yerr`` k - As a :class:`DataFrame` or ``dict`` of errors with column names matching the ``columns`` attribute of the plotting :class:`DataFrame` or matching the ``name`` attribute of the :class:`Series` - As a ``str`` indicating which of the columns of plotting :class:`DataFrame` contain the error values +- As a single ``number`` which is used as the error for every value - As raw values (``list``, ``tuple``, or ``np.ndarray``). Must be the same length as the plotting :class:`DataFrame`/:class:`Series` -Asymmetrical error bars are also supported, however raw error values must be provided in this case. For a ``M`` length :class:`Series`, a ``Mx2`` array should be provided indicating lower and upper (or left and right) errors. For a ``MxN`` :class:`DataFrame`, asymmetrical errors should be in a ``Mx2xN`` array. +Asymmetrical error bars are also supported, however raw error values must be provided in this case. For a ``N`` length :class:`Series`, a ``2xN`` array should be provided indicating lower and upper (or left and right) errors. For a ``MxN`` :class:`DataFrame`, asymmetrical errors should be in a ``Mx2xN`` array. Here is an example of one way to easily plot group means with standard deviations from the raw data. diff --git a/doc/source/whatsnew/v0.18.1.txt b/doc/source/whatsnew/v0.18.1.txt index 152187f1e6681..d9e5b3985403f 100644 --- a/doc/source/whatsnew/v0.18.1.txt +++ b/doc/source/whatsnew/v0.18.1.txt @@ -60,12 +60,13 @@ Other Enhancements - ``pd.read_msgpack()`` now always gives writeable ndarrays even when compression is used (:issue:`12359`). - ``Index.take`` now handles ``allow_fill`` and ``fill_value`` consistently (:issue:`12631`) -.. ipython:: python + .. ipython:: python - idx = pd.Index([1., 2., 3., 4.], dtype='float') - idx.take([2, -1]) # default, allow_fill=True, fill_value=None - idx.take([2, -1], fill_value=True) + idx = pd.Index([1., 2., 3., 4.], dtype='float') + idx.take([2, -1]) # default, allow_fill=True, fill_value=None + idx.take([2, -1], fill_value=True) +- ``Series.plot`` allows now asymmetric error bars in the shape of 2xN array (:issue:`9536`) .. _whatsnew_0181.api: diff --git a/pandas/tests/test_graphics.py b/pandas/tests/test_graphics.py index 45d3fd0dad855..3c22d3f925618 100644 --- a/pandas/tests/test_graphics.py +++ b/pandas/tests/test_graphics.py @@ -1180,6 +1180,20 @@ def test_errorbar_plot(self): with tm.assertRaises((ValueError, TypeError)): s.plot(yerr=s_err) + def test_errorbar_asymmetrical(self): + # github issue #9536 + s = Series(np.random.randn(5)) + err = np.random.rand(2, 5) + + ax = _check_plot_works(s.plot, yerr=err, xerr=(err / 2)) + self._check_has_errorbars(ax, yerr=1, xerr=1) + + assert_allclose(ax.lines[2].get_ydata(), s.values - err[0]) + assert_allclose(ax.lines[3].get_ydata(), s.values + err[1]) + + assert_allclose(ax.lines[0].get_xdata(), s.index - (err[0] / 2)) + assert_allclose(ax.lines[1].get_xdata(), s.index + (err[1] / 2)) + def test_table(self): _check_plot_works(self.series.plot, table=True) _check_plot_works(self.series.plot, table=self.series) diff --git a/pandas/tools/plotting.py b/pandas/tools/plotting.py index 103b7484ea138..7758ae75e139a 100644 --- a/pandas/tools/plotting.py +++ b/pandas/tools/plotting.py @@ -1419,10 +1419,17 @@ def _parse_errorbars(self, label, err): Error bars can be specified in several ways: Series: the user provides a pandas.Series object of the same length as the data - ndarray: provides a np.ndarray of the same length as the data + list_like (list/tuple/ndarray/iterator): either a list like of the + same length N as the data has to be provided + or a list like of the shape Mx2xN for asymmetrical error + bars when plotting a DataFrame of shape MxN + or a list like of the shape 2xN for asymmetrical error bars + when plotting a Series. DataFrame/dict: error values are paired with keys matching the key in the plotted DataFrame str: the name of the column within the plotted DataFrame + numeric scalar: the error provided as a number is used for every + data point ''' if err is None: @@ -1458,22 +1465,33 @@ def match_labels(data, e): elif com.is_list_like(err): if com.is_iterator(err): - err = np.atleast_2d(list(err)) + err = np.asanyarray(list(err)) else: # raw error values - err = np.atleast_2d(err) + err = np.asanyarray(err) - err_shape = err.shape + if self.nseries == 1 and err.ndim == 2 and len(err) == 2: + # asymmetrical errors bars for a series as a 2xN array + err = np.expand_dims(err, 0) + err_shape = err.shape - # asymmetrical error bars - if err.ndim == 3: - if (err_shape[0] != self.nseries) or \ - (err_shape[1] != 2) or \ - (err_shape[2] != len(self.data)): + if err_shape[2] != len(self.data): msg = "Asymmetrical error bars should be provided " + \ - "with the shape (%u, 2, %u)" % \ - (self.nseries, len(self.data)) + "with the shape (2, %u)" % (len(self.data)) raise ValueError(msg) + else: + err = np.atleast_2d(err) + err_shape = err.shape + + # asymmetrical error bars + if err.ndim == 3: + if (err_shape[0] != self.nseries) or \ + (err_shape[1] != 2) or \ + (err_shape[2] != len(self.data)): + msg = "Asymmetrical error bars should be provided " + \ + "with the shape (%u, 2, %u)" % \ + (self.nseries, len(self.data)) + raise ValueError(msg) # broadcast errors to each data series if len(err) == 1: