diff --git a/doc/data-structures.rst b/doc/data-structures.rst index 7705c7954d5..f1dd6a5f482 100644 --- a/doc/data-structures.rst +++ b/doc/data-structures.rst @@ -115,11 +115,9 @@ If you create a ``DataArray`` by supplying a pandas df xr.DataArray(df) -xarray does not (yet!) support labeling coordinate values with a -:py:class:`pandas.MultiIndex` (see :issue:`164`). -However, the alternate ``from_series`` constructor will automatically unpack -any hierarchical indexes it encounters by expanding the series into a -multi-dimensional array, as described in :doc:`pandas`. +Xarray supports labeling coordinate values with a :py:class:`pandas.MultiIndex`. +While it handles multi-indexes with unnamed levels, it is recommended that you +explicitly set the names of the levels. DataArray properties ~~~~~~~~~~~~~~~~~~~~ diff --git a/doc/indexing.rst b/doc/indexing.rst index a64e20610d5..d21adda2c8e 100644 --- a/doc/indexing.rst +++ b/doc/indexing.rst @@ -294,6 +294,51 @@ elements that are fully masked: arr2.where(arr2.y < 2, drop=True) +.. _multi-level indexing: + +Multi-level indexing +-------------------- + +Just like pandas, advanced indexing on multi-level indexes is possible with +``loc`` and ``sel``. You can slice a multi-index by providing multiple indexers, +i.e., a tuple of slices, labels, list of labels, or any selector allowed by +pandas: + +.. ipython:: python + + midx = pd.MultiIndex.from_product([list('abc'), [0, 1]], + names=('one', 'two')) + mda = xr.DataArray(np.random.rand(6, 3), + [('x', midx), ('y', range(3))]) + mda + mda.sel(x=(list('ab'), [0])) + +You can also select multiple elements by providing a list of labels or tuples or +a slice of tuples: + +.. ipython:: python + + mda.sel(x=[('a', 0), ('b', 1)]) + +Additionally, xarray supports dictionaries: + +.. ipython:: python + + mda.sel(x={'one': 'a', 'two': 0}) + mda.loc[{'one': 'a'}, ...] + +Like pandas, xarray handles partial selection on multi-index (level drop). +As shown in the last example above, it also renames the dimension / coordinate +when the multi-index is reduced to a single index. + +Unlike pandas, xarray does not guess whether you provide index levels or +dimensions when using ``loc`` in some ambiguous cases. For example, for +``mda.loc[{'one': 'a', 'two': 0}]`` and ``mda.loc['a', 0]`` xarray +always interprets ('one', 'two') and ('a', 0) as the names and +labels of the 1st and 2nd dimension, respectively. You must specify all +dimensions or use the ellipsis in the ``loc`` specifier, e.g. in the example +above, ``mda.loc[{'one': 'a', 'two': 0}, :]`` or ``mda.loc[('a', 0), ...]``. + Multi-dimensional indexing -------------------------- diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 1946b5dd67b..93b973467ae 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -26,6 +26,9 @@ Breaking changes ~~~~~~~~~~~~~~~~ - Dropped support for Python 2.6 (:issue:`855`). +- Indexing on multi-index now drop levels, which is consitent with pandas. + It also changes the name of the dimension / coordinate when the multi-index is + reduced to a single index. Enhancements ~~~~~~~~~~~~ @@ -39,10 +42,16 @@ Enhancements attributes are retained in the resampled object. By `Jeremy McGibbon `_. +- Better multi-index support in DataArray and Dataset :py:meth:`sel` and + :py:meth:`loc` methods, which now behave more closely to pandas and which + also accept dictionaries for indexing based on given level names and labels + (see :ref:`multi-level indexing`). By + `Benoit Bovy `_. + - New (experimental) decorators :py:func:`~xarray.register_dataset_accessor` and :py:func:`~xarray.register_dataarray_accessor` for registering custom xarray extensions without subclassing. They are described in the new documentation - page on :ref:`internals`. By `Stephan Hoyer ` + page on :ref:`internals`. By `Stephan Hoyer `_. - Round trip boolean datatypes. Previously, writing boolean datatypes to netCDF formats would raise an error since netCDF does not have a `bool` datatype. diff --git a/xarray/core/dataarray.py b/xarray/core/dataarray.py index fc8a73335cb..c996365b190 100644 --- a/xarray/core/dataarray.py +++ b/xarray/core/dataarray.py @@ -86,24 +86,19 @@ def __init__(self, data_array): self.data_array = data_array def _remap_key(self, key): - def lookup_positions(dim, labels): - index = self.data_array.indexes[dim] - return indexing.convert_label_indexer(index, labels) - - if utils.is_dict_like(key): - return dict((dim, lookup_positions(dim, labels)) - for dim, labels in iteritems(key)) - else: + if not utils.is_dict_like(key): # expand the indexer so we can handle Ellipsis - key = indexing.expanded_indexer(key, self.data_array.ndim) - return tuple(lookup_positions(dim, labels) for dim, labels - in zip(self.data_array.dims, key)) + labels = indexing.expanded_indexer(key, self.data_array.ndim) + key = dict(zip(self.data_array.dims, labels)) + return indexing.remap_label_indexers(self.data_array, key) def __getitem__(self, key): - return self.data_array[self._remap_key(key)] + pos_indexers, new_indexes = self._remap_key(key) + return self.data_array[pos_indexers]._replace_indexes(new_indexes) def __setitem__(self, key, value): - self.data_array[self._remap_key(key)] = value + pos_indexers, _ = self._remap_key(key) + self.data_array[pos_indexers] = value class _ThisArray(object): @@ -244,6 +239,23 @@ def _replace_maybe_drop_dims(self, variable, name=__default): if set(v.dims) <= allowed_dims) return self._replace(variable, coords, name) + def _replace_indexes(self, indexes): + if not len(indexes): + return self + coords = self._coords.copy() + for name, idx in indexes.items(): + coords[name] = Coordinate(name, idx) + obj = self._replace(coords=coords) + + # switch from dimension to level names, if necessary + dim_names = {} + for dim, idx in indexes.items(): + if not isinstance(idx, pd.MultiIndex) and idx.name != dim: + dim_names[dim] = idx.name + if dim_names: + obj = obj.rename(dim_names) + return obj + __this_array = _ThisArray() def _to_temp_dataset(self): @@ -599,8 +611,10 @@ def sel(self, method=None, tolerance=None, **indexers): Dataset.sel DataArray.isel """ - return self.isel(**indexing.remap_label_indexers( - self, indexers, method=method, tolerance=tolerance)) + pos_indexers, new_indexes = indexing.remap_label_indexers( + self, indexers, method=method, tolerance=tolerance + ) + return self.isel(**pos_indexers)._replace_indexes(new_indexes) def isel_points(self, dim='points', **indexers): """Return a new DataArray whose dataset is given by pointwise integer diff --git a/xarray/core/dataset.py b/xarray/core/dataset.py index 660a349e06d..aec64a574bd 100644 --- a/xarray/core/dataset.py +++ b/xarray/core/dataset.py @@ -419,6 +419,23 @@ def _replace_vars_and_dims(self, variables, coord_names=None, obj = self._construct_direct(variables, coord_names, dims, attrs) return obj + def _replace_indexes(self, indexes): + if not len(indexes): + return self + variables = self._variables.copy() + for name, idx in indexes.items(): + variables[name] = Coordinate(name, idx) + obj = self._replace_vars_and_dims(variables) + + # switch from dimension to level names, if necessary + dim_names = {} + for dim, idx in indexes.items(): + if not isinstance(idx, pd.MultiIndex) and idx.name != dim: + dim_names[dim] = idx.name + if dim_names: + obj = obj.rename(dim_names) + return obj + def copy(self, deep=False): """Returns a copy of this dataset. @@ -954,7 +971,9 @@ def sel(self, method=None, tolerance=None, **indexers): Requires pandas>=0.17. **indexers : {dim: indexer, ...} Keyword arguments with names matching dimensions and values given - by scalars, slices or arrays of tick labels. + by scalars, slices or arrays of tick labels. For dimensions with + multi-index, the indexer may also be a dict-like object with keys + matching index level names. Returns ------- @@ -972,8 +991,10 @@ def sel(self, method=None, tolerance=None, **indexers): Dataset.isel_points DataArray.sel """ - return self.isel(**indexing.remap_label_indexers( - self, indexers, method=method, tolerance=tolerance)) + pos_indexers, new_indexes = indexing.remap_label_indexers( + self, indexers, method=method, tolerance=tolerance + ) + return self.isel(**pos_indexers)._replace_indexes(new_indexes) def isel_points(self, dim='points', **indexers): """Returns a new dataset with each array indexed pointwise along the @@ -1114,8 +1135,9 @@ def sel_points(self, dim='points', method=None, tolerance=None, Dataset.isel_points DataArray.sel_points """ - pos_indexers = indexing.remap_label_indexers( - self, indexers, method=method, tolerance=tolerance) + pos_indexers, _ = indexing.remap_label_indexers( + self, indexers, method=method, tolerance=tolerance + ) return self.isel_points(dim=dim, **pos_indexers) def reindex_like(self, other, method=None, tolerance=None, copy=True): @@ -1396,9 +1418,6 @@ def unstack(self, dim): obj = self.reindex(copy=False, **{dim: full_idx}) new_dim_names = index.names - if any(name is None for name in new_dim_names): - raise ValueError('cannot unstack dimension with unnamed levels') - new_dim_sizes = [lev.size for lev in index.levels] variables = OrderedDict() diff --git a/xarray/core/indexing.py b/xarray/core/indexing.py index 6ca00e67cab..8c685a24b26 100644 --- a/xarray/core/indexing.py +++ b/xarray/core/indexing.py @@ -4,7 +4,7 @@ from . import utils from .pycompat import iteritems, range, dask_array_type, suppress -from .utils import is_full_slice +from .utils import is_full_slice, is_dict_like def expanded_indexer(key, ndim): @@ -135,11 +135,18 @@ def _asarray_tuplesafe(values): return result +def _is_nested_tuple(possible_tuple): + return (isinstance(possible_tuple, tuple) + and any(isinstance(value, (tuple, list, slice)) + for value in possible_tuple)) + + def convert_label_indexer(index, label, index_name='', method=None, tolerance=None): """Given a pandas.Index and labels (e.g., from __getitem__) for one dimension, return an indexer suitable for indexing an ndarray along that - dimension + dimension. If `index` is a pandas.MultiIndex and depending on `label`, + return a new pandas.Index or pandas.MultiIndex (otherwise return None). """ # backwards compatibility for pandas<0.16 (method) or pandas<0.17 # (tolerance) @@ -152,6 +159,8 @@ def convert_label_indexer(index, label, index_name='', method=None, 'the tolerance argument requires pandas v0.17 or newer') kwargs['tolerance'] = tolerance + new_index = None + if isinstance(label, slice): if method is not None or tolerance is not None: raise NotImplementedError( @@ -166,10 +175,35 @@ def convert_label_indexer(index, label, index_name='', method=None, raise KeyError('cannot represent labeled-based slice indexer for ' 'dimension %r with a slice over integer positions; ' 'the index is unsorted or non-unique') + + elif is_dict_like(label): + is_nested_vals = _is_nested_tuple(tuple(label.values())) + if not isinstance(index, pd.MultiIndex): + raise ValueError('cannot use a dict-like object for selection on a ' + 'dimension that does not have a MultiIndex') + elif len(label) == index.nlevels and not is_nested_vals: + indexer = index.get_loc(tuple((label[k] for k in index.names))) + else: + indexer, new_index = index.get_loc_level(tuple(label.values()), + level=tuple(label.keys())) + + elif isinstance(label, tuple) and isinstance(index, pd.MultiIndex): + if _is_nested_tuple(label): + indexer = index.get_locs(label) + elif len(label) == index.nlevels: + indexer = index.get_loc(label) + else: + indexer, new_index = index.get_loc_level( + label, level=list(range(len(label))) + ) + else: label = _asarray_tuplesafe(label) if label.ndim == 0: - indexer = index.get_loc(label.item(), **kwargs) + if isinstance(index, pd.MultiIndex): + indexer, new_index = index.get_loc_level(label.item(), level=0) + else: + indexer = index.get_loc(label.item(), **kwargs) elif label.dtype.kind == 'b': indexer, = np.nonzero(label) else: @@ -177,18 +211,27 @@ def convert_label_indexer(index, label, index_name='', method=None, if np.any(indexer < 0): raise KeyError('not all values found in index %r' % index_name) - return indexer + return indexer, new_index def remap_label_indexers(data_obj, indexers, method=None, tolerance=None): """Given an xarray data object and label based indexers, return a mapping - of equivalent location based indexers. + of equivalent location based indexers. Also return a mapping of updated + pandas index objects (in case of multi-index level drop). """ if method is not None and not isinstance(method, str): raise TypeError('``method`` must be a string') - return dict((dim, convert_label_indexer(data_obj[dim].to_index(), label, - dim, method, tolerance)) - for dim, label in iteritems(indexers)) + + pos_indexers, new_indexes = {}, {} + for dim, label in iteritems(indexers): + index = data_obj[dim].to_index() + idxr, new_idx = convert_label_indexer(index, label, + dim, method, tolerance) + pos_indexers[dim] = idxr + if new_idx is not None: + new_indexes[dim] = new_idx + + return pos_indexers, new_indexes def slice_slice(old_slice, applied_slice, size): diff --git a/xarray/core/variable.py b/xarray/core/variable.py index cb3041ddcd8..c6e9a1ead8b 100644 --- a/xarray/core/variable.py +++ b/xarray/core/variable.py @@ -1096,7 +1096,13 @@ def to_index(self): # basically free as pandas.Index objects are immutable assert self.ndim == 1 index = self._data_cached().array - if not isinstance(index, pd.MultiIndex): + if isinstance(index, pd.MultiIndex): + # set default names for multi-index unnamed levels so that + # we can safely rename dimension / coordinate later + valid_level_names = [name or '{}_level_{}'.format(self.name, i) + for i, name in enumerate(index.names)] + index = index.set_names(valid_level_names) + else: index = index.set_names(self.name) return index diff --git a/xarray/test/test_dataarray.py b/xarray/test/test_dataarray.py index 20f2d01bdec..02d7da50188 100644 --- a/xarray/test/test_dataarray.py +++ b/xarray/test/test_dataarray.py @@ -486,15 +486,45 @@ def test_loc_single_boolean(self): self.assertEqual(data.loc[False], 1) def test_multiindex(self): - idx = pd.MultiIndex.from_product([list('abc'), [0, 1]]) - data = DataArray(range(6), [('x', idx)]) - - self.assertDataArrayIdentical(data.sel(x=('a', 0)), data.isel(x=0)) - self.assertDataArrayIdentical(data.sel(x=('c', 1)), data.isel(x=-1)) - self.assertDataArrayIdentical(data.sel(x=[('a', 0)]), data.isel(x=[0])) - self.assertDataArrayIdentical(data.sel(x=[('a', 0), ('c', 1)]), - data.isel(x=[0, -1])) - self.assertDataArrayIdentical(data.sel(x='a'), data.isel(x=slice(2))) + mindex = pd.MultiIndex.from_product([['a', 'b'], [1, 2], [-1, -2]], + names=('one', 'two', 'three')) + mdata = DataArray(range(8), [('x', mindex)]) + + def test_sel(lab_indexer, pos_indexer, replaced_idx=False, + renamed_dim=None): + da = mdata.sel(x=lab_indexer) + expected_da = mdata.isel(x=pos_indexer) + if not replaced_idx: + self.assertDataArrayIdentical(da, expected_da) + else: + if renamed_dim: + self.assertEqual(da.dims[0], renamed_dim) + da = da.rename({renamed_dim: 'x'}) + self.assertVariableIdentical(da, expected_da) + self.assertVariableNotEqual(da['x'], expected_da['x']) + + test_sel(('a', 1, -1), 0) + test_sel(('b', 2, -2), -1) + test_sel(('a', 1), [0, 1], replaced_idx=True, renamed_dim='three') + test_sel(('a',), range(4), replaced_idx=True) + test_sel('a', range(4), replaced_idx=True) + test_sel([('a', 1, -1), ('b', 2, -2)], [0, 7]) + test_sel(slice('a', 'b'), range(8)) + test_sel(slice(('a', 1), ('b', 1)), range(6)) + test_sel({'one': 'a', 'two': 1, 'three': -1}, 0) + test_sel({'one': 'a', 'two': 1}, [0, 1], replaced_idx=True, + renamed_dim='three') + test_sel({'one': 'a'}, range(4), replaced_idx=True) + + self.assertDataArrayIdentical(mdata.loc['a'], mdata.sel(x='a')) + self.assertDataArrayIdentical(mdata.loc[('a', 1), ...], + mdata.sel(x=('a', 1))) + self.assertDataArrayIdentical(mdata.loc[{'one': 'a'}, ...], + mdata.sel(x={'one': 'a'})) + with self.assertRaises(KeyError): + mdata.loc[{'one': 'a'}] + with self.assertRaises(IndexError): + mdata.loc[('a', 1)] def test_time_components(self): dates = pd.date_range('2000-01-01', periods=10) @@ -1818,29 +1848,29 @@ def test_full_like(self): actual = _full_like(DataArray([1, 2, 3]), fill_value=np.nan) self.assertEqual(actual.dtype, np.float) np.testing.assert_equal(actual.values, np.nan) - + def test_dot(self): x = np.linspace(-3, 3, 6) y = np.linspace(-3, 3, 5) - z = range(4) + z = range(4) da_vals = np.arange(6 * 5 * 4).reshape((6, 5, 4)) da = DataArray(da_vals, coords=[x, y, z], dims=['x', 'y', 'z']) - + dm_vals = range(4) dm = DataArray(dm_vals, coords=[z], dims=['z']) - + # nd dot 1d actual = da.dot(dm) expected_vals = np.tensordot(da_vals, dm_vals, [2, 0]) expected = DataArray(expected_vals, coords=[x, y], dims=['x', 'y']) self.assertDataArrayEqual(expected, actual) - + # all shared dims actual = da.dot(da) expected_vals = np.tensordot(da_vals, da_vals, axes=([0, 1, 2], [0, 1, 2])) expected = DataArray(expected_vals) self.assertDataArrayEqual(expected, actual) - + # multiple shared dims dm_vals = np.arange(20 * 5 * 4).reshape((20, 5, 4)) j = np.linspace(-3, 3, 20) @@ -1849,7 +1879,7 @@ def test_dot(self): expected_vals = np.tensordot(da_vals, dm_vals, axes=([1, 2], [1, 2])) expected = DataArray(expected_vals, coords=[x, j], dims=['x', 'j']) self.assertDataArrayEqual(expected, actual) - + with self.assertRaises(NotImplementedError): da.dot(dm.to_dataset(name='dm')) with self.assertRaises(TypeError): diff --git a/xarray/test/test_dataset.py b/xarray/test/test_dataset.py index 69e2a582b4c..c44cc373ec3 100644 --- a/xarray/test/test_dataset.py +++ b/xarray/test/test_dataset.py @@ -840,6 +840,49 @@ def test_loc(self): with self.assertRaises(TypeError): data.loc[dict(dim3='a')] = 0 + def test_multiindex(self): + mindex = pd.MultiIndex.from_product([['a', 'b'], [1, 2], [-1, -2]], + names=('one', 'two', 'three')) + mdata = Dataset(data_vars={'var': ('x', range(8))}, + coords={'x': mindex}) + + def test_sel(lab_indexer, pos_indexer, replaced_idx=False, + renamed_dim=None): + ds = mdata.sel(x=lab_indexer) + expected_ds = mdata.isel(x=pos_indexer) + if not replaced_idx: + self.assertDatasetIdentical(ds, expected_ds) + else: + if renamed_dim: + self.assertEqual(ds['var'].dims[0], renamed_dim) + ds = ds.rename({renamed_dim: 'x'}) + self.assertVariableIdentical(ds['var'], expected_ds['var']) + self.assertVariableNotEqual(ds['x'], expected_ds['x']) + + test_sel(('a', 1, -1), 0) + test_sel(('b', 2, -2), -1) + test_sel(('a', 1), [0, 1], replaced_idx=True, renamed_dim='three') + test_sel(('a',), range(4), replaced_idx=True) + test_sel('a', range(4), replaced_idx=True) + test_sel([('a', 1, -1), ('b', 2, -2)], [0, 7]) + test_sel(slice('a', 'b'), range(8)) + test_sel(slice(('a', 1), ('b', 1)), range(6)) + test_sel({'one': 'a', 'two': 1, 'three': -1}, 0) + test_sel({'one': 'a', 'two': 1}, [0, 1], replaced_idx=True, + renamed_dim='three') + test_sel({'one': 'a'}, range(4), replaced_idx=True) + + self.assertDatasetIdentical(mdata.loc[{'x': {'one': 'a'}}], + mdata.sel(x={'one': 'a'})) + self.assertDatasetIdentical(mdata.loc[{'x': 'a'}], + mdata.sel(x='a')) + self.assertDatasetIdentical(mdata.loc[{'x': ('a', 1)}], + mdata.sel(x=('a', 1))) + self.assertDatasetIdentical(mdata.loc[{'x': ('a', 1, -1)}], + mdata.sel(x=('a', 1, -1))) + with self.assertRaises(KeyError): + mdata.loc[{'one': 'a'}] + def test_reindex_like(self): data = create_test_data() data['letters'] = ('dim3', 10 * ['a']) @@ -1177,10 +1220,6 @@ def test_unstack_errors(self): with self.assertRaisesRegexp(ValueError, 'does not have a MultiIndex'): ds.unstack('x') - ds2 = Dataset({'x': pd.Index([(0, 1)])}) - with self.assertRaisesRegexp(ValueError, 'unnamed levels'): - ds2.unstack('x') - def test_stack_unstack(self): ds = Dataset({'a': ('x', [0, 1]), 'b': (('x', 'y'), [[0, 1], [2, 3]]), diff --git a/xarray/test/test_indexing.py b/xarray/test/test_indexing.py index 805d245bacf..1dca99ec99a 100644 --- a/xarray/test/test_indexing.py +++ b/xarray/test/test_indexing.py @@ -1,7 +1,7 @@ import numpy as np import pandas as pd -from xarray import Dataset, Variable +from xarray import Dataset, DataArray, Variable from xarray.core import indexing from . import TestCase, ReturnItem @@ -85,6 +85,19 @@ def test_convert_label_indexer(self): indexing.convert_label_indexer(index, [0]) with self.assertRaises(KeyError): indexing.convert_label_indexer(index, 0) + with self.assertRaisesRegexp(ValueError, 'does not have a MultiIndex'): + indexing.convert_label_indexer(index, {'one': 0}) + + mindex = pd.MultiIndex.from_product([['a', 'b'], [1, 2]], + names=('one', 'two')) + with self.assertRaisesRegexp(KeyError, 'not all values found'): + indexing.convert_label_indexer(mindex, [0]) + with self.assertRaises(KeyError): + indexing.convert_label_indexer(mindex, 0) + with self.assertRaises(ValueError): + indexing.convert_label_indexer(index, {'three': 0}) + with self.assertRaisesRegexp(KeyError, 'index to be fully lexsorted'): + indexing.convert_label_indexer(mindex, (slice(None), 1, 'no_level')) def test_convert_unsorted_datetime_index_raises(self): index = pd.to_datetime(['2001', '2000', '2002']) @@ -96,13 +109,41 @@ def test_convert_unsorted_datetime_index_raises(self): def test_remap_label_indexers(self): # TODO: fill in more tests! + def test_indexer(data, x, expected_pos, expected_idx=None): + pos, idx = indexing.remap_label_indexers(data, {'x': x}) + self.assertArrayEqual(pos.get('x'), expected_pos) + self.assertArrayEqual(idx.get('x'), expected_idx) + data = Dataset({'x': ('x', [1, 2, 3])}) + mindex = pd.MultiIndex.from_product([['a', 'b'], [1, 2], [-1, -2]], + names=('one', 'two', 'three')) + mdata = DataArray(range(8), [('x', mindex)]) - def test_indexer(x): - return indexing.remap_label_indexers(data, {'x': x}) - self.assertEqual({'x': 0}, test_indexer(1)) - self.assertEqual({'x': 0}, test_indexer(np.int32(1))) - self.assertEqual({'x': 0}, test_indexer(Variable([], 1))) + test_indexer(data, 1, 0) + test_indexer(data, np.int32(1), 0) + test_indexer(data, Variable([], 1), 0) + test_indexer(mdata, ('a', 1, -1), 0) + test_indexer(mdata, ('a', 1), + [True, True, False, False, False, False, False, False], + [-1, -2]) + test_indexer(mdata, 'a', slice(0, 4, None), + pd.MultiIndex.from_product([[1, 2], [-1, -2]])) + test_indexer(mdata, ('a',), + [True, True, True, True, False, False, False, False], + pd.MultiIndex.from_product([[1, 2], [-1, -2]])) + test_indexer(mdata, [('a', 1, -1), ('b', 2, -2)], [0, 7]) + test_indexer(mdata, slice('a', 'b'), slice(0, 8, None)) + test_indexer(mdata, slice(('a', 1), ('b', 1)), slice(0, 6, None)) + test_indexer(mdata, {'one': 'a', 'two': 1, 'three': -1}, 0) + test_indexer(mdata, {'one': 'a', 'two': 1}, + [True, True, False, False, False, False, False, False], + [-1, -2]) + test_indexer(mdata, {'one': 'a', 'three': -1}, + [True, False, True, False, False, False, False, False], + [1, 2]) + test_indexer(mdata, {'one': 'a'}, + [True, True, True, True, False, False, False, False], + pd.MultiIndex.from_product([[1, 2], [-1, -2]])) class TestLazyArray(TestCase): diff --git a/xarray/test/test_variable.py b/xarray/test/test_variable.py index 8304b50c315..70592c04a05 100644 --- a/xarray/test/test_variable.py +++ b/xarray/test/test_variable.py @@ -971,6 +971,11 @@ def test_to_index(self): v = Coordinate(['time'], data, {'foo': 'bar'}) self.assertTrue(pd.Index(data, name='time').identical(v.to_index())) + def test_multiindex_default_level_names(self): + midx = pd.MultiIndex.from_product([['a', 'b'], [1, 2]]) + v = Coordinate(['x'], midx, {'foo': 'bar'}) + self.assertEqual(v.to_index().names, ('x_level_0', 'x_level_1')) + def test_data(self): x = Coordinate('x', np.arange(3.0)) # data should be initially saved as an ndarray