Repository: scipy/scipy

Issue:
ENH: interp1d Matlab compatibility
**Is your feature request related to a problem? Please describe.**
Yes. I want to rewrite Matlab code in Python. Unfortunately the interp1d with 'nearest' is not fully compatible with Matlab interp1. Matlab rounds up and Scipy rounds down. If one wants to interpolate 1.5 between 1 and 2, Matlab gives you the value at 2 and Scipy gives you the value at 1. That's really a mess, if one expects same results.

**Describe the solution you'd like**
An additional parameter left/right or nearest_up/nearest_down



Discussion:
Matching matlab is not a requirement. 

interp1d is already loaded with options. Maybe better introduce a new interpolation kind, "nearest-down" or some such. This route needs a champion :-).

The other route, of course, is to borrow the source into your user code,

https://github.com/scipy/scipy/blob/v1.5.2/scipy/interpolate/interpolate.py#L612

and adjust as needed.
Thanks for your fast reply.
You are right, Matlab is not an requirement. But in this case it would make thinks easier.

Yes, a new interpolation kind would be fine. In this case nearest-up, because nearest-down is already the default behavior.
As I understand this is a minor effort, because of the requested functionality is already there (searchsorted(x, y, side='right')). One needs to copy the existing code, change this little piece of code (left -> right) and introduce a new kind='nearest-up'.
> As I understand this is a minor effort, because of the requested functionality is already there (searchsorted(x, y, side='right')). One needs to copy the existing code, change this little piece of code (left -> right) and introduce a new kind='nearest-up'.

A pull request would speed things up.

Resolving patch:
diff --git a/scipy/interpolate/interpolate.py b/scipy/interpolate/interpolate.py
--- a/scipy/interpolate/interpolate.py
+++ b/scipy/interpolate/interpolate.py
@@ -342,14 +342,16 @@ class interp1d(_Interpolator1D):
         A N-D array of real values. The length of `y` along the interpolation
         axis must be equal to the length of `x`.
     kind : str or int, optional
-        Specifies the kind of interpolation as a string
-        ('linear', 'nearest', 'zero', 'slinear', 'quadratic', 'cubic',
-        'previous', 'next', where 'zero', 'slinear', 'quadratic' and 'cubic'
-        refer to a spline interpolation of zeroth, first, second or third
-        order; 'previous' and 'next' simply return the previous or next value
-        of the point) or as an integer specifying the order of the spline
-        interpolator to use.
-        Default is 'linear'.
+        Specifies the kind of interpolation as a string or as an integer
+        specifying the order of the spline interpolator to use.
+        The string has to be one of 'linear', 'nearest', 'nearest-up', 'zero',
+        'slinear', 'quadratic', 'cubic', 'previous', or 'next'. 'zero',
+        'slinear', 'quadratic' and 'cubic' refer to a spline interpolation of
+        zeroth, first, second or third order; 'previous' and 'next' simply
+        return the previous or next value of the point; 'nearest-up' and
+        'nearest' differ when interpolating half-integers (e.g. 0.5, 1.5)
+        in that 'nearest-up' rounds up and 'nearest' rounds down. Default
+        is 'linear'.
     axis : int, optional
         Specifies the axis of `y` along which to interpolate.
         Interpolation defaults to the last axis of `y`.
@@ -402,7 +404,7 @@ class interp1d(_Interpolator1D):
     Calling `interp1d` with NaNs present in input values results in
     undefined behaviour.
 
-    Input values `x` and `y` must be convertible to `float` values like 
+    Input values `x` and `y` must be convertible to `float` values like
     `int` or `float`.
 
 
@@ -436,7 +438,8 @@ def __init__(self, x, y, kind='linear', axis=-1,
         elif isinstance(kind, int):
             order = kind
             kind = 'spline'
-        elif kind not in ('linear', 'nearest', 'previous', 'next'):
+        elif kind not in ('linear', 'nearest', 'nearest-up', 'previous',
+                          'next'):
             raise NotImplementedError("%s is unsupported: Use fitpack "
                                       "routines for other types." % kind)
         x = array(x, copy=self.copy)
@@ -471,13 +474,22 @@ def __init__(self, x, y, kind='linear', axis=-1,
         # interpolation methods, in order to avoid circular references to self
         # stored in the bound instance methods, and therefore delayed garbage
         # collection.  See: https://docs.python.org/reference/datamodel.html
-        if kind in ('linear', 'nearest', 'previous', 'next'):
+        if kind in ('linear', 'nearest', 'nearest-up', 'previous', 'next'):
             # Make a "view" of the y array that is rotated to the interpolation
             # axis.
             minval = 2
             if kind == 'nearest':
                 # Do division before addition to prevent possible integer
                 # overflow
+                self._side = 'left'
+                self.x_bds = self.x / 2.0
+                self.x_bds = self.x_bds[1:] + self.x_bds[:-1]
+
+                self._call = self.__class__._call_nearest
+            elif kind == 'nearest-up':
+                # Do division before addition to prevent possible integer
+                # overflow
+                self._side = 'right'
                 self.x_bds = self.x / 2.0
                 self.x_bds = self.x_bds[1:] + self.x_bds[:-1]
 
@@ -622,7 +634,7 @@ def _call_nearest(self, x_new):
         #    would be inserted.
         #    Note: use side='left' (right) to searchsorted() to define the
         #    halfway point to be nearest to the left (right) neighbor
-        x_new_indices = searchsorted(self.x_bds, x_new, side='left')
+        x_new_indices = searchsorted(self.x_bds, x_new, side=self._side)
 
         # 3. Clip x_new_indices so that they are within the range of x indices.
         x_new_indices = x_new_indices.clip(0, len(self.x)-1).astype(intp)


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
@@ -135,8 +135,8 @@ def test_validation(self):
         # are given to the constructor.
 
         # These should all work.
-        for kind in ('nearest', 'zero', 'linear', 'slinear', 'quadratic',
-                     'cubic', 'previous', 'next'):
+        for kind in ('nearest', 'nearest-up', 'zero', 'linear', 'slinear',
+                     'quadratic', 'cubic', 'previous', 'next'):
             interp1d(self.x10, self.y10, kind=kind)
             interp1d(self.x10, self.y10, kind=kind, fill_value="extrapolate")
         interp1d(self.x10, self.y10, kind='linear', fill_value=(-1, 1))
@@ -300,14 +300,17 @@ def test_cubic(self):
         interp10 = interp1d(self.x10, self.y10, kind='cubic')
         assert_array_almost_equal(interp10(self.x10), self.y10)
         assert_array_almost_equal(interp10(1.2), np.array([1.2]))
+        assert_array_almost_equal(interp10(1.5), np.array([1.5]))
         assert_array_almost_equal(interp10([2.4, 5.6, 6.0]),
                                   np.array([2.4, 5.6, 6.0]),)
 
     def test_nearest(self):
         # Check the actual implementation of nearest-neighbour interpolation.
+        # Nearest asserts that half-integer case (1.5) rounds down to 1
         interp10 = interp1d(self.x10, self.y10, kind='nearest')
         assert_array_almost_equal(interp10(self.x10), self.y10)
         assert_array_almost_equal(interp10(1.2), np.array(1.))
+        assert_array_almost_equal(interp10(1.5), np.array(1.))
         assert_array_almost_equal(interp10([2.4, 5.6, 6.0]),
                                   np.array([2., 6., 6.]),)
 
@@ -322,11 +325,33 @@ def test_nearest(self):
                     bounds_error=True)
         assert_raises(ValueError, interp1d, self.x10, self.y10, **opts)
 
+    def test_nearest_up(self):
+        # Check the actual implementation of nearest-neighbour interpolation.
+        # Nearest-up asserts that half-integer case (1.5) rounds up to 2
+        interp10 = interp1d(self.x10, self.y10, kind='nearest-up')
+        assert_array_almost_equal(interp10(self.x10), self.y10)
+        assert_array_almost_equal(interp10(1.2), np.array(1.))
+        assert_array_almost_equal(interp10(1.5), np.array(2.))
+        assert_array_almost_equal(interp10([2.4, 5.6, 6.0]),
+                                  np.array([2., 6., 6.]),)
+
+        # test fill_value="extrapolate"
+        extrapolator = interp1d(self.x10, self.y10, kind='nearest-up',
+                                fill_value='extrapolate')
+        assert_allclose(extrapolator([-1., 0, 9, 11]),
+                        [0, 0, 9, 9], rtol=1e-14)
+
+        opts = dict(kind='nearest-up',
+                    fill_value='extrapolate',
+                    bounds_error=True)
+        assert_raises(ValueError, interp1d, self.x10, self.y10, **opts)
+
     def test_previous(self):
         # Check the actual implementation of previous interpolation.
         interp10 = interp1d(self.x10, self.y10, kind='previous')
         assert_array_almost_equal(interp10(self.x10), self.y10)
         assert_array_almost_equal(interp10(1.2), np.array(1.))
+        assert_array_almost_equal(interp10(1.5), np.array(1.))
         assert_array_almost_equal(interp10([2.4, 5.6, 6.0]),
                                   np.array([2., 5., 6.]),)
 
@@ -346,6 +371,7 @@ def test_next(self):
         interp10 = interp1d(self.x10, self.y10, kind='next')
         assert_array_almost_equal(interp10(self.x10), self.y10)
         assert_array_almost_equal(interp10(1.2), np.array(2.))
+        assert_array_almost_equal(interp10(1.5), np.array(2.))
         assert_array_almost_equal(interp10([2.4, 5.6, 6.0]),
                                   np.array([3., 6., 6.]),)
 
@@ -365,6 +391,7 @@ def test_zero(self):
         interp10 = interp1d(self.x10, self.y10, kind='zero')
         assert_array_almost_equal(interp10(self.x10), self.y10)
         assert_array_almost_equal(interp10(1.2), np.array(1.))
+        assert_array_almost_equal(interp10(1.5), np.array(1.))
         assert_array_almost_equal(interp10([2.4, 5.6, 6.0]),
                                   np.array([2., 5., 6.]))