Skip to content

Commit bd911aa

Browse files
committed
Merge pull request #372 from shoyer/swap_dims
API: new methods {Dataset/DataArray}.swap_dims
2 parents 9b7064a + 4be2e38 commit bd911aa

File tree

7 files changed

+164
-35
lines changed

7 files changed

+164
-35
lines changed

doc/api.rst

+2
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ Dataset contents
6565
Dataset.copy
6666
Dataset.merge
6767
Dataset.rename
68+
Dataset.swap_dims
6869
Dataset.drop
6970
Dataset.set_coords
7071
Dataset.reset_coords
@@ -166,6 +167,7 @@ DataArray contents
166167
:toctree: generated/
167168

168169
DataArray.rename
170+
DataArray.swap_dims
169171
DataArray.drop
170172
DataArray.reset_coords
171173
DataArray.copy

doc/whats-new.rst

+13-3
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Enhancements
2020

2121
- New documentation sections on :ref:`time-series` and
2222
:ref:`combining multiple files`.
23-
- :py:meth`~xray.Dataset.resample` lets you resample a dataset or data array to
23+
- :py:meth:`~xray.Dataset.resample` lets you resample a dataset or data array to
2424
a new temporal resolution. The syntax is the `same as pandas`_, except you
2525
need to supply the time dimension explicitly:
2626

@@ -57,13 +57,23 @@ Enhancements
5757
5858
array.resample('1D', dim='time', how='first')
5959
60+
.. _same as pandas: http://pandas.pydata.org/pandas-docs/stable/timeseries.html#up-and-downsampling
61+
62+
- :py:meth:`~xray.Dataset.swap_dims` allows for easily swapping one dimension
63+
out for another:
64+
65+
.. ipython:: python
66+
67+
ds = xray.Dataset({'x': range(3), 'y': ('x', list('abc'))})
68+
ds
69+
ds.swap_dims({'x': 'y'})
70+
71+
This was possible in earlier versions of xray, but required some contortions.
6072
- :py:func:`~xray.open_dataset` and :py:meth:`~xray.Dataset.to_netcdf` now
6173
accept an ``engine`` argument to explicitly select which underlying library
6274
(netcdf4 or scipy) is used for reading/writing a netCDF file.
6375
- New documentation section on :ref:`combining multiple files`.
6476

65-
TODO: write full docs on time-series!
66-
6777
.. _same as pandas: http://pandas.pydata.org/pandas-docs/stable/timeseries.html#up-and-downsampling
6878

6979
Bug fixes

xray/core/dataarray.py

+41-11
Original file line numberDiff line numberDiff line change
@@ -309,14 +309,8 @@ def dims(self):
309309

310310
@dims.setter
311311
def dims(self, value):
312-
with self._set_new_dataset() as ds:
313-
if not len(value) == self.ndim:
314-
raise ValueError('%s dimensions supplied but data has ndim=%s'
315-
% (len(value), self.ndim))
316-
name_map = dict(zip(self.dims, value))
317-
ds.rename(name_map, inplace=True)
318-
if self.name in name_map:
319-
self._name = name_map[self.name]
312+
raise AttributeError('you cannot assign dims on a DataArray. Use '
313+
'.rename() or .swap_dims() instead.')
320314

321315
def _item_key_to_dict(self, key):
322316
if utils.is_dict_like(key):
@@ -553,13 +547,23 @@ def reindex(self, method=None, copy=True, **indexers):
553547
def rename(self, new_name_or_name_dict):
554548
"""Returns a new DataArray with renamed coordinates and/or a new name.
555549
556-
If the argument is dict-like, it it used as a mapping from old names to
557-
new names for dataset variables. Otherwise, use the argument as the new
558-
name for this array.
550+
551+
Parameters
552+
----------
553+
new_name_or_name_dict : str or dict-like
554+
If the argument is dict-like, it it used as a mapping from old
555+
names to new names for coordinates (and/or this array itself).
556+
Otherwise, use the argument as the new name for this array.
557+
558+
Returns
559+
-------
560+
renamed : DataArray
561+
Renamed array or array with renamed coordinates.
559562
560563
See Also
561564
--------
562565
Dataset.rename
566+
DataArray.swap_dims
563567
"""
564568
if utils.is_dict_like(new_name_or_name_dict):
565569
name_dict = new_name_or_name_dict
@@ -570,6 +574,32 @@ def rename(self, new_name_or_name_dict):
570574
renamed_dataset = self._dataset.rename(name_dict)
571575
return renamed_dataset[new_name]
572576

