Skip to content

Commit 503ae20

Browse files
committed
Fix flipped array after stacking decreasing coordinate values
Fixes GH980
1 parent 97e69dc commit 503ae20

File tree

5 files changed

+80
-21
lines changed

5 files changed

+80
-21
lines changed

doc/whats-new.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ Enhancements
2727
Bug fixes
2828
~~~~~~~~~
2929

30+
- Unstacking produces flipped array after stacking decreasing coordinate values
31+
(:issue:`980`).
32+
By `Stephan Hoyer <https://github.com/shoyer>`_.
33+
3034
.. _whats-new.0.8.2:
3135

3236
v0.8.2 (18 August 2016)

xarray/core/dataset.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1324,8 +1324,8 @@ def _stack_once(self, dims, new_dim):
13241324
else:
13251325
variables[name] = var.copy(deep=False)
13261326

1327-
idx = pd.MultiIndex.from_product([self.indexes[d] for d in dims],
1328-
names=dims)
1327+
idx = utils.multiindex_from_product_levels(
1328+
[self.indexes[d] for d in dims], names=dims)
13291329
variables[new_dim] = Coordinate(new_dim, idx)
13301330

13311331
coord_names = set(self._coord_names) - set(dims) | set([new_dim])

xarray/core/utils.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,29 @@ def safe_cast_to_index(array):
5959
return index
6060

6161

