Skip to content

Commit 21a3800

Browse files
jschendelTomAugspurger
authored andcommitted
ENH: Implement MultiIndex.is_monotonic_decreasing (#17455)
Implemented MultiIndex.is_monotonic_decreasing, and added associated tests. Also added tests for IntervalIndex.is_monotonic_decreasing, as it uses MultiIndex under the hood.
1 parent 3795272 commit 21a3800

File tree

4 files changed

+178
-14
lines changed

4 files changed

+178
-14
lines changed

doc/source/whatsnew/v0.21.0.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ Other Enhancements
114114
- :func:`pd.read_sas()` now recognizes much more of the most frequently used date (datetime) formats in SAS7BDAT files (:issue:`15871`).
115115
- :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`)
116116
- :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`).
117-
117+
- :func:`MultiIndex.is_monotonic_decreasing` has been implemented. Previously returned ``False`` in all cases. (:issue:`16554`)
118118

119119

120120
.. _whatsnew_0210.api_breaking:

pandas/core/indexes/multi.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -706,13 +706,14 @@ def is_monotonic_increasing(self):
706706
# we have mixed types and np.lexsort is not happy
707707
return Index(self.values).is_monotonic
708708

709-
@property
709+
@cache_readonly
710710
def is_monotonic_decreasing(self):
711711
"""
712712
return if the index is monotonic decreasing (only equal or
713713
decreasing) values.
714714
"""
715-
return False
715+
# monotonic decreasing if and only if reverse is monotonic increasing
716+
return self[::-1].is_monotonic_increasing
716717

717718
@cache_readonly
718719
def is_unique(self):

pandas/tests/indexes/test_interval.py

+95-7
Original file line numberDiff line numberDiff line change
@@ -263,21 +263,109 @@ def test_take(self):
263263
actual = self.index.take([0, 0, 1])
264264
assert expected.equals(actual)
265265

266-
def test_monotonic_and_unique(self):
267-
assert self.index.is_monotonic
268-
assert self.index.is_unique
266+
def test_unique(self):
267+
# unique non-overlapping
268+
idx = IntervalIndex.from_tuples([(0, 1), (2, 3), (4, 5)])
269+
assert idx.is_unique
269270

271+
# unique overlapping - distinct endpoints
270272
idx = IntervalIndex.from_tuples([(0, 1), (0.5, 1.5)])
271-
assert idx.is_monotonic
272273
assert idx.is_unique
273274

274-
idx = IntervalIndex.from_tuples([(0, 1), (2, 3), (1, 2)])
275-
assert not idx.is_monotonic
275+
# unique overlapping - shared endpoints
276+
idx = pd.IntervalIndex.from_tuples([(1, 2), (1, 3), (2, 3)])
277+
assert idx.is_unique
278+
279+
# unique nested
280+
idx = IntervalIndex.from_tuples([(-1, 1), (-2, 2)])
281+
assert idx.is_unique
282+
283+
# duplicate
284+
idx = IntervalIndex.from_tuples([(0, 1), (0, 1), (2, 3)])
285+
assert not idx.is_unique
286+
287+
# unique mixed
288+
idx = IntervalIndex.from_tuples([(0, 1), ('a', 'b')])
276289
assert idx.is_unique
277290

278-
idx = IntervalIndex.from_tuples([(0, 2), (0, 2)])
291+
# duplicate mixed
292+
idx = IntervalIndex.from_tuples([(0, 1), ('a', 'b'), (0, 1)])
279293
assert not idx.is_unique
294+
295+
# empty
296+
idx = IntervalIndex([])
297+
assert idx.is_unique
298+
299+
def test_monotonic(self):
300+
# increasing non-overlapping
301+
idx = IntervalIndex.from_tuples([(0, 1), (2, 3), (4, 5)])
302+
assert idx.is_monotonic
303+
assert idx._is_strictly_monotonic_increasing
304+
assert not idx.is_monotonic_decreasing
305+
assert not idx._is_strictly_monotonic_decreasing
306+
307+
# decreasing non-overlapping
308+
idx = IntervalIndex.from_tuples([(4, 5), (2, 3), (1, 2)])
309+
assert not idx.is_monotonic
310+
assert not idx._is_strictly_monotonic_increasing
311+
assert idx.is_monotonic_decreasing
312+
assert idx._is_strictly_monotonic_decreasing
313+
314+
# unordered non-overlapping
315+
idx = IntervalIndex.from_tuples([(0, 1), (4, 5), (2, 3)])
316+
assert not idx.is_monotonic
317+
assert not idx._is_strictly_monotonic_increasing
318+
assert not idx.is_monotonic_decreasing
319+
assert not idx._is_strictly_monotonic_decreasing
320+
321+
# increasing overlapping
322+
idx = IntervalIndex.from_tuples([(0, 2), (0.5, 2.5), (1, 3)])
323+
assert idx.is_monotonic
324+
assert idx._is_strictly_monotonic_increasing
325+
assert not idx.is_monotonic_decreasing
326+
assert not idx._is_strictly_monotonic_decreasing
327+
328+
# decreasing overlapping
329+
idx = IntervalIndex.from_tuples([(1, 3), (0.5, 2.5), (0, 2)])
330+
assert not idx.is_monotonic
331+
assert not idx._is_strictly_monotonic_increasing
332+
assert idx.is_monotonic_decreasing
333+
assert idx._is_strictly_monotonic_decreasing
334+
335+
# unordered overlapping
336+
idx = IntervalIndex.from_tuples([(0.5, 2.5), (0, 2), (1, 3)])
337+
assert not idx.is_monotonic
338+
assert not idx._is_strictly_monotonic_increasing
339+
assert not idx.is_monotonic_decreasing
340+
assert not idx._is_strictly_monotonic_decreasing
341+
342+
# increasing overlapping shared endpoints
343+
idx = pd.IntervalIndex.from_tuples([(1, 2), (1, 3), (2, 3)])
344+
assert idx.is_monotonic
345+
assert idx._is_strictly_monotonic_increasing
346+
assert not idx.is_monotonic_decreasing
347+
assert not idx._is_strictly_monotonic_decreasing
348+
349+
# decreasing overlapping shared endpoints
350+
idx = pd.IntervalIndex.from_tuples([(2, 3), (1, 3), (1, 2)])
351+
assert not idx.is_monotonic
352+
assert not idx._is_strictly_monotonic_increasing
353+
assert idx.is_monotonic_decreasing
354+
assert idx._is_strictly_monotonic_decreasing
355+
356+
# stationary
357+
idx = IntervalIndex.from_tuples([(0, 1), (0, 1)])
358+
assert idx.is_monotonic
359+
assert not idx._is_strictly_monotonic_increasing
360+
assert idx.is_monotonic_decreasing
361+
assert not idx._is_strictly_monotonic_decreasing
362+
363+
# empty
364+
idx = IntervalIndex([])
280365
assert idx.is_monotonic
366+
assert idx._is_strictly_monotonic_increasing
367+
assert idx.is_monotonic_decreasing
368+
assert idx._is_strictly_monotonic_decreasing
281369

282370
@pytest.mark.xfail(reason='not a valid repr as we use interval notation')
283371
def test_repr(self):

pandas/tests/indexes/test_multi.py

+79-4
Original file line numberDiff line numberDiff line change
@@ -2381,7 +2381,7 @@ def test_level_setting_resets_attributes(self):
23812381
# if this fails, probably didn't reset the cache correctly.
23822382
assert not ind.is_monotonic
23832383

2384-
def test_is_monotonic(self):
2384+
def test_is_monotonic_increasing(self):
23852385
i = MultiIndex.from_product([np.arange(10),
23862386
np.arange(10)], names=['one', 'two'])
23872387
assert i.is_monotonic
@@ -2442,14 +2442,89 @@ def test_is_monotonic(self):
24422442
assert not i.is_monotonic
24432443
assert not i._is_strictly_monotonic_increasing
24442444

2445-
def test_is_strictly_monotonic(self):
2445+
# empty
2446+
i = MultiIndex.from_arrays([[], []])
2447+
assert i.is_monotonic
2448+
assert Index(i.values).is_monotonic
2449+
assert i._is_strictly_monotonic_increasing
2450+
assert Index(i.values)._is_strictly_monotonic_increasing
2451+
2452+
def test_is_monotonic_decreasing(self):
2453+
i = MultiIndex.from_product([np.arange(9, -1, -1),
2454+
np.arange(9, -1, -1)],
2455+
names=['one', 'two'])
2456+
assert i.is_monotonic_decreasing
2457+
assert i._is_strictly_monotonic_decreasing
2458+
assert Index(i.values).is_monotonic_decreasing
2459+
assert i._is_strictly_monotonic_decreasing
2460+
2461+
i = MultiIndex.from_product([np.arange(10),
2462+
np.arange(10, 0, -1)],
2463+
names=['one', 'two'])
2464+
assert not i.is_monotonic_decreasing
2465+
assert not i._is_strictly_monotonic_decreasing
2466+
assert not Index(i.values).is_monotonic_decreasing
2467+
assert not Index(i.values)._is_strictly_monotonic_decreasing
2468+
2469+
i = MultiIndex.from_product([np.arange(10, 0, -1),
2470+
np.arange(10)], names=['one', 'two'])
2471+
assert not i.is_monotonic_decreasing
2472+
assert not i._is_strictly_monotonic_decreasing
2473+
assert not Index(i.values).is_monotonic_decreasing
2474+
assert not Index(i.values)._is_strictly_monotonic_decreasing
2475+
2476+
i = MultiIndex.from_product([[2.0, np.nan, 1.0], ['c', 'b', 'a']])
2477+
assert not i.is_monotonic_decreasing
2478+
assert not i._is_strictly_monotonic_decreasing
2479+
assert not Index(i.values).is_monotonic_decreasing
2480+
assert not Index(i.values)._is_strictly_monotonic_decreasing
2481+
2482+
# string ordering
2483+
i = MultiIndex(levels=[['qux', 'foo', 'baz', 'bar'],
2484+
['three', 'two', 'one']],
2485+
labels=[[0, 0, 0, 1, 1, 2, 2, 3, 3, 3],
2486+
[0, 1, 2, 0, 1, 1, 2, 0, 1, 2]],
2487+
names=['first', 'second'])
2488+
assert not i.is_monotonic_decreasing
2489+
assert not Index(i.values).is_monotonic_decreasing
2490+
assert not i._is_strictly_monotonic_decreasing
2491+
assert not Index(i.values)._is_strictly_monotonic_decreasing
2492+
2493+
i = MultiIndex(levels=[['qux', 'foo', 'baz', 'bar'],
2494+
['zenith', 'next', 'mom']],
2495+
labels=[[0, 0, 0, 1, 1, 2, 2, 3, 3, 3],
2496+
[0, 1, 2, 0, 1, 1, 2, 0, 1, 2]],
2497+
names=['first', 'second'])
2498+
assert i.is_monotonic_decreasing
2499+
assert Index(i.values).is_monotonic_decreasing
2500+
assert i._is_strictly_monotonic_decreasing
2501+
assert Index(i.values)._is_strictly_monotonic_decreasing
2502+
2503+
# mixed levels, hits the TypeError
2504+
i = MultiIndex(
2505+
levels=[[4, 3, 2, 1], ['nl0000301109', 'nl0000289965',
2506+
'nl0000289783', 'lu0197800237',
2507+
'gb00b03mlx29']],
2508+
labels=[[0, 1, 1, 2, 2, 2, 3], [4, 2, 0, 0, 1, 3, -1]],
2509+
names=['household_id', 'asset_id'])
2510+
2511+
assert not i.is_monotonic_decreasing
2512+
assert not i._is_strictly_monotonic_decreasing
2513+
2514+
# empty
2515+
i = MultiIndex.from_arrays([[], []])
2516+
assert i.is_monotonic_decreasing
2517+
assert Index(i.values).is_monotonic_decreasing
2518+
assert i._is_strictly_monotonic_decreasing
2519+
assert Index(i.values)._is_strictly_monotonic_decreasing
2520+
2521+
def test_is_strictly_monotonic_increasing(self):
24462522
idx = pd.MultiIndex(levels=[['bar', 'baz'], ['mom', 'next']],
24472523
labels=[[0, 0, 1, 1], [0, 0, 0, 1]])
24482524
assert idx.is_monotonic_increasing
24492525
assert not idx._is_strictly_monotonic_increasing
24502526

2451-
@pytest.mark.xfail(reason="buggy MultiIndex.is_monotonic_decresaing.")
2452-
def test__is_strictly_monotonic_decreasing(self):
2527+
def test_is_strictly_monotonic_decreasing(self):
24532528
idx = pd.MultiIndex(levels=[['baz', 'bar'], ['next', 'mom']],
24542529
labels=[[0, 0, 1, 1], [0, 0, 0, 1]])
24552530
assert idx.is_monotonic_decreasing

0 commit comments

Comments
 (0)