577+
def swap_dims(self, dims_dict):
578+
"""Returns a new DataArray with swapped dimensions.
579+
580+
Parameters
581+
----------
582+
dims_dict : dict-like
583+
Dictionary whose keys are current dimension names and whose values
584+
are new names. Each value must already be a coordinate on this
585+
array.
586+
inplace : bool, optional
587+
If True, swap dimensions in-place. Otherwise, return a new object.
588+
589+
Returns
590+
-------
591+
renamed : Dataset
592+
DataArray with swapped dimensions.
593+
594+
See Also
595+
--------
596+
597+
DataArray.rename
598+
Dataset.swap_dims
599+
"""
600+
ds = self._dataset.swap_dims(dims_dict)
601+
return self._with_replaced_dataset(ds)
602+
573603
def transpose(self, *dims):
574604
"""Return a new DataArray object with transposed dimensions.
575605

xray/core/dataset.py

+73-14
Original file line numberDiff line numberDiff line change
@@ -607,7 +607,7 @@ def _construct_direct(cls, variables, coord_names, dims, attrs,
607607
__default_attrs = object()
608608

609609
def _replace_vars_and_dims(self, variables, coord_names=None,
610-
attrs=__default_attrs):
610+
attrs=__default_attrs, inplace=False):
611611
"""Fastpath constructor for internal use.
612612
613613
Preserves coord names and attributes; dimensions are recalculated from
@@ -628,11 +628,21 @@ def _replace_vars_and_dims(self, variables, coord_names=None,
628628
new : Dataset
629629
"""
630630
dims = _calculate_dims(variables)
631-
if coord_names is None:
632-
coord_names = self._coord_names.copy()
633-
if attrs is self.__default_attrs:
634-
attrs = self._attrs_copy()
635-
return self._construct_direct(variables, coord_names, dims, attrs)
631+
if inplace:
632+
self._dims = dims
633+
self._variables = variables
634+
if coord_names is not None:
635+
self._coord_names = coord_names
636+
if attrs is not self.__default_attrs:
637+
self._attrs = attrs
638+
obj = self
639+
else:
640+
if coord_names is None:
641+
coord_names = self._coord_names.copy()
642+
if attrs is self.__default_attrs:
643+
attrs = self._attrs_copy()
644+
obj = self._construct_direct(variables, coord_names, dims, attrs)
645+
return obj
636646

637647
def copy(self, deep=False):
638648
"""Returns a copy of this dataset.
@@ -1221,6 +1231,12 @@ def rename(self, name_dict, inplace=False):
12211231
-------
12221232
renamed : Dataset
12231233
Dataset with renamed variables and dimensions.
1234+
1235+
See Also
1236+
--------
1237+
1238+
Dataset.swap_dims
1239+
DataArray.rename
12241240
"""
12251241
for k in name_dict:
12261242
if k not in self:
@@ -1237,14 +1253,57 @@ def rename(self, name_dict, inplace=False):
12371253
if k in self._coord_names:
12381254
coord_names.add(name)
12391255

1240-
if inplace:
1241-
self._dims = _calculate_dims(variables)
1242-
self._variables = variables
1243-
self._coord_names = coord_names
1244-
obj = self
1245-
else:
1246-
obj = self._replace_vars_and_dims(variables, coord_names)
1247-
return obj
1256+
return self._replace_vars_and_dims(variables, coord_names,
1257+
inplace=inplace)
1258+
1259+
def swap_dims(self, dims_dict, inplace=False):
1260+
"""Returns a new object with swapped dimensions.
1261+
1262+
Parameters
1263+
----------
1264+
dims_dict : dict-like
1265+
Dictionary whose keys are current dimension names and whose values
1266+
are new names. Each value must already be a variable in the
1267+
dataset.
1268+
inplace : bool, optional
1269+
If True, swap dimensions in-place. Otherwise, return a new dataset
1270+
object.
1271+
1272+
Returns
1273+
-------
1274+
renamed : Dataset
1275+
Dataset with swapped dimensions.
1276+
1277+
See Also
1278+
--------
1279+
1280+
Dataset.rename
1281+
DataArray.swap_dims
1282+
"""
1283+
for k, v in dims_dict.items():
1284+
if k not in self.dims:
1285+
raise ValueError('cannot swap from dimension %r because it is '
1286+
'not an existing dimension' % k)
1287+
if self.variables[v].dims != (k,):
1288+
raise ValueError('replacement dimension %r is not a 1D '
1289+
'variable along the old dimension %r'
1290+
% (v, k))
1291+
1292+
result_dims = set(dims_dict.get(dim, dim) for dim in self.dims)
1293+
1294+
variables = OrderedDict()
1295+
1296+
coord_names = self._coord_names.copy()
1297+
coord_names.update(dims_dict.values())
1298+
1299+
for k, v in iteritems(self.variables):
1300+
dims = tuple(dims_dict.get(dim, dim) for dim in v.dims)
1301+
var = v.to_coord() if k in result_dims else v.to_variable()
1302+
var.dims = dims
1303+
variables[k] = var
1304+
1305+
return self._replace_vars_and_dims(variables, coord_names,
1306+
inplace=inplace)
12481307

12491308
def update(self, other, inplace=True):
12501309
"""Update this dataset's variables with those from another dataset.

xray/core/variable.py

+5
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,11 @@ def values(self, values):
332332
"replacement values must match the Variable's shape")
333333
self._data = values
334334

