From a9f9dc4a8ed278dddf2a28f06ad9c3242021efb4 Mon Sep 17 00:00:00 2001 From: steffen911 Date: Mon, 1 Jun 2020 16:40:56 +0200 Subject: [PATCH 01/11] BUG: asymmetric error bars for series (GH9536) --- doc/source/user_guide/visualization.rst | 2 +- doc/source/whatsnew/v1.1.0.rst | 1 + pandas/plotting/_matplotlib/core.py | 16 +++++++++++++++- pandas/tests/plotting/test_series.py | 16 ++++++++++++++++ 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/doc/source/user_guide/visualization.rst b/doc/source/user_guide/visualization.rst index 814627043cfc8..4a526ea5f7dd0 100644 --- a/doc/source/user_guide/visualization.rst +++ b/doc/source/user_guide/visualization.rst @@ -1397,7 +1397,7 @@ Horizontal and vertical error bars can be supplied to the ``xerr`` and ``yerr`` * As a ``str`` indicating which of the columns of plotting :class:`DataFrame` contain the error values. * 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/v1.1.0.rst b/doc/source/whatsnew/v1.1.0.rst index 198cece207ebf..fe3d652e4a68f 100644 --- a/doc/source/whatsnew/v1.1.0.rst +++ b/doc/source/whatsnew/v1.1.0.rst @@ -288,6 +288,7 @@ Other enhancements - :meth:`HDFStore.put` now accepts `track_times` parameter. Parameter is passed to ``create_table`` method of ``PyTables`` (:issue:`32682`). - Make :class:`pandas.core.window.Rolling` and :class:`pandas.core.window.Expanding` iterable(:issue:`11704`) - Make ``option_context`` a :class:`contextlib.ContextDecorator`, which allows it to be used as a decorator over an entire function (:issue:`34253`). +- ``Series.plot`` fix asymmetric error bars (:issue:`9536`) .. --------------------------------------------------------------------------- diff --git a/pandas/plotting/_matplotlib/core.py b/pandas/plotting/_matplotlib/core.py index 1d87c56ab959a..68af421bbcf45 100644 --- a/pandas/plotting/_matplotlib/core.py +++ b/pandas/plotting/_matplotlib/core.py @@ -754,6 +754,12 @@ def _parse_errorbars(self, label, err): 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 + + 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. """ if err is None: return None @@ -794,7 +800,15 @@ def match_labels(data, e): err_shape = err.shape # asymmetrical error bars - if err.ndim == 3: + if isinstance(self.data, ABCSeries) and err_shape[0] == 2: + err = np.expand_dims(err, 0) + err_shape = err.shape + if err_shape[2] != len(self.data): + raise ValueError( + "Asymmetrical error bars should be provided " + f"with the shape (2, {len(self.data)})" + ) + if isinstance(self.data, ABCDataFrame) and err.ndim == 3: if ( (err_shape[0] != self.nseries) or (err_shape[1] != 2) diff --git a/pandas/tests/plotting/test_series.py b/pandas/tests/plotting/test_series.py index 5341878d4986e..8a0c4b69b3077 100644 --- a/pandas/tests/plotting/test_series.py +++ b/pandas/tests/plotting/test_series.py @@ -729,6 +729,22 @@ def test_dup_datetime_index_plot(self): s = Series(values, index=index) _check_plot_works(s.plot) + def test_errorbar_asymmetrical(self): + # GH9536 + s = Series(np.arange(10), name="x") + err = np.random.rand(2, 10) + + ax = s.plot(yerr=err, xerr=err) + + yerr_0_0 = ax.collections[1].get_paths()[0].vertices[:, 1] + expected_0_0 = err[:, 0] * np.array([-1, 1]) + tm.assert_almost_equal(yerr_0_0, expected_0_0) + + with pytest.raises(ValueError): + s.plot(yerr=err.T) + + tm.close() + @pytest.mark.slow def test_errorbar_plot(self): From 0ea50d35ec302c0015c9cabedb77b8b91b1a41df Mon Sep 17 00:00:00 2001 From: steffen911 Date: Mon, 1 Jun 2020 18:25:10 +0200 Subject: [PATCH 02/11] feedback: verify error message --- pandas/tests/plotting/test_series.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pandas/tests/plotting/test_series.py b/pandas/tests/plotting/test_series.py index 8a0c4b69b3077..760c1eb0e56a4 100644 --- a/pandas/tests/plotting/test_series.py +++ b/pandas/tests/plotting/test_series.py @@ -740,8 +740,11 @@ def test_errorbar_asymmetrical(self): expected_0_0 = err[:, 0] * np.array([-1, 1]) tm.assert_almost_equal(yerr_0_0, expected_0_0) - with pytest.raises(ValueError): - s.plot(yerr=err.T) + msg = ( + f"Asymmetrical error bars should be provided with the shape \(2, {len(s)}\)" + ) + with pytest.raises(ValueError, match=msg): + s.plot(yerr=np.random.rand(2, 11)) tm.close() From 4a2eb9cbb4753ed6b1fc2c9e14fe4ef3b4fc6f18 Mon Sep 17 00:00:00 2001 From: steffen911 Date: Mon, 1 Jun 2020 18:25:29 +0200 Subject: [PATCH 03/11] feedback: explain previous and current behavior --- doc/source/whatsnew/v1.1.0.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v1.1.0.rst b/doc/source/whatsnew/v1.1.0.rst index fe3d652e4a68f..e7ac203e79c0e 100644 --- a/doc/source/whatsnew/v1.1.0.rst +++ b/doc/source/whatsnew/v1.1.0.rst @@ -288,7 +288,9 @@ Other enhancements - :meth:`HDFStore.put` now accepts `track_times` parameter. Parameter is passed to ``create_table`` method of ``PyTables`` (:issue:`32682`). - Make :class:`pandas.core.window.Rolling` and :class:`pandas.core.window.Expanding` iterable(:issue:`11704`) - Make ``option_context`` a :class:`contextlib.ContextDecorator`, which allows it to be used as a decorator over an entire function (:issue:`34253`). -- ``Series.plot`` fix asymmetric error bars (:issue:`9536`) +- :meth:`Series.plot` now supports asymmetric error bars. Previously, if :meth:`Series.plot` received a "2xN" array with + error values for `yerr` and/or `xerr`, the left/lower values (first row) would be mirrored, while the right/upper values (second row) are + ignored. Now, the first row represents the left/lower error values and the second row the right/upper error values. (:issue:`9536`) .. --------------------------------------------------------------------------- From 99b51fc91279a486b9fe56eab6df6f93f40d292f Mon Sep 17 00:00:00 2001 From: steffen911 Date: Mon, 1 Jun 2020 18:27:25 +0200 Subject: [PATCH 04/11] chore: update escape sequence --- pandas/tests/plotting/test_series.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/tests/plotting/test_series.py b/pandas/tests/plotting/test_series.py index 760c1eb0e56a4..6c12dba88a1d0 100644 --- a/pandas/tests/plotting/test_series.py +++ b/pandas/tests/plotting/test_series.py @@ -741,7 +741,7 @@ def test_errorbar_asymmetrical(self): tm.assert_almost_equal(yerr_0_0, expected_0_0) msg = ( - f"Asymmetrical error bars should be provided with the shape \(2, {len(s)}\)" + f"Asymmetrical error bars should be provided with the shape \\(2, {len(s)}\\)" ) with pytest.raises(ValueError, match=msg): s.plot(yerr=np.random.rand(2, 11)) From 4a277e5acd1b40dfe5b0dcc468a221a928792dcf Mon Sep 17 00:00:00 2001 From: steffen911 Date: Mon, 1 Jun 2020 18:30:56 +0200 Subject: [PATCH 05/11] chore: pep8 --- pandas/tests/plotting/test_series.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pandas/tests/plotting/test_series.py b/pandas/tests/plotting/test_series.py index 6c12dba88a1d0..036e18b216941 100644 --- a/pandas/tests/plotting/test_series.py +++ b/pandas/tests/plotting/test_series.py @@ -741,7 +741,8 @@ def test_errorbar_asymmetrical(self): tm.assert_almost_equal(yerr_0_0, expected_0_0) msg = ( - f"Asymmetrical error bars should be provided with the shape \\(2, {len(s)}\\)" + "Asymmetrical error bars should be provided " + f"with the shape \\(2, {len(s)}\\)" ) with pytest.raises(ValueError, match=msg): s.plot(yerr=np.random.rand(2, 11)) From 2975406501f8942ded75091594f094e8d3095a2a Mon Sep 17 00:00:00 2001 From: steffen911 Date: Sun, 7 Jun 2020 15:53:56 +0200 Subject: [PATCH 06/11] feedback: verify error bars completely --- pandas/tests/plotting/test_series.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pandas/tests/plotting/test_series.py b/pandas/tests/plotting/test_series.py index 036e18b216941..ee3f8f96587e6 100644 --- a/pandas/tests/plotting/test_series.py +++ b/pandas/tests/plotting/test_series.py @@ -736,9 +736,9 @@ def test_errorbar_asymmetrical(self): ax = s.plot(yerr=err, xerr=err) - yerr_0_0 = ax.collections[1].get_paths()[0].vertices[:, 1] - expected_0_0 = err[:, 0] * np.array([-1, 1]) - tm.assert_almost_equal(yerr_0_0, expected_0_0) + result = np.vstack([i.vertices[:, 1] for i in ax.collections[1].get_paths()]) + expected = (err.T * np.array([-1, 1])) + list(zip(s, s)) + tm.assert_almost_equal(result, expected) msg = ( "Asymmetrical error bars should be provided " From fa4915b7e39251b6ad6d19cdd6355009f94bce80 Mon Sep 17 00:00:00 2001 From: steffen911 Date: Sun, 7 Jun 2020 15:59:13 +0200 Subject: [PATCH 07/11] feedback: update tenses and inline changelog --- doc/source/whatsnew/v1.1.0.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/doc/source/whatsnew/v1.1.0.rst b/doc/source/whatsnew/v1.1.0.rst index cea0d6468e894..37c26f8e19547 100644 --- a/doc/source/whatsnew/v1.1.0.rst +++ b/doc/source/whatsnew/v1.1.0.rst @@ -291,9 +291,7 @@ Other enhancements - :meth:`groupby.transform` now allows ``func`` to be ``pad``, ``backfill`` and ``cumcount`` (:issue:`31269`). - :meth:`~pandas.io.json.read_json` now accepts `nrows` parameter. (:issue:`33916`). - :meth `~pandas.io.gbq.read_gbq` now allows to disable progress bar (:issue:`33360`). -- :meth:`Series.plot` now supports asymmetric error bars. Previously, if :meth:`Series.plot` received a "2xN" array with - error values for `yerr` and/or `xerr`, the left/lower values (first row) would be mirrored, while the right/upper values (second row) are - ignored. Now, the first row represents the left/lower error values and the second row the right/upper error values. (:issue:`9536`) +- :meth:`Series.plot` now supports asymmetric error bars. Previously, if :meth:`Series.plot` received a "2xN" array with error values for `yerr` and/or `xerr`, the left/lower values (first row) were mirrored, while the right/upper values (second row) were ignored. Now, the first row represents the left/lower error values and the second row the right/upper error values. (:issue:`9536`) .. --------------------------------------------------------------------------- From 4f430b3feafc948875783f847c0ba0a1d64692e1 Mon Sep 17 00:00:00 2001 From: steffen911 Date: Fri, 12 Jun 2020 21:14:40 +0200 Subject: [PATCH 08/11] feedback: use assert_numpy_array_equal for comparison --- pandas/tests/plotting/test_series.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/tests/plotting/test_series.py b/pandas/tests/plotting/test_series.py index ee3f8f96587e6..d42ab83d52cd8 100644 --- a/pandas/tests/plotting/test_series.py +++ b/pandas/tests/plotting/test_series.py @@ -738,7 +738,7 @@ def test_errorbar_asymmetrical(self): result = np.vstack([i.vertices[:, 1] for i in ax.collections[1].get_paths()]) expected = (err.T * np.array([-1, 1])) + list(zip(s, s)) - tm.assert_almost_equal(result, expected) + tm.assert_numpy_array_equal(result, expected) msg = ( "Asymmetrical error bars should be provided " From 67b534d26acd5c722e561c3739a4ee91a6550736 Mon Sep 17 00:00:00 2001 From: steffen911 Date: Fri, 12 Jun 2020 21:20:21 +0200 Subject: [PATCH 09/11] feedback: use numpy syntax for expectation construction --- pandas/tests/plotting/test_series.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/tests/plotting/test_series.py b/pandas/tests/plotting/test_series.py index d42ab83d52cd8..24de168cd93a0 100644 --- a/pandas/tests/plotting/test_series.py +++ b/pandas/tests/plotting/test_series.py @@ -737,7 +737,7 @@ def test_errorbar_asymmetrical(self): ax = s.plot(yerr=err, xerr=err) result = np.vstack([i.vertices[:, 1] for i in ax.collections[1].get_paths()]) - expected = (err.T * np.array([-1, 1])) + list(zip(s, s)) + expected = (err.T * np.array([-1, 1])) + s.to_numpy().reshape(-1, 1) tm.assert_numpy_array_equal(result, expected) msg = ( From 01bfa7e85c6e35969495109e7d0620e54fc60886 Mon Sep 17 00:00:00 2001 From: Steffen Schmitz Date: Fri, 26 Jun 2020 15:35:53 +0200 Subject: [PATCH 10/11] feedback: use elif Co-authored-by: Marc Garcia --- pandas/plotting/_matplotlib/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/plotting/_matplotlib/core.py b/pandas/plotting/_matplotlib/core.py index c976dfb9abe08..500271ef08a46 100644 --- a/pandas/plotting/_matplotlib/core.py +++ b/pandas/plotting/_matplotlib/core.py @@ -809,7 +809,7 @@ def match_labels(data, e): "Asymmetrical error bars should be provided " f"with the shape (2, {len(self.data)})" ) - if isinstance(self.data, ABCDataFrame) and err.ndim == 3: + elif isinstance(self.data, ABCDataFrame) and err.ndim == 3: if ( (err_shape[0] != self.nseries) or (err_shape[1] != 2) From 26ed805ab9e197b4a2798c36785e9a8cc3b9579f Mon Sep 17 00:00:00 2001 From: steffen911 Date: Fri, 17 Jul 2020 15:02:58 +0200 Subject: [PATCH 11/11] chore: update whatsnew --- doc/source/whatsnew/v1.1.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v1.1.0.rst b/doc/source/whatsnew/v1.1.0.rst index 3924c00c9d640..d74c1bca61de8 100644 --- a/doc/source/whatsnew/v1.1.0.rst +++ b/doc/source/whatsnew/v1.1.0.rst @@ -340,7 +340,7 @@ Other enhancements - :class:`pandas.core.window.ExponentialMovingWindow` now supports a ``times`` argument that allows ``mean`` to be calculated with observations spaced by the timestamps in ``times`` (:issue:`34839`) - :meth:`DataFrame.agg` and :meth:`Series.agg` now accept named aggregation for renaming the output columns/indexes. (:issue:`26513`) - ``compute.use_numba`` now exists as a configuration option that utilizes the numba engine when available (:issue:`33966`) -- :meth:`DataFrame.agg` and :meth:`Series.agg` now accept named aggregation for renaming the output columns/indexes. (:issue:`26513`) +- :meth:`Series.plot` now supports asymmetric error bars. Previously, if :meth:`Series.plot` received a "2xN" array with error values for `yerr` and/or `xerr`, the left/lower values (first row) were mirrored, while the right/upper values (second row) were ignored. Now, the first row represents the left/lower error values and the second row the right/upper error values. (:issue:`9536`) .. ---------------------------------------------------------------------------