62+
def multiindex_from_product_levels(levels, names=None):
63+
"""Creating a MultiIndex from a product without refactorizing levels.
64+
65+
Keeping levels the same is faster, and also gives back the original labels
66+
when we unstack.
67+
68+
Parameters
69+
----------
70+
levels : sequence of arrays
71+
Unique labels for each level.
72+
names : optional sequence of objects
73+
Names for each level.
74+
75+
Returns
76+
-------
77+
pandas.MultiIndex
78+
"""
79+
labels_mesh = np.meshgrid(*[np.arange(len(lev)) for lev in levels],
80+
indexing='ij')
81+
labels = [x.ravel() for x in labels_mesh]
82+
return pd.MultiIndex(levels, labels, sortorder=0, names=names)
83+
84+
6285
def maybe_wrap_array(original, new_array):
6386
"""Wrap a transformed array with __array_wrap__ is it can be done safely.
6487

xarray/test/test_dataarray.py

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -993,6 +993,15 @@ def test_stack_unstack(self):
993993
actual = orig.stack(z=['x', 'y']).unstack('z')
994994
self.assertDataArrayIdentical(orig, actual)
995995

996+
def test_stack_unstack_decreasing_coordinate(self):
997+
# regression test for GH980
998+
orig = DataArray(np.random.rand(3, 4), dims=('y', 'x'),
999+
coords={'x': np.arange(4),
1000+
'y': np.arange(3, 0, -1)})
1001+
stacked = orig.stack(allpoints=['y', 'x'])
1002+
actual = stacked.unstack('allpoints')
1003+
self.assertDataArrayIdentical(orig, actual)
1004+
9961005
def test_unstack_pandas_consistency(self):
9971006
df = pd.DataFrame({'foo': range(3),
9981007
'x': ['a', 'b', 'b'],
@@ -1628,15 +1637,15 @@ def test_align_dtype(self):
16281637
def test_align_copy(self):
16291638
x = DataArray([1, 2, 3], coords=[('a', [1, 2, 3])])
16301639
y = DataArray([1, 2], coords=[('a', [3, 1])])
1631-
1640+
16321641
expected_x2 = x
16331642
expected_y2 = DataArray([2, np.nan, 1], coords=[('a', [1, 2, 3])])
16341643

16351644
x2, y2 = align(x, y, join='outer', copy=False)
16361645
self.assertDataArrayIdentical(expected_x2, x2)
16371646
self.assertDataArrayIdentical(expected_y2, y2)
16381647
assert source_ndarray(x2.data) is source_ndarray(x.data)
1639-
1648+
16401649
x2, y2 = align(x, y, join='outer', copy=True)
16411650
self.assertDataArrayIdentical(expected_x2, x2)
16421651
self.assertDataArrayIdentical(expected_y2, y2)
@@ -1647,23 +1656,28 @@ def test_align_copy(self):
16471656
x2, = align(x, copy=False)
16481657
self.assertDataArrayIdentical(x, x2)
16491658
assert source_ndarray(x2.data) is source_ndarray(x.data)
1650-
1659+
16511660
x2, = align(x, copy=True)
16521661
self.assertDataArrayIdentical(x, x2)
16531662
assert source_ndarray(x2.data) is not source_ndarray(x.data)
16541663

16551664
def test_align_exclude(self):
1656-
x = DataArray([[1, 2], [3, 4]], coords=[('a', [-1, -2]), ('b', [3, 4])])
1657-
y = DataArray([[1, 2], [3, 4]], coords=[('a', [-1, 20]), ('b', [5, 6])])
1665+
x = DataArray([[1, 2], [3, 4]],
1666+
coords=[('a', [-1, -2]), ('b', [3, 4])])
1667+
y = DataArray([[1, 2], [3, 4]],
1668+
coords=[('a', [-1, 20]), ('b', [5, 6])])
16581669
z = DataArray([1], dims=['a'], coords={'a': [20], 'b': 7})
1659-
1670+
16601671
x2, y2, z2 = align(x, y, z, join='outer', exclude=['b'])
1661-
expected_x2 = DataArray([[3, 4], [1, 2], [np.nan, np.nan]], coords=[('a', [-2, -1, 20]), ('b', [3, 4])])
1662-
expected_y2 = DataArray([[np.nan, np.nan], [1, 2], [3, 4]], coords=[('a', [-2, -1, 20]), ('b', [5, 6])])
1663-
expected_z2 = DataArray([np.nan, np.nan, 1], dims=['a'], coords={'a': [-2, -1, 20], 'b': 7})
1672+
expected_x2 = DataArray([[3, 4], [1, 2], [np.nan, np.nan]],
1673+
coords=[('a', [-2, -1, 20]), ('b', [3, 4])])
1674+
expected_y2 = DataArray([[np.nan, np.nan], [1, 2], [3, 4]],
1675+
coords=[('a', [-2, -1, 20]), ('b', [5, 6])])
1676+
expected_z2 = DataArray([np.nan, np.nan, 1], dims=['a'],
1677+
coords={'a': [-2, -1, 20], 'b': 7})
16641678
self.assertDataArrayIdentical(expected_x2, x2)
16651679
self.assertDataArrayIdentical(expected_y2, y2)
1666-
self.assertDataArrayIdentical(expected_z2, z2)
1680+
self.assertDataArrayIdentical(expected_z2, z2)
16671681

16681682
def test_align_indexes(self):
16691683
x = DataArray([1, 2, 3], coords=[('a', [-1, 10, -2])])
@@ -1676,7 +1690,8 @@ def test_align_indexes(self):
16761690
self.assertDataArrayIdentical(expected_y2, y2)
16771691

16781692
x2, = align(x, join='outer', indexes={'a': [-2, 7, 10, -1]})
1679-
expected_x2 = DataArray([3, np.nan, 2, 1], coords=[('a', [-2, 7, 10, -1])])
1693+
expected_x2 = DataArray([3, np.nan, 2, 1],
1694+
coords=[('a', [-2, 7, 10, -1])])
16801695
self.assertDataArrayIdentical(expected_x2, x2)
16811696

16821697
def test_broadcast_arrays(self):
@@ -1699,10 +1714,13 @@ def test_broadcast_arrays(self):
16991714

17001715
def test_broadcast_arrays_misaligned(self):
17011716
# broadcast on misaligned coords must auto-align
1702-
x = DataArray([[1, 2], [3, 4]], coords=[('a', [-1, -2]), ('b', [3, 4])])
1717+
x = DataArray([[1, 2], [3, 4]],
1718+
coords=[('a', [-1, -2]), ('b', [3, 4])])
17031719
y = DataArray([1, 2], coords=[('a', [-1, 20])])
1704-
expected_x2 = DataArray([[3, 4], [1, 2], [np.nan, np.nan]], coords=[('a', [-2, -1, 20]), ('b', [3, 4])])
1705-
expected_y2 = DataArray([[np.nan, np.nan], [1, 1], [2, 2]], coords=[('a', [-2, -1, 20]), ('b', [3, 4])])
1720+
expected_x2 = DataArray([[3, 4], [1, 2], [np.nan, np.nan]],
1721+
coords=[('a', [-2, -1, 20]), ('b', [3, 4])])
1722+
expected_y2 = DataArray([[np.nan, np.nan], [1, 1], [2, 2]],
1723+
coords=[('a', [-2, -1, 20]), ('b', [3, 4])])
17061724
x2, y2 = broadcast(x, y)
17071725
self.assertDataArrayIdentical(expected_x2, x2)
17081726
self.assertDataArrayIdentical(expected_y2, y2)
@@ -1718,21 +1736,24 @@ def test_broadcast_arrays_nocopy(self):
17181736
self.assertDataArrayIdentical(expected_x2, x2)
17191737
self.assertDataArrayIdentical(expected_y2, y2)
17201738
assert source_ndarray(x2.data) is source_ndarray(x.data)
1721-
1739+
17221740
# single-element broadcast (trivial case)
17231741
x2, = broadcast(x)
17241742
self.assertDataArrayIdentical(x, x2)
17251743
assert source_ndarray(x2.data) is source_ndarray(x.data)
17261744

17271745
def test_broadcast_arrays_exclude(self):
1728-
x = DataArray([[1, 2], [3, 4]], coords=[('a', [-1, -2]), ('b', [3, 4])])
1746+
x = DataArray([[1, 2], [3, 4]],
1747+
coords=[('a', [-1, -2]), ('b', [3, 4])])
17291748
y = DataArray([1, 2], coords=[('a', [-1, 20])])
17301749
z = DataArray(5, coords={'b': 5})
1731-
1750+
17321751
x2, y2, z2 = broadcast(x, y, z, exclude=['b'])
1733-
expected_x2 = DataArray([[3, 4], [1, 2], [np.nan, np.nan]], coords=[('a', [-2, -1, 20]), ('b', [3, 4])])
1752+
expected_x2 = DataArray([[3, 4], [1, 2], [np.nan, np.nan]],
1753+
coords=[('a', [-2, -1, 20]), ('b', [3, 4])])
17341754
expected_y2 = DataArray([np.nan, 1, 2], coords=[('a', [-2, -1, 20])])
1735-
expected_z2 = DataArray([5, 5, 5], dims=['a'], coords={'a': [-2, -1, 20], 'b': 5})
1755+
expected_z2 = DataArray([5, 5, 5], dims=['a'],
1756+
coords={'a': [-2, -1, 20], 'b': 5})
17361757
self.assertDataArrayIdentical(expected_x2, x2)
17371758
self.assertDataArrayIdentical(expected_y2, y2)
17381759
self.assertDataArrayIdentical(expected_z2, z2)

xarray/test/test_utils.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,17 @@ def test(self):
2222
self.assertEqual(expected.dtype, actual.dtype)
2323

2424

25+
def test_multiindex_from_product_levels():
26+
result = utils.multiindex_from_product_levels([['b', 'a'], [1, 3, 2]])
27+
np.testing.assert_array_equal(
28+
result.labels, [[0, 0, 0, 1, 1, 1], [0, 1, 2, 0, 1, 2]])
29+
np.testing.assert_array_equal(result.levels[0], ['b', 'a'])
30+
np.testing.assert_array_equal(result.levels[1], [1, 3, 2])
31+
32+
other = pd.MultiIndex.from_product([['b', 'a'], [1, 3, 2]])
33+
np.testing.assert_array_equal(result.values, other.values)
34+
35+
2536
class TestArrayEquiv(TestCase):
2637
def test_0d(self):
2738
# verify our work around for pd.isnull not working for 0-dimensional

0 commit comments

Comments
 (0)