335+
def to_variable(self):
336+
"""Return this variable as a base xray.Variable"""
337+
return Variable(self.dims, self._data, self._attrs,
338+
encoding=self._encoding, fastpath=True)
339+
335340
def to_coord(self):
336341
"""Return this variable as an xray.Coordinate"""
337342
return Coordinate(self.dims, self._data, self._attrs,

xray/test/test_dataarray.py

+10-7
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,8 @@ def test_dims(self):
6969
arr = self.dv
7070
self.assertEqual(arr.dims, ('x', 'y'))
7171

72-
arr.dims = ('w', 'z')
73-
self.assertEqual(arr.dims, ('w', 'z'))
74-
75-
x = Dataset({'x': ('x', np.arange(5))})['x']
76-
x.dims = ('y',)
77-
self.assertEqual(x.dims, ('y',))
78-
self.assertEqual(x.name, 'y')
72+
with self.assertRaisesRegexp(AttributeError, 'you cannot assign'):
73+
arr.dims = ('w', 'z')
7974

8075
def test_encoding(self):
8176
expected = {'foo': 'bar'}
@@ -513,6 +508,14 @@ def test_rename(self):
513508
renamed.to_dataset(), self.ds.rename({'foo': 'bar'}))
514509
self.assertEqual(renamed.name, 'bar')
515510

511+
def test_swap_dims(self):
512+
array = DataArray(np.random.randn(3), {'y': ('x', list('abc'))}, 'x')
513+
expected = DataArray(array.values,
514+
{'y': list('abc'), 'x': ('y', range(3))},
515+
dims='y')
516+
actual = array.swap_dims({'x': 'y'})
517+
self.assertDataArrayIdentical(expected, actual)
518+
516519
def test_dataset_getitem(self):
517520
dv = self.ds['foo']
518521
self.assertDataArrayIdentical(dv, self.dv)

xray/test/test_dataset.py

+20
Original file line numberDiff line numberDiff line change
@@ -824,6 +824,26 @@ def test_rename_inplace(self):
824824
# check virtual variables
825825
self.assertArrayEqual(data['t.dayofyear'], [1, 2, 3])
826826

827+
def test_swap_dims(self):
828+
original = Dataset({'x': [1, 2, 3], 'y': ('x', list('abc')), 'z': 42})
829+
expected = Dataset({'z': 42}, {'x': ('y', [1, 2, 3]), 'y': list('abc')})
830+
actual = original.swap_dims({'x': 'y'})
831+
self.assertDatasetIdentical(expected, actual)
832+
self.assertIsInstance(actual.variables['y'], Coordinate)
833+
self.assertIsInstance(actual.variables['x'], Variable)
834+
835+
roundtripped = actual.swap_dims({'y': 'x'})
836+
self.assertDatasetIdentical(original.set_coords('y'), roundtripped)
837+
838+
actual = original.copy()
839+
actual.swap_dims({'x': 'y'}, inplace=True)
840+
self.assertDatasetIdentical(expected, actual)
841+
842+
with self.assertRaisesRegexp(ValueError, 'cannot swap'):
843+
original.swap_dims({'y': 'x'})
844+
with self.assertRaisesRegexp(ValueError, 'replacement dimension'):
845+
original.swap_dims({'x': 'z'})
846+
827847
def test_update(self):
828848
data = create_test_data(seed=0)
829849
expected = data.copy()

0 commit comments

Comments
 (0)