Repository: pandas-dev/pandas

Issue:
BUG: IntervalIndex scalar lookup raises for included right endpoints when sorted decreasing.
#### Code Sample, a copy-pastable example if possible

```python
# Series definition
ivs = [pd.Interval(10, 12), pd.Interval(7, 9), pd.Interval(4, 6), pd.Interval(1, 3)]
s = pd.Series(['foo', 'bar', 'baz', 'qux'], pd.IntervalIndex(ivs))

(10, 12]    foo
(7, 9]      bar
(4, 6]      baz
(1, 3]      qux
dtype: object

# Scalar lookup of included right endpoint raises a KeyError.
s[12]
KeyError: 12
```
#### Problem description
The right endpoint is included in the interval `(10, 12]`, so a `KeyError` should not be raised for `s[12]`.  This occurs for all endpoints (12, 9, 6, 3) in the example above.  Using `loc` also raises.

This issue only occurs when the intervals are sorted decreasing; the lookup is successful for any other permutation of the interval order, e.g. `[pd.Interval(7, 9), pd.Interval(10, 12), pd.Interval(4, 6), pd.Interval(1, 3)]`.  It also only occurs for right endpoints (`closed='right'` or `closed='both'`);  when the left endpoints are included (`closed='left'` or `closed='both'`) the lookup is successful for all left endpoints for any ordering of the intervals.

This seems similar to #16410, but I don't know that it's an exact dupe.

#### Expected Output
Successful lookup

#### Output of ``pd.show_versions()``

<details>

INSTALLED VERSIONS
------------------
commit: None
python: 3.5.2.final.0
python-bits: 64
OS: Windows
OS-release: 10
machine: AMD64
processor: Intel64 Family 6 Model 78 Stepping 3, GenuineIntel
byteorder: little
LC_ALL: None
LANG: None
LOCALE: None.None

pandas: 0.20.1
pytest: 3.0.7
pip: 9.0.1
setuptools: 34.4.1
Cython: 0.25.2
numpy: 1.13.0rc2
scipy: 0.18.1
xarray: 0.9.5
IPython: 5.3.0
sphinx: None
patsy: 0.4.1
dateutil: 2.6.0
pytz: 2017.2
blosc: 1.4.4
bottleneck: 1.2.0
tables: 3.4.2
numexpr: 2.6.1
feather: None
matplotlib: 2.0.2
openpyxl: 2.4.0
xlrd: 1.0.0
xlwt: None
xlsxwriter: None
lxml: 3.7.3
bs4: None
html5lib: None
sqlalchemy: 1.1.10
pymysql: None
psycopg2: None
jinja2: 2.9.6
s3fs: None
pandas_gbq: 0.1.6
pandas_datareader: 0.4.0

</details>



Discussion:

Resolving patch:
diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py
--- a/pandas/core/indexes/base.py
+++ b/pandas/core/indexes/base.py
@@ -3465,7 +3465,7 @@ def _searchsorted_monotonic(self, label, side='left'):
             # everything for it to work (element ordering, search side and
             # resulting value).
             pos = self[::-1].searchsorted(label, side='right' if side == 'left'
-                                          else 'right')
+                                          else 'left')
             return len(self) - pos
 
         raise ValueError('index must be monotonic increasing or decreasing')


Test patch:
diff --git a/pandas/tests/indexes/common.py b/pandas/tests/indexes/common.py
--- a/pandas/tests/indexes/common.py
+++ b/pandas/tests/indexes/common.py
@@ -11,6 +11,7 @@
                     RangeIndex, MultiIndex, CategoricalIndex, DatetimeIndex,
                     TimedeltaIndex, PeriodIndex, IntervalIndex,
                     notna, isna)
+from pandas.core.indexes.base import InvalidIndexError
 from pandas.core.indexes.datetimelike import DatetimeIndexOpsMixin
 from pandas.core.dtypes.common import needs_i8_conversion
 from pandas._libs.tslib import iNaT
@@ -138,9 +139,14 @@ def test_get_indexer_consistency(self):
             if isinstance(index, IntervalIndex):
                 continue
 
-            indexer = index.get_indexer(index[0:2])
-            assert isinstance(indexer, np.ndarray)
-            assert indexer.dtype == np.intp
+            if index.is_unique or isinstance(index, CategoricalIndex):
+                indexer = index.get_indexer(index[0:2])
+                assert isinstance(indexer, np.ndarray)
+                assert indexer.dtype == np.intp
+            else:
+                e = "Reindexing only valid with uniquely valued Index objects"
+                with tm.assert_raises_regex(InvalidIndexError, e):
+                    indexer = index.get_indexer(index[0:2])
 
             indexer, _ = index.get_indexer_non_unique(index[0:2])
             assert isinstance(indexer, np.ndarray)
