Repository: pydata/xarray

Issue:
DataArray.set_index throws error on documented input
#### Problem Description

[Docs for `DataArray.set_index`](http://xarray.pydata.org/en/stable/generated/xarray.DataArray.set_index.html) describe the main `indexes` argument as:

> Mapping from names matching dimensions and values given by (lists of) the names of existing coordinates or variables to set as new (multi-)index.

This suggests that one can set a `DataArray` instance's coordinates by passing in a dimension and a list-like object of coordinates.

#### MCVE

```python
In [1]: import numpy as np

In [2]: import xarray as xr

In [3]: arr = xr.DataArray(data=np.ones((2, 3)), dims=['x', 'y'])

In [4]: arr.dims
Out[4]: ('x', 'y')

In [5]: arr.set_index({'x': range(2)})
KeyError   
...
    144         for n in var_names:
--> 145             var = variables[n]
    146             if (current_index_variable is not None and
    147                     var.dims != current_index_variable.dims):

KeyError: 0
```

At first, I thought it might be because `coords` and `_coords` were not being set in this case:

```python
In [18]: arr.coords
Out[18]: 
Coordinates:
    *empty*

In [19]: arr._coords
Out[19]: OrderedDict()
```

but even if I set the coordinates first and then try to re-index, it fails:

```python
In [20]: arr = xr.DataArray(data=np.ones((2, 3)), dims=['x', 'y'], coords={'x': range(2), 'y': range(3)})
In [21]: arr.set_index({'x': ['a', 'b', 'c']})
...
    144         for n in var_names:
--> 145             var = variables[n]
    146             if (current_index_variable is not None and
    147                     var.dims != current_index_variable.dims):
```

#### Expected Output

I expect my MCVE to work based on the documentation.

#### Problem Solution

My guess is that the issue is Xarray is using the `merge_indexes` function ([see here](https://github.com/pydata/xarray/blob/master/xarray/core/dataarray.py#L1444)) from the `Dataset` module, and there is no concept of a `variable` in a `DataArray`.

Discussion:
max-sixty: This is great! Thanks @gwgundersen !

Patch:
diff --git a/doc/whats-new.rst b/doc/whats-new.rst
index 8729f586..a127cef8 100644
--- a/doc/whats-new.rst
+++ b/doc/whats-new.rst
@@ -65,9 +65,15 @@ Enhancements
   ``append_dim`` is set, as it will automatically be set to ``'a'`` internally.
   By `David Brochart <https://github.com/davidbrochart>`_.
 
-- :py:meth:`~xarray.Dataset.drop` now supports keyword arguments; dropping index labels by specifying both ``dim`` and ``labels`` is deprecated (:issue:`2910`).
+- :py:meth:`~xarray.Dataset.drop` now supports keyword arguments; dropping index
+  labels by specifying both ``dim`` and ``labels`` is deprecated (:issue:`2910`).
   By `Gregory Gundersen <https://github.com/gwgundersen/>`_.
 
+- Added examples of :py:meth:`Dataset.set_index` and
+  :py:meth:`DataArray.set_index`, as well are more specific error messages
+  when the user passes invalid arguments (:issue:`3176`).
+  By `Gregory Gundersen <https://github.com/gwgundersen>`_.
+
 Bug fixes
 ~~~~~~~~~
 - Fix regression introduced in v0.12.2 where ``copy(deep=True)`` would convert
diff --git a/xarray/core/dataarray.py b/xarray/core/dataarray.py
index b311a847..52c11429 100644
--- a/xarray/core/dataarray.py
+++ b/xarray/core/dataarray.py
@@ -1524,6 +1524,30 @@ class DataArray(AbstractArray, DataWithCoords):
             Another DataArray, with this data but replaced coordinates.
             Return None if inplace=True.
 
+        Example
+        -------
+        >>> arr = xr.DataArray(data=np.ones((2, 3)),
+        ...                    dims=['x', 'y'],
+        ...                    coords={'x':
+        ...                        range(2), 'y':
+        ...                        range(3), 'a': ('x', [3, 4])
+        ...                    })
+        >>> arr
+        <xarray.DataArray (x: 2, y: 3)>
+        array([[1., 1., 1.],
+               [1., 1., 1.]])
+        Coordinates:
+          * x        (x) int64 0 1
+          * y        (y) int64 0 1 2
+            a        (x) int64 3 4
+        >>> arr.set_index(x='a')
+        <xarray.DataArray (x: 2, y: 3)>
+        array([[1., 1., 1.],
+               [1., 1., 1.]])
+        Coordinates:
+          * x        (x) int64 3 4
+          * y        (y) int64 0 1 2
+
         See Also
         --------
         DataArray.reset_index
diff --git a/xarray/core/dataset.py b/xarray/core/dataset.py
index d25f24c1..6a606fd0 100644
--- a/xarray/core/dataset.py
+++ b/xarray/core/dataset.py
@@ -198,6 +198,7 @@ def merge_indexes(
     """
     vars_to_replace = {}  # Dict[Any, Variable]
     vars_to_remove = []  # type: list
+    error_msg = "{} is not the name of an existing variable."
 
     for dim, var_names in indexes.items():
         if isinstance(var_names, str) or not isinstance(var_names, Sequence):
@@ -207,7 +208,10 @@ def merge_indexes(
         current_index_variable = variables.get(dim)
 
         for n in var_names:
-            var = variables[n]
+            try:
+                var = variables[n]
+            except KeyError:
+                raise ValueError(error_msg.format(n))
             if (
                 current_index_variable is not None
                 and var.dims != current_index_variable.dims
@@ -239,8 +243,11 @@ def merge_indexes(
 
         else:
             for n in var_names:
+                try:
+                    var = variables[n]
+                except KeyError:
+                    raise ValueError(error_msg.format(n))
                 names.append(n)
-                var = variables[n]
                 cat = pd.Categorical(var.values, ordered=True)
                 codes.append(cat.codes)
                 levels.append(cat.categories)
@@ -2952,6 +2959,33 @@ class Dataset(Mapping, ImplementsDatasetReduce, DataWithCoords):
         obj : Dataset
             Another dataset, with this dataset's data but replaced coordinates.
 
+        Examples
+        --------
+        >>> arr = xr.DataArray(data=np.ones((2, 3)),
+        ...                    dims=['x', 'y'],
+        ...                    coords={'x':
+        ...                        range(2), 'y':
+        ...                        range(3), 'a': ('x', [3, 4])
+        ...                    })
+        >>> ds = xr.Dataset({'v': arr})
+        >>> ds
+        <xarray.Dataset>
+        Dimensions:  (x: 2, y: 3)
+        Coordinates:
+          * x        (x) int64 0 1
+          * y        (y) int64 0 1 2
+            a        (x) int64 3 4
+        Data variables:
+            v        (x, y) float64 1.0 1.0 1.0 1.0 1.0 1.0
+        >>> ds.set_index(x='a')
+        <xarray.Dataset>
+        Dimensions:  (x: 2, y: 3)
+        Coordinates:
+          * x        (x) int64 3 4
+          * y        (y) int64 0 1 2
+        Data variables:
+            v        (x, y) float64 1.0 1.0 1.0 1.0 1.0 1.0
+
         See Also
         --------
         Dataset.reset_index


Test patch:
diff --git a/xarray/tests/test_dataarray.py b/xarray/tests/test_dataarray.py
index 18aed7a0..506c437c 100644
--- a/xarray/tests/test_dataarray.py
+++ b/xarray/tests/test_dataarray.py
@@ -1716,6 +1716,11 @@ class TestDataArray:
         with raises_regex(ValueError, "dimension mismatch"):
             array2d.set_index(x="level")
 
+        # Issue 3176: Ensure clear error message on key error.
+        with pytest.raises(ValueError) as excinfo:
+            obj.set_index(x="level_4")
+        assert str(excinfo.value) == "level_4 is not the name of an existing variable."
+
     def test_reset_index(self):
         indexes = [self.mindex.get_level_values(n) for n in self.mindex.names]
         coords = {idx.name: ("x", idx) for idx in indexes}
diff --git a/xarray/tests/test_dataset.py b/xarray/tests/test_dataset.py
index 1b78d139..23bc2b47 100644
--- a/xarray/tests/test_dataset.py
+++ b/xarray/tests/test_dataset.py
@@ -2712,6 +2712,11 @@ class TestDataset:
         expected = Dataset(coords={"x": [0, 1, 2]})
         assert_identical(ds.set_index(x="x_var"), expected)
 
+        # Issue 3176: Ensure clear error message on key error.
+        with pytest.raises(ValueError) as excinfo:
+            ds.set_index(foo="bar")
+        assert str(excinfo.value) == "bar is not the name of an existing variable."
+
     def test_reset_index(self):
         ds = create_test_multiindex()
         mindex = ds["x"].to_index()