Repository: mwaskom/seaborn (Python)

Issue:
Error during numeric legend creation with object-typed data
I thought this was fully squashed, but apparently not:



```
import seaborn as sns, pandas as pd
sns.lineplot(x=[0, 1, 0, 1], y=[0, 1, 0, 2], hue=pd.Series([1, 1, 3, 3], dtype=object))
```
```python-traceback
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
AttributeError: 'float' object has no attribute 'rint'

The above exception was the direct cause of the following exception:

TypeError                                 Traceback (most recent call last)
~/miniconda3/envs/seaborn-refactor/lib/python3.8/site-packages/numpy/core/fromnumeric.py in _wrapfunc(obj, method, *args, **kwds)
     60     try:
---> 61         return bound(*args, **kwds)
     62     except TypeError:

TypeError: loop of ufunc does not support argument 0 of type float which has no callable rint method

During handling of the above exception, another exception occurred:

AttributeError                            Traceback (most recent call last)
AttributeError: 'float' object has no attribute 'rint'

The above exception was the direct cause of the following exception:

TypeError                                 Traceback (most recent call last)
<ipython-input-76-61b464cca20e> in <module>
----> 1 sns.lineplot(x=[0, 1, 0, 1], y=[0, 1, 0, 2], hue=pd.Series([1, 1, 3, 3], dtype=object))

~/code/seaborn/seaborn/_decorators.py in inner_f(*args, **kwargs)
     44             )
     45         kwargs.update({k: arg for k, arg in zip(sig.parameters, args)})
---> 46         return f(**kwargs)
     47     return inner_f
     48 

~/code/seaborn/seaborn/relational.py in lineplot(x, y, hue, size, style, data, palette, hue_order, hue_norm, sizes, size_order, size_norm, dashes, markers, style_order, units, estimator, ci, n_boot, seed, sort, err_style, err_kws, legend, ax, **kwargs)
    642         ax = plt.gca()
    643 
--> 644     p.plot(ax, kwargs)
    645     return ax
    646 

~/code/seaborn/seaborn/relational.py in plot(self, ax, kws)
    362         self.label_axes(ax)
    363         if self.legend:
--> 364             self.add_legend_data(ax)
    365             handles, _ = ax.get_legend_handles_labels()
    366             if handles:

~/code/seaborn/seaborn/relational.py in add_legend_data(self, ax)
     70                 locator = mpl.ticker.MaxNLocator(nbins=3)
     71             limits = min(self._hue_map.levels), max(self._hue_map.levels)
---> 72             hue_levels, hue_formatted_levels = locator_to_legend_entries(
     73                 locator, limits, self.plot_data["hue"].dtype
     74             )

~/code/seaborn/seaborn/utils.py in locator_to_legend_entries(locator, limits, dtype)
    564     # once pinned matplotlib>=3.1.0 with:
    565     # formatted_levels = formatter.format_ticks(raw_levels)
--> 566     formatter.set_locs(raw_levels)
    567     formatted_levels = [formatter(x) for x in raw_levels]
    568 

~/miniconda3/envs/seaborn-refactor/lib/python3.8/site-packages/matplotlib/ticker.py in set_locs(self, locs)
    687                 self._compute_offset()
    688             self._set_order_of_magnitude()
--> 689             self._set_format()
    690 
    691     def _compute_offset(self):

~/miniconda3/envs/seaborn-refactor/lib/python3.8/site-packages/matplotlib/ticker.py in _set_format(self)
    789         thresh = 1e-3 * 10 ** loc_range_oom
    790         while sigfigs >= 0:
--> 791             if np.abs(locs - np.round(locs, decimals=sigfigs)).max() < thresh:
    792                 sigfigs -= 1
    793             else:

<__array_function__ internals> in round_(*args, **kwargs)

~/miniconda3/envs/seaborn-refactor/lib/python3.8/site-packages/numpy/core/fromnumeric.py in round_(a, decimals, out)
   3597     around : equivalent function; see for details.
   3598     """
-> 3599     return around(a, decimals=decimals, out=out)
   3600 
   3601 

<__array_function__ internals> in around(*args, **kwargs)

~/miniconda3/envs/seaborn-refactor/lib/python3.8/site-packages/numpy/core/fromnumeric.py in around(a, decimals, out)
   3222 
   3223     """
-> 3224     return _wrapfunc(a, 'round', decimals=decimals, out=out)
   3225 
   3226 

~/miniconda3/envs/seaborn-refactor/lib/python3.8/site-packages/numpy/core/fromnumeric.py in _wrapfunc(obj, method, *args, **kwds)
     68         # Call _wrapit from within the except clause to ensure a potential
     69         # exception has a traceback chain.
---> 70         return _wrapit(obj, method, *args, **kwds)
     71 
     72 

~/miniconda3/envs/seaborn-refactor/lib/python3.8/site-packages/numpy/core/fromnumeric.py in _wrapit(obj, method, *args, **kwds)
     45     except AttributeError:
     46         wrap = None
---> 47     result = getattr(asarray(obj), method)(*args, **kwds)
     48     if wrap:
     49         if not isinstance(result, mu.ndarray):

TypeError: loop of ufunc does not support argument 0 of type float which has no callable rint method

```


