Skip to content

Commit

Permalink
Strictly monotonic (#16555)
Browse files Browse the repository at this point in the history
  • Loading branch information
TomAugspurger authored and jreback committed Jun 1, 2017
1 parent a67c7aa commit cab2b6b
Show file tree
Hide file tree
Showing 9 changed files with 124 additions and 4 deletions.
2 changes: 2 additions & 0 deletions doc/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1286,6 +1286,8 @@ Attributes
Index.is_monotonic
Index.is_monotonic_increasing
Index.is_monotonic_decreasing
Index.is_strictly_monotonic_increasing
Index.is_strictly_monotonic_decreasing
Index.is_unique
Index.has_duplicates
Index.dtype
Expand Down
3 changes: 2 additions & 1 deletion doc/source/whatsnew/v0.20.2.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Enhancements

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

.. _whatsnew_0202.performance:

Expand Down Expand Up @@ -61,7 +62,7 @@ Indexing
^^^^^^^^

- Bug in ``DataFrame.reset_index(level=)`` with single level index (:issue:`16263`)

- Bug in partial string indexing with a monotonic, but not strictly-monotonic, index incorrectly reversing the slice bounds (:issue:`16515`)

I/O
^^^
Expand Down
50 changes: 50 additions & 0 deletions pandas/core/indexes/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1191,6 +1191,15 @@ def is_monotonic_increasing(self):
"""
return if the index is monotonic increasing (only equal or
increasing) values.
Examples
--------
>>> Index([1, 2, 3]).is_monotonic_increasing
True
>>> Index([1, 2, 2]).is_monotonic_increasing
True
>>> Index([1, 3, 2]).is_monotonic_increasing
False
"""
return self._engine.is_monotonic_increasing

Expand All @@ -1199,9 +1208,50 @@ def is_monotonic_decreasing(self):
"""
return if the index is monotonic decreasing (only equal or
decreasing) values.
Examples
--------
>>> Index([3, 2, 1]).is_monotonic_decreasing
True
>>> Index([3, 2, 2]).is_monotonic_decreasing
True
>>> Index([3, 1, 2]).is_monotonic_decreasing
False
"""
return self._engine.is_monotonic_decreasing

@property
def is_strictly_monotonic_increasing(self):
"""return if the index is strictly monotonic increasing
(only increasing) values
Examples
--------
>>> Index([1, 2, 3]).is_strictly_monotonic_increasing
True
>>> Index([1, 2, 2]).is_strictly_monotonic_increasing
False
>>> Index([1, 3, 2]).is_strictly_monotonic_increasing
False
"""
return self.is_unique and self.is_monotonic_increasing

@property
def is_strictly_monotonic_decreasing(self):
"""return if the index is strictly monotonic decreasing
(only decreasing) values
Examples
--------
>>> Index([3, 2, 1]).is_strictly_monotonic_decreasing
True
>>> Index([3, 2, 2]).is_strictly_monotonic_decreasing
False
>>> Index([3, 1, 2]).is_strictly_monotonic_decreasing
False
"""
return self.is_unique and self.is_monotonic_decreasing

def is_lexsorted_for_tuple(self, tup):
return True

Expand Down
2 changes: 1 addition & 1 deletion pandas/core/indexes/datetimes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1472,7 +1472,7 @@ def _maybe_cast_slice_bound(self, label, side, kind):
# the bounds need swapped if index is reverse sorted and has a
# length > 1 (is_monotonic_decreasing gives True for empty
# and length 1 index)
if self.is_monotonic_decreasing and len(self) > 1:
if self.is_strictly_monotonic_decreasing and len(self) > 1:
return upper if side == 'left' else lower
return lower if side == 'left' else upper
else:
Expand Down
7 changes: 7 additions & 0 deletions pandas/tests/indexes/datetimes/test_datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -771,3 +771,10 @@ def test_slice_bounds_empty(self):
left = empty_idx._maybe_cast_slice_bound('2015-01-02', 'left', 'loc')
exp = Timestamp('2015-01-02 00:00:00')
assert left == exp

def test_slice_duplicate_monotonic(self):
# https://github.com/pandas-dev/pandas/issues/16515
idx = pd.DatetimeIndex(['2017', '2017'])
result = idx._maybe_cast_slice_bound('2017-01-01', 'left', 'loc')
expected = Timestamp('2017-01-01')
assert result == expected
6 changes: 5 additions & 1 deletion pandas/tests/indexes/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1328,8 +1328,10 @@ def test_tuple_union_bug(self):

def test_is_monotonic_incomparable(self):
index = Index([5, datetime.now(), 7])
assert not index.is_monotonic
assert not index.is_monotonic_increasing
assert not index.is_monotonic_decreasing
assert not index.is_strictly_monotonic_increasing
assert not index.is_strictly_monotonic_decreasing

def test_get_set_value(self):
values = np.random.randn(100)
Expand Down Expand Up @@ -2028,6 +2030,8 @@ def test_is_monotonic_na(self):
for index in examples:
assert not index.is_monotonic_increasing
assert not index.is_monotonic_decreasing
assert not index.is_strictly_monotonic_increasing
assert not index.is_strictly_monotonic_decreasing

def test_repr_summary(self):
with cf.option_context('display.max_seq_items', 10):
Expand Down
26 changes: 26 additions & 0 deletions pandas/tests/indexes/test_multi.py
Original file line number Diff line number Diff line change
Expand Up @@ -2373,22 +2373,30 @@ def test_is_monotonic(self):
i = MultiIndex.from_product([np.arange(10),
np.arange(10)], names=['one', 'two'])
assert i.is_monotonic
assert i.is_strictly_monotonic_increasing
assert Index(i.values).is_monotonic
assert i.is_strictly_monotonic_increasing

i = MultiIndex.from_product([np.arange(10, 0, -1),
np.arange(10)], names=['one', 'two'])
assert not i.is_monotonic
assert not i.is_strictly_monotonic_increasing
assert not Index(i.values).is_monotonic
assert not Index(i.values).is_strictly_monotonic_increasing

i = MultiIndex.from_product([np.arange(10),
np.arange(10, 0, -1)],
names=['one', 'two'])
assert not i.is_monotonic
assert not i.is_strictly_monotonic_increasing
assert not Index(i.values).is_monotonic
assert not Index(i.values).is_strictly_monotonic_increasing

i = MultiIndex.from_product([[1.0, np.nan, 2.0], ['a', 'b', 'c']])
assert not i.is_monotonic
assert not i.is_strictly_monotonic_increasing
assert not Index(i.values).is_monotonic
assert not Index(i.values).is_strictly_monotonic_increasing

# string ordering
i = MultiIndex(levels=[['foo', 'bar', 'baz', 'qux'],
Expand All @@ -2398,6 +2406,8 @@ def test_is_monotonic(self):
names=['first', 'second'])
assert not i.is_monotonic
assert not Index(i.values).is_monotonic
assert not i.is_strictly_monotonic_increasing
assert not Index(i.values).is_strictly_monotonic_increasing

i = MultiIndex(levels=[['bar', 'baz', 'foo', 'qux'],
['mom', 'next', 'zenith']],
Expand All @@ -2406,6 +2416,8 @@ def test_is_monotonic(self):
names=['first', 'second'])
assert i.is_monotonic
assert Index(i.values).is_monotonic
assert i.is_strictly_monotonic_increasing
assert Index(i.values).is_strictly_monotonic_increasing

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

assert not i.is_monotonic
assert not i.is_strictly_monotonic_increasing

def test_is_strictly_monotonic(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):
idx = pd.MultiIndex(levels=[['baz', 'bar'], ['next', 'mom']],
labels=[[0, 0, 1, 1], [0, 0, 0, 1]])
assert idx.is_monotonic_decreasing
assert not idx.is_strictly_monotonic_decreasing

def test_reconstruct_sort(self):

Expand Down
22 changes: 21 additions & 1 deletion pandas/tests/indexes/test_numeric.py
Original file line number Diff line number Diff line change
Expand Up @@ -465,16 +465,36 @@ def test_view(self):
def test_is_monotonic(self):
assert self.index.is_monotonic
assert self.index.is_monotonic_increasing
assert self.index.is_strictly_monotonic_increasing
assert not self.index.is_monotonic_decreasing
assert not self.index.is_strictly_monotonic_decreasing

index = self._holder([4, 3, 2, 1])
assert not index.is_monotonic
assert index.is_monotonic_decreasing
assert not index.is_strictly_monotonic_increasing
assert index.is_strictly_monotonic_decreasing

index = self._holder([1])
assert index.is_monotonic
assert index.is_monotonic_increasing
assert index.is_monotonic_decreasing
assert index.is_strictly_monotonic_increasing
assert index.is_strictly_monotonic_decreasing

def test_is_strictly_monotonic(self):
index = self._holder([1, 1, 2, 3])
assert index.is_monotonic_increasing
assert not index.is_strictly_monotonic_increasing

index = self._holder([3, 2, 1, 1])
assert index.is_monotonic_decreasing
assert not index.is_strictly_monotonic_decreasing

index = self._holder([1, 1])
assert index.is_monotonic_increasing
assert index.is_monotonic_decreasing
assert not index.is_strictly_monotonic_increasing
assert not index.is_strictly_monotonic_decreasing

def test_logical_compat(self):
idx = self.create_index()
Expand Down
10 changes: 10 additions & 0 deletions pandas/tests/indexes/test_range.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,25 +331,35 @@ def test_is_monotonic(self):
assert self.index.is_monotonic
assert self.index.is_monotonic_increasing
assert not self.index.is_monotonic_decreasing
assert self.index.is_strictly_monotonic_increasing
assert not self.index.is_strictly_monotonic_decreasing

index = RangeIndex(4, 0, -1)
assert not index.is_monotonic
assert not index.is_strictly_monotonic_increasing
assert index.is_monotonic_decreasing
assert index.is_strictly_monotonic_decreasing

index = RangeIndex(1, 2)
assert index.is_monotonic
assert index.is_monotonic_increasing
assert index.is_monotonic_decreasing
assert index.is_strictly_monotonic_increasing
assert index.is_strictly_monotonic_decreasing

index = RangeIndex(2, 1)
assert index.is_monotonic
assert index.is_monotonic_increasing
assert index.is_monotonic_decreasing
assert index.is_strictly_monotonic_increasing
assert index.is_strictly_monotonic_decreasing

index = RangeIndex(1, 1)
assert index.is_monotonic
assert index.is_monotonic_increasing
assert index.is_monotonic_decreasing
assert index.is_strictly_monotonic_increasing
assert index.is_strictly_monotonic_decreasing

def test_equals_range(self):
equiv_pairs = [(RangeIndex(0, 9, 2), RangeIndex(0, 10, 2)),
Expand Down

0 comments on commit cab2b6b

Please sign in to comment.