From 49c5bd05866bbcbb2524b1b6d8b69a8ed710a4f3 Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Mon, 29 May 2017 09:52:45 -0500 Subject: [PATCH 1/2] ENH: Implement strictly monotonic methods --- doc/source/api.rst | 2 ++ doc/source/whatsnew/v0.20.2.txt | 1 + pandas/core/indexes/base.py | 50 ++++++++++++++++++++++++++++ pandas/tests/indexes/test_base.py | 6 +++- pandas/tests/indexes/test_multi.py | 26 +++++++++++++++ pandas/tests/indexes/test_numeric.py | 22 +++++++++++- pandas/tests/indexes/test_range.py | 10 ++++++ 7 files changed, 115 insertions(+), 2 deletions(-) diff --git a/doc/source/api.rst b/doc/source/api.rst index 888bb6d67e94b..e210849d9a0ca 100644 --- a/doc/source/api.rst +++ b/doc/source/api.rst @@ -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 diff --git a/doc/source/whatsnew/v0.20.2.txt b/doc/source/whatsnew/v0.20.2.txt index 676da5c370041..13435b2ba428b 100644 --- a/doc/source/whatsnew/v0.20.2.txt +++ b/doc/source/whatsnew/v0.20.2.txt @@ -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: diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index 2af4f112ca941..95978d063230d 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -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 @@ -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 diff --git a/pandas/tests/indexes/test_base.py b/pandas/tests/indexes/test_base.py index 02561cba784b8..a6933316e4291 100644 --- a/pandas/tests/indexes/test_base.py +++ b/pandas/tests/indexes/test_base.py @@ -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) @@ -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): diff --git a/pandas/tests/indexes/test_multi.py b/pandas/tests/indexes/test_multi.py index 1fe4d85815c4b..388a49d25cb82 100644 --- a/pandas/tests/indexes/test_multi.py +++ b/pandas/tests/indexes/test_multi.py @@ -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'], @@ -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']], @@ -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( @@ -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): diff --git a/pandas/tests/indexes/test_numeric.py b/pandas/tests/indexes/test_numeric.py index 3d06f1672ae32..77f34dbf210e0 100644 --- a/pandas/tests/indexes/test_numeric.py +++ b/pandas/tests/indexes/test_numeric.py @@ -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() diff --git a/pandas/tests/indexes/test_range.py b/pandas/tests/indexes/test_range.py index c7af0954cf483..db8180cb736c4 100644 --- a/pandas/tests/indexes/test_range.py +++ b/pandas/tests/indexes/test_range.py @@ -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)), From dcd38f0b91df3d5ef17c046c16e4a35ccc9213ea Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Wed, 31 May 2017 11:34:36 -0500 Subject: [PATCH 2/2] BUG: Fixed partial slicing on monotonic index with dupes Fixed an edge case in partial string indexing where we would incorrectly flip the endpoint on a slice, since we checked for monotonicity when we needed strict monotonicity. Closes https://github.com/pandas-dev/pandas/issues/16515 xref https://github.com/dask/dask/issues/2389 --- doc/source/whatsnew/v0.20.2.txt | 2 +- pandas/core/indexes/datetimes.py | 2 +- pandas/tests/indexes/datetimes/test_datetime.py | 7 +++++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/doc/source/whatsnew/v0.20.2.txt b/doc/source/whatsnew/v0.20.2.txt index 13435b2ba428b..44ad6eeab9d98 100644 --- a/doc/source/whatsnew/v0.20.2.txt +++ b/doc/source/whatsnew/v0.20.2.txt @@ -63,7 +63,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 ^^^ diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index ec678b1577d81..60560374cd420 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -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: diff --git a/pandas/tests/indexes/datetimes/test_datetime.py b/pandas/tests/indexes/datetimes/test_datetime.py index 6cba7e17abf8e..f99dcee9e5c8a 100644 --- a/pandas/tests/indexes/datetimes/test_datetime.py +++ b/pandas/tests/indexes/datetimes/test_datetime.py @@ -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