Discussion:

Patch:
diff --git a/seaborn/_decorators.py b/seaborn/_decorators.py
index 00401c16dc..d1c24b870b 100644
--- a/seaborn/_decorators.py
+++ b/seaborn/_decorators.py
@@ -37,7 +37,7 @@ def inner_f(*args, **kwargs):
                 "Pass the following variable{} as {}keyword arg{}: {}. "
                 "From version 0.12, the only valid positional argument "
                 "will be `data`, and passing other arguments without an "
-                "explcit keyword will result in an error or misinterpretation."
+                "explicit keyword will result in an error or misinterpretation."
                 .format(plural, article, plural,
                         ", ".join(kwonly_args[:extra_args])),
                 FutureWarning
diff --git a/seaborn/relational.py b/seaborn/relational.py
index 3b104cedf9..96d0e94311 100644
--- a/seaborn/relational.py
+++ b/seaborn/relational.py
@@ -229,7 +229,7 @@ def update(var_name, val_name, **kws):
                 locator = mpl.ticker.MaxNLocator(nbins=3)
             limits = min(self._hue_map.levels), max(self._hue_map.levels)
             hue_levels, hue_formatted_levels = locator_to_legend_entries(
-                locator, limits, self.plot_data["hue"].dtype
+                locator, limits, self.plot_data["hue"].infer_objects().dtype
             )
         elif self._hue_map.levels is None:
             hue_levels = hue_formatted_levels = []
@@ -258,7 +258,7 @@ def update(var_name, val_name, **kws):
             # Define the min/max data values
             limits = min(self._size_map.levels), max(self._size_map.levels)
             size_levels, size_formatted_levels = locator_to_legend_entries(
-                locator, limits, self.plot_data["size"].dtype
+                locator, limits, self.plot_data["size"].infer_objects().dtype
             )
         elif self._size_map.levels is None:
             size_levels = size_formatted_levels = []


Test patch:
diff --git a/seaborn/conftest.py b/seaborn/conftest.py
index f4809c7cc8..da26b268fc 100644
--- a/seaborn/conftest.py
+++ b/seaborn/conftest.py
@@ -196,6 +196,16 @@ def missing_df(rng, long_df):
     return df
 
 
+@pytest.fixture
+def object_df(rng, long_df):
+
+    df = long_df.copy()
+    # objectify numeric columns
+    for col in ["c", "s", "f"]:
+        df[col] = df[col].astype(object)
+    return df
+
+
 @pytest.fixture
 def null_series(flat_series):
 
diff --git a/seaborn/tests/test_relational.py b/seaborn/tests/test_relational.py
index dc96d76ea6..bb3afbcfee 100644
--- a/seaborn/tests/test_relational.py
+++ b/seaborn/tests/test_relational.py
@@ -1246,7 +1246,7 @@ def test_lineplot_smoke(
         wide_df, wide_array,
         wide_list_of_series, wide_list_of_arrays, wide_list_of_lists,
         flat_array, flat_series, flat_list,
-        long_df, missing_df
+        long_df, missing_df, object_df
     ):
 
         f, ax = plt.subplots()
@@ -1320,6 +1320,15 @@ def test_lineplot_smoke(
         lineplot(x="x", y="y", hue="a", size="s", data=missing_df)
         ax.clear()
 
+        lineplot(x="x", y="y", hue="f", data=object_df)
+        ax.clear()
+
+        lineplot(x="x", y="y", hue="c", size="f", data=object_df)
+        ax.clear()
+
+        lineplot(x="x", y="y", hue="f", size="s", data=object_df)
+        ax.clear()
+
 
 class TestScatterPlotter(Helpers):
 
@@ -1683,7 +1692,7 @@ def test_scatterplot_smoke(
         wide_df, wide_array,
         flat_series, flat_array, flat_list,
         wide_list_of_series, wide_list_of_arrays, wide_list_of_lists,
-        long_df, missing_df
+        long_df, missing_df, object_df
     ):
 
         f, ax = plt.subplots()
@@ -1753,3 +1762,12 @@ def test_scatterplot_smoke(
 
         scatterplot(x="x", y="y", hue="a", size="s", data=missing_df)
         ax.clear()
+
+        scatterplot(x="x", y="y", hue="f", data=object_df)
+        ax.clear()
+
+        scatterplot(x="x", y="y", hue="c", size="f", data=object_df)
+        ax.clear()
+
+        scatterplot(x="x", y="y", hue="f", size="s", data=object_df)
+        ax.clear()