diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index abf574ae109fd7..406ca9ba045c96 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -205,6 +205,13 @@ Strings - - +Interval +^^^^^^^^ + +- Bug in the :class:`IntervalIndex` constructor where the ``closed`` parameter did not always override the inferred ``closed`` (:issue:`19370`) +- +- + Indexing ^^^^^^^^ diff --git a/pandas/_libs/interval.pyx b/pandas/_libs/interval.pyx index 5dbf509fda65e6..fbb7265a17f8b6 100644 --- a/pandas/_libs/interval.pyx +++ b/pandas/_libs/interval.pyx @@ -335,11 +335,17 @@ cdef class Interval(IntervalMixin): @cython.wraparound(False) @cython.boundscheck(False) -cpdef intervals_to_interval_bounds(ndarray intervals): +cpdef intervals_to_interval_bounds(ndarray intervals, + bint validate_closed=True): """ Parameters ---------- - intervals: ndarray object array of Intervals / nulls + intervals : ndarray + object array of Intervals / nulls + + validate_closed: boolean, default True + boolean indicating if all intervals must be closed on the same side. + Mismatching closed will raise if True, else return None for closed. Returns ------- @@ -353,6 +359,7 @@ cpdef intervals_to_interval_bounds(ndarray intervals): object closed = None, interval int64_t n = len(intervals) ndarray left, right + bint seen_closed = False left = np.empty(n, dtype=intervals.dtype) right = np.empty(n, dtype=intervals.dtype) @@ -370,10 +377,14 @@ cpdef intervals_to_interval_bounds(ndarray intervals): left[i] = interval.left right[i] = interval.right - if closed is None: + if not seen_closed: + seen_closed = True closed = interval.closed elif closed != interval.closed: - raise ValueError('intervals must all be closed on the same side') + closed = None + if validate_closed: + msg = 'intervals must all be closed on the same side' + raise ValueError(msg) return left, right, closed diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index 23a655b9a51ee8..80619c7beb28c1 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -233,7 +233,7 @@ def __new__(cls, data, closed=None, dtype=None, copy=False, if isinstance(data, IntervalIndex): left = data.left right = data.right - closed = data.closed + closed = closed or data.closed else: # don't allow scalars @@ -241,16 +241,8 @@ def __new__(cls, data, closed=None, dtype=None, copy=False, cls._scalar_data_error(data) data = maybe_convert_platform_interval(data) - left, right, infer_closed = intervals_to_interval_bounds(data) - - if (com._all_not_none(closed, infer_closed) and - closed != infer_closed): - # GH 18421 - msg = ("conflicting values for closed: constructor got " - "'{closed}', inferred from data '{infer_closed}'" - .format(closed=closed, infer_closed=infer_closed)) - raise ValueError(msg) - + left, right, infer_closed = intervals_to_interval_bounds( + data, validate_closed=closed is None) closed = closed or infer_closed return cls._simple_new(left, right, closed, name, copy=copy, diff --git a/pandas/tests/indexes/interval/test_construction.py b/pandas/tests/indexes/interval/test_construction.py index ac946a3421e539..3745f79d7d65d0 100644 --- a/pandas/tests/indexes/interval/test_construction.py +++ b/pandas/tests/indexes/interval/test_construction.py @@ -312,13 +312,7 @@ def test_generic_errors(self, constructor): pass def test_constructor_errors(self, constructor): - # mismatched closed inferred from intervals vs constructor. - ivs = [Interval(0, 1, closed='both'), Interval(1, 2, closed='both')] - msg = 'conflicting values for closed' - with tm.assert_raises_regex(ValueError, msg): - constructor(ivs, closed='neither') - - # mismatched closed within intervals + # mismatched closed within intervals with no constructor override ivs = [Interval(0, 1, closed='right'), Interval(2, 3, closed='left')] msg = 'intervals must all be closed on the same side' with tm.assert_raises_regex(ValueError, msg): @@ -336,6 +330,24 @@ def test_constructor_errors(self, constructor): with tm.assert_raises_regex(TypeError, msg): constructor([0, 1]) + @pytest.mark.parametrize('data, closed', [ + ([], 'both'), + ([np.nan, np.nan], 'neither'), + ([Interval(0, 3, closed='neither'), + Interval(2, 5, closed='neither')], 'left'), + ([Interval(0, 3, closed='left'), + Interval(2, 5, closed='right')], 'neither'), + (IntervalIndex.from_breaks(range(5), closed='both'), 'right')]) + def test_override_inferred_closed(self, constructor, data, closed): + # GH 19370 + if isinstance(data, IntervalIndex): + tuples = data.to_tuples() + else: + tuples = [(iv.left, iv.right) if notna(iv) else iv for iv in data] + expected = IntervalIndex.from_tuples(tuples, closed=closed) + result = constructor(data, closed=closed) + tm.assert_index_equal(result, expected) + class TestFromIntervals(TestClassConstructors): """