Repository: pydata/xarray

Issue:
Removing dimensions from Dataset objects
```python
test_dataset = xr.Dataset(dict(
    empty_array=xr.DataArray([], dims='a'), 
    populated_array=xr.DataArray([1], {'b':['1']}, 'b')
))
```

I have a dataset that is produced programatically and can at times end up producing an empty DataArray. This isn't an issue, per-se, because I can remove those empty DataArrays later. However, I cannot find any way in which to remove the unused, empty dimension! I've tried deleting, dropping, resetting indeces, etc., and have had no luck in purging this empty dimension. It causes issues down the line as existence of entries in the dimensions list triggers certain events.

Is there a way to remove a dimension (and possibly then all data variables which depend on it)?

Discussion:
pep8speaks: Hello @kmsquire! Thanks for submitting the PR.

 - In the file [`xarray/core/dataset.py`](https://github.com/pydata/xarray/blob/14853f00a9be28ea54f3b8a77fbc5986fe67a5cb/xarray/core/dataset.py), following are the PEP8 issues :

> [Line 2833:80](https://github.com/pydata/xarray/blob/14853f00a9be28ea54f3b8a77fbc5986fe67a5cb/xarray/core/dataset.py#L2833): [E501](https://duckduckgo.com/?q=pep8%20E501) line too long (83 > 79 characters)

 - In the file [`xarray/tests/test_dataset.py`](https://github.com/pydata/xarray/blob/14853f00a9be28ea54f3b8a77fbc5986fe67a5cb/xarray/tests/test_dataset.py), following are the PEP8 issues :

> [Line 1872:34](https://github.com/pydata/xarray/blob/14853f00a9be28ea54f3b8a77fbc5986fe67a5cb/xarray/tests/test_dataset.py#L1872): [E231](https://duckduckgo.com/?q=pep8%20E231) missing whitespace after ','
> [Line 1872:38](https://github.com/pydata/xarray/blob/14853f00a9be28ea54f3b8a77fbc5986fe67a5cb/xarray/tests/test_dataset.py#L1872): [E231](https://duckduckgo.com/?q=pep8%20E231) missing whitespace after ','
> [Line 1879:37](https://github.com/pydata/xarray/blob/14853f00a9be28ea54f3b8a77fbc5986fe67a5cb/xarray/tests/test_dataset.py#L1879): [E231](https://duckduckgo.com/?q=pep8%20E231) missing whitespace after ','
> [Line 1880:34](https://github.com/pydata/xarray/blob/14853f00a9be28ea54f3b8a77fbc5986fe67a5cb/xarray/tests/test_dataset.py#L1880): [E231](https://duckduckgo.com/?q=pep8%20E231) missing whitespace after ','
> [Line 1880:38](https://github.com/pydata/xarray/blob/14853f00a9be28ea54f3b8a77fbc5986fe67a5cb/xarray/tests/test_dataset.py#L1880): [E231](https://duckduckgo.com/?q=pep8%20E231) missing whitespace after ','
> [Line 1890:34](https://github.com/pydata/xarray/blob/14853f00a9be28ea54f3b8a77fbc5986fe67a5cb/xarray/tests/test_dataset.py#L1890): [E231](https://duckduckgo.com/?q=pep8%20E231) missing whitespace after ','
> [Line 1890:38](https://github.com/pydata/xarray/blob/14853f00a9be28ea54f3b8a77fbc5986fe67a5cb/xarray/tests/test_dataset.py#L1890): [E231](https://duckduckgo.com/?q=pep8%20E231) missing whitespace after ','
> [Line 1894:30](https://github.com/pydata/xarray/blob/14853f00a9be28ea54f3b8a77fbc5986fe67a5cb/xarray/tests/test_dataset.py#L1894): [E231](https://duckduckgo.com/?q=pep8%20E231) missing whitespace after ','
> [Line 1895:34](https://github.com/pydata/xarray/blob/14853f00a9be28ea54f3b8a77fbc5986fe67a5cb/xarray/tests/test_dataset.py#L1895): [E231](https://duckduckgo.com/?q=pep8%20E231) missing whitespace after ','
> [Line 1895:38](https://github.com/pydata/xarray/blob/14853f00a9be28ea54f3b8a77fbc5986fe67a5cb/xarray/tests/test_dataset.py#L1895): [E231](https://duckduckgo.com/?q=pep8%20E231) missing whitespace after ','


kmsquire: I was looking for this functionality and ran across #1949.

There was one bit of behavior that was unclear to me.  Say I have

```python
In [6]: data = xr.Dataset({'A': (['x', 'y'], np.random.randn(2, 3)),
   ...:                    'B': ('x', np.random.randn(2)),
   ...:                    'x': ['a', 'b'], 'z': np.pi})

In [7]: data
Out[7]:
<xarray.Dataset>
Dimensions:  (x: 2, y: 3)
Coordinates:
  * x        (x) <U1 'a' 'b'
Dimensions without coordinates: y
Data variables:
    A        (x, y) float64 -0.662 -0.705 -0.1866 -0.8655 0.5816 -0.2308
    B        (x) float64 -0.1002 2.402
    z        float64 3.142

In [8]: data.drop_dims('x')
Out[8]:
<xarray.Dataset>
Dimensions:  ()
Data variables:
    z        float64 3.142
```

In this case, I dropped `x`, but because that eliminated variable `A`, I also dropped my `y` dimension, since `A` was the only variable using `y`.

At one point, I implemented this slightly differently, which instead gave

```python
In [8]: data.drop_dims('x')
Out[8]:
<xarray.Dataset>
Dimensions:  (y: 3)
Dimensions without coordinates: y
Data variables:
    z        float64 3.142
```

In some ways, this seems preferable, especially if `y` has coordinates.  The only downside I can see is that this version called `Dataset._replace_vars_and_dims()`, the docstring for which says it's deprecated (although it's still widely used).
kmsquire: @shoyer I'm sure you're busy... wondering if you might have time to review this (or direct it to someone else to review)? 
Other than my question above, it should be pretty straightforward.
shoyer: It's OK to use `_replace_with_new_dims` still. I think we'll eventually replace it in favor of `_replace_with_new_dims` but we aren't there yet.

I agree that it makes sense to recompute dimensions from variables. There's not much point in keeping around extra dimensions that don't have corresponding variables, and I'm pretty sure that this is consistent with how `Dataset.drop()` works.
kmsquire: > I agree that it makes sense to recompute dimensions from variables.

Okay, that's what this PR already did (via the call to `_replace_with_new_dims`).

Rebased on master.  From my perspective, this should be good to go.
kmsquire: @shoyer I force pushed the requested changes.  Please let me know if there is anything else.  Thanks!
kmsquire: Rebased to fix the NEWS conflict.

Patch:
diff --git a/doc/api.rst b/doc/api.rst
index 552582a5..00b33959 100644
--- a/doc/api.rst
+++ b/doc/api.rst
@@ -87,6 +87,7 @@ Dataset contents
    Dataset.swap_dims
    Dataset.expand_dims
    Dataset.drop
+   Dataset.drop_dims
    Dataset.set_coords
    Dataset.reset_coords
 
diff --git a/doc/data-structures.rst b/doc/data-structures.rst
index 618ccccf..a8887471 100644
--- a/doc/data-structures.rst
+++ b/doc/data-structures.rst
@@ -408,6 +408,13 @@ operations keep around coordinates:
     list(ds[['x']])
     list(ds.drop('temperature'))
 
+To remove a dimension, you can use :py:meth:`~xarray.Dataset.drop_dims` method.
+Any variables using that dimension are dropped:
+
+.. ipython:: python
+
+    ds.drop_dims('time')
+
 As an alternate to dictionary-like modifications, you can use
 :py:meth:`~xarray.Dataset.assign` and :py:meth:`~xarray.Dataset.assign_coords`.
 These methods return a new dataset with additional (or replaced) or values:
diff --git a/doc/indexing.rst b/doc/indexing.rst
index 77ec7428..9af9db22 100644
--- a/doc/indexing.rst
+++ b/doc/indexing.rst
@@ -229,8 +229,8 @@ arrays). However, you can do normal indexing with dimension names:
 Using indexing to *assign* values to a subset of dataset (e.g.,
 ``ds[dict(space=0)] = 1``) is not yet supported.
 
-Dropping labels
----------------
+Dropping labels and dimensions
+------------------------------
 
 The :py:meth:`~xarray.Dataset.drop` method returns a new object with the listed
 index labels along a dimension dropped:
@@ -241,6 +241,13 @@ index labels along a dimension dropped:
 
 ``drop`` is both a ``Dataset`` and ``DataArray`` method.
 
+Use :py:meth:`~xarray.Dataset.drop_dims` to drop a full dimension from a Dataset.
+Any variables with these dimensions are also dropped:
+
+.. ipython:: python
+
+    ds.drop_dims('time')
+
 
 .. _masking with where:
 
diff --git a/doc/whats-new.rst b/doc/whats-new.rst
index 090b6022..aad6bb41 100644
--- a/doc/whats-new.rst
+++ b/doc/whats-new.rst
@@ -78,6 +78,9 @@ Enhancements
   serialization warnings raised if dates from a standard calendar are found to
   be outside the :py:class:`pandas.Timestamp`-valid range (:issue:`2754`).  By
   `Spencer Clark <https://github.com/spencerkclark>`_. 
+  
+- Added :py:meth:`~xarray.Dataset.drop_dims` (:issue:`1949`).
+  By `Kevin Squire <https://github.com/kmsquire>`_.
 
 Bug fixes
 ~~~~~~~~~
diff --git a/xarray/core/dataset.py b/xarray/core/dataset.py
index 7bb08584..f3e6cac1 100644
--- a/xarray/core/dataset.py
+++ b/xarray/core/dataset.py
@@ -2797,6 +2797,37 @@ class Dataset(Mapping, ImplementsDatasetReduce, DataWithCoords):
         coord_names = set(k for k in self._coord_names if k in variables)
         return self._replace_vars_and_dims(variables, coord_names)
 
+    def drop_dims(self, drop_dims):
+        """Drop dimensions and associated variables from this dataset.
+
+        Parameters
+        ----------
+        drop_dims : str or list
+            Dimension or dimensions to drop.
+
+        Returns
+        -------
+        obj : Dataset
+            The dataset without the given dimensions (or any variables
+            containing those dimensions)
+        """
+        if utils.is_scalar(drop_dims):
+            drop_dims = [drop_dims]
+
+        missing_dimensions = [d for d in drop_dims if d not in self.dims]
+        if missing_dimensions:
+            raise ValueError('Dataset does not contain the dimensions: %s'
+                             % missing_dimensions)
+
+        drop_vars = set(k for k, v in self._variables.items()
+                        for d in v.dims if d in drop_dims)
+
+        variables = OrderedDict((k, v) for k, v in self._variables.items()
+                                if k not in drop_vars)
+        coord_names = set(k for k in self._coord_names if k in variables)
+
+        return self._replace_with_new_dims(variables, coord_names)
+
     def transpose(self, *dims):
         """Return a new Dataset object with all array dimensions transposed.
 


Test patch:
diff --git a/xarray/tests/test_dataset.py b/xarray/tests/test_dataset.py
index c26968b1..7063e217 100644
--- a/xarray/tests/test_dataset.py
+++ b/xarray/tests/test_dataset.py
@@ -1863,6 +1863,26 @@ class TestDataset(object):
                 ValueError, 'does not have coordinate labels'):
             data.drop(1, 'y')
 
+    def test_drop_dims(self):
+        data = xr.Dataset({'A': (['x', 'y'], np.random.randn(2, 3)),
+                           'B': ('x', np.random.randn(2)),
+                           'x': ['a', 'b'], 'z': np.pi})
+
+        actual = data.drop_dims('x')
+        expected = data.drop(['A', 'B', 'x'])
+        assert_identical(expected, actual)
+
+        actual = data.drop_dims('y')
+        expected = data.drop('A')
+        assert_identical(expected, actual)
+
+        actual = data.drop_dims(['x', 'y'])
+        expected = data.drop(['A', 'B', 'x'])
+        assert_identical(expected, actual)
+
+        with pytest.raises((ValueError, KeyError)):
+            data.drop_dims('z')  # not a dimension
+
     def test_copy(self):
         data = create_test_data()