Repository: pandas-dev/pandas

Issue:
PERF: Use RangeIndex properties to compute max/min
#### Problem description
Currently `RangeIndex.max` and `RangeIndex.min` fallback to `nanops.nanmax` and `nanops.nanmin`, but it's possible to determine these more efficiently using properties of `RangeIndex`.

I don't imagine that these are used very frequently, but the implementation is straightforward and appears to yield a good performance boost, so seems worthwhile.  Wanted to check here first before putting in too much effort though.

Did a preliminary implementation and created some asv benchmarks:

```
      before           after         ratio
     [b59f107a]       [85c7fef1]
-        29.3±0ms      1.74±0.06μs     0.00  index_object.Range.time_max
-        27.3±1ms      1.39±0.06μs     0.00  index_object.Range.time_min
-        24.6±0ms      1.18±0.08μs     0.00  index_object.Range.time_min_trivial
-      27.3±0.7ms      1.15±0.04μs     0.00  index_object.Range.time_max_trivial

SOME BENCHMARKS HAVE CHANGED SIGNIFICANTLY.
```

Where the benchmarks are generated from:
```python
class Range(object):
    goal_time = 0.2

    def setup(self):
        self.idx_inc = RangeIndex(start=0, stop=10**7, step=3)
        self.idx_dec = RangeIndex(start=10**7, stop=-1, step=-3)

    def time_max(self):
        self.idx_inc.max()

    def time_max_trivial(self):
        self.idx_dec.max()

    def time_min(self):
        self.idx_dec.min()

    def time_min_trivial(self):
        self.idx_inc.min()
```

Note that the `_trivial` suffix denotes a fastpath for when the max/min are just the `_start` value (e.g. the minimum value of an increasing `RangeIndex` is just `_start`).  This isn't necessarily the case with `_stop`, as it may not be included if misaligned with `_step` (e.g. `RangeIndex(0, 10, 3)`).


Discussion:

Resolving patch:
diff --git a/asv_bench/benchmarks/index_object.py b/asv_bench/benchmarks/index_object.py
--- a/asv_bench/benchmarks/index_object.py
+++ b/asv_bench/benchmarks/index_object.py
@@ -199,3 +199,23 @@ def time_datetime_level_values_full(self):
 
     def time_datetime_level_values_sliced(self):
         self.mi[:10].values
+
+
+class Range(object):
+    goal_time = 0.2
+
+    def setup(self):
+        self.idx_inc = RangeIndex(start=0, stop=10**7, step=3)
+        self.idx_dec = RangeIndex(start=10**7, stop=-1, step=-3)
+
+    def time_max(self):
+        self.idx_inc.max()
+
+    def time_max_trivial(self):
+        self.idx_dec.max()
+
+    def time_min(self):
+        self.idx_dec.min()
+
+    def time_min_trivial(self):
+        self.idx_inc.min()
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
@@ -269,6 +269,24 @@ def copy(self, name=None, deep=False, dtype=None, **kwargs):
         return RangeIndex(name=name, fastpath=True,
                           **dict(self._get_data_as_items()))
 
+    def _minmax(self, meth):
+        no_steps = len(self) - 1
+        if no_steps == -1:
+            return np.nan
+        elif ((meth == 'min' and self._step > 0) or
+              (meth == 'max' and self._step < 0)):
+            return self._start
+
+        return self._start + self._step * no_steps
+
+    def min(self):
+        """The minimum value of the RangeIndex"""
+        return self._minmax('min')
+
+    def max(self):
+        """The maximum value of the RangeIndex"""
+        return self._minmax('max')
+
     def argsort(self, *args, **kwargs):
         """
         Returns the indices that would sort the index and its


Test patch:
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
@@ -10,7 +10,7 @@
 
 import numpy as np
 
-from pandas import (notna, Series, Index, Float64Index,
+from pandas import (isna, notna, Series, Index, Float64Index,
                     Int64Index, RangeIndex)
 
 import pandas.util.testing as tm
@@ -994,3 +994,22 @@ def test_append(self):
                 # Append single item rather than list
                 result2 = indices[0].append(indices[1])
                 tm.assert_index_equal(result2, expected, exact=True)
+
+    @pytest.mark.parametrize('start,stop,step',
+                             [(0, 400, 3), (500, 0, -6), (-10**6, 10**6, 4),
+                              (10**6, -10**6, -4), (0, 10, 20)])
+    def test_max_min(self, start, stop, step):
+        # GH17607
+        idx = RangeIndex(start, stop, step)
+        expected = idx._int64index.max()
+        result = idx.max()
+        assert result == expected
+
+        expected = idx._int64index.min()
+        result = idx.min()
+        assert result == expected
+
+        # empty
+        idx = RangeIndex(start, stop, -step)
+        assert isna(idx.max())
+        assert isna(idx.min())