Repository: mwaskom/seaborn

Issue:
scatterplot throws an error when referencing int column by name 
## Summary
`scatterplot` throws an error when trying to reference column by name if the column name is an int

In seaborn 0.10 and lower, I was able to pass column names to `scatterplot` that were integers (e.g. `scatterplot(data=data, x=0, y=1)`). After updating to 0.11, I'm getting the error below. 

Apologies if already posted but I did not see anything about this in the issue tracker. Thanks for the awesome package!

## Environment
seaborn version: 0.11.0 (with 0.10.1, the code below will run fine)
matplotlib version: 3.1.3
matplotlib backend: TkAgg

## Code to reproduce
```
import seaborn as sns
import matplotlib as mpl

print(f"seaborn version: {sns.__version__}")
print(f"matplotlib version: {mpl.__version__}")
print(f"matplotlib backend: {mpl.get_backend()}")

iris_data = sns.load_dataset("iris")

# this one works
sns.scatterplot(data=iris_data, x="sepal_length", y="sepal_width")

# make a new column called int(0)
iris_data[0] = iris_data["sepal_length"].copy()

# this one breaks (used to work in seaborn <0.11)
sns.scatterplot(data=iris_data, x=0, y="sepal_width")

```

## Output
```
seaborn version: 0.11.0
matplotlib version: 3.1.3
matplotlib backend: TkAgg
Traceback (most recent call last):
  File "/Users/bpedigo/JHU_code/graspy_misc/graspy-misc/localization/sns_bug.py", line 16, in <module>
    sns.scatterplot(data=iris_data, x=0, y="sepal_width")
  File "/Users/bpedigo/miniconda3/envs/graspy_misc/lib/python3.7/site-packages/seaborn/_decorators.py", line 46, in inner_f
    return f(**kwargs)
  File "/Users/bpedigo/miniconda3/envs/graspy_misc/lib/python3.7/site-packages/seaborn/relational.py", line 798, in scatterplot
    alpha=alpha, x_jitter=x_jitter, y_jitter=y_jitter, legend=legend,
  File "/Users/bpedigo/miniconda3/envs/graspy_misc/lib/python3.7/site-packages/seaborn/relational.py", line 580, in __init__
    super().__init__(data=data, variables=variables)
  File "/Users/bpedigo/miniconda3/envs/graspy_misc/lib/python3.7/site-packages/seaborn/_core.py", line 604, in __init__
    self.assign_variables(data, variables)
  File "/Users/bpedigo/miniconda3/envs/graspy_misc/lib/python3.7/site-packages/seaborn/_core.py", line 668, in assign_variables
    data, **variables,
  File "/Users/bpedigo/miniconda3/envs/graspy_misc/lib/python3.7/site-packages/seaborn/_core.py", line 895, in _assign_variables_longform
    if val is not None and len(data) != len(val):
TypeError: object of type 'int' has no len()
```

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/mwaskom/seaborn/pull/2386?src=pr&el=h1) Report
> Merging [#2386](https://codecov.io/gh/mwaskom/seaborn/pull/2386?src=pr&el=desc) (5394b93) into [master](https://codecov.io/gh/mwaskom/seaborn/commit/4fe84d4ff25276f0ef4ea2a18d0a31741547cb0f?el=desc) (4fe84d4) will **decrease** coverage by `0.51%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/mwaskom/seaborn/pull/2386/graphs/tree.svg?width=650&height=150&src=pr&token=SLuzHnA9Bl)](https://codecov.io/gh/mwaskom/seaborn/pull/2386?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #2386      +/-   ##
==========================================
- Coverage   96.94%   96.43%   -0.52%     
==========================================
  Files          17       17              
  Lines        6053     6027      -26     
==========================================
- Hits         5868     5812      -56     
- Misses        185      215      +30     
```


| [Impacted Files](https://codecov.io/gh/mwaskom/seaborn/pull/2386?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [seaborn/\_core.py](https://codecov.io/gh/mwaskom/seaborn/pull/2386/diff?src=pr&el=tree#diff-c2VhYm9ybi9fY29yZS5weQ==) | `98.11% <100.00%> (-0.16%)` | :arrow_down: |
| [seaborn/\_docstrings.py](https://codecov.io/gh/mwaskom/seaborn/pull/2386/diff?src=pr&el=tree#diff-c2VhYm9ybi9fZG9jc3RyaW5ncy5weQ==) | `97.05% <0.00%> (-2.95%)` | :arrow_down: |
| [seaborn/rcmod.py](https://codecov.io/gh/mwaskom/seaborn/pull/2386/diff?src=pr&el=tree#diff-c2VhYm9ybi9yY21vZC5weQ==) | `98.21% <0.00%> (-1.79%)` | :arrow_down: |
| [seaborn/distributions.py](https://codecov.io/gh/mwaskom/seaborn/pull/2386/diff?src=pr&el=tree#diff-c2VhYm9ybi9kaXN0cmlidXRpb25zLnB5) | `94.67% <0.00%> (-1.76%)` | :arrow_down: |
| [seaborn/regression.py](https://codecov.io/gh/mwaskom/seaborn/pull/2386/diff?src=pr&el=tree#diff-c2VhYm9ybi9yZWdyZXNzaW9uLnB5) | `96.52% <0.00%> (-1.28%)` | :arrow_down: |
| [seaborn/\_statistics.py](https://codecov.io/gh/mwaskom/seaborn/pull/2386/diff?src=pr&el=tree#diff-c2VhYm9ybi9fc3RhdGlzdGljcy5weQ==) | `97.95% <0.00%> (-1.03%)` | :arrow_down: |
| [seaborn/utils.py](https://codecov.io/gh/mwaskom/seaborn/pull/2386/diff?src=pr&el=tree#diff-c2VhYm9ybi91dGlscy5weQ==) | `88.25% <0.00%> (-0.36%)` | :arrow_down: |
| [seaborn/categorical.py](https://codecov.io/gh/mwaskom/seaborn/pull/2386/diff?src=pr&el=tree#diff-c2VhYm9ybi9jYXRlZ29yaWNhbC5weQ==) | `97.82% <0.00%> (-0.18%)` | :arrow_down: |
| [seaborn/axisgrid.py](https://codecov.io/gh/mwaskom/seaborn/pull/2386/diff?src=pr&el=tree#diff-c2VhYm9ybi9heGlzZ3JpZC5weQ==) | `95.30% <0.00%> (-0.15%)` | :arrow_down: |
| ... and [2 more](https://codecov.io/gh/mwaskom/seaborn/pull/2386/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/mwaskom/seaborn/pull/2386?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/mwaskom/seaborn/pull/2386?src=pr&el=footer). Last update [4fe84d4...5394b93](https://codecov.io/gh/mwaskom/seaborn/pull/2386?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

mwaskom: One possibility would be to only enable the new functionality for proper columns of the dataframe, requiring string keys to reference the index. This probably makes sense? (Otherwise there's actually an ambiguity in many cases, although it's sensible to expect that the dataframe columns are prioritized over the index levels). But it does add complexity to what was otherwise a simple change.
mwaskom: Decided to go with the approach proposed in the previous comment ... it is the easiest for the user to work around because if you really want to access index columns that have no name, you can just do `df.reset_index()`, whereas if a value of `0` always pulled out the index (unless it had a name) I expect it would be more surprising and annoying to work around.

Patch:
diff --git a/doc/releases/v0.11.1.txt b/doc/releases/v0.11.1.txt
index 0f9c2fb0..49f884b8 100644
--- a/doc/releases/v0.11.1.txt
+++ b/doc/releases/v0.11.1.txt
@@ -2,13 +2,15 @@
 v0.11.1 (Unreleased)
 --------------------
 
+- |Fix| Restored support for using tuples or numeric keys to reference fields in a long-form `data` object (:pr:`2386`).
+
 - |Fix| Fixed a bug in :func:`lineplot` where NAs were propagating into the confidence interval, sometimes erasing it from the plot (:pr:`2273`).
 
 - |Fix| Fixed a bug in :class:`PairGrid`/:func:`pairplot` where diagonal axes would be empty when the grid was not square and the diagonal axes did not contain the marginal plots (:pr:`2270`).
 
 - |Fix| Fixed a bug in :class:`PairGrid`/:func:`pairplot` where off-diagonal plots would not appear when column names in `data` had non-string type (:pr:`2368`).
 
-- |Fix| Fixed a bug where categorical dtype information was ignored when data consisted of boolean values (:pr:`2379`).
+- |Fix| Fixed a bug where categorical dtype information was ignored when data consisted of boolean or boolean-like values (:pr:`2379`).
 
 - |Fix| Fixed a bug in :class:`FacetGrid` where interior tick labels would be hidden when only the orthogonal axis was shared (:pr:`2347`).
 
@@ -18,12 +20,12 @@ v0.11.1 (Unreleased)
 
 - |Fix| Fixed a bug in :func:`displot` where the ``row_order`` and ``col_order`` parameters were not used (:pr:`2262`).
 
-- |Fix| Fixed a bug in :class:`PairGrid`/:func:`pairplot` that caused an exception when using `corent=True` and `diag_kind=None` (:pr:`2382`).
-
-- |Fix| Raised a more informative error in :class:`PairGrid`/:func:`pairplot` when no variables cold be found to define the rows/columns of the grid (:func:`2382`).
+- |Fix| Fixed a bug in :class:`PairGrid`/:func:`pairplot` that caused an exception when using `corner=True` and `diag_kind=None` (:pr:`2382`).
 
 - |Fix| Fixed a bug in :func:`clustermap` where `annot=False` was ignored (:pr:`2323`).
 
 - |Fix| Fixed a bug in :func:`boxenplot` where the `linewidth` parameter was ignored (:func:`2287`).
 
-- |Fix| Raised a more informative error from :func:`clustermap` if row/col color objects have semantic index but data object does not (:pr:`2313`).
\ No newline at end of file
+- |Fix| Raise a more informative error in :class:`PairGrid`/:func:`pairplot` when no variables can be found to define the rows/columns of the grid (:func:`2382`).
+
+- |Fix| Raise a more informative error from :func:`clustermap` if row/col color objects have semantic index but data object does not (:pr:`2313`).
\ No newline at end of file
diff --git a/seaborn/_core.py b/seaborn/_core.py
index 0531ba63..cccd50ae 100644
--- a/seaborn/_core.py
+++ b/seaborn/_core.py
@@ -871,28 +871,43 @@ class VectorPlotter:
         # The caller will determine the order of variables in plot_data
         for key, val in kwargs.items():
 
-            if isinstance(val, (str, bytes)):
-                # String inputs trigger __getitem__
+            # First try to treat the argument as a key for the data collection.
+            # But be flexible about what can be used as a key.
+            # Usually it will be a string, but allow numbers or tuples too when
+            # taking from the main data object. Only allow strings to reference
+            # fields in the index, because otherwise there is too much ambiguity.
+            try:
+                val_as_data_key = (
+                    val in data
+                    or (isinstance(val, (str, bytes)) and val in index)
+                )
+            except (KeyError, TypeError):
+                val_as_data_key = False
+
+            if val_as_data_key:
+
+                # We know that __getitem__ will work
+
                 if val in data:
-                    # First try to get an entry in the data object
                     plot_data[key] = data[val]
-                    variables[key] = val
                 elif val in index:
-                    # Failing that, try to get an entry in the index object
                     plot_data[key] = index[val]
-                    variables[key] = val
-                else:
-                    # We don't know what this name means
-                    err = f"Could not interpret value `{val}` for parameter `{key}`"
-                    raise ValueError(err)
+                variables[key] = val
+
+            elif isinstance(val, (str, bytes)):
+
+                # This looks like a column name but we don't know what it means!
+
+                err = f"Could not interpret value `{val}` for parameter `{key}`"
+                raise ValueError(err)
 
             else:
 
-                # Otherwise, assume the value is itself a vector of data
+                # Otherwise, assume the value is itself data
 
-                # Raise when data is present and a vector can't be combined with it
+                # Raise when data object is present and a vector can't matched
                 if isinstance(data, pd.DataFrame) and not isinstance(val, pd.Series):
-                    if val is not None and len(data) != len(val):
+                    if np.ndim(val) and len(data) != len(val):
                         val_cls = val.__class__.__name__
                         err = (
                             f"Length of {val_cls} vectors must match length of `data`"


Test patch:
diff --git a/seaborn/tests/test_core.py b/seaborn/tests/test_core.py
index d60ef6d9..e5a90c46 100644
--- a/seaborn/tests/test_core.py
+++ b/seaborn/tests/test_core.py
@@ -605,6 +605,36 @@ class TestVectorPlotter:
     # TODO note that most of the other tests that exercise the core
     # variable assignment code still live in test_relational
 
+    @pytest.mark.parametrize("name", [3, 4.5])
+    def test_long_numeric_name(self, long_df, name):
+
+        long_df[name] = long_df["x"]
+        p = VectorPlotter()
+        p.assign_variables(data=long_df, variables={"x": name})
+        assert_array_equal(p.plot_data["x"], long_df[name])
+        assert p.variables["x"] == name
+
+    def test_long_hierarchical_index(self, rng):
+
+        cols = pd.MultiIndex.from_product([["a"], ["x", "y"]])
+        data = rng.uniform(size=(50, 2))
+        df = pd.DataFrame(data, columns=cols)
+
+        name = ("a", "y")
+        var = "y"
+
+        p = VectorPlotter()
+        p.assign_variables(data=df, variables={var: name})
+        assert_array_equal(p.plot_data[var], df[name])
+        assert p.variables[var] == name
+
+    def test_long_scalar_and_data(self, long_df):
+
+        val = 22
+        p = VectorPlotter(data=long_df, variables={"x": "x", "y": val})
+        assert (p.plot_data["y"] == val).all()
+        assert p.variables["y"] is None
+
     def test_wide_semantic_error(self, wide_df):
 
         err = "The following variable cannot be assigned with wide-form data: `hue`"