diff --git a/doc/source/whatsnew/v0.23.0.txt b/doc/source/whatsnew/v0.23.0.txt index acbfa2eb3ccac..3cef716069ba2 100644 --- a/doc/source/whatsnew/v0.23.0.txt +++ b/doc/source/whatsnew/v0.23.0.txt @@ -986,6 +986,7 @@ Plotting ^^^^^^^^ - :func:`DataFrame.plot` now raises a ``ValueError`` when the ``x`` or ``y`` argument is improperly formed (:issue:`18671`) +- Bug in :func:`DataFrame.plot` when ``x`` and ``y`` arguments given as positions caused incorrect referenced columns for line, bar and area plots (:issue:`20056`) - Bug in formatting tick labels with ``datetime.time()`` and fractional seconds (:issue:`18478`). - :meth:`Series.plot.kde` has exposed the args ``ind`` and ``bw_method`` in the docstring (:issue:`18461`). The argument ``ind`` may now also be an integer (number of sample points). @@ -1041,3 +1042,4 @@ Other - Improved error message when attempting to use a Python keyword as an identifier in a ``numexpr`` backed query (:issue:`18221`) - Bug in accessing a :func:`pandas.get_option`, which raised ``KeyError`` rather than ``OptionError`` when looking up a non-existant option key in some cases (:issue:`19789`) +- :func:`DataFrame.plot` now supports multiple columns to the ``y`` argument (:issue:`19699`) diff --git a/pandas/plotting/_core.py b/pandas/plotting/_core.py index da7c58428fb54..21b69522523cc 100644 --- a/pandas/plotting/_core.py +++ b/pandas/plotting/_core.py @@ -1751,22 +1751,22 @@ def _plot(data, x=None, y=None, subplots=False, plot_obj = klass(data, subplots=subplots, ax=ax, kind=kind, **kwds) else: if isinstance(data, ABCDataFrame): + data_cols = data.columns if x is not None: if is_integer(x) and not data.columns.holds_integer(): - x = data.columns[x] + x = data_cols[x] elif not isinstance(data[x], ABCSeries): raise ValueError("x must be a label or position") data = data.set_index(x) if y is not None: - if is_integer(y) and not data.columns.holds_integer(): - y = data.columns[y] - elif not isinstance(data[y], ABCSeries): - raise ValueError("y must be a label or position") - label = kwds['label'] if 'label' in kwds else y - series = data[y].copy() # Don't modify - series.name = label + # check if we have y as int or list of ints + int_ylist = is_list_like(y) and all(is_integer(c) for c in y) + int_y_arg = is_integer(y) or int_ylist + if int_y_arg and not data.columns.holds_integer(): + y = data_cols[y] + label_kw = kwds['label'] if 'label' in kwds else False for kw in ['xerr', 'yerr']: if (kw in kwds) and \ (isinstance(kwds[kw], string_types) or @@ -1775,7 +1775,22 @@ def _plot(data, x=None, y=None, subplots=False, kwds[kw] = data[kwds[kw]] except (IndexError, KeyError, TypeError): pass - data = series + + # don't overwrite + data = data[y].copy() + + if isinstance(data, ABCSeries): + label_name = label_kw or y + data.name = label_name + else: + match = is_list_like(label_kw) and len(label_kw) == len(y) + if label_kw and not match: + raise ValueError( + "label should be list-like and same length as y" + ) + label_name = label_kw or data.columns + data.columns = label_name + plot_obj = klass(data, subplots=subplots, ax=ax, kind=kind, **kwds) plot_obj.generate() @@ -1788,7 +1803,7 @@ def _plot(data, x=None, y=None, subplots=False, series_kind = "" df_coord = """x : label or position, default None - y : label or position, default None + y : label, position or list of label, positions, default None Allows plotting of one column versus another""" series_coord = "" diff --git a/pandas/tests/plotting/test_frame.py b/pandas/tests/plotting/test_frame.py index b29afcb404ac6..ac02f5f4e4283 100644 --- a/pandas/tests/plotting/test_frame.py +++ b/pandas/tests/plotting/test_frame.py @@ -2170,26 +2170,51 @@ def test_invalid_kind(self): with pytest.raises(ValueError): df.plot(kind='aasdf') - @pytest.mark.parametrize("x,y", [ - (['B', 'C'], 'A'), - ('A', ['B', 'C']) + @pytest.mark.parametrize("x,y,lbl", [ + (['B', 'C'], 'A', 'a'), + (['A'], ['B', 'C'], ['b', 'c']), + ('A', ['B', 'C'], 'badlabel') ]) - def test_invalid_xy_args(self, x, y): - # GH 18671 + def test_invalid_xy_args(self, x, y, lbl): + # GH 18671, 19699 allows y to be list-like but not x df = DataFrame({"A": [1, 2], 'B': [3, 4], 'C': [5, 6]}) with pytest.raises(ValueError): - df.plot(x=x, y=y) + df.plot(x=x, y=y, label=lbl) @pytest.mark.parametrize("x,y", [ ('A', 'B'), - ('B', 'A') + (['A'], 'B') ]) def test_invalid_xy_args_dup_cols(self, x, y): - # GH 18671 + # GH 18671, 19699 allows y to be list-like but not x df = DataFrame([[1, 3, 5], [2, 4, 6]], columns=list('AAB')) with pytest.raises(ValueError): df.plot(x=x, y=y) + @pytest.mark.parametrize("x,y,lbl,colors", [ + ('A', ['B'], ['b'], ['red']), + ('A', ['B', 'C'], ['b', 'c'], ['red', 'blue']), + (0, [1, 2], ['bokeh', 'cython'], ['green', 'yellow']) + ]) + def test_y_listlike(self, x, y, lbl, colors): + # GH 19699: tests list-like y and verifies lbls & colors + df = DataFrame({"A": [1, 2], 'B': [3, 4], 'C': [5, 6]}) + _check_plot_works(df.plot, x='A', y=y, label=lbl) + + ax = df.plot(x=x, y=y, label=lbl, color=colors) + assert len(ax.lines) == len(y) + self._check_colors(ax.get_lines(), linecolors=colors) + + @pytest.mark.parametrize("x,y,colnames", [ + (0, 1, ['A', 'B']), + (1, 0, [0, 1]) + ]) + def test_xy_args_integer(self, x, y, colnames): + # GH 20056: tests integer args for xy and checks col names + df = DataFrame({"A": [1, 2], 'B': [3, 4]}) + df.columns = colnames + _check_plot_works(df.plot, x=x, y=y) + @pytest.mark.slow def test_hexbin_basic(self): df = self.hexbin_df