@@ -632,7 +638,8 @@ def test_difference_base(self):
                     pass
                 elif isinstance(idx, (DatetimeIndex, TimedeltaIndex)):
                     assert result.__class__ == answer.__class__
-                    tm.assert_numpy_array_equal(result.asi8, answer.asi8)
+                    tm.assert_numpy_array_equal(result.sort_values().asi8,
+                                                answer.sort_values().asi8)
                 else:
                     result = first.difference(case)
                     assert tm.equalContents(result, answer)
@@ -954,3 +961,47 @@ def test_join_self_unique(self, how):
         if index.is_unique:
             joined = index.join(index, how=how)
             assert (index == joined).all()
+
+    def test_searchsorted_monotonic(self):
+        # GH17271
+        for index in self.indices.values():
+            # not implemented for tuple searches in MultiIndex
+            # or Intervals searches in IntervalIndex
+            if isinstance(index, (MultiIndex, IntervalIndex)):
+                continue
+
+            # nothing to test if the index is empty
+            if index.empty:
+                continue
+            value = index[0]
+
+            # determine the expected results (handle dupes for 'right')
+            expected_left, expected_right = 0, (index == value).argmin()
+            if expected_right == 0:
+                # all values are the same, expected_right should be length
+                expected_right = len(index)
+
+            # test _searchsorted_monotonic in all cases
+            # test searchsorted only for increasing
+            if index.is_monotonic_increasing:
+                ssm_left = index._searchsorted_monotonic(value, side='left')
+                assert expected_left == ssm_left
+
+                ssm_right = index._searchsorted_monotonic(value, side='right')
+                assert expected_right == ssm_right
+
+                ss_left = index.searchsorted(value, side='left')
+                assert expected_left == ss_left
+
+                ss_right = index.searchsorted(value, side='right')
+                assert expected_right == ss_right
+            elif index.is_monotonic_decreasing:
+                ssm_left = index._searchsorted_monotonic(value, side='left')
+                assert expected_left == ssm_left
+
+                ssm_right = index._searchsorted_monotonic(value, side='right')
+                assert expected_right == ssm_right
+            else:
+                # non-monotonic should raise.
+                with pytest.raises(ValueError):
+                    index._searchsorted_monotonic(value, side='left')
diff --git a/pandas/tests/indexes/datetimes/test_datetimelike.py b/pandas/tests/indexes/datetimes/test_datetimelike.py
--- a/pandas/tests/indexes/datetimes/test_datetimelike.py
+++ b/pandas/tests/indexes/datetimes/test_datetimelike.py
@@ -12,7 +12,9 @@ class TestDatetimeIndex(DatetimeLike):
     _holder = DatetimeIndex
 
     def setup_method(self, method):
-        self.indices = dict(index=tm.makeDateIndex(10))
+        self.indices = dict(index=tm.makeDateIndex(10),
+                            index_dec=date_range('20130110', periods=10,
+                                                 freq='-1D'))
         self.setup_indices()
 
     def create_index(self):
diff --git a/pandas/tests/indexes/period/test_period.py b/pandas/tests/indexes/period/test_period.py
--- a/pandas/tests/indexes/period/test_period.py
+++ b/pandas/tests/indexes/period/test_period.py
@@ -18,7 +18,9 @@ class TestPeriodIndex(DatetimeLike):
     _multiprocess_can_split_ = True
 
     def setup_method(self, method):
-        self.indices = dict(index=tm.makePeriodIndex(10))
+        self.indices = dict(index=tm.makePeriodIndex(10),
+                            index_dec=period_range('20130101', periods=10,
+                                                   freq='D')[::-1])
         self.setup_indices()
 
     def create_index(self):
diff --git a/pandas/tests/indexes/test_base.py b/pandas/tests/indexes/test_base.py
--- a/pandas/tests/indexes/test_base.py
+++ b/pandas/tests/indexes/test_base.py
@@ -46,7 +46,8 @@ def setup_method(self, method):
                             catIndex=tm.makeCategoricalIndex(100),
                             empty=Index([]),
                             tuples=MultiIndex.from_tuples(lzip(
-                                ['foo', 'bar', 'baz'], [1, 2, 3])))
+                                ['foo', 'bar', 'baz'], [1, 2, 3])),
+                            repeats=Index([0, 0, 1, 1, 2, 2]))
         self.setup_indices()
 
     def create_index(self):
diff --git a/pandas/tests/indexes/test_numeric.py b/pandas/tests/indexes/test_numeric.py
--- a/pandas/tests/indexes/test_numeric.py
+++ b/pandas/tests/indexes/test_numeric.py
@@ -181,7 +181,9 @@ class TestFloat64Index(Numeric):
 
     def setup_method(self, method):
         self.indices = dict(mixed=Float64Index([1.5, 2, 3, 4, 5]),
-                            float=Float64Index(np.arange(5) * 2.5))
+                            float=Float64Index(np.arange(5) * 2.5),
+                            mixed_dec=Float64Index([5, 4, 3, 2, 1.5]),
+                            float_dec=Float64Index(np.arange(4, -1, -1) * 2.5))
         self.setup_indices()
 
     def create_index(self):
@@ -654,7 +656,8 @@ class TestInt64Index(NumericInt):
     _holder = Int64Index
 
     def setup_method(self, method):
-        self.indices = dict(index=Int64Index(np.arange(0, 20, 2)))
+        self.indices = dict(index=Int64Index(np.arange(0, 20, 2)),
+                            index_dec=Int64Index(np.arange(19, -1, -1)))
         self.setup_indices()
 
     def create_index(self):
@@ -949,8 +952,9 @@ class TestUInt64Index(NumericInt):
     _holder = UInt64Index
 
     def setup_method(self, method):
-        self.indices = dict(index=UInt64Index([2**63, 2**63 + 10, 2**63 + 15,
-                                               2**63 + 20, 2**63 + 25]))
+        vals = [2**63, 2**63 + 10, 2**63 + 15, 2**63 + 20, 2**63 + 25]
+        self.indices = dict(index=UInt64Index(vals),
+                            index_dec=UInt64Index(reversed(vals)))
         self.setup_indices()
 
     def create_index(self):
diff --git a/pandas/tests/indexes/test_range.py b/pandas/tests/indexes/test_range.py
--- a/pandas/tests/indexes/test_range.py
+++ b/pandas/tests/indexes/test_range.py
@@ -25,7 +25,8 @@ class TestRangeIndex(Numeric):
     _compat_props = ['shape', 'ndim', 'size', 'itemsize']
 
     def setup_method(self, method):
-        self.indices = dict(index=RangeIndex(0, 20, 2, name='foo'))
+        self.indices = dict(index=RangeIndex(0, 20, 2, name='foo'),
+                            index_dec=RangeIndex(18, -1, -2, name='bar'))
         self.setup_indices()
 
     def create_index(self):
diff --git a/pandas/tests/indexing/test_interval.py b/pandas/tests/indexing/test_interval.py
--- a/pandas/tests/indexing/test_interval.py
+++ b/pandas/tests/indexing/test_interval.py
@@ -3,6 +3,7 @@
 import pandas as pd
 
 from pandas import Series, DataFrame, IntervalIndex, Interval
+from pandas.compat import product
 import pandas.util.testing as tm
 
 
@@ -14,16 +15,6 @@ def setup_method(self, method):
     def test_loc_with_scalar(self):
 
         s = self.s
-        expected = 0
-
-        result = s.loc[0.5]
-        assert result == expected
-
-        result = s.loc[1]
-        assert result == expected
-
-        with pytest.raises(KeyError):
-            s.loc[0]
 
         expected = s.iloc[:3]
         tm.assert_series_equal(expected, s.loc[:3])
@@ -42,16 +33,6 @@ def test_loc_with_scalar(self):
     def test_getitem_with_scalar(self):
 
         s = self.s
-        expected = 0
-
-        result = s[0.5]
-        assert result == expected
-
-        result = s[1]
-        assert result == expected
-
-        with pytest.raises(KeyError):
-            s[0]
 
         expected = s.iloc[:3]
         tm.assert_series_equal(expected, s[:3])
@@ -67,6 +48,41 @@ def test_getitem_with_scalar(self):
         expected = s.iloc[2:5]
         tm.assert_series_equal(expected, s[s >= 2])
 
+    @pytest.mark.parametrize('direction, closed',
+                             product(('increasing', 'decreasing'),
+                                     ('left', 'right', 'neither', 'both')))
+    def test_nonoverlapping_monotonic(self, direction, closed):
+        tpls = [(0, 1), (2, 3), (4, 5)]
+        if direction == 'decreasing':
+            tpls = reversed(tpls)
+
+        idx = IntervalIndex.from_tuples(tpls, closed=closed)
+        s = Series(list('abc'), idx)
+
+        for key, expected in zip(idx.left, s):
+            if idx.closed_left:
+                assert s[key] == expected
+                assert s.loc[key] == expected
+            else:
+                with pytest.raises(KeyError):
+                    s[key]
+                with pytest.raises(KeyError):
+                    s.loc[key]
+
+        for key, expected in zip(idx.right, s):
+            if idx.closed_right:
+                assert s[key] == expected
+                assert s.loc[key] == expected
+            else:
+                with pytest.raises(KeyError):
+                    s[key]
+                with pytest.raises(KeyError):
+                    s.loc[key]
+
+        for key, expected in zip(idx.mid, s):
+            assert s[key] == expected
+            assert s.loc[key] == expected
+
     def test_with_interval(self):
 
         s = self.s