Repository: pandas-dev/pandas
Pull request: ENH/VIZ: Allowing `s` parameter of scatter plots to be a column name
- [x] closes #32904 
- [x] tests added
- [x] tests passed
- [x] passes `black pandas`
- [x] passes `git diff upstream/master -u -- "*.py" | flake8 --diff`
- [x] whatsnew entry

This PR is a continuation of #32937 (made an error when pulling changes from the master).

I had a more elaborate decision tree for what to do with a passed size variable `s`, here https://github.com/pandas-dev/pandas/pull/32937, but in the end decided to go with the simpler version since the other checks are redundant (they would contain `s=s` or `pass`).

Problem statement:
Bug: TypeError: ufunc 'sqrt' not supported for the input types, and the inputs could not be safely coerced to any supported types according to the casting rule ''safe''
#### Code Sample, a copy-pastable example if possible

```python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

df = pd.DataFrame(np.random.random((10,3))*100, columns=['a', 'b', 'c'])

plt.scatter(x='a', y='b', s='c', data=df, alpha=0.3)

df.plot.scatter(x='a', y='b', s='c')
```
#### Problem description

Calling plot.scatter on a pandas dataframe with option for size results in a TypeError. The same data can be plotted directly with matplotlib.

#### Output of ``pd.show_versions()``

<details>

```

INSTALLED VERSIONS
------------------
commit           : None
python           : 3.7.6.final.0
python-bits      : 64
OS               : Darwin
OS-release       : 19.3.0
machine          : x86_64
processor        : i386
byteorder        : little
LC_ALL           : None
LANG             : en_GB.UTF-8
LOCALE           : en_GB.UTF-8

pandas           : 1.0.3
numpy            : 1.18.1
pytz             : 2019.3
dateutil         : 2.8.1
pip              : 19.3.1
setuptools       : 46.0.0.post20200311
Cython           : None
pytest           : None
hypothesis       : None
sphinx           : None
blosc            : None
feather          : None
xlsxwriter       : None
lxml.etree       : 4.5.0
html5lib         : None
pymysql          : None
psycopg2         : None
jinja2           : 2.11.1
IPython          : 7.13.0
pandas_datareader: None
bs4              : None
bottleneck       : None
fastparquet      : 0.3.3
gcsfs            : None
lxml.etree       : 4.5.0
matplotlib       : 3.2.0
numexpr          : None
odfpy            : None
openpyxl         : 3.0.3
pandas_gbq       : None
pyarrow          : 0.16.0
pytables         : None
pytest           : None
pyxlsb           : None
s3fs             : None
scipy            : 1.4.1
sqlalchemy       : 1.3.15
tables           : None
tabulate         : None
xarray           : None
xlrd             : 1.2.0
xlwt             : None
xlsxwriter       : None
numba            : 0.48.0
```

</details>

