Skip to content

Commit

Permalink
Fix bugs in IntervalIndex.is_non_overlapping_monotonic
Browse files Browse the repository at this point in the history
IntervalIndex.is_non_overlapping_monotonic returns a Python bool instead of numpy.bool_, and correctly handles the closed='both' case where endpoints are shared.
  • Loading branch information
jschendel committed Aug 14, 2017
1 parent 0f25426 commit 92d8070
Show file tree
Hide file tree
Showing 3 changed files with 57 additions and 5 deletions.
3 changes: 2 additions & 1 deletion doc/source/whatsnew/v0.21.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
^^^^^^^^
Expand Down Expand Up @@ -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`)
13 changes: 11 additions & 2 deletions pandas/core/indexes/interval.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
46 changes: 44 additions & 2 deletions pandas/tests/indexes/test_interval.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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):

Expand Down

0 comments on commit 92d8070

Please sign in to comment.