Skip to content

Commit a620e72

Browse files
authored
BUG: Let IntervalIndex constructor override inferred closed (#21584)
1 parent f9cc39f commit a620e72

File tree

4 files changed

+44
-22
lines changed

4 files changed

+44
-22
lines changed

doc/source/whatsnew/v0.24.0.txt

+7
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,13 @@ Strings
205205
-
206206
-
207207

208+
Interval
209+
^^^^^^^^
210+
211+
- Bug in the :class:`IntervalIndex` constructor where the ``closed`` parameter did not always override the inferred ``closed`` (:issue:`19370`)
212+
-
213+
-
214+
208215
Indexing
209216
^^^^^^^^
210217

pandas/_libs/interval.pyx

+15-4
Original file line numberDiff line numberDiff line change
@@ -335,11 +335,17 @@ cdef class Interval(IntervalMixin):
335335

336336
@cython.wraparound(False)
337337
@cython.boundscheck(False)
338-
cpdef intervals_to_interval_bounds(ndarray intervals):
338+
cpdef intervals_to_interval_bounds(ndarray intervals,
339+
bint validate_closed=True):
339340
"""
340341
Parameters
341342
----------
342-
intervals: ndarray object array of Intervals / nulls
343+
intervals : ndarray
344+
object array of Intervals / nulls
345+
346+
validate_closed: boolean, default True
347+
boolean indicating if all intervals must be closed on the same side.
348+
Mismatching closed will raise if True, else return None for closed.
343349
344350
Returns
345351
-------
@@ -353,6 +359,7 @@ cpdef intervals_to_interval_bounds(ndarray intervals):
353359
object closed = None, interval
354360
int64_t n = len(intervals)
355361
ndarray left, right
362+
bint seen_closed = False
356363

357364
left = np.empty(n, dtype=intervals.dtype)
358365
right = np.empty(n, dtype=intervals.dtype)
@@ -370,10 +377,14 @@ cpdef intervals_to_interval_bounds(ndarray intervals):
370377

371378
left[i] = interval.left
372379
right[i] = interval.right
373-
if closed is None:
380+
if not seen_closed:
381+
seen_closed = True
374382
closed = interval.closed
375383
elif closed != interval.closed:
376-
raise ValueError('intervals must all be closed on the same side')
384+
closed = None
385+
if validate_closed:
386+
msg = 'intervals must all be closed on the same side'
387+
raise ValueError(msg)
377388

378389
return left, right, closed
379390

pandas/core/indexes/interval.py

+3-11
Original file line numberDiff line numberDiff line change
@@ -233,24 +233,16 @@ def __new__(cls, data, closed=None, dtype=None, copy=False,
233233
if isinstance(data, IntervalIndex):
234234
left = data.left
235235
right = data.right
236-
closed = data.closed
236+
closed = closed or data.closed
237237
else:
238238

239239
# don't allow scalars
240240
if is_scalar(data):
241241
cls._scalar_data_error(data)
242242

243243
data = maybe_convert_platform_interval(data)
244-
left, right, infer_closed = intervals_to_interval_bounds(data)
245-
246-
if (com._all_not_none(closed, infer_closed) and
247-
closed != infer_closed):
248-
# GH 18421
249-
msg = ("conflicting values for closed: constructor got "
250-
"'{closed}', inferred from data '{infer_closed}'"
251-
.format(closed=closed, infer_closed=infer_closed))
252-
raise ValueError(msg)
253-
244+
left, right, infer_closed = intervals_to_interval_bounds(
245+
data, validate_closed=closed is None)
254246
closed = closed or infer_closed
255247

256248
return cls._simple_new(left, right, closed, name, copy=copy,

pandas/tests/indexes/interval/test_construction.py

+19-7
Original file line numberDiff line numberDiff line change
@@ -312,13 +312,7 @@ def test_generic_errors(self, constructor):
312312
pass
313313

314314
def test_constructor_errors(self, constructor):
315-
# mismatched closed inferred from intervals vs constructor.
316-
ivs = [Interval(0, 1, closed='both'), Interval(1, 2, closed='both')]
317-
msg = 'conflicting values for closed'
318-
with tm.assert_raises_regex(ValueError, msg):
319-
constructor(ivs, closed='neither')
320-
321-
# mismatched closed within intervals
315+
# mismatched closed within intervals with no constructor override
322316
ivs = [Interval(0, 1, closed='right'), Interval(2, 3, closed='left')]
323317
msg = 'intervals must all be closed on the same side'
324318
with tm.assert_raises_regex(ValueError, msg):
@@ -336,6 +330,24 @@ def test_constructor_errors(self, constructor):
336330
with tm.assert_raises_regex(TypeError, msg):
337331
constructor([0, 1])
338332

333+
@pytest.mark.parametrize('data, closed', [
334+
([], 'both'),
335+
([np.nan, np.nan], 'neither'),
336+
([Interval(0, 3, closed='neither'),
337+
Interval(2, 5, closed='neither')], 'left'),
338+
([Interval(0, 3, closed='left'),
339+
Interval(2, 5, closed='right')], 'neither'),
340+
(IntervalIndex.from_breaks(range(5), closed='both'), 'right')])
341+
def test_override_inferred_closed(self, constructor, data, closed):
342+
# GH 19370
343+
if isinstance(data, IntervalIndex):
344+
tuples = data.to_tuples()
345+
else:
346+
tuples = [(iv.left, iv.right) if notna(iv) else iv for iv in data]
347+
expected = IntervalIndex.from_tuples(tuples, closed=closed)
348+
result = constructor(data, closed=closed)
349+
tm.assert_index_equal(result, expected)
350+
339351

340352
class TestFromIntervals(TestClassConstructors):
341353
"""

0 commit comments

Comments
 (0)