Repository: scipy/scipy

Issue:
scipy.interpolate.interp1d with kind=“previous” doesn't extrapolate as expected
<!-- 
Thank you for taking the time to report a SciPy issue.
Please describe the issue in detail, and for bug reports
fill in the fields below. You can delete the sections that 
don't apply to your issue. You can view the final output
by clicking the preview button above.
-->
````
import numpy as np
from scipy.interpolate import interp1d

a = np.arange(10)
b = a + 100

iterp = interp1d(a, b, kind="previous", bounds_error=False)
print(iterp([-1,-2,5,8,12,25]))
# [nan nan  105.  108. nan nan]
````
First two `nan` values makes sense, but last two don't since a previous value is available.

Now if I use extrapolate:
````
iterp = interp1d(a, b, kind="previous", fill_value="extrapolate")
print(iterp([-1,-2,5,8,12,25]))
# [100. 100. 105. 108. 109. 109.]
````
The last two `109.` make sense but the first two `100.` don't since there is no previous value.

I believe the result should be the intuitive `[nan  nan  105.  108.  109.  109.]`

Issue reported on stackoverflow too, if anyone wants to add comments there...
https://stackoverflow.com/questions/53753562/scipy-interpolate-interp1d-with-kind-previous-doesnt-extrapolate-as-expected

### Scipy/Numpy/Python version information:
<!-- You can simply run the following and paste the result in a code block
```
import sys, scipy, numpy; print(scipy.__version__, numpy.__version__, sys.version_info)
```
-->
1.1.0 1.15.0 sys.version_info(major=3, minor=6, micro=5, releaselevel='final', serial=0)


Discussion:
Sorry if I 'm wrong but to what I understand interpolation can only be done for a point in [0,9] in your case which is the range of a.Rest is extrapolation beyond this range and 12,25,-1,-2 are outside this range."Previous" in your expression means previous neighbour interpolation that also since is interpolation is only in the range specified.  

Please read the documentation:
https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.interp1d.html#scipy.interpolate.interp1d
Yes, asking for numbers above 9 is stricto sensus extrapolation. So I guess my first claim "but last two [nan] don't [make sense] since a previous value is available." is very debatable.
However the second claim (which really is the crux of my post) is hard to argue against: you can't extrapolate to previous value if there in no previous value. IMO in that case the call should return either `nan` or `-inf`, or throw. But definitely not silently return the smallest value which makes absolutely no sense.
And BTW the docs don't say a word about what happens when using "extrapolate"...
I 'm interested in fixing this bug .I would definitely try to remove this bug if I can understand what you want.To what I understand previous is  your nearest neighbor extrapolation/interpolation in which nearest neighbor is checked(therefore beyond your [0,9] is 12 or 25 which first of all cannot be interpolated so result goes to nan which is your fillvalue and talking about extrapolate it is giving you the value of nearest neighbor of -1,-2 which correspond to a=0 and is b =100) .If I 'm wrong please tell what your desired output is so that I can add a code which would act accordingly.I think it's  badly termed.But obviously the code can be modified in accordance with the result you want.See this image on the top right on this page and tell me if it does not matches the results https://en.wikipedia.org/wiki/Nearest-neighbor_interpolation.  
I just think if you use "previous" for interpolation, it should use "previous" for extrapolation too, not "nearest". Now here there is no previous value to extrapolate so instead of silently defaulting back to "nearest" it should either extrapolate to `nan` or `-inf` (or raise an exception).
That is why I wrote badly named.Now I can try fixing it and transforming the code to make it interpolate/extrapolate according to previous value but the current version is acting like nearest neighbor.Should I make for a pull request?
I'm not sure I understand, interpolation using "previous" acts like what "previous" means, not "nearest". It is acting as "nearest" only when extrapolating. In any case I think the current behavior is wrong since rather inconsistent, but what I personally want may not be what Bob wants. And given extrapolating with previous is in essence a degenerate thing, I think a pull request to add a raise exception and a suggestion to use something along my proposed workaround instead (`fill_value=(np.nan, b.max()), bounds_error=False` or `fill_value=(-np.inf, b.max()), bounds_error=False`) would be good.
Haven't done any open source contrib either and I'm not even a github user so I don't think I have the shoulders for that job (yet?), however small it might be. Good on you if you can do it! :) 
Thanks:)
I confirmed this issue still exists in the latest v1.5.2
I've been trying to fix this problem for the past week, I think I can fix it.
But I am not sure if I should fix it as has been suggested now...

Current implementation just fill requested points outside of the data range with `fill_value` whichever `kind` is used.
And, if `fill_value` is "extrapolate", it just fill requested points outside of the data range with first and last value of `y` whichever `kind` is used.
This strategy looks straightforward for me.
I think if I change the `fill_value` behaviors based on which `kind` is used, that might make some users be confused and break backward compatibilities.

So, I think we should update docstring or add notes to explain this strategy and workarounds for it.
(For example, there is a workaround for the first issue:
```
axis = -1
iterp = interp1d(a, b, kind="previous", axis=axis,
                 bounds_error=False, fill_value=(np.nan, b.max(axis=axis)))
print(iterp([-1,-2,5,8,12,25]))
# [ nan  nan 105. 108. 109. 109.]
```
)

Does someone have any comments for this? 
 
Apologies for this slipping under the radar @AtsushiSakai . I think your workaround is great! 
Maybe the right thing to do is to actually implement your workaround as the strategy for `kind="previous"` and `fill_value="extrapolate"` and change the internal state of the extrapolation switch in the constructor.

A likely similar issue exists for `kind="previous".`

PR welcome.

Resolving patch:
diff --git a/scipy/interpolate/_interpolate.py b/scipy/interpolate/_interpolate.py
--- a/scipy/interpolate/_interpolate.py
+++ b/scipy/interpolate/_interpolate.py
@@ -485,7 +485,6 @@ def __init__(self, x, y, kind='linear', axis=-1,
         self.x = x
         del y, x  # clean up namespace to prevent misuse; use attributes
         self._kind = kind
-        self.fill_value = fill_value  # calls the setter, can modify bounds_err
 
         # Adjust to interpolation kind; store reference to *unbound*
         # interpolation methods, in order to avoid circular references to self
@@ -518,12 +517,18 @@ def __init__(self, x, y, kind='linear', axis=-1,
                 # Move x by one floating point value to the left
                 self._x_shift = np.nextafter(self.x, -np.inf)
                 self._call = self.__class__._call_previousnext
+                if _do_extrapolate(fill_value):
+                    self._check_and_update_bounds_error_for_extrapolation()
+                    fill_value = (np.nan, self.y.max(axis=axis))
             elif kind == 'next':
                 self._side = 'right'
                 self._ind = 1
                 # Move x by one floating point value to the right
                 self._x_shift = np.nextafter(self.x, np.inf)
                 self._call = self.__class__._call_previousnext
+                if _do_extrapolate(fill_value):
+                    self._check_and_update_bounds_error_for_extrapolation()
+                    fill_value = (self.y.min(axis=axis), np.nan)
             else:
                 # Check if we can delegate to numpy.interp (2x-10x faster).
                 np_types = (np.float_, np.int_)
@@ -572,6 +577,8 @@ def __init__(self, x, y, kind='linear', axis=-1,
             raise ValueError("x and y arrays must have at "
                              "least %d entries" % minval)
 
+        self.fill_value = fill_value  # calls the setter, can modify bounds_err
+
     @property
     def fill_value(self):
         """The fill value."""
@@ -582,10 +589,7 @@ def fill_value(self):
     def fill_value(self, fill_value):
         # extrapolation only works for nearest neighbor and linear methods
         if _do_extrapolate(fill_value):
-            if self.bounds_error:
-                raise ValueError("Cannot extrapolate and raise "
-                                 "at the same time.")
-            self.bounds_error = False
+            self._check_and_update_bounds_error_for_extrapolation()
             self._extrapolate = True
         else:
             broadcast_shape = (self.y.shape[:self.axis] +
@@ -612,6 +616,12 @@ def fill_value(self, fill_value):
         # backwards compat: fill_value was a public attr; make it writeable
         self._fill_value_orig = fill_value
 
+    def _check_and_update_bounds_error_for_extrapolation(self):
+        if self.bounds_error:
+            raise ValueError("Cannot extrapolate and raise "
+                             "at the same time.")
+        self.bounds_error = False
+
     def _call_linear_np(self, x_new):
         # Note that out-of-bounds values are taken care of in self._evaluate
         return np.interp(x_new, self.x, self.y)


Test patch:
diff --git a/scipy/interpolate/tests/test_interpolate.py b/scipy/interpolate/tests/test_interpolate.py
--- a/scipy/interpolate/tests/test_interpolate.py
+++ b/scipy/interpolate/tests/test_interpolate.py
@@ -366,7 +366,26 @@ def test_previous(self):
         extrapolator = interp1d(self.x10, self.y10, kind='previous',
                                 fill_value='extrapolate')
         assert_allclose(extrapolator([-1., 0, 9, 11]),
-                        [0, 0, 9, 9], rtol=1e-14)
+                        [np.nan, 0, 9, 9], rtol=1e-14)
+
+        # Tests for gh-9591
+        interpolator1D = interp1d(self.x10, self.y10, kind="previous",
+                                  fill_value='extrapolate')
+        assert_allclose(interpolator1D([-1, -2, 5, 8, 12, 25]),
+                        [np.nan, np.nan, 5, 8, 9, 9])
+
+        interpolator2D = interp1d(self.x10, self.y210, kind="previous",
+                                  fill_value='extrapolate')
+        assert_allclose(interpolator2D([-1, -2, 5, 8, 12, 25]),
+                        [[np.nan, np.nan, 5, 8, 9, 9],
+                         [np.nan, np.nan, 15, 18, 19, 19]])
+
+        interpolator2DAxis0 = interp1d(self.x10, self.y102, kind="previous",
+                                       axis=0, fill_value='extrapolate')
+        assert_allclose(interpolator2DAxis0([-2, 5, 12]),
+                        [[np.nan, np.nan],
+                         [10, 11],
+                         [18, 19]])
 
         opts = dict(kind='previous',
                     fill_value='extrapolate',
@@ -386,7 +405,26 @@ def test_next(self):
         extrapolator = interp1d(self.x10, self.y10, kind='next',
                                 fill_value='extrapolate')
         assert_allclose(extrapolator([-1., 0, 9, 11]),
-                        [0, 0, 9, 9], rtol=1e-14)
+                        [0, 0, 9, np.nan], rtol=1e-14)
+
+        # Tests for gh-9591
+        interpolator1D = interp1d(self.x10, self.y10, kind="next",
+                                  fill_value='extrapolate')
+        assert_allclose(interpolator1D([-1, -2, 5, 8, 12, 25]),
+                        [0, 0, 5, 8, np.nan, np.nan])
+
+        interpolator2D = interp1d(self.x10, self.y210, kind="next",
+                                  fill_value='extrapolate')
+        assert_allclose(interpolator2D([-1, -2, 5, 8, 12, 25]),
+                        [[0, 0, 5, 8, np.nan, np.nan],
+                         [10, 10, 15, 18, np.nan, np.nan]])
+
+        interpolator2DAxis0 = interp1d(self.x10, self.y102, kind="next",
+                                       axis=0, fill_value='extrapolate')
+        assert_allclose(interpolator2DAxis0([-2, 5, 12]),
+                        [[0, 1],
+                         [10, 11],
+                         [np.nan, np.nan]])
 
         opts = dict(kind='next',
                     fill_value='extrapolate',