Skip to content

Commit 51bf8d5

Browse files
committed
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.
1 parent 330b8c1 commit 51bf8d5

File tree

3 files changed

+54
-3
lines changed

3 files changed

+54
-3
lines changed

Diff for: doc/source/whatsnew/v0.21.0.txt

+2-1
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ Conversion
309309

310310
- Bug in assignment against datetime-like data with ``int`` may incorrectly converte to datetime-like (:issue:`14145`)
311311
- Bug in assignment against ``int64`` data with ``np.ndarray`` with ``float64`` dtype may keep ``int64`` dtype (:issue:`14001`)
312-
312+
- Bug in the return type of ``IntervalIndex.is_non_overlapping_monotonic``, which returned ``numpy.bool_`` instead of Python ``bool`` (:issue:`17237`)
313313

314314
Indexing
315315
^^^^^^^^
@@ -385,3 +385,4 @@ Other
385385
- Bug in :func:`eval` where the ``inplace`` parameter was being incorrectly handled (:issue:`16732`)
386386
- Bug in ``.isin()`` in which checking membership in empty ``Series`` objects raised an error (:issue:`16991`)
387387
- Bug in :func:`unique` where checking a tuple of strings raised a ``TypeError`` (:issue:`17108`)
388+
- Bug in ``IntervalIndex.is_non_overlapping_monotonic`` when intervals are closed on both sides and overlap at a point (:issue:`16560`)

Diff for: pandas/core/indexes/interval.py

+11-2
Original file line numberDiff line numberDiff line change
@@ -556,8 +556,17 @@ def is_non_overlapping_monotonic(self):
556556
# must be increasing (e.g., [0, 1), [1, 2), [2, 3), ... )
557557
# or decreasing (e.g., [-1, 0), [-2, -1), [-3, -2), ...)
558558
# we already require left <= right
559-
return ((self.right[:-1] <= self.left[1:]).all() or
560-
(self.left[:-1] >= self.right[1:]).all())
559+
560+
# strict inequality for closed == 'both'; equality implies overlapping
561+
# at a point when both sides of intervals are included
562+
if self.closed == 'both':
563+
return bool((self.right[:-1] < self.left[1:]).all() or
564+
(self.left[:-1] > self.right[1:]).all())
565+
566+
# non-strict inequality when closed != 'both'; at least one side is
567+
# not included in the intervals, so equality does not imply overlapping
568+
return bool((self.right[:-1] <= self.left[1:]).all() or
569+
(self.left[:-1] >= self.right[1:]).all())
561570

562571
@Appender(_index_shared_docs['_convert_scalar_indexer'])
563572
def _convert_scalar_indexer(self, key, kind=None):

Diff for: pandas/tests/indexing/test_interval.py

+41
Original file line numberDiff line numberDiff line change
@@ -243,3 +243,44 @@ def test_loc_getitem_frame(self):
243243
# partial missing
244244
with pytest.raises(KeyError):
245245
df.loc[[10, 4]]
246+
247+
def test_is_non_overlapping_monotonic(self):
248+
# Verify that a Python Boolean is returned (GH17237)
249+
for closed in ('left', 'right', 'neither', 'both'):
250+
idx = IntervalIndex.from_breaks(range(4), closed=closed)
251+
assert type(idx.is_non_overlapping_monotonic) is bool
252+
253+
# Should be True in all cases
254+
tpls = [(0, 1), (2, 3), (4, 5), (6, 7)]
255+
for closed in ('left', 'right', 'neither', 'both'):
256+
idx = IntervalIndex.from_tuples(tpls, closed=closed)
257+
assert idx.is_non_overlapping_monotonic is True
258+
259+
idx = IntervalIndex.from_tuples(reversed(tpls), closed=closed)
260+
assert idx.is_non_overlapping_monotonic is True
261+
262+
# Should be False in all cases (overlapping)
263+
tpls = [(0, 2), (1, 3), (4, 5), (6, 7)]
264+
for closed in ('left', 'right', 'neither', 'both'):
265+
idx = IntervalIndex.from_tuples(tpls, closed=closed)
266+
assert idx.is_non_overlapping_monotonic is False
267+
268+
idx = IntervalIndex.from_tuples(reversed(tpls), closed=closed)
269+
assert idx.is_non_overlapping_monotonic is False
270+
271+
# Should be False in all cases (non-monotonic)
272+
tpls = [(0, 1), (2, 3), (6, 7), (4, 5)]
273+
for closed in ('left', 'right', 'neither', 'both'):
274+
idx = IntervalIndex.from_tuples(tpls, closed=closed)
275+
assert idx.is_non_overlapping_monotonic is False
276+
277+
idx = IntervalIndex.from_tuples(reversed(tpls), closed=closed)
278+
assert idx.is_non_overlapping_monotonic is False
279+
280+
# Should be False for closed='both', overwise True (GH16560)
281+
idx = IntervalIndex.from_breaks(range(4), closed='both')
282+
assert idx.is_non_overlapping_monotonic is False
283+
284+
for closed in ('left', 'right', 'neither'):
285+
idx = IntervalIndex.from_breaks(range(4), closed=closed)
286+
assert idx.is_non_overlapping_monotonic is True

0 commit comments

Comments
 (0)