Repository: pandas-dev/pandas

Issue:
PERF: Unnecessary hash table with RangeIndex
Example
```python
def log_memory():
    import os
    import gc
    import psutil
    for i in range(3):
        gc.collect(i)
    process = psutil.Process(os.getpid())
    mem_usage = process.memory_info().rss / float(2 ** 20)
    print("[Memory usage] {:12.1f} MB".format(
        mem_usage
    ))

In [20]: df = pd.DataFrame({'a': np.arange(1000000)})

In [23]: log_memory()
[Memory usage]        132.4 MB

In [24]: df.loc[5, :]
Out[24]: 
a    5
Name: 5, dtype: int32

In [25]: log_memory()
[Memory usage]        172.2 MB
```

Rather than materializing the hash table, should directly convert labels into positions.  Low priority in my opinion, atypical to be using `loc` with a `RangeIndex`.

`pandas 0.20.2`


Discussion:
hmm ,this should not be creating a hash table at all (though it would if it morphed to an int64index)
The `_engine` for `RangeIndex` still points at the `Int64Index` one, so a hash table will be created if something calls `self._engine`.    So either need to make a simplified `RangeIndexEngine` or override the indexing methods.

https://github.com/pandas-dev/pandas/blob/2e24a8f8886a1481af8324d44224cddb2ee1b53c/pandas/core/indexes/range.py#L43
I think that ``_engine`` is purely for compat on some methods. might be easiest/best to create a pretty dummy RI Engine in cython.
> I think that _engine is purely for compat on some methods

`_engine` is still used in eg `get_loc`, so often created.

Related to this, I just stumbled on the fact that currenlty `RangeIndex.memory_usage()` is inaccurate, as it does not take into account the engine.

And given that the actual engine takes a lot more memory as the values (for int64index it is 5x more memory on an example case 500k range-like int index), a `RangeIndex` is not much more memory-efficient as a `Int64Index` (its big selling point I thought?) once the engine is created.
> RangeIndex is not much more memory-efficient as a Int64Index (its big selling point I thought?) once the engine is created.

This is true, but I think it would be relatively rare for the engine to populated when a `RangeIndex` is being used - typically it is when someone doesn't care about the index at all, so would likely be using `iloc` if any indexing at all.

Resolving patch:
diff --git a/pandas/core/indexes/range.py b/pandas/core/indexes/range.py
--- a/pandas/core/indexes/range.py
+++ b/pandas/core/indexes/range.py
@@ -13,8 +13,8 @@
 
 from pandas.core.dtypes import concat as _concat
 from pandas.core.dtypes.common import (
-    ensure_python_int, is_int64_dtype, is_integer, is_scalar,
-    is_timedelta64_dtype)
+    ensure_platform_int, ensure_python_int, is_int64_dtype, is_integer,
+    is_integer_dtype, is_list_like, is_scalar, is_timedelta64_dtype)
 from pandas.core.dtypes.generic import (
     ABCDataFrame, ABCSeries, ABCTimedeltaIndex)
 
@@ -348,6 +348,36 @@ def get_loc(self, key, method=None, tolerance=None):
                 raise KeyError(key)
         return super().get_loc(key, method=method, tolerance=tolerance)
 
+    @Appender(_index_shared_docs['get_indexer'])
+    def get_indexer(self, target, method=None, limit=None, tolerance=None):
+        if not (method is None and tolerance is None and is_list_like(target)):
+            return super().get_indexer(target, method=method,
+                                       tolerance=tolerance)
+
+        if self.step > 0:
+            start, stop, step = self.start, self.stop, self.step
+        else:
+            # Work on reversed range for simplicity:
+            start, stop, step = (self.stop - self.step,
+                                 self.start + 1,
+                                 - self.step)
+
+        target_array = np.asarray(target)
+        if not (is_integer_dtype(target_array) and target_array.ndim == 1):
+            # checks/conversions/roundings are delegated to general method
+            return super().get_indexer(target, method=method,
+                                       tolerance=tolerance)
+
+        locs = target_array - start
+        valid = (locs % step == 0) & (locs >= 0) & (target_array < stop)
+        locs[~valid] = -1
+        locs[valid] = locs[valid] / step
+
+        if step != self.step:
+            # We reversed this range: transform to original locs
+            locs[valid] = len(self) - 1 - locs[valid]
+        return ensure_platform_int(locs)
+
     def tolist(self):
         return list(self._range)
 


Test patch:
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
@@ -1439,9 +1439,12 @@ def test_get_indexer_strings_raises(self):
             index.get_indexer(['a', 'b', 'c', 'd'], method='pad',
                               tolerance=[2, 2, 2, 2])
 
-    def test_get_indexer_numeric_index_boolean_target(self):
+    @pytest.mark.parametrize("idx_class", [Int64Index, RangeIndex,
+                                           Float64Index])
+    def test_get_indexer_numeric_index_boolean_target(self, idx_class):
         # GH 16877
-        numeric_index = pd.Index(range(4))
+
+        numeric_index = idx_class(RangeIndex((4)))
         result = numeric_index.get_indexer([True, False, True])
         expected = np.array([-1, -1, -1], dtype=np.intp)
         tm.assert_numpy_array_equal(result, expected)
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
@@ -3,6 +3,8 @@
 import numpy as np
 import pytest
 
+from pandas.core.dtypes.common import ensure_platform_int
+
 import pandas as pd
 from pandas import Float64Index, Index, Int64Index, RangeIndex, Series
 import pandas.util.testing as tm
@@ -965,3 +967,23 @@ def test_append(self, appends):
             # Append single item rather than list
             result2 = indices[0].append(indices[1])
             tm.assert_index_equal(result2, expected, exact=True)
+
+    def test_engineless_lookup(self):
+        # GH 16685
+        # Standard lookup on RangeIndex should not require the engine to be
+        # created
+        idx = RangeIndex(2, 10, 3)
+
+        assert idx.get_loc(5) == 1
+        tm.assert_numpy_array_equal(idx.get_indexer([2, 8]),
+                                    ensure_platform_int(np.array([0, 2])))
+        with pytest.raises(KeyError):
+            idx.get_loc(3)
+
+        assert '_engine' not in idx._cache
+
+        # The engine is still required for lookup of a different dtype scalar:
+        with pytest.raises(KeyError):
+            assert idx.get_loc('a') == -1
+
+        assert '_engine' in idx._cache