Hints from issue discussion:
Can you post the full traceback?
```
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-1-2a07d407e318> in <module>
      7 plt.scatter(x='a', y='b', s='c', data=df, alpha=0.3)
      8 
----> 9 df.plot.scatter(x='a', y='b', s='c')

~/miniconda/envs/mydef/lib/python3.7/site-packages/pandas/plotting/_core.py in scatter(self, x, y, s, c, **kwargs)
   1497             ...                       colormap='viridis')
   1498         """
-> 1499         return self(kind="scatter", x=x, y=y, s=s, c=c, **kwargs)
   1500 
   1501     def hexbin(self, x, y, C=None, reduce_C_function=None, gridsize=None, **kwargs):

~/miniconda/envs/mydef/lib/python3.7/site-packages/pandas/plotting/_core.py in __call__(self, *args, **kwargs)
    790         if kind in self._dataframe_kinds:
    791             if isinstance(data, ABCDataFrame):
--> 792                 return plot_backend.plot(data, x=x, y=y, kind=kind, **kwargs)
    793             else:
    794                 raise ValueError(f"plot kind {kind} can only be used for data frames")

~/miniconda/envs/mydef/lib/python3.7/site-packages/pandas/plotting/_matplotlib/__init__.py in plot(data, kind, **kwargs)
     59             kwargs["ax"] = getattr(ax, "left_ax", ax)
     60     plot_obj = PLOT_CLASSES[kind](data, **kwargs)
---> 61     plot_obj.generate()
     62     plot_obj.draw()
     63     return plot_obj.result

~/miniconda/envs/mydef/lib/python3.7/site-packages/pandas/plotting/_matplotlib/core.py in generate(self)
    261         self._compute_plot_data()
    262         self._setup_subplots()
--> 263         self._make_plot()
    264         self._add_table()
    265         self._make_legend()

~/miniconda/envs/mydef/lib/python3.7/site-packages/pandas/plotting/_matplotlib/core.py in _make_plot(self)
    973             label=label,
    974             cmap=cmap,
--> 975             **self.kwds,
    976         )
    977         if cb:

~/miniconda/envs/mydef/lib/python3.7/site-packages/matplotlib/__init__.py in inner(ax, data, *args, **kwargs)
   1541     def inner(ax, *args, data=None, **kwargs):
   1542         if data is None:
-> 1543             return func(ax, *map(sanitize_sequence, args), **kwargs)
   1544 
   1545         bound = new_sig.bind(ax, *args, **kwargs)

~/miniconda/envs/mydef/lib/python3.7/site-packages/matplotlib/cbook/deprecation.py in wrapper(*args, **kwargs)
    356                 f"%(removal)s.  If any parameter follows {name!r}, they "
    357                 f"should be pass as keyword, not positionally.")
--> 358         return func(*args, **kwargs)
    359 
    360     return wrapper

~/miniconda/envs/mydef/lib/python3.7/site-packages/matplotlib/axes/_axes.py in scatter(self, x, y, s, c, marker, cmap, norm, vmin, vmax, alpha, linewidths, verts, edgecolors, plotnonfinite, **kwargs)
   4427                 offsets=offsets,
   4428                 transOffset=kwargs.pop('transform', self.transData),
-> 4429                 alpha=alpha
   4430                 )
   4431         collection.set_transform(mtransforms.IdentityTransform())

~/miniconda/envs/mydef/lib/python3.7/site-packages/matplotlib/collections.py in __init__(self, paths, sizes, **kwargs)
    912         Collection.__init__(self, **kwargs)
    913         self.set_paths(paths)
--> 914         self.set_sizes(sizes)
    915         self.stale = True
    916 

~/miniconda/envs/mydef/lib/python3.7/site-packages/matplotlib/collections.py in set_sizes(self, sizes, dpi)
    884             self._sizes = np.asarray(sizes)
    885             self._transforms = np.zeros((len(self._sizes), 3, 3))
--> 886             scale = np.sqrt(self._sizes) * dpi / 72.0 * self._factor
    887             self._transforms[:, 0, 0] = scale
    888             self._transforms[:, 1, 1] = scale

TypeError: ufunc 'sqrt' not supported for the input types, and the inputs could not be safely coerced to any supported types according to the casting rule ''safe''
```
We're eventually passing the string `s='c'` to matplotlib, rather than extracting the column. In https://github.com/pandas-dev/pandas/blob/d3e54851b5ab2f2c99c22b4bf5c49cea41141072/pandas/plotting/_matplotlib/core.py#L933-L936 I think we need an else clause checking for the user providing a column as a string.

As a workaround, you can pass the column directly with `s=df['c']`.
Thank you for the pointer! Submitted a PR here: https://github.com/pandas-dev/pandas/pull/32937
We'll leave this open until it's fixed.

Commit message: corrected the versionchanged directive

Patch to review:
diff --git a/doc/source/whatsnew/v1.1.0.rst b/doc/source/whatsnew/v1.1.0.rst
index 20415bba99476..aeb0eeab0b3ba 100644
--- a/doc/source/whatsnew/v1.1.0.rst
+++ b/doc/source/whatsnew/v1.1.0.rst
@@ -452,6 +452,7 @@ Other
 - Fixed bug in :func:`pandas.testing.assert_series_equal` where dtypes were checked for ``Interval`` and ``ExtensionArray`` operands when ``check_dtype`` was ``False`` (:issue:`32747`)
 - Bug in :meth:`Series.map` not raising on invalid ``na_action`` (:issue:`32815`)
 - Bug in :meth:`DataFrame.__dir__` caused a segfault when using unicode surrogates in a column name (:issue:`25509`)
