From 4989ceb653cf871d077cc1065e4dfa64d662a690 Mon Sep 17 00:00:00 2001 From: Aly Sivji Date: Fri, 2 Jun 2017 17:52:04 -0500 Subject: [PATCH 1/2] Fix IntervalIndex is_non_overlapping_monotonic bug where closed='both' returned incorrect values --- .gitignore | 1 + pandas/core/indexes/interval.py | 8 ++++++-- pandas/tests/indexes/test_interval.py | 13 +++++++++++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index ff0a6aef47163..4b540f38e8adf 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ .ipynb_checkpoints .tags .cache/ +.vscode # Compiled source # ################### diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index b1523cd6c0d0c..047aac7c018d6 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -556,8 +556,12 @@ 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()) + if self.closed == 'both': + return ((self.right[:-1] < self.left[1:]).all() or + (self.left[:-1] > self.right[1:]).all()) + else: + return ((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 33745017fe3d6..27593da52fa88 100644 --- a/pandas/tests/indexes/test_interval.py +++ b/pandas/tests/indexes/test_interval.py @@ -371,10 +371,19 @@ 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_is_non_overlapping_monotonic(self): + index = IntervalIndex.from_tuples( + [(0, 1), (2, 3), (4, 5)], closed='both') + assert index.is_non_overlapping_monotonic == True + + index = IntervalIndex.from_breaks(range(4), closed='both') + assert index.is_non_overlapping_monotonic == False + def test_slice_locs_int64(self): self.slice_locs_cases([0, 1, 2]) From bdf1146436c21ee9ce9327c71337bf11ad02452f Mon Sep 17 00:00:00 2001 From: Aly Sivji Date: Sat, 10 Jun 2017 11:34:32 -0500 Subject: [PATCH 2/2] Clean up code / add comments + additional test cases. Update WhatsNew with change information. --- doc/source/whatsnew/v0.21.0.txt | 1 + pandas/core/indexes/interval.py | 7 ++++--- pandas/tests/indexes/test_interval.py | 19 ++++++++++++++++++- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/doc/source/whatsnew/v0.21.0.txt b/doc/source/whatsnew/v0.21.0.txt index 3dd8bb2ac2de5..3788d6b17b6b1 100644 --- a/doc/source/whatsnew/v0.21.0.txt +++ b/doc/source/whatsnew/v0.21.0.txt @@ -90,6 +90,7 @@ Bug Fixes Conversion ^^^^^^^^^^ +- Bug in ``IntervalIndex`` where ``is_non_overlapping_monotonic`` returned incorrect value for intervals closed on both sides (:issue:`16560`) Indexing diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index 047aac7c018d6..00d904a0d5ebf 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -559,9 +559,10 @@ def is_non_overlapping_monotonic(self): if self.closed == 'both': return ((self.right[:-1] < self.left[1:]).all() or (self.left[:-1] > self.right[1:]).all()) - else: - return ((self.right[:-1] <= self.left[1:]).all() or - (self.left[:-1] >= self.right[1:]).all()) + + # self.closed == 'left' or 'right' or 'neither' + return ((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 27593da52fa88..a327702542a42 100644 --- a/pandas/tests/indexes/test_interval.py +++ b/pandas/tests/indexes/test_interval.py @@ -380,10 +380,27 @@ def test_is_non_overlapping_monotonic(self): index = IntervalIndex.from_tuples( [(0, 1), (2, 3), (4, 5)], closed='both') assert index.is_non_overlapping_monotonic == True - index = IntervalIndex.from_breaks(range(4), closed='both') assert index.is_non_overlapping_monotonic == False + index = IntervalIndex.from_breaks([0, 1, 2], closed='neither') + assert index.is_non_overlapping_monotonic == True + index = IntervalIndex.from_tuples( + [(0, 2), (1, 3), (3, 4)], closed='neither') + assert index.is_non_overlapping_monotonic == False + + index = IntervalIndex.from_breaks(range(4), closed='left') + assert index.is_non_overlapping_monotonic == True + index = IntervalIndex.from_tuples( + [(0, 1), (1, 2), (1.5, 3)], closed='left') + assert index.is_non_overlapping_monotonic == False + + index = IntervalIndex.from_breaks(range(4), closed='right') + assert index.is_non_overlapping_monotonic == True + index = IntervalIndex.from_tuples( + [(0, 1), (1, 2), (1.5, 3)], closed='right') + assert index.is_non_overlapping_monotonic == False + def test_slice_locs_int64(self): self.slice_locs_cases([0, 1, 2])