From 92d8070deb4d388256e21dc3b804041e702edd4a Mon Sep 17 00:00:00 2001 From: jschendel Date: Sat, 12 Aug 2017 16:35:59 -0600 Subject: [PATCH 1/2] Fix bugs in IntervalIndex.is_non_overlapping_monotonic IntervalIndex.is_non_overlapping_monotonic returns a Python bool instead of numpy.bool_, and correctly handles the closed='both' case where endpoints are shared. --- doc/source/whatsnew/v0.21.0.txt | 3 +- pandas/core/indexes/interval.py | 13 ++++++-- pandas/tests/indexes/test_interval.py | 46 +++++++++++++++++++++++++-- 3 files changed, 57 insertions(+), 5 deletions(-) diff --git a/doc/source/whatsnew/v0.21.0.txt b/doc/source/whatsnew/v0.21.0.txt index e21ee8d7d31f5..99fa4b0a5aa62 100644 --- a/doc/source/whatsnew/v0.21.0.txt +++ b/doc/source/whatsnew/v0.21.0.txt @@ -310,7 +310,7 @@ Conversion - Bug in assignment against datetime-like data with ``int`` may incorrectly converte to datetime-like (:issue:`14145`) - Bug in assignment against ``int64`` data with ``np.ndarray`` with ``float64`` dtype may keep ``int64`` dtype (:issue:`14001`) - +- Bug in the return type of ``IntervalIndex.is_non_overlapping_monotonic``, which returned ``numpy.bool_`` instead of Python ``bool`` (:issue:`17237`) Indexing ^^^^^^^^ @@ -386,3 +386,4 @@ Other - Bug in :func:`eval` where the ``inplace`` parameter was being incorrectly handled (:issue:`16732`) - Bug in ``.isin()`` in which checking membership in empty ``Series`` objects raised an error (:issue:`16991`) - Bug in :func:`unique` where checking a tuple of strings raised a ``TypeError`` (:issue:`17108`) +- Bug in ``IntervalIndex.is_non_overlapping_monotonic`` when intervals are closed on both sides and overlap at a point (:issue:`16560`) diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index aa2ad21ae37fd..e90378184e3f3 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -556,8 +556,17 @@ def is_non_overlapping_monotonic(self): # must be increasing (e.g., [0, 1), [1, 2), [2, 3), ... ) # or decreasing (e.g., [-1, 0), [-2, -1), [-3, -2), ...) # we already require left <= right - return ((self.right[:-1] <= self.left[1:]).all() or - (self.left[:-1] >= self.right[1:]).all()) + + # strict inequality for closed == 'both'; equality implies overlapping + # at a point when both sides of intervals are included + if self.closed == 'both': + return bool((self.right[:-1] < self.left[1:]).all() or + (self.left[:-1] > self.right[1:]).all()) + + # non-strict inequality when closed != 'both'; at least one side is + # not included in the intervals, so equality does not imply overlapping + return bool((self.right[:-1] <= self.left[1:]).all() or + (self.left[:-1] >= self.right[1:]).all()) @Appender(_index_shared_docs['_convert_scalar_indexer']) def _convert_scalar_indexer(self, key, kind=None): diff --git a/pandas/tests/indexes/test_interval.py b/pandas/tests/indexes/test_interval.py index fe86a2121761a..92cec5bb1d29f 100644 --- a/pandas/tests/indexes/test_interval.py +++ b/pandas/tests/indexes/test_interval.py @@ -371,8 +371,9 @@ def slice_locs_cases(self, breaks): assert index.slice_locs(1, 1) == (1, 1) assert index.slice_locs(1, 2) == (1, 2) - index = IntervalIndex.from_breaks([0, 1, 2], closed='both') - assert index.slice_locs(1, 1) == (0, 2) + index = IntervalIndex.from_tuples([(0, 1), (2, 3), (4, 5)], + closed='both') + assert index.slice_locs(1, 1) == (0, 1) assert index.slice_locs(1, 2) == (0, 2) def test_slice_locs_int64(self): @@ -681,6 +682,47 @@ def f(): pytest.raises(ValueError, f) + def test_is_non_overlapping_monotonic(self): + # Verify that a Python Boolean is returned (GH17237) + for closed in ('left', 'right', 'neither', 'both'): + idx = IntervalIndex.from_breaks(range(4), closed=closed) + assert type(idx.is_non_overlapping_monotonic) is bool + + # Should be True in all cases + tpls = [(0, 1), (2, 3), (4, 5), (6, 7)] + for closed in ('left', 'right', 'neither', 'both'): + idx = IntervalIndex.from_tuples(tpls, closed=closed) + assert idx.is_non_overlapping_monotonic is True + + idx = IntervalIndex.from_tuples(reversed(tpls), closed=closed) + assert idx.is_non_overlapping_monotonic is True + + # Should be False in all cases (overlapping) + tpls = [(0, 2), (1, 3), (4, 5), (6, 7)] + for closed in ('left', 'right', 'neither', 'both'): + idx = IntervalIndex.from_tuples(tpls, closed=closed) + assert idx.is_non_overlapping_monotonic is False + + idx = IntervalIndex.from_tuples(reversed(tpls), closed=closed) + assert idx.is_non_overlapping_monotonic is False + + # Should be False in all cases (non-monotonic) + tpls = [(0, 1), (2, 3), (6, 7), (4, 5)] + for closed in ('left', 'right', 'neither', 'both'): + idx = IntervalIndex.from_tuples(tpls, closed=closed) + assert idx.is_non_overlapping_monotonic is False + + idx = IntervalIndex.from_tuples(reversed(tpls), closed=closed) + assert idx.is_non_overlapping_monotonic is False + + # Should be False for closed='both', overwise True (GH16560) + idx = IntervalIndex.from_breaks(range(4), closed='both') + assert idx.is_non_overlapping_monotonic is False + + for closed in ('left', 'right', 'neither'): + idx = IntervalIndex.from_breaks(range(4), closed=closed) + assert idx.is_non_overlapping_monotonic is True + class TestIntervalRange(object): From 017d272e77270c23ab8e78912c64b76f32fc45b6 Mon Sep 17 00:00:00 2001 From: jschendel Date: Mon, 14 Aug 2017 13:33:30 -0600 Subject: [PATCH 2/2] Review related fixes --- doc/source/whatsnew/v0.21.0.txt | 4 ++-- pandas/tests/indexes/test_interval.py | 5 ----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/doc/source/whatsnew/v0.21.0.txt b/doc/source/whatsnew/v0.21.0.txt index 99fa4b0a5aa62..5ac1984b6a519 100644 --- a/doc/source/whatsnew/v0.21.0.txt +++ b/doc/source/whatsnew/v0.21.0.txt @@ -310,7 +310,8 @@ Conversion - Bug in assignment against datetime-like data with ``int`` may incorrectly converte to datetime-like (:issue:`14145`) - Bug in assignment against ``int64`` data with ``np.ndarray`` with ``float64`` dtype may keep ``int64`` dtype (:issue:`14001`) -- Bug in the return type of ``IntervalIndex.is_non_overlapping_monotonic``, which returned ``numpy.bool_`` instead of Python ``bool`` (:issue:`17237`) +- Fixed the return type of ``IntervalIndex.is_non_overlapping_monotonic`` to be a Python ``bool`` for consistency with similar attributes/methods. Previously returned a ``numpy.bool_``. (:issue:`17237`) +- Bug in ``IntervalIndex.is_non_overlapping_monotonic`` when intervals are closed on both sides and overlap at a point (:issue:`16560`) Indexing ^^^^^^^^ @@ -386,4 +387,3 @@ Other - Bug in :func:`eval` where the ``inplace`` parameter was being incorrectly handled (:issue:`16732`) - Bug in ``.isin()`` in which checking membership in empty ``Series`` objects raised an error (:issue:`16991`) - Bug in :func:`unique` where checking a tuple of strings raised a ``TypeError`` (:issue:`17108`) -- Bug in ``IntervalIndex.is_non_overlapping_monotonic`` when intervals are closed on both sides and overlap at a point (:issue:`16560`) diff --git a/pandas/tests/indexes/test_interval.py b/pandas/tests/indexes/test_interval.py index 92cec5bb1d29f..18eefc3fbdca6 100644 --- a/pandas/tests/indexes/test_interval.py +++ b/pandas/tests/indexes/test_interval.py @@ -683,11 +683,6 @@ def f(): pytest.raises(ValueError, f) def test_is_non_overlapping_monotonic(self): - # Verify that a Python Boolean is returned (GH17237) - for closed in ('left', 'right', 'neither', 'both'): - idx = IntervalIndex.from_breaks(range(4), closed=closed) - assert type(idx.is_non_overlapping_monotonic) is bool - # Should be True in all cases tpls = [(0, 1), (2, 3), (4, 5), (6, 7)] for closed in ('left', 'right', 'neither', 'both'):