+- Bug in :meth:`DataFrame.plot.scatter` caused an error when plotting variable marker sizes (:issue:`32904`)
 
 .. ---------------------------------------------------------------------------
 
diff --git a/pandas/plotting/_core.py b/pandas/plotting/_core.py
index d3db539084609..765952ad8ef9e 100644
--- a/pandas/plotting/_core.py
+++ b/pandas/plotting/_core.py
@@ -1468,9 +1468,13 @@ def scatter(self, x, y, s=None, c=None, **kwargs):
         y : int or str
             The column name or column position to be used as vertical
             coordinates for each point.
-        s : scalar or array_like, optional
+        s : str, scalar or array_like, optional
             The size of each point. Possible values are:
 
+            .. versionchanged:: 1.1.0
+
+            - A string with the name of the column to be used for marker's size.
+
             - A single scalar so all points have the same size.
 
             - A sequence of scalars, which will be used for each point's size
diff --git a/pandas/plotting/_matplotlib/core.py b/pandas/plotting/_matplotlib/core.py
index 63d0b8abe59d9..bc8346fd48433 100644
--- a/pandas/plotting/_matplotlib/core.py
+++ b/pandas/plotting/_matplotlib/core.py
@@ -934,6 +934,8 @@ def __init__(self, data, x, y, s=None, c=None, **kwargs):
             # hide the matplotlib default for size, in case we want to change
             # the handling of this argument later
             s = 20
+        elif is_hashable(s) and s in data.columns:
+            s = data[s]
         super().__init__(data, x, y, s=s, **kwargs)
         if is_integer(c) and not self.data.columns.holds_integer():
             c = self.data.columns[c]
diff --git a/pandas/tests/plotting/test_frame.py b/pandas/tests/plotting/test_frame.py
index 45ac18b2661c3..db002cfe2130a 100644
--- a/pandas/tests/plotting/test_frame.py
+++ b/pandas/tests/plotting/test_frame.py
@@ -1306,6 +1306,13 @@ def test_plot_scatter_with_c(self):
         float_array = np.array([0.0, 1.0])
         df.plot.scatter(x="A", y="B", c=float_array, cmap="spring")
 
+    def test_plot_scatter_with_s(self):
+        # this refers to GH 32904
+        df = DataFrame(np.random.random((10, 3)) * 100, columns=["a", "b", "c"],)
+
+        ax = df.plot.scatter(x="a", y="b", s="c")
+        assert (df["c"].values == ax.collections[0].get_sizes()).all()
+
     def test_scatter_colors(self):
         df = DataFrame({"a": [1, 2, 3], "b": [1, 2, 3], "c": [1, 2, 3]})
         with pytest.raises(TypeError):


Code review comments:
diff_hunk: @@ -1306,6 +1306,13 @@ def test_plot_scatter_with_c(self):
         float_array = np.array([0.0, 1.0])
         df.plot.scatter(x="A", y="B", c=float_array, cmap="spring")
 
+    def test_plot_scatter_with_s(self):
+        # this refers to GH 32904
+        df = DataFrame(np.random.random((10, 3)) * 100, columns=["a", "b", "c"],)
+
+        ax = df.plot.scatter(x="a", y="b", s="c")
+        assert (df["c"].values == ax.collections[0].get_sizes()).all()
path: pandas/tests/plotting/test_frame.py
text: @user1:
Use `tm.assert_numpy_array_equal(...)`

diff_hunk: @@ -1468,9 +1468,13 @@ def scatter(self, x, y, s=None, c=None, **kwargs):
         y : int or str
             The column name or column position to be used as vertical
             coordinates for each point.
-        s : scalar or array_like, optional
+        s : str, scalar or array_like, optional
             The size of each point. Possible values are:
 
+            .. versionchanged:: 1.1.0
path: pandas/plotting/_core.py
text: @user1:
Typically these `versionchanged` are put at the end up the description for this parameter. SO around line 1483