Repository: pandas-dev/pandas

Issue:
MultiIndex is_monotonic_decreasing is incorrect
Or just isn't implemented properly yet?
This MultiIndex is identical on the second level, so its `is_monotonic_decreasing` should be the same as `get_level_values(0).is_monotonic_decreasing` (True)

```python
In [27]: idx = pd.MultiIndex([['baz', 'bar'], ['a', 'b']], labels=[[0, 1], [0, 0]])

In [28]: idx
Out[28]:
MultiIndex(levels=[['baz', 'bar'], ['a', 'b']],
           labels=[[0, 1], [0, 0]])

In [29]: idx.is_monotonic_decreasing
Out[29]: False

In [30]: idx.get_level_values(0).is_monotonic_decreasing
Out[30]: True
```

They should both return True.


Discussion:
only implemented is_monotonic_increasing. It needs the same impl. (but its not used anywhere ATM).
`is_monotonic_increasing` is based on `numpy.lexsort`which seems not to support ordering in descending order.
Can we define `is_monotonic_decreasing` in terms of increasing? Something like

```
def is_monotonic_decreasing(self):
    return not self.is_strictly_monotonic_increasing or idx.nunique() == 1
```

not sure if that will work. Looks like `nunique` may not be implemented on MI.
I don't think that will work in general.  You can't get monotonic decreasing from monotonic increasing in all scenarios, unless there's an assumption that the data is monotonic/sorted in some regard.  For example, a MultiIndex generated by `[(3, 3), (1, 1), (2, 2)]` is not monotonic decreasing, but will return `True`.

I think you can implement this similar to `is_monotonic_increasing`.  It's true that `numpy.lexsort`  only seems to support ordering in descending order, but you can get around this.  My initial thought was to just reverse the output of `numpy.lexsort`, but that messes up the order if you have non-unique indices, e.g. `[(3, 3), (2, 2), (2, 2), (1, 1)]`.  The fix would be to add a fake unique decreasing level to the input of `numpy.lexsort` to force uniqueness.  Looking at the source code for `is_monotonic_increasing`, I think you'd just need to make changes along the lines of `values = [np.arange(len(idx) - 1, -1, -1)] + [existing comprehension]` followed by `sort_order = np.lexsort(values)[::-1]`.  And in the `except` clause `pd.Index(idx.values).is_monotonic_decreasing`.

Resolving patch:
diff --git a/pandas/core/indexes/multi.py b/pandas/core/indexes/multi.py
--- 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):


Test patch:
diff --git a/pandas/tests/indexes/test_interval.py b/pandas/tests/indexes/test_interval.py
--- 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
--- 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