Repository: pandas-dev/pandas

Issue:
Allow list-like for `y` in `DataFrame.plot`.
In pandas 0.22 and earlier, we passing multiple values for `y` worked by accident. The code clearly assumed that `y` was a scalar, but things usually worked, perhaps with an incidental warning:

```python
In [12]: df = pd.DataFrame(np.random.uniform(size=(10, 2)), columns=['a', 'b'])

In [15]: df.plot(x='c', y=['a', 'b'])
/Users/taugspurger/miniconda3/envs/pandas-0.21.0/lib/python3.6/site-packages/pandas/plotting/_core.py:1714: UserWarning: Pandas doesn't allow columns to be created via a new attribute name - see https://pandas.pydata.org/pandas-docs/stable/indexing.html#attribute-access
  series.name = label
Out[15]: <matplotlib.axes._subplots.AxesSubplot at 0x10f83b198>
```

On master, this will currently raise. I think that we should explicitly support list-likes for `y`.

Tagging for 0.23 since I think this should be a blocker.

xref https://github.com/pandas-dev/pandas/pull/18695 where we made the change.


Discussion:
@MasonGallo do you have any time / interest to work on this?
@TomAugspurger yes I would be happy to help once I get some time, hopefully later this week.

Can you give a bit more details on what you would expect to return in your example above (or a similar example where the user supplies multiple y columns)?
Thanks! I think it should be equivalent to `df.set_index('c')[['a', 'b']].plot()`. So multiple lines, each plotted against the `x`.
Gotcha - this only makes sense if `a` and `b` are on the "same" scale right?

The `plot` method has a `secondary_y` arg but I'm not sure it makes sense to use in a general case like this.
> Gotcha - this only makes sense if a and b are on the "same" scale right?

To get a sensible plot, yes, but users are free to do whatever :)

`secondary_y` is for creating a plot with two independent y-axes. Accepting
a list-like for `y` would allow for an arbitrary number of lines all with
the same y axis.

On Mon, Feb 26, 2018 at 11:29 AM, Mason Gallo <notifications@github.com>
wrote:

> Gotcha - this only makes sense if a and b are on the "same" scale right?
>
> The plot method has a secondary_y arg but I'm not sure it makes sense to
> use in a general case like this.
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/pandas-dev/pandas/issues/19699#issuecomment-368581524>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/ABQHIic1OXLIgvMTc6vIdrxOQ3f6sn6Jks5tYuoNgaJpZM4SFw5J>
> .
>

> Accepting a list-like for `y` would allow for an arbitrary number of lines all with
the same y axis.

This makes sense to me & I think the usecase is clear. 

Resolving patch:
diff --git a/pandas/plotting/_core.py b/pandas/plotting/_core.py
--- 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 = ""
 


Test patch:
diff --git a/pandas/tests/plotting/test_frame.py b/pandas/tests/plotting/test_frame.py
--- 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