Skip to content

Commit 6e1f73e

Browse files
committed
BUG: Fix IntervalIndex constructor and copy with non-default closed
1 parent 774030c commit 6e1f73e

File tree

3 files changed

+68
-41
lines changed

3 files changed

+68
-41
lines changed

doc/source/whatsnew/v0.21.1.txt

+2
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ Bug Fixes
6262
- Bug in ``pd.Series.rolling.skew()`` and ``rolling.kurt()`` with all equal values has floating issue (:issue:`18044`)
6363
- Bug in ``pd.DataFrameGroupBy.count()`` when counting over a datetimelike column (:issue:`13393`)
6464
- Bug in ``pd.concat`` when empty and non-empty DataFrames or Series are concatenated (:issue:`18178` :issue:`18187`)
65+
- Bug in :class:`IntervalIndex` constructor when a list of intervals is passed with non-default ``closed`` (:issue:`18334`)
66+
- Bug in :meth:`IntervalIndex.copy` when copying and ``IntervalIndex`` with non-default ``closed`` (:issue:`18339`)
6567

6668
Conversion
6769
^^^^^^^^^^

pandas/core/indexes/interval.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -190,15 +190,15 @@ def __new__(cls, data, closed='right',
190190
if isinstance(data, IntervalIndex):
191191
left = data.left
192192
right = data.right
193-
193+
closed = data.closed
194194
else:
195195

196196
# don't allow scalars
197197
if is_scalar(data):
198198
cls._scalar_data_error(data)
199199

200200
data = IntervalIndex.from_intervals(data, name=name)
201-
left, right = data.left, data.right
201+
left, right, closed = data.left, data.right, data.closed
202202

203203
return cls._simple_new(left, right, closed, name,
204204
copy=copy, verify_integrity=verify_integrity)
@@ -580,7 +580,8 @@ def copy(self, deep=False, name=None):
580580
left = self.left.copy(deep=True) if deep else self.left
581581
right = self.right.copy(deep=True) if deep else self.right
582582
name = name if name is not None else self.name
583-
return type(self).from_arrays(left, right, name=name)
583+
closed = self.closed
584+
return type(self).from_arrays(left, right, closed=closed, name=name)
584585

585586
@Appender(_index_shared_docs['astype'])
586587
def astype(self, dtype, copy=True):

pandas/tests/indexes/test_interval.py

+62-38
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from pandas import (Interval, IntervalIndex, Index, isna,
77
interval_range, Timestamp, Timedelta,
88
compat, date_range, timedelta_range, DateOffset)
9+
from pandas.compat import product, zip
910
from pandas.tseries.offsets import Day
1011
from pandas._libs.interval import IntervalTree
1112
from pandas.tests.indexes.common import Base
@@ -25,31 +26,39 @@ def setup_method(self, method):
2526
def create_index(self):
2627
return IntervalIndex.from_breaks(np.arange(10))
2728

28-
def test_constructors(self):
29-
expected = self.index
30-
actual = IntervalIndex.from_breaks(np.arange(3), closed='right')
31-
assert expected.equals(actual)
29+
@pytest.mark.parametrize('closed, name', product(
30+
['left', 'right', 'both', 'neither'], [None, 'foo']))
31+
def test_constructors(self, closed, name):
32+
left, right = Index([0, 1, 2, 3]), Index([1, 2, 3, 4])
33+
ivs = [Interval(l, r, closed=closed) for l, r in zip(left, right)]
34+
expected = IntervalIndex._simple_new(
35+
left=left, right=right, closed=closed, name=name)
3236

33-
alternate = IntervalIndex.from_breaks(np.arange(3), closed='left')
34-
assert not expected.equals(alternate)
37+
result = IntervalIndex(ivs, name=name)
38+
tm.assert_index_equal(result, expected)
3539

36-
actual = IntervalIndex.from_intervals([Interval(0, 1), Interval(1, 2)])
37-
assert expected.equals(actual)
40+
result = IntervalIndex.from_intervals(ivs, name=name)
41+
tm.assert_index_equal(result, expected)
3842

39-
actual = IntervalIndex([Interval(0, 1), Interval(1, 2)])
40-
assert expected.equals(actual)
43+
result = IntervalIndex.from_breaks(
44+
np.arange(5), closed=closed, name=name)
45+
tm.assert_index_equal(result, expected)
4146

42-
actual = IntervalIndex.from_arrays(np.arange(2), np.arange(2) + 1,
43-
closed='right')
44-
assert expected.equals(actual)
47+
result = IntervalIndex.from_arrays(
48+
left.values, right.values, closed=closed, name=name)
49+
tm.assert_index_equal(result, expected)
4550

46-
actual = Index([Interval(0, 1), Interval(1, 2)])
47-
assert isinstance(actual, IntervalIndex)
48-
assert expected.equals(actual)
51+
result = IntervalIndex.from_tuples(
52+
zip(left, right), closed=closed, name=name)
53+
tm.assert_index_equal(result, expected)
4954

50-
actual = Index(expected)
51-
assert isinstance(actual, IntervalIndex)
52-
assert expected.equals(actual)
55+
result = Index(ivs, name=name)
56+
assert isinstance(result, IntervalIndex)
57+
tm.assert_index_equal(result, expected)
58+
59+
# idempotent
60+
tm.assert_index_equal(IntervalIndex(expected), expected)
61+
tm.assert_index_equal(Index(expected), expected)
5362

5463
def test_constructors_other(self):
5564

@@ -165,13 +174,16 @@ def test_with_nans(self):
165174
tm.assert_numpy_array_equal(index.isna(),
166175
np.array([False, True, False]))
167176

168-
def test_copy(self):
169-
actual = self.index.copy()
170-
assert actual.equals(self.index)
177+
@pytest.mark.parametrize('closed', ['left', 'right', 'both', 'neither'])
178+
def test_copy(self, closed):
179+
expected = IntervalIndex.from_breaks(np.arange(5), closed=closed)
180+
181+
result = expected.copy()
182+
assert result.equals(expected)
171183

172-
actual = self.index.copy(deep=True)
173-
assert actual.equals(self.index)
174-
assert actual.left is not self.index.left
184+
result = expected.copy(deep=True)
185+
assert result.equals(expected)
186+
assert result.left is not expected.left
175187

176188
def test_ensure_copied_data(self):
177189
# exercise the copy flag in the constructor
@@ -191,19 +203,31 @@ def test_ensure_copied_data(self):
191203
tm.assert_numpy_array_equal(index.right.values, result.right.values,
192204
check_same='copy')
193205

194-
def test_equals(self):
195-
196-
idx = self.index
197-
assert idx.equals(idx)
198-
assert idx.equals(idx.copy())
199-
200-
assert not idx.equals(idx.astype(object))
201-
assert not idx.equals(np.array(idx))
202-
assert not idx.equals(list(idx))
203-
204-
assert not idx.equals([1, 2])
205-
assert not idx.equals(np.array([1, 2]))
206-
assert not idx.equals(pd.date_range('20130101', periods=2))
206+
@pytest.mark.parametrize('closed', ['left', 'right', 'both', 'neither'])
207+
def test_equals(self, closed):
208+
expected = IntervalIndex.from_breaks(np.arange(5), closed=closed)
209+
assert expected.equals(expected)
210+
assert expected.equals(expected.copy())
211+
212+
assert not expected.equals(expected.astype(object))
213+
assert not expected.equals(np.array(expected))
214+
assert not expected.equals(list(expected))
215+
216+
assert not expected.equals([1, 2])
217+
assert not expected.equals(np.array([1, 2]))
218+
assert not expected.equals(pd.date_range('20130101', periods=2))
219+
220+
expected_name1 = IntervalIndex.from_breaks(
221+
np.arange(5), closed=closed, name='foo')
222+
expected_name2 = IntervalIndex.from_breaks(
223+
np.arange(5), closed=closed, name='bar')
224+
assert expected.equals(expected_name1)
225+
assert expected_name1.equals(expected_name2)
226+
227+
for other_closed in {'left', 'right', 'both', 'neither'} - {closed}:
228+
expected_other_closed = IntervalIndex.from_breaks(
229+
np.arange(5), closed=other_closed)
230+
assert not expected.equals(expected_other_closed)
207231

208232
def test_astype(self):
209233

0 commit comments

Comments
 (0)