diff --git a/doc/source/whatsnew/v0.21.0.txt b/doc/source/whatsnew/v0.21.0.txt index c808babeee5d9..3e26f3137e4fc 100644 --- a/doc/source/whatsnew/v0.21.0.txt +++ b/doc/source/whatsnew/v0.21.0.txt @@ -114,7 +114,7 @@ Other Enhancements - :func:`pd.read_sas()` now recognizes much more of the most frequently used date (datetime) formats in SAS7BDAT files (:issue:`15871`). - :func:`DataFrame.items` and :func:`Series.items` is now present in both Python 2 and 3 and is lazy in all cases (:issue:`13918`, :issue:`17213`) - :func:`Styler.where` has been implemented. It is as a convenience for :func:`Styler.applymap` and enables simple DataFrame styling on the Jupyter notebook (:issue:`17474`). - +- :func:`MultiIndex.is_monotonic_decreasing` has been implemented. Previously returned ``False`` in all cases. (:issue:`16554`) .. _whatsnew_0210.api_breaking: diff --git a/pandas/core/indexes/multi.py b/pandas/core/indexes/multi.py index 8b2cf0e7c0b40..ea613a27b6521 100644 --- a/pandas/core/indexes/multi.py +++ b/pandas/core/indexes/multi.py @@ -706,13 +706,14 @@ def is_monotonic_increasing(self): # we have mixed types and np.lexsort is not happy return Index(self.values).is_monotonic - @property + @cache_readonly def is_monotonic_decreasing(self): """ return if the index is monotonic decreasing (only equal or decreasing) values. """ - return False + # monotonic decreasing if and only if reverse is monotonic increasing + return self[::-1].is_monotonic_increasing @cache_readonly def is_unique(self): diff --git a/pandas/tests/indexes/test_interval.py b/pandas/tests/indexes/test_interval.py index 13c3b35e4d85d..dc59495f619b0 100644 --- a/pandas/tests/indexes/test_interval.py +++ b/pandas/tests/indexes/test_interval.py @@ -263,21 +263,109 @@ def test_take(self): actual = self.index.take([0, 0, 1]) assert expected.equals(actual) - def test_monotonic_and_unique(self): - assert self.index.is_monotonic - assert self.index.is_unique + def test_unique(self): + # unique non-overlapping + idx = IntervalIndex.from_tuples([(0, 1), (2, 3), (4, 5)]) + assert idx.is_unique + # unique overlapping - distinct endpoints idx = IntervalIndex.from_tuples([(0, 1), (0.5, 1.5)]) - assert idx.is_monotonic assert idx.is_unique - idx = IntervalIndex.from_tuples([(0, 1), (2, 3), (1, 2)]) - assert not idx.is_monotonic + # unique overlapping - shared endpoints + idx = pd.IntervalIndex.from_tuples([(1, 2), (1, 3), (2, 3)]) + assert idx.is_unique + + # unique nested + idx = IntervalIndex.from_tuples([(-1, 1), (-2, 2)]) + assert idx.is_unique + + # duplicate + idx = IntervalIndex.from_tuples([(0, 1), (0, 1), (2, 3)]) + assert not idx.is_unique + + # unique mixed + idx = IntervalIndex.from_tuples([(0, 1), ('a', 'b')]) assert idx.is_unique - idx = IntervalIndex.from_tuples([(0, 2), (0, 2)]) + # duplicate mixed + idx = IntervalIndex.from_tuples([(0, 1), ('a', 'b'), (0, 1)]) assert not idx.is_unique + + # empty + idx = IntervalIndex([]) + assert idx.is_unique + + def test_monotonic(self): + # increasing non-overlapping + idx = IntervalIndex.from_tuples([(0, 1), (2, 3), (4, 5)]) + assert idx.is_monotonic + assert idx._is_strictly_monotonic_increasing + assert not idx.is_monotonic_decreasing + assert not idx._is_strictly_monotonic_decreasing + + # decreasing non-overlapping + idx = IntervalIndex.from_tuples([(4, 5), (2, 3), (1, 2)]) + assert not idx.is_monotonic + assert not idx._is_strictly_monotonic_increasing + assert idx.is_monotonic_decreasing + assert idx._is_strictly_monotonic_decreasing + + # unordered non-overlapping + idx = IntervalIndex.from_tuples([(0, 1), (4, 5), (2, 3)]) + assert not idx.is_monotonic + assert not idx._is_strictly_monotonic_increasing + assert not idx.is_monotonic_decreasing + assert not idx._is_strictly_monotonic_decreasing + + # increasing overlapping + idx = IntervalIndex.from_tuples([(0, 2), (0.5, 2.5), (1, 3)]) + assert idx.is_monotonic + assert idx._is_strictly_monotonic_increasing + assert not idx.is_monotonic_decreasing + assert not idx._is_strictly_monotonic_decreasing + + # decreasing overlapping + idx = IntervalIndex.from_tuples([(1, 3), (0.5, 2.5), (0, 2)]) + assert not idx.is_monotonic + assert not idx._is_strictly_monotonic_increasing + assert idx.is_monotonic_decreasing + assert idx._is_strictly_monotonic_decreasing + + # unordered overlapping + idx = IntervalIndex.from_tuples([(0.5, 2.5), (0, 2), (1, 3)]) + assert not idx.is_monotonic + assert not idx._is_strictly_monotonic_increasing + assert not idx.is_monotonic_decreasing + assert not idx._is_strictly_monotonic_decreasing + + # increasing overlapping shared endpoints + idx = pd.IntervalIndex.from_tuples([(1, 2), (1, 3), (2, 3)]) + assert idx.is_monotonic + assert idx._is_strictly_monotonic_increasing + assert not idx.is_monotonic_decreasing + assert not idx._is_strictly_monotonic_decreasing + + # decreasing overlapping shared endpoints + idx = pd.IntervalIndex.from_tuples([(2, 3), (1, 3), (1, 2)]) + assert not idx.is_monotonic + assert not idx._is_strictly_monotonic_increasing + assert idx.is_monotonic_decreasing + assert idx._is_strictly_monotonic_decreasing + + # stationary + idx = IntervalIndex.from_tuples([(0, 1), (0, 1)]) + assert idx.is_monotonic + assert not idx._is_strictly_monotonic_increasing + assert idx.is_monotonic_decreasing + assert not idx._is_strictly_monotonic_decreasing + + # empty + idx = IntervalIndex([]) assert idx.is_monotonic + assert idx._is_strictly_monotonic_increasing + assert idx.is_monotonic_decreasing + assert idx._is_strictly_monotonic_decreasing @pytest.mark.xfail(reason='not a valid repr as we use interval notation') def test_repr(self): diff --git a/pandas/tests/indexes/test_multi.py b/pandas/tests/indexes/test_multi.py index 86308192c9166..b1b5413b4d081 100644 --- a/pandas/tests/indexes/test_multi.py +++ b/pandas/tests/indexes/test_multi.py @@ -2381,7 +2381,7 @@ def test_level_setting_resets_attributes(self): # if this fails, probably didn't reset the cache correctly. assert not ind.is_monotonic - def test_is_monotonic(self): + def test_is_monotonic_increasing(self): i = MultiIndex.from_product([np.arange(10), np.arange(10)], names=['one', 'two']) assert i.is_monotonic @@ -2442,14 +2442,89 @@ def test_is_monotonic(self): assert not i.is_monotonic assert not i._is_strictly_monotonic_increasing - def test_is_strictly_monotonic(self): + # empty + i = MultiIndex.from_arrays([[], []]) + assert i.is_monotonic + assert Index(i.values).is_monotonic + assert i._is_strictly_monotonic_increasing + assert Index(i.values)._is_strictly_monotonic_increasing + + def test_is_monotonic_decreasing(self): + i = MultiIndex.from_product([np.arange(9, -1, -1), + np.arange(9, -1, -1)], + names=['one', 'two']) + assert i.is_monotonic_decreasing + assert i._is_strictly_monotonic_decreasing + assert Index(i.values).is_monotonic_decreasing + assert i._is_strictly_monotonic_decreasing + + i = MultiIndex.from_product([np.arange(10), + np.arange(10, 0, -1)], + names=['one', 'two']) + assert not i.is_monotonic_decreasing + assert not i._is_strictly_monotonic_decreasing + assert not Index(i.values).is_monotonic_decreasing + assert not Index(i.values)._is_strictly_monotonic_decreasing + + i = MultiIndex.from_product([np.arange(10, 0, -1), + np.arange(10)], names=['one', 'two']) + assert not i.is_monotonic_decreasing + assert not i._is_strictly_monotonic_decreasing + assert not Index(i.values).is_monotonic_decreasing + assert not Index(i.values)._is_strictly_monotonic_decreasing + + i = MultiIndex.from_product([[2.0, np.nan, 1.0], ['c', 'b', 'a']]) + assert not i.is_monotonic_decreasing + assert not i._is_strictly_monotonic_decreasing + assert not Index(i.values).is_monotonic_decreasing + assert not Index(i.values)._is_strictly_monotonic_decreasing + + # string ordering + i = MultiIndex(levels=[['qux', 'foo', 'baz', 'bar'], + ['three', 'two', 'one']], + labels=[[0, 0, 0, 1, 1, 2, 2, 3, 3, 3], + [0, 1, 2, 0, 1, 1, 2, 0, 1, 2]], + names=['first', 'second']) + assert not i.is_monotonic_decreasing + assert not Index(i.values).is_monotonic_decreasing + assert not i._is_strictly_monotonic_decreasing + assert not Index(i.values)._is_strictly_monotonic_decreasing + + i = MultiIndex(levels=[['qux', 'foo', 'baz', 'bar'], + ['zenith', 'next', 'mom']], + labels=[[0, 0, 0, 1, 1, 2, 2, 3, 3, 3], + [0, 1, 2, 0, 1, 1, 2, 0, 1, 2]], + names=['first', 'second']) + assert i.is_monotonic_decreasing + assert Index(i.values).is_monotonic_decreasing + assert i._is_strictly_monotonic_decreasing + assert Index(i.values)._is_strictly_monotonic_decreasing + + # mixed levels, hits the TypeError + i = MultiIndex( + levels=[[4, 3, 2, 1], ['nl0000301109', 'nl0000289965', + 'nl0000289783', 'lu0197800237', + 'gb00b03mlx29']], + labels=[[0, 1, 1, 2, 2, 2, 3], [4, 2, 0, 0, 1, 3, -1]], + names=['household_id', 'asset_id']) + + assert not i.is_monotonic_decreasing + assert not i._is_strictly_monotonic_decreasing + + # empty + i = MultiIndex.from_arrays([[], []]) + assert i.is_monotonic_decreasing + assert Index(i.values).is_monotonic_decreasing + assert i._is_strictly_monotonic_decreasing + assert Index(i.values)._is_strictly_monotonic_decreasing + + def test_is_strictly_monotonic_increasing(self): idx = pd.MultiIndex(levels=[['bar', 'baz'], ['mom', 'next']], labels=[[0, 0, 1, 1], [0, 0, 0, 1]]) assert idx.is_monotonic_increasing assert not idx._is_strictly_monotonic_increasing - @pytest.mark.xfail(reason="buggy MultiIndex.is_monotonic_decresaing.") - def test__is_strictly_monotonic_decreasing(self): + def test_is_strictly_monotonic_decreasing(self): idx = pd.MultiIndex(levels=[['baz', 'bar'], ['next', 'mom']], labels=[[0, 0, 1, 1], [0, 0, 0, 1]]) assert idx.is_monotonic_decreasing