Skip to content

Commit cab2b6b

Browse files
TomAugspurgerjreback
authored andcommitted
Strictly monotonic (pandas-dev#16555)
1 parent a67c7aa commit cab2b6b

File tree

9 files changed

+124
-4
lines changed

9 files changed

+124
-4
lines changed

doc/source/api.rst

+2
Original file line numberDiff line numberDiff line change
@@ -1286,6 +1286,8 @@ Attributes
12861286
Index.is_monotonic
12871287
Index.is_monotonic_increasing
12881288
Index.is_monotonic_decreasing
1289+
Index.is_strictly_monotonic_increasing
1290+
Index.is_strictly_monotonic_decreasing
12891291
Index.is_unique
12901292
Index.has_duplicates
12911293
Index.dtype

doc/source/whatsnew/v0.20.2.txt

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Enhancements
2121

2222
- Unblocked access to additional compression types supported in pytables: 'blosc:blosclz, 'blosc:lz4', 'blosc:lz4hc', 'blosc:snappy', 'blosc:zlib', 'blosc:zstd' (:issue:`14478`)
2323
- ``Series`` provides a ``to_latex`` method (:issue:`16180`)
24+
- Added :attr:`Index.is_strictly_monotonic_increasing` and :attr:`Index.is_strictly_monotonic_decreasing` properties (:issue:`16515`)
2425

2526
.. _whatsnew_0202.performance:
2627

@@ -61,7 +62,7 @@ Indexing
6162
^^^^^^^^
6263

6364
- Bug in ``DataFrame.reset_index(level=)`` with single level index (:issue:`16263`)
64-
65+
- Bug in partial string indexing with a monotonic, but not strictly-monotonic, index incorrectly reversing the slice bounds (:issue:`16515`)
6566

6667
I/O
6768
^^^

pandas/core/indexes/base.py

+50
Original file line numberDiff line numberDiff line change
@@ -1191,6 +1191,15 @@ def is_monotonic_increasing(self):
11911191
"""
11921192
return if the index is monotonic increasing (only equal or
11931193
increasing) values.
1194+
1195+
Examples
1196+
--------
1197+
>>> Index([1, 2, 3]).is_monotonic_increasing
1198+
True
1199+
>>> Index([1, 2, 2]).is_monotonic_increasing
1200+
True
1201+
>>> Index([1, 3, 2]).is_monotonic_increasing
1202+
False
11941203
"""
11951204
return self._engine.is_monotonic_increasing
11961205

@@ -1199,9 +1208,50 @@ def is_monotonic_decreasing(self):
11991208
"""
12001209
return if the index is monotonic decreasing (only equal or
12011210
decreasing) values.
1211+
1212+
Examples
1213+
--------
1214+
>>> Index([3, 2, 1]).is_monotonic_decreasing
1215+
True
1216+
>>> Index([3, 2, 2]).is_monotonic_decreasing
1217+
True
1218+
>>> Index([3, 1, 2]).is_monotonic_decreasing
1219+
False
12021220
"""
12031221
return self._engine.is_monotonic_decreasing
12041222

1223+
@property
1224+
def is_strictly_monotonic_increasing(self):
1225+
"""return if the index is strictly monotonic increasing
1226+
(only increasing) values
1227+
1228+
Examples
1229+
--------
1230+
>>> Index([1, 2, 3]).is_strictly_monotonic_increasing
1231+
True
1232+
>>> Index([1, 2, 2]).is_strictly_monotonic_increasing
1233+
False
1234+
>>> Index([1, 3, 2]).is_strictly_monotonic_increasing
1235+
False
1236+
"""
1237+
return self.is_unique and self.is_monotonic_increasing
1238+
1239+
@property
1240+
def is_strictly_monotonic_decreasing(self):
1241+
"""return if the index is strictly monotonic decreasing
1242+
(only decreasing) values
1243+
1244+
Examples
1245+
--------
1246+
>>> Index([3, 2, 1]).is_strictly_monotonic_decreasing
1247+
True
1248+
>>> Index([3, 2, 2]).is_strictly_monotonic_decreasing
1249+
False
1250+
>>> Index([3, 1, 2]).is_strictly_monotonic_decreasing
1251+
False
1252+
"""
1253+
return self.is_unique and self.is_monotonic_decreasing
1254+
12051255
def is_lexsorted_for_tuple(self, tup):
12061256
return True
12071257

pandas/core/indexes/datetimes.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1472,7 +1472,7 @@ def _maybe_cast_slice_bound(self, label, side, kind):
14721472
# the bounds need swapped if index is reverse sorted and has a
14731473
# length > 1 (is_monotonic_decreasing gives True for empty
14741474
# and length 1 index)
1475-
if self.is_monotonic_decreasing and len(self) > 1:
1475+
if self.is_strictly_monotonic_decreasing and len(self) > 1:
14761476
return upper if side == 'left' else lower
14771477
return lower if side == 'left' else upper
14781478
else:

pandas/tests/indexes/datetimes/test_datetime.py

+7
Original file line numberDiff line numberDiff line change
@@ -771,3 +771,10 @@ def test_slice_bounds_empty(self):
771771
left = empty_idx._maybe_cast_slice_bound('2015-01-02', 'left', 'loc')
772772
exp = Timestamp('2015-01-02 00:00:00')
773773
assert left == exp
774+
775+
def test_slice_duplicate_monotonic(self):
776+
# https://github.com/pandas-dev/pandas/issues/16515
777+
idx = pd.DatetimeIndex(['2017', '2017'])
778+
result = idx._maybe_cast_slice_bound('2017-01-01', 'left', 'loc')
779+
expected = Timestamp('2017-01-01')
780+
assert result == expected

pandas/tests/indexes/test_base.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -1328,8 +1328,10 @@ def test_tuple_union_bug(self):
13281328

13291329
def test_is_monotonic_incomparable(self):
13301330
index = Index([5, datetime.now(), 7])
1331-
assert not index.is_monotonic
1331+
assert not index.is_monotonic_increasing
13321332
assert not index.is_monotonic_decreasing
1333+
assert not index.is_strictly_monotonic_increasing
1334+
assert not index.is_strictly_monotonic_decreasing
13331335

13341336
def test_get_set_value(self):
13351337
values = np.random.randn(100)
@@ -2028,6 +2030,8 @@ def test_is_monotonic_na(self):
20282030
for index in examples:
20292031
assert not index.is_monotonic_increasing
20302032
assert not index.is_monotonic_decreasing
2033+
assert not index.is_strictly_monotonic_increasing
2034+
assert not index.is_strictly_monotonic_decreasing
20312035

20322036
def test_repr_summary(self):
20332037
with cf.option_context('display.max_seq_items', 10):

pandas/tests/indexes/test_multi.py

+26
Original file line numberDiff line numberDiff line change
@@ -2373,22 +2373,30 @@ def test_is_monotonic(self):
23732373
i = MultiIndex.from_product([np.arange(10),
23742374
np.arange(10)], names=['one', 'two'])
23752375
assert i.is_monotonic
2376+
assert i.is_strictly_monotonic_increasing
23762377
assert Index(i.values).is_monotonic
2378+
assert i.is_strictly_monotonic_increasing
23772379

23782380
i = MultiIndex.from_product([np.arange(10, 0, -1),
23792381
np.arange(10)], names=['one', 'two'])
23802382
assert not i.is_monotonic
2383+
assert not i.is_strictly_monotonic_increasing
23812384
assert not Index(i.values).is_monotonic
2385+
assert not Index(i.values).is_strictly_monotonic_increasing
23822386

23832387
i = MultiIndex.from_product([np.arange(10),
23842388
np.arange(10, 0, -1)],
23852389
names=['one', 'two'])
23862390
assert not i.is_monotonic
2391+
assert not i.is_strictly_monotonic_increasing
23872392
assert not Index(i.values).is_monotonic
2393+
assert not Index(i.values).is_strictly_monotonic_increasing
23882394

23892395
i = MultiIndex.from_product([[1.0, np.nan, 2.0], ['a', 'b', 'c']])
23902396
assert not i.is_monotonic
2397+
assert not i.is_strictly_monotonic_increasing
23912398
assert not Index(i.values).is_monotonic
2399+
assert not Index(i.values).is_strictly_monotonic_increasing
23922400

23932401
# string ordering
23942402
i = MultiIndex(levels=[['foo', 'bar', 'baz', 'qux'],
@@ -2398,6 +2406,8 @@ def test_is_monotonic(self):
23982406
names=['first', 'second'])
23992407
assert not i.is_monotonic
24002408
assert not Index(i.values).is_monotonic
2409+
assert not i.is_strictly_monotonic_increasing
2410+
assert not Index(i.values).is_strictly_monotonic_increasing
24012411

24022412
i = MultiIndex(levels=[['bar', 'baz', 'foo', 'qux'],
24032413
['mom', 'next', 'zenith']],
@@ -2406,6 +2416,8 @@ def test_is_monotonic(self):
24062416
names=['first', 'second'])
24072417
assert i.is_monotonic
24082418
assert Index(i.values).is_monotonic
2419+
assert i.is_strictly_monotonic_increasing
2420+
assert Index(i.values).is_strictly_monotonic_increasing
24092421

24102422
# mixed levels, hits the TypeError
24112423
i = MultiIndex(
@@ -2416,6 +2428,20 @@ def test_is_monotonic(self):
24162428
names=['household_id', 'asset_id'])
24172429

24182430
assert not i.is_monotonic
2431+
assert not i.is_strictly_monotonic_increasing
2432+
2433+
def test_is_strictly_monotonic(self):
2434+
idx = pd.MultiIndex(levels=[['bar', 'baz'], ['mom', 'next']],
2435+
labels=[[0, 0, 1, 1], [0, 0, 0, 1]])
2436+
assert idx.is_monotonic_increasing
2437+
assert not idx.is_strictly_monotonic_increasing
2438+
2439+
@pytest.mark.xfail(reason="buggy MultiIndex.is_monotonic_decresaing.")
2440+
def test_is_strictly_monotonic_decreasing(self):
2441+
idx = pd.MultiIndex(levels=[['baz', 'bar'], ['next', 'mom']],
2442+
labels=[[0, 0, 1, 1], [0, 0, 0, 1]])
2443+
assert idx.is_monotonic_decreasing
2444+
assert not idx.is_strictly_monotonic_decreasing
24192445

24202446
def test_reconstruct_sort(self):
24212447

pandas/tests/indexes/test_numeric.py

+21-1
Original file line numberDiff line numberDiff line change
@@ -465,16 +465,36 @@ def test_view(self):
465465
def test_is_monotonic(self):
466466
assert self.index.is_monotonic
467467
assert self.index.is_monotonic_increasing
468+
assert self.index.is_strictly_monotonic_increasing
468469
assert not self.index.is_monotonic_decreasing
470+
assert not self.index.is_strictly_monotonic_decreasing
469471

470472
index = self._holder([4, 3, 2, 1])
471473
assert not index.is_monotonic
472-
assert index.is_monotonic_decreasing
474+
assert not index.is_strictly_monotonic_increasing
475+
assert index.is_strictly_monotonic_decreasing
473476

474477
index = self._holder([1])
475478
assert index.is_monotonic
476479
assert index.is_monotonic_increasing
477480
assert index.is_monotonic_decreasing
481+
assert index.is_strictly_monotonic_increasing
482+
assert index.is_strictly_monotonic_decreasing
483+
484+
def test_is_strictly_monotonic(self):
485+
index = self._holder([1, 1, 2, 3])
486+
assert index.is_monotonic_increasing
487+
assert not index.is_strictly_monotonic_increasing
488+
489+
index = self._holder([3, 2, 1, 1])
490+
assert index.is_monotonic_decreasing
491+
assert not index.is_strictly_monotonic_decreasing
492+
493+
index = self._holder([1, 1])
494+
assert index.is_monotonic_increasing
495+
assert index.is_monotonic_decreasing
496+
assert not index.is_strictly_monotonic_increasing
497+
assert not index.is_strictly_monotonic_decreasing
478498

479499
def test_logical_compat(self):
480500
idx = self.create_index()

pandas/tests/indexes/test_range.py

+10
Original file line numberDiff line numberDiff line change
@@ -331,25 +331,35 @@ def test_is_monotonic(self):
331331
assert self.index.is_monotonic
332332
assert self.index.is_monotonic_increasing
333333
assert not self.index.is_monotonic_decreasing
334+
assert self.index.is_strictly_monotonic_increasing
335+
assert not self.index.is_strictly_monotonic_decreasing
334336

335337
index = RangeIndex(4, 0, -1)
336338
assert not index.is_monotonic
339+
assert not index.is_strictly_monotonic_increasing
337340
assert index.is_monotonic_decreasing
341+
assert index.is_strictly_monotonic_decreasing
338342

339343
index = RangeIndex(1, 2)
340344
assert index.is_monotonic
341345
assert index.is_monotonic_increasing
342346
assert index.is_monotonic_decreasing
347+
assert index.is_strictly_monotonic_increasing
348+
assert index.is_strictly_monotonic_decreasing
343349

344350
index = RangeIndex(2, 1)
345351
assert index.is_monotonic
346352
assert index.is_monotonic_increasing
347353
assert index.is_monotonic_decreasing
354+
assert index.is_strictly_monotonic_increasing
355+
assert index.is_strictly_monotonic_decreasing
348356

349357
index = RangeIndex(1, 1)
350358
assert index.is_monotonic
351359
assert index.is_monotonic_increasing
352360
assert index.is_monotonic_decreasing
361+
assert index.is_strictly_monotonic_increasing
362+
assert index.is_strictly_monotonic_decreasing
353363

354364
def test_equals_range(self):
355365
equiv_pairs = [(RangeIndex(0, 9, 2), RangeIndex(0, 10, 2)),

0 commit comments

Comments
 (0)