-
-
Notifications
You must be signed in to change notification settings - Fork 18.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
BUG: Fix IntervalIndex constructor inconsistencies #18424
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,7 +6,7 @@ | |
from pandas import (Interval, IntervalIndex, Index, isna, | ||
interval_range, Timestamp, Timedelta, | ||
compat, date_range, timedelta_range, DateOffset) | ||
from pandas.compat import zip | ||
from pandas.compat import lzip | ||
from pandas.tseries.offsets import Day | ||
from pandas._libs.interval import IntervalTree | ||
from pandas.tests.indexes.common import Base | ||
|
@@ -38,7 +38,7 @@ def create_index_with_nan(self, closed='right'): | |
@pytest.mark.parametrize('name', [None, 'foo']) | ||
def test_constructors(self, closed, name): | ||
left, right = Index([0, 1, 2, 3]), Index([1, 2, 3, 4]) | ||
ivs = [Interval(l, r, closed=closed) for l, r in zip(left, right)] | ||
ivs = [Interval(l, r, closed=closed) for l, r in lzip(left, right)] | ||
expected = IntervalIndex._simple_new( | ||
left=left, right=right, closed=closed, name=name) | ||
|
||
|
@@ -57,7 +57,7 @@ def test_constructors(self, closed, name): | |
tm.assert_index_equal(result, expected) | ||
|
||
result = IntervalIndex.from_tuples( | ||
zip(left, right), closed=closed, name=name) | ||
lzip(left, right), closed=closed, name=name) | ||
tm.assert_index_equal(result, expected) | ||
|
||
result = Index(ivs, name=name) | ||
|
@@ -68,6 +68,9 @@ def test_constructors(self, closed, name): | |
tm.assert_index_equal(Index(expected), expected) | ||
tm.assert_index_equal(IntervalIndex(expected), expected) | ||
|
||
result = IntervalIndex.from_intervals(expected) | ||
tm.assert_index_equal(result, expected) | ||
|
||
result = IntervalIndex.from_intervals( | ||
expected.values, name=expected.name) | ||
tm.assert_index_equal(result, expected) | ||
|
@@ -86,63 +89,118 @@ def test_constructors(self, closed, name): | |
breaks, closed=expected.closed, name=expected.name) | ||
tm.assert_index_equal(result, expected) | ||
|
||
def test_constructors_other(self): | ||
|
||
# all-nan | ||
result = IntervalIndex.from_intervals([np.nan]) | ||
expected = np.array([np.nan], dtype=object) | ||
tm.assert_numpy_array_equal(result.values, expected) | ||
|
||
# empty | ||
result = IntervalIndex.from_intervals([]) | ||
expected = np.array([], dtype=object) | ||
tm.assert_numpy_array_equal(result.values, expected) | ||
@pytest.mark.parametrize('data', [[np.nan], [np.nan] * 2, [np.nan] * 50]) | ||
def test_constructors_nan(self, closed, data): | ||
# GH 18421 | ||
expected_values = np.array(data, dtype=object) | ||
expected_idx = IntervalIndex(data, closed=closed) | ||
|
||
# validate the expected index | ||
assert expected_idx.closed == closed | ||
tm.assert_numpy_array_equal(expected_idx.values, expected_values) | ||
|
||
result = IntervalIndex.from_tuples(data, closed=closed) | ||
tm.assert_index_equal(result, expected_idx) | ||
tm.assert_numpy_array_equal(result.values, expected_values) | ||
|
||
result = IntervalIndex.from_breaks([np.nan] + data, closed=closed) | ||
tm.assert_index_equal(result, expected_idx) | ||
tm.assert_numpy_array_equal(result.values, expected_values) | ||
|
||
result = IntervalIndex.from_arrays(data, data, closed=closed) | ||
tm.assert_index_equal(result, expected_idx) | ||
tm.assert_numpy_array_equal(result.values, expected_values) | ||
|
||
if closed == 'right': | ||
# Can't specify closed for IntervalIndex.from_intervals | ||
result = IntervalIndex.from_intervals(data) | ||
tm.assert_index_equal(result, expected_idx) | ||
tm.assert_numpy_array_equal(result.values, expected_values) | ||
|
||
@pytest.mark.parametrize('data', [ | ||
[], | ||
np.array([], dtype='int64'), | ||
np.array([], dtype='float64'), | ||
np.array([], dtype=object)]) | ||
def test_constructors_empty(self, data, closed): | ||
# GH 18421 | ||
expected_dtype = data.dtype if isinstance(data, np.ndarray) else object | ||
expected_values = np.array([], dtype=object) | ||
expected_index = IntervalIndex(data, closed=closed) | ||
|
||
# validate the expected index | ||
assert expected_index.empty | ||
assert expected_index.closed == closed | ||
assert expected_index.dtype.subtype == expected_dtype | ||
tm.assert_numpy_array_equal(expected_index.values, expected_values) | ||
|
||
result = IntervalIndex.from_tuples(data, closed=closed) | ||
tm.assert_index_equal(result, expected_index) | ||
tm.assert_numpy_array_equal(result.values, expected_values) | ||
|
||
result = IntervalIndex.from_breaks(data, closed=closed) | ||
tm.assert_index_equal(result, expected_index) | ||
tm.assert_numpy_array_equal(result.values, expected_values) | ||
|
||
result = IntervalIndex.from_arrays(data, data, closed=closed) | ||
tm.assert_index_equal(result, expected_index) | ||
tm.assert_numpy_array_equal(result.values, expected_values) | ||
|
||
if closed == 'right': | ||
# Can't specify closed for IntervalIndex.from_intervals | ||
result = IntervalIndex.from_intervals(data) | ||
tm.assert_index_equal(result, expected_index) | ||
tm.assert_numpy_array_equal(result.values, expected_values) | ||
|
||
def test_constructors_errors(self): | ||
|
||
# scalar | ||
msg = ('IntervalIndex(...) must be called with a collection of ' | ||
msg = ('IntervalIndex\(...\) must be called with a collection of ' | ||
'some kind, 5 was passed') | ||
with pytest.raises(TypeError, message=msg): | ||
with tm.assert_raises_regex(TypeError, msg): | ||
IntervalIndex(5) | ||
|
||
# not an interval | ||
msg = "type <class 'numpy.int32'> with value 0 is not an interval" | ||
with pytest.raises(TypeError, message=msg): | ||
msg = ("type <(class|type) 'numpy.int64'> with value 0 " | ||
"is not an interval") | ||
with tm.assert_raises_regex(TypeError, msg): | ||
IntervalIndex([0, 1]) | ||
|
||
with pytest.raises(TypeError, message=msg): | ||
with tm.assert_raises_regex(TypeError, msg): | ||
IntervalIndex.from_intervals([0, 1]) | ||
|
||
# invalid closed | ||
msg = "invalid options for 'closed': invalid" | ||
with pytest.raises(ValueError, message=msg): | ||
with tm.assert_raises_regex(ValueError, msg): | ||
IntervalIndex.from_arrays([0, 1], [1, 2], closed='invalid') | ||
|
||
# mismatched closed | ||
# mismatched closed within intervals | ||
msg = 'intervals must all be closed on the same side' | ||
with pytest.raises(ValueError, message=msg): | ||
with tm.assert_raises_regex(ValueError, msg): | ||
IntervalIndex.from_intervals([Interval(0, 1), | ||
Interval(1, 2, closed='left')]) | ||
|
||
with pytest.raises(ValueError, message=msg): | ||
IntervalIndex.from_arrays([0, 10], [3, 5]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test was in the wrong section; raised a |
||
|
||
with pytest.raises(ValueError, message=msg): | ||
with tm.assert_raises_regex(ValueError, msg): | ||
Index([Interval(0, 1), Interval(2, 3, closed='left')]) | ||
|
||
# mismatched closed inferred from intervals vs constructor. | ||
msg = 'conflicting values for closed' | ||
with tm.assert_raises_regex(ValueError, msg): | ||
iv = [Interval(0, 1, closed='both'), Interval(1, 2, closed='both')] | ||
IntervalIndex(iv, closed='neither') | ||
|
||
# no point in nesting periods in an IntervalIndex | ||
msg = 'Period dtypes are not supported, use a PeriodIndex instead' | ||
with pytest.raises(ValueError, message=msg): | ||
with tm.assert_raises_regex(ValueError, msg): | ||
IntervalIndex.from_breaks( | ||
pd.period_range('2000-01-01', periods=3)) | ||
|
||
# decreasing breaks/arrays | ||
msg = 'left side of interval must be <= right side' | ||
with pytest.raises(ValueError, message=msg): | ||
with tm.assert_raises_regex(ValueError, msg): | ||
IntervalIndex.from_breaks(range(10, -1, -1)) | ||
|
||
with pytest.raises(ValueError, message=msg): | ||
with tm.assert_raises_regex(ValueError, msg): | ||
IntervalIndex.from_arrays(range(10, -1, -1), range(9, -2, -1)) | ||
|
||
def test_constructors_datetimelike(self, closed): | ||
|
@@ -865,23 +923,23 @@ def test_is_non_overlapping_monotonic(self, closed): | |
idx = IntervalIndex.from_tuples(tpls, closed=closed) | ||
assert idx.is_non_overlapping_monotonic is True | ||
|
||
idx = IntervalIndex.from_tuples(reversed(tpls), closed=closed) | ||
idx = IntervalIndex.from_tuples(tpls[::-1], 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)] | ||
idx = IntervalIndex.from_tuples(tpls, closed=closed) | ||
assert idx.is_non_overlapping_monotonic is False | ||
|
||
idx = IntervalIndex.from_tuples(reversed(tpls), closed=closed) | ||
idx = IntervalIndex.from_tuples(tpls[::-1], 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)] | ||
idx = IntervalIndex.from_tuples(tpls, closed=closed) | ||
assert idx.is_non_overlapping_monotonic is False | ||
|
||
idx = IntervalIndex.from_tuples(reversed(tpls), closed=closed) | ||
idx = IntervalIndex.from_tuples(tpls[::-1], closed=closed) | ||
assert idx.is_non_overlapping_monotonic is False | ||
|
||
# Should be False for closed='both', overwise True (GH16560) | ||
|
@@ -1054,10 +1112,6 @@ def test_constructor_coverage(self): | |
end=end.to_pydatetime()) | ||
tm.assert_index_equal(result, expected) | ||
|
||
result = pd.interval_range(start=start.tz_localize('UTC'), | ||
end=end.tz_localize('UTC')) | ||
tm.assert_index_equal(result, expected) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Deleted this check because it's invalid; just didn't appear as so until these fixes went in and fixed an unrelated issue. Here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thinking about it now, I should add a tz aware related test, but will wait until there are other comments on this PR. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yep this is broken
also need to validate that left/right have the same tz (if constructed that way) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The changes in this commit seem to resolve tz related issues. Not super well versed on the tz aware code, but looks right to me:
I think it was just a matter of making sure the dates were handled properly, and the validation follows from deferring to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmmm...the only issue appears to be an inconsistency when one date is tz aware but the other isn't:
Again, seems to be an underlying issue with
Will open a new issue for this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes |
||
|
||
result = pd.interval_range(start=start.asm8, end=end.asm8) | ||
tm.assert_index_equal(result, expected) | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For some reason
pytest.raises
didn't appear to actually be checking the message, so I switched totm.assert_raises_regex
.