Skip to content

Commit 8675197

Browse files
jschendelalanbato
authored andcommitted
BUG: Index._searchsorted_monotonic(..., side='right') returns the left side position for monotonic decreasing indexes (pandas-dev#17272)
1 parent 2a9c262 commit 8675197

File tree

9 files changed

+111
-33
lines changed

9 files changed

+111
-33
lines changed

doc/source/whatsnew/v0.21.0.txt

+1
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,7 @@ Indexing
400400
- Bug in ``.isin()`` in which checking membership in empty ``Series`` objects raised an error (:issue:`16991`)
401401
- Bug in ``CategoricalIndex`` reindexing in which specified indices containing duplicates were not being respected (:issue:`17323`)
402402
- Bug in intersection of ``RangeIndex`` with negative step (:issue:`17296`)
403+
- Bug in ``IntervalIndex`` where performing a scalar lookup fails for included right endpoints of non-overlapping monotonic decreasing indexes (:issue:`16417`, :issue:`17271`)
403404

404405
I/O
405406
^^^

pandas/core/indexes/base.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -3465,7 +3465,7 @@ def _searchsorted_monotonic(self, label, side='left'):
34653465
# everything for it to work (element ordering, search side and
34663466
# resulting value).
34673467
pos = self[::-1].searchsorted(label, side='right' if side == 'left'
3468-
else 'right')
3468+
else 'left')
34693469
return len(self) - pos
34703470

34713471
raise ValueError('index must be monotonic increasing or decreasing')

pandas/tests/indexes/common.py

+55-4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
RangeIndex, MultiIndex, CategoricalIndex, DatetimeIndex,
1212
TimedeltaIndex, PeriodIndex, IntervalIndex,
1313
notna, isna)
14+
from pandas.core.indexes.base import InvalidIndexError
1415
from pandas.core.indexes.datetimelike import DatetimeIndexOpsMixin
1516
from pandas.core.dtypes.common import needs_i8_conversion
1617
from pandas._libs.tslib import iNaT
@@ -138,9 +139,14 @@ def test_get_indexer_consistency(self):
138139
if isinstance(index, IntervalIndex):
139140
continue
140141

141-
indexer = index.get_indexer(index[0:2])
142-
assert isinstance(indexer, np.ndarray)
143-
assert indexer.dtype == np.intp
142+
if index.is_unique or isinstance(index, CategoricalIndex):
143+
indexer = index.get_indexer(index[0:2])
144+
assert isinstance(indexer, np.ndarray)
145+
assert indexer.dtype == np.intp
146+
else:
147+
e = "Reindexing only valid with uniquely valued Index objects"
148+
with tm.assert_raises_regex(InvalidIndexError, e):
149+
indexer = index.get_indexer(index[0:2])
144150

145151
indexer, _ = index.get_indexer_non_unique(index[0:2])
146152
assert isinstance(indexer, np.ndarray)
@@ -632,7 +638,8 @@ def test_difference_base(self):
632638
pass
633639
elif isinstance(idx, (DatetimeIndex, TimedeltaIndex)):
634640
assert result.__class__ == answer.__class__
635-
tm.assert_numpy_array_equal(result.asi8, answer.asi8)
641+
tm.assert_numpy_array_equal(result.sort_values().asi8,
642+
answer.sort_values().asi8)
636643
else:
637644
result = first.difference(case)
638645
assert tm.equalContents(result, answer)
@@ -954,3 +961,47 @@ def test_join_self_unique(self, how):
954961
if index.is_unique:
955962
joined = index.join(index, how=how)
956963
assert (index == joined).all()
964+
965+
def test_searchsorted_monotonic(self):
966+
# GH17271
967+
for index in self.indices.values():
968+
# not implemented for tuple searches in MultiIndex
969+
# or Intervals searches in IntervalIndex
970+
if isinstance(index, (MultiIndex, IntervalIndex)):
971+
continue
972+
973+
# nothing to test if the index is empty
974+
if index.empty:
975+
continue
976+
value = index[0]
977+
978+
# determine the expected results (handle dupes for 'right')
979+
expected_left, expected_right = 0, (index == value).argmin()
980+
if expected_right == 0:
981+
# all values are the same, expected_right should be length
982+
expected_right = len(index)
983+
984+
# test _searchsorted_monotonic in all cases
985+
# test searchsorted only for increasing
986+
if index.is_monotonic_increasing:
987+
ssm_left = index._searchsorted_monotonic(value, side='left')
988+
assert expected_left == ssm_left
989+
990+
ssm_right = index._searchsorted_monotonic(value, side='right')
991+
assert expected_right == ssm_right
992+
993+
ss_left = index.searchsorted(value, side='left')
994+
assert expected_left == ss_left
995+
996+
ss_right = index.searchsorted(value, side='right')
997+
assert expected_right == ss_right
998+
elif index.is_monotonic_decreasing:
999+
ssm_left = index._searchsorted_monotonic(value, side='left')
1000+
assert expected_left == ssm_left
1001+
1002+
ssm_right = index._searchsorted_monotonic(value, side='right')
1003+
assert expected_right == ssm_right
1004+
else:
1005+
# non-monotonic should raise.
1006+
with pytest.raises(ValueError):
1007+
index._searchsorted_monotonic(value, side='left')

pandas/tests/indexes/datetimes/test_datetimelike.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ class TestDatetimeIndex(DatetimeLike):
1212
_holder = DatetimeIndex
1313

1414
def setup_method(self, method):
15-
self.indices = dict(index=tm.makeDateIndex(10))
15+
self.indices = dict(index=tm.makeDateIndex(10),
16+
index_dec=date_range('20130110', periods=10,
17+
freq='-1D'))
1618
self.setup_indices()
1719

1820
def create_index(self):

pandas/tests/indexes/period/test_period.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ class TestPeriodIndex(DatetimeLike):
1818
_multiprocess_can_split_ = True
1919

2020
def setup_method(self, method):
21-
self.indices = dict(index=tm.makePeriodIndex(10))
21+
self.indices = dict(index=tm.makePeriodIndex(10),
22+
index_dec=period_range('20130101', periods=10,
23+
freq='D')[::-1])
2224
self.setup_indices()
2325

2426
def create_index(self):

pandas/tests/indexes/test_base.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ def setup_method(self, method):
4646
catIndex=tm.makeCategoricalIndex(100),
4747
empty=Index([]),
4848
tuples=MultiIndex.from_tuples(lzip(
49-
['foo', 'bar', 'baz'], [1, 2, 3])))
49+
['foo', 'bar', 'baz'], [1, 2, 3])),
50+
repeats=Index([0, 0, 1, 1, 2, 2]))
5051
self.setup_indices()
5152

5253
def create_index(self):

pandas/tests/indexes/test_numeric.py

+8-4
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,9 @@ class TestFloat64Index(Numeric):
181181

182182
def setup_method(self, method):
183183
self.indices = dict(mixed=Float64Index([1.5, 2, 3, 4, 5]),
184-
float=Float64Index(np.arange(5) * 2.5))
184+
float=Float64Index(np.arange(5) * 2.5),
185+
mixed_dec=Float64Index([5, 4, 3, 2, 1.5]),
186+
float_dec=Float64Index(np.arange(4, -1, -1) * 2.5))
185187
self.setup_indices()
186188

187189
def create_index(self):
@@ -654,7 +656,8 @@ class TestInt64Index(NumericInt):
654656
_holder = Int64Index
655657

656658
def setup_method(self, method):
657-
self.indices = dict(index=Int64Index(np.arange(0, 20, 2)))
659+
self.indices = dict(index=Int64Index(np.arange(0, 20, 2)),
660+
index_dec=Int64Index(np.arange(19, -1, -1)))
658661
self.setup_indices()
659662

660663
def create_index(self):
@@ -949,8 +952,9 @@ class TestUInt64Index(NumericInt):
949952
_holder = UInt64Index
950953

951954
def setup_method(self, method):
952-
self.indices = dict(index=UInt64Index([2**63, 2**63 + 10, 2**63 + 15,
953-
2**63 + 20, 2**63 + 25]))
955+
vals = [2**63, 2**63 + 10, 2**63 + 15, 2**63 + 20, 2**63 + 25]
956+
self.indices = dict(index=UInt64Index(vals),
957+
index_dec=UInt64Index(reversed(vals)))
954958
self.setup_indices()
955959

956960
def create_index(self):

pandas/tests/indexes/test_range.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ class TestRangeIndex(Numeric):
2525
_compat_props = ['shape', 'ndim', 'size', 'itemsize']
2626

2727
def setup_method(self, method):
28-
self.indices = dict(index=RangeIndex(0, 20, 2, name='foo'))
28+
self.indices = dict(index=RangeIndex(0, 20, 2, name='foo'),
29+
index_dec=RangeIndex(18, -1, -2, name='bar'))
2930
self.setup_indices()
3031

3132
def create_index(self):

pandas/tests/indexing/test_interval.py

+36-20
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import pandas as pd
44

55
from pandas import Series, DataFrame, IntervalIndex, Interval
6+
from pandas.compat import product
67
import pandas.util.testing as tm
78

89

@@ -14,16 +15,6 @@ def setup_method(self, method):
1415
def test_loc_with_scalar(self):
1516

1617
s = self.s
17-
expected = 0
18-
19-
result = s.loc[0.5]
20-
assert result == expected
21-
22-
result = s.loc[1]
23-
assert result == expected
24-
25-
with pytest.raises(KeyError):
26-
s.loc[0]
2718

2819
expected = s.iloc[:3]
2920
tm.assert_series_equal(expected, s.loc[:3])
@@ -42,16 +33,6 @@ def test_loc_with_scalar(self):
4233
def test_getitem_with_scalar(self):
4334

4435
s = self.s
45-
expected = 0
46-
47-
result = s[0.5]
48-
assert result == expected
49-
50-
result = s[1]
51-
assert result == expected
52-
53-
with pytest.raises(KeyError):
54-
s[0]
5536

5637
expected = s.iloc[:3]
5738
tm.assert_series_equal(expected, s[:3])
@@ -67,6 +48,41 @@ def test_getitem_with_scalar(self):
6748
expected = s.iloc[2:5]
6849
tm.assert_series_equal(expected, s[s >= 2])
6950

51+
@pytest.mark.parametrize('direction, closed',
52+
product(('increasing', 'decreasing'),
53+
('left', 'right', 'neither', 'both')))
54+
def test_nonoverlapping_monotonic(self, direction, closed):
55+
tpls = [(0, 1), (2, 3), (4, 5)]
56+
if direction == 'decreasing':
57+
tpls = reversed(tpls)
58+
59+
idx = IntervalIndex.from_tuples(tpls, closed=closed)
60+
s = Series(list('abc'), idx)
61+
62+
for key, expected in zip(idx.left, s):
63+
if idx.closed_left:
64+
assert s[key] == expected
65+
assert s.loc[key] == expected
66+
else:
67+
with pytest.raises(KeyError):
68+
s[key]
69+
with pytest.raises(KeyError):
70+
s.loc[key]
71+
72+
for key, expected in zip(idx.right, s):
73+
if idx.closed_right:
74+
assert s[key] == expected
75+
assert s.loc[key] == expected
76+
else:
77+
with pytest.raises(KeyError):
78+
s[key]
79+
with pytest.raises(KeyError):
80+
s.loc[key]
81+
82+
for key, expected in zip(idx.mid, s):
83+
assert s[key] == expected
84+
assert s.loc[key] == expected
85+
7086
def test_with_interval(self):
7187

7288
s = self.s

0 commit comments

Comments
 (0)