Skip to content

Commit c783c08

Browse files
committed
API: new function Dataset.swap_dims
Fixes GH276 Exmaple usage: In [8]: ds = xray.Dataset({'x': range(3), 'y': ('x', list('abc'))}) In [9]: ds Out[9]: <xray.Dataset> Dimensions: (x: 3) Coordinates: * x (x) int64 0 1 2 Data variables: y (x) |S1 'a' 'b' 'c' In [10]: ds.swap_dims({'x': 'y'}) Out[10]: <xray.Dataset> Dimensions: (y: 3) Coordinates: * y (y) |S1 'a' 'b' 'c' x (y) int64 0 1 2 Data variables: *empty* This is a slightly more verbose API than strictly necessary, because the new dimension names must be along existing dimensions (e.g., we could spell this `ds.set_dims(['y'])`). But I still think it's a good idea, for two reasons: 1. It's more explicit. Users control know which dimensions are being swapped. 2. It opens up the possibility of specifying new dimensions with dictionary like syntax, e.g., `ds.swap_dims('x': ('y', list('abc')))` CC aykuznetsova
1 parent 48ce8c2 commit c783c08

File tree

7 files changed

+160
-20
lines changed

7 files changed

+160
-20
lines changed

doc/api.rst

Lines changed: 2 additions & 0 deletions
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

Lines changed: 13 additions & 3 deletions
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

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -553,13 +553,23 @@ def reindex(self, method=None, copy=True, **indexers):
553553
def rename(self, new_name_or_name_dict):
554554
"""Returns a new DataArray with renamed coordinates and/or a new name.
555555
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.
556+
557+
Parameters
558+
----------
559+
new_name_or_name_dict : str or dict-like
560+
If the argument is dict-like, it it used as a mapping from old
561+
names to new names for coordinates (and/or this array itself).
562+
Otherwise, use the argument as the new name for this array.
563+
564+
Returns
565+
-------
566+
renamed : DataArray
567+
Renamed array or array with renamed coordinates.
559568
560569
See Also
561570
--------
562571
Dataset.rename
572+
DataArray.swap_dims
563573
"""
564574
if utils.is_dict_like(new_name_or_name_dict):
565575
name_dict = new_name_or_name_dict
@@ -570,6 +580,32 @@ def rename(self, new_name_or_name_dict):
570580
renamed_dataset = self._dataset.rename(name_dict)
571581
return renamed_dataset[new_name]
572582

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

xray/core/dataset.py

Lines changed: 73 additions & 14 deletions
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

Lines changed: 5 additions & 0 deletions
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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,14 @@ def test_rename(self):
513513
renamed.to_dataset(), self.ds.rename({'foo': 'bar'}))
514514
self.assertEqual(renamed.name, 'bar')
515515

516+
def test_swap_dims(self):
517+
array = DataArray(np.random.randn(3), {'y': ('x', list('abc'))}, 'x')
518+
expected = DataArray(array.values,
519+
{'y': list('abc'), 'x': ('y', range(3))},
520+
dims='y')
521+
actual = array.swap_dims({'x': 'y'})
522+
self.assertDataArrayIdentical(expected, actual)
523+
516524
def test_dataset_getitem(self):
517525
dv = self.ds['foo']
518526
self.assertDataArrayIdentical(dv, self.dv)

xray/test/test_dataset.py

Lines changed: 20 additions & 0 deletions
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)