100

I'm trying to port a program which uses a hand-rolled interpolator (developed by a mathematician colleage) over to use the interpolators provided by scipy. I'd like to use or wrap the scipy interpolator so that it has as close as possible behavior to the old interpolator.

A key difference between the two functions is that in our original interpolator - if the input value is above or below the input range, our original interpolator will extrapolate the result. If you try this with the scipy interpolator it raises a ValueError. Consider this program as an example:

import numpy as np
from scipy import interpolate

x = np.arange(0,10)
y = np.exp(-x/3.0)
f = interpolate.interp1d(x, y)

print f(9)
print f(11) # Causes ValueError, because it's greater than max(x)

Is there a sensible way to make it so that instead of crashing, the final line will simply do a linear extrapolate, continuing the gradients defined by the first and last two points to infinity.

Note, that in the real software I'm not actually using the exp function - that's here for illustration only!

ryggyr
  • 3,341
  • 2
  • 20
  • 14
Salim Fadhley
  • 22,020
  • 23
  • 75
  • 102

11 Answers11

99

As of SciPy version 0.17.0, there is a new option for scipy.interpolate.interp1d that allows extrapolation. Simply set fill_value='extrapolate' in the call. Modifying your code in this way gives:

import numpy as np
from scipy import interpolate

x = np.arange(0,10)
y = np.exp(-x/3.0)
f = interpolate.interp1d(x, y, fill_value='extrapolate')

print f(9)
print f(11)

and the output is:

0.0497870683679
0.010394302658
David Nehme
  • 21,379
  • 8
  • 78
  • 117
Moot
  • 2,195
  • 2
  • 17
  • 14
  • Is the extrapolation kind similar to interpolation kind? For example, can we have linear interpolation with nearest point extrapolation? – a.sam May 26 '17 at 22:49
  • If kind='cubic', fill_value='extrapolate' doesn't work. – vlmercado May 30 '17 at 07:02
  • @a.sam: I'm not sure what you mean... Presumably, if you use kind='linear' with fill_value='interpolation' then you get a linear interpolation, and if you use it with fill_value='extrapolation' then you get a linear extrapolation, no? – Moot Jun 02 '17 at 05:02
  • @vlmercado: can you explain in what way it doesn't work? I tried running the above example with the addition of kind='cubic', and it works fine for me. – Moot Jun 02 '17 at 05:05
  • @Moot, using scipy 0.18.1, I get the following: ValueError: Extrapolation does not work with kind=spline – vlmercado Jun 02 '17 at 05:59
  • @vlmercado, indeed, I have SciPy 0.19 and I get an error as well when using kind='spline', so agreed. However, your previous comment was about kind='cubic' not working, but that does seem to work. Also, the documentation for SciPy 0.19 does not mention kind='spline' as a valid option, and also specifies that the options ‘zero’, ‘slinear’, ‘quadratic’ and ‘cubic’ all refer to splines of the respective order, so maybe that takes care of a wanted spline extrapolation? – Moot Jun 03 '17 at 06:59
  • @Moot, I used kind='cubic', but the ValueError refers to kind=spline. Here's the complete code: import numpy as np from scipy import interpolate x = np.array([ 21, 63, 126, 252, 504, 756, 1260, 1764, 2520, 5040, 7560]) y = np.array([0.77, 0.93, 1.07, 1.16, 1.28, 1.44, 1.76, 2.02, 2.21, 2.61, 2.88]) tck = interpolate.interp1d(x, y, kind='cubic', fill_value='extrapolate') ValueError: Extrapolation does not work with kind=spline – vlmercado Jun 03 '17 at 07:39
  • @vlmercado, I tried your code and it does not give any errors for me. I suggest you try upgrading Scipy to 0.19.0 and try again. – Moot Jun 04 '17 at 15:17
  • @Moot, I should've qualified my original comment from the start. I shared the error because your initial answer referred to "As of SciPy version 0.17.0..." I hadn't tested on version 0.19.0, but knew it didn't work on version 0.18.0. I don't need to extrapolation feature at this time. Thanks for your effort, however. – vlmercado Jun 04 '17 at 19:35
  • Does anybody know the method of extrapolation?. You can't just say it extrapolates to something. There are an infinity of options to extrapolate, throw a random number and that is one way to extrapolate. It would be nice to know. – Wilmer E. Henao Jan 13 '20 at 14:53
  • @WilmerE.Henao Presumably the choice for the kind argument sets both the kind of interpolation and the type of extrapolation (the default is 'linear'). – Moot Jan 14 '20 at 20:03
90

You can take a look at InterpolatedUnivariateSpline

Here an example using it:

import matplotlib.pyplot as plt
import numpy as np
from scipy.interpolate import InterpolatedUnivariateSpline

# given values
xi = np.array([0.2, 0.5, 0.7, 0.9])
yi = np.array([0.3, -0.1, 0.2, 0.1])
# positions to inter/extrapolate
x = np.linspace(0, 1, 50)
# spline order: 1 linear, 2 quadratic, 3 cubic ... 
order = 1
# do inter/extrapolation
s = InterpolatedUnivariateSpline(xi, yi, k=order)
y = s(x)

# example showing the interpolation for linear, quadratic and cubic interpolation
plt.figure()
plt.plot(xi, yi)
for order in range(1, 4):
    s = InterpolatedUnivariateSpline(xi, yi, k=order)
    y = s(x)
    plt.plot(x, y)
plt.show()
Display Name
  • 8,022
  • 3
  • 31
  • 66
Joma
  • 2,334
  • 2
  • 20
  • 25
  • 3
    this is the best answer. That's what i did. `I used k=1 (order)`, so it becomes a linear interpolation, and `I used bbox=[xmin-w, xmax+w] where w is my tolerance` – imbr Aug 13 '14 at 18:38
41

1. Constant extrapolation

You can use interp function from scipy, it extrapolates left and right values as constant beyond the range:

>>> from scipy import interp, arange, exp
>>> x = arange(0,10)
>>> y = exp(-x/3.0)
>>> interp([9,10], x, y)
array([ 0.04978707,  0.04978707])

2. Linear (or other custom) extrapolation

You can write a wrapper around an interpolation function which takes care of linear extrapolation. For example:

from scipy.interpolate import interp1d
from scipy import arange, array, exp

def extrap1d(interpolator):
    xs = interpolator.x
    ys = interpolator.y

    def pointwise(x):
        if x < xs[0]:
            return ys[0]+(x-xs[0])*(ys[1]-ys[0])/(xs[1]-xs[0])
        elif x > xs[-1]:
            return ys[-1]+(x-xs[-1])*(ys[-1]-ys[-2])/(xs[-1]-xs[-2])
        else:
            return interpolator(x)

    def ufunclike(xs):
        return array(list(map(pointwise, array(xs))))

    return ufunclike

extrap1d takes an interpolation function and returns a function which can also extrapolate. And you can use it like this:

x = arange(0,10)
y = exp(-x/3.0)
f_i = interp1d(x, y)
f_x = extrap1d(f_i)

print f_x([9,10])

Output:

[ 0.04978707  0.03009069]
sastanin
  • 40,473
  • 13
  • 103
  • 130
  • 1
    In Python 3.6, I had to add `list` to the return: `return array(list(map(pointwise, array(xs))))` to resolve the iterator. – mostlyWright Mar 28 '19 at 18:34
  • This solution is more flexible than the fill_value="extrapolate" option. I was able to adapt the inner function 'pointwise' to my needs, I second the comment above and insert the list when needed. Having said that, sometimes you may just want to have a generator. – Wilmer E. Henao Jan 13 '20 at 17:25
  • 1
    Note that the first solution based on ```scipy.interp``` is no longer recommanded as it is deprecated and will disappear in SciPy 2.0.0. They recommand using ```numpy.interp``` instead but as stated in the question, it won't work here – Yosko Sep 18 '20 at 12:29
9

What about scipy.interpolate.splrep (with degree 1 and no smoothing):

>> tck = scipy.interpolate.splrep([1, 2, 3, 4, 5], [1, 4, 9, 16, 25], k=1, s=0)
>> scipy.interpolate.splev(6, tck)
34.0

It seems to do what you want, since 34 = 25 + (25 - 16).

subnivean
  • 1,132
  • 11
  • 19
7

It may be faster to use boolean indexing with large datasets, since the algorithm checks if every point is in outside the interval, whereas boolean indexing allows an easier and faster comparison.

For example:

# Necessary modules
import numpy as np
from scipy.interpolate import interp1d

# Original data
x = np.arange(0,10)
y = np.exp(-x/3.0)

# Interpolator class
f = interp1d(x, y)

# Output range (quite large)
xo = np.arange(0, 10, 0.001)

# Boolean indexing approach

# Generate an empty output array for "y" values
yo = np.empty_like(xo)

# Values lower than the minimum "x" are extrapolated at the same time
low = xo < f.x[0]
yo[low] =  f.y[0] + (xo[low]-f.x[0])*(f.y[1]-f.y[0])/(f.x[1]-f.x[0])

# Values higher than the maximum "x" are extrapolated at same time
high = xo > f.x[-1]
yo[high] = f.y[-1] + (xo[high]-f.x[-1])*(f.y[-1]-f.y[-2])/(f.x[-1]-f.x[-2])

# Values inside the interpolation range are interpolated directly
inside = np.logical_and(xo >= f.x[0], xo <= f.x[-1])
yo[inside] = f(xo[inside])

In my case, with a data set of 300000 points, this means an speed up from 25.8 to 0.094 seconds, this is more than 250 times faster.

7

Here's an alternative method that uses only the numpy package. It takes advantage of numpy's array functions, so may be faster when interpolating/extrapolating large arrays:

import numpy as np

def extrap(x, xp, yp):
    """np.interp function with linear extrapolation"""
    y = np.interp(x, xp, yp)
    y = np.where(x<xp[0], yp[0]+(x-xp[0])*(yp[0]-yp[1])/(xp[0]-xp[1]), y)
    y = np.where(x>xp[-1], yp[-1]+(x-xp[-1])*(yp[-1]-yp[-2])/(xp[-1]-xp[-2]), y)
    return y

x = np.arange(0,10)
y = np.exp(-x/3.0)
xtest = np.array((8.5,9.5))

print np.exp(-xtest/3.0)
print np.interp(xtest, x, y)
print extrap(xtest, x, y)

Edit: Mark Mikofski's suggested modification of the "extrap" function:

def extrap(x, xp, yp):
    """np.interp function with linear extrapolation"""
    y = np.interp(x, xp, yp)
    y[x < xp[0]] = yp[0] + (x[x<xp[0]]-xp[0]) * (yp[0]-yp[1]) / (xp[0]-xp[1])
    y[x > xp[-1]]= yp[-1] + (x[x>xp[-1]]-xp[-1])*(yp[-1]-yp[-2])/(xp[-1]-xp[-2])
    return y
ryggyr
  • 3,341
  • 2
  • 20
  • 14
  • 2
    +1 for an actual example, but you could also use [boolean indexing](http://docs.scipy.org/doc/numpy/user/basics.indexing.html#boolean-or-mask-index-arrays) and [here](http://www.scipy.org/Tentative_NumPy_Tutorial#head-d55e594d46b4f347c20efe1b4c65c92779f06268) `y[x < xp[0]] = fp[0] + (x[x < xp[0]] - xp[0]) / (xp[1] - xp[0]) * (fp[1] - fp[0])` and `y[x > xp[-1]] = fp[-1] + (x[x > xp[-1]] - xp[-1]) / (xp[-2] - xp[-1]) * (fp[-2] - fp[-1])` instead of `np.where`, since the `False` option, `y` doesn't change. – Mark Mikofski Jun 13 '12 at 20:07
2

I did it by adding a point to my initial arrays. In this way I avoid defining self-made functions, and the linear extrapolation (in the example below: right extrapolation) looks ok.

import numpy as np  
from scipy import interp as itp  

xnew = np.linspace(0,1,51)  
x1=xold[-2]  
x2=xold[-1]  
y1=yold[-2]  
y2=yold[-1]  
right_val=y1+(xnew[-1]-x1)*(y2-y1)/(x2-x1)  
x=np.append(xold,xnew[-1])  
y=np.append(yold,right_val)  
f = itp(xnew,x,y)  
bananafish
  • 2,877
  • 20
  • 29
Giovanni
  • 21
  • 1
2

I don't have enough reputation to comment, but in case somebody is looking for an extrapolation wrapper for a linear 2d-interpolation with scipy, I have adapted the answer that was given here for the 1d interpolation.

def extrap2d(interpolator):
xs = interpolator.x
ys = interpolator.y
zs = interpolator.z
zs = np.reshape(zs, (-1, len(xs)))
def pointwise(x, y):
    if x < xs[0] or y < ys[0]:
        x1_index = np.argmin(np.abs(xs - x))
        x2_index = x1_index + 1
        y1_index = np.argmin(np.abs(ys - y))
        y2_index = y1_index + 1
        x1 = xs[x1_index]
        x2 = xs[x2_index]
        y1 = ys[y1_index]
        y2 = ys[y2_index]
        z11 = zs[x1_index, y1_index]
        z12 = zs[x1_index, y2_index]
        z21 = zs[x2_index, y1_index]
        z22 = zs[x2_index, y2_index]

        return (z11 * (x2 - x) * (y2 - y) +
        z21 * (x - x1) * (y2 - y) +
        z12 * (x2 - x) * (y - y1) +
        z22 * (x - x1) * (y - y1)
       ) / ((x2 - x1) * (y2 - y1) + 0.0)


    elif x > xs[-1] or y > ys[-1]:
        x1_index = np.argmin(np.abs(xs - x))
        x2_index = x1_index - 1
        y1_index = np.argmin(np.abs(ys - y))
        y2_index = y1_index - 1
        x1 = xs[x1_index]
        x2 = xs[x2_index]
        y1 = ys[y1_index]
        y2 = ys[y2_index]
        z11 = zs[x1_index, y1_index]
        z12 = zs[x1_index, y2_index]
        z21 = zs[x2_index, y1_index]
        z22 = zs[x2_index, y2_index]#

        return (z11 * (x2 - x) * (y2 - y) +
        z21 * (x - x1) * (y2 - y) +
        z12 * (x2 - x) * (y - y1) +
        z22 * (x - x1) * (y - y1)
        ) / ((x2 - x1) * (y2 - y1) + 0.0)
    else:
        return interpolator(x, y)
def ufunclike(xs, ys):
    if  isinstance(xs, int) or isinstance(ys, int) or isinstance(xs, np.int32) or isinstance(ys, np.int32):
        res_array = pointwise(xs, ys)
    else:
        res_array = np.zeros((len(xs), len(ys)))
        for x_c in range(len(xs)):
            res_array[x_c, :] = np.array([pointwise(xs[x_c], ys[y_c]) for y_c in range(len(ys))]).T

    return res_array
return ufunclike

I haven't commented a lot and I am aware, that the code isn't super clean. If anybody sees any errors, please let me know. In my current use-case it is working without a problem :)

KRS
  • 44
  • 1
  • Very useful code, thanks - however I think there is an error. The reshape doesn't work properly when len(xs) != len(ys). I think using `unravel_index`may fix that. – 16Aghnar Nov 03 '22 at 16:02
1

The below code gives you the simple extrapolation module. k is the value to which the data set y has to be extrapolated based on the data set x. The numpy module is required.

 def extrapol(k,x,y):
        xm=np.mean(x);
        ym=np.mean(y);
        sumnr=0;
        sumdr=0;
        length=len(x);
        for i in range(0,length):
            sumnr=sumnr+((x[i]-xm)*(y[i]-ym));
            sumdr=sumdr+((x[i]-xm)*(x[i]-xm));

        m=sumnr/sumdr;
        c=ym-(m*xm);
        return((m*k)+c)
tripleee
  • 175,061
  • 34
  • 275
  • 318
1

I'm afraid that there is no easy to do this in Scipy to my knowledge. You can, as I'm fairly sure that you are aware, turn off the bounds errors and fill all function values beyond the range with a constant, but that doesn't really help. See this question on the mailing list for some more ideas. Maybe you could use some kind of piecewise function, but that seems like a major pain.

Justin Peel
  • 46,722
  • 6
  • 58
  • 80
  • That's the conclusion I came to, at least with scipy 0.7, however this tutorial written 21 months ago suggests that the interp1d function has a high and low attribute which can be set to "linear", the tutorial is not clear which version of scipy this applies to: http://projects.scipy.org/scipy/browser/branches/Interpolate1D/docs/tutorial.rst?rev=4591 – Salim Fadhley Apr 30 '10 at 15:57
  • It looks like this is part of a branch that hasn't been assimilated into the main version yet so there might still be some problems with it. The current code for this is at http://projects.scipy.org/scipy/browser/branches/interpolate/interpolate1d.py though you might want to scroll to the bottom of the page and click to download it as plain text. I think that this looks promising though I haven't tried it yet myself. – Justin Peel Apr 30 '10 at 16:52
-1

Standard interpolate + linear extrapolate:

    def interpola(v, x, y):
        if v <= x[0]:
            return y[0]+(y[1]-y[0])/(x[1]-x[0])*(v-x[0])
        elif v >= x[-1]:
            return y[-2]+(y[-1]-y[-2])/(x[-1]-x[-2])*(v-x[-2])
        else:
            f = interp1d(x, y, kind='cubic') 
            return f(v)
  • 1
    Hey Federico! If you wonder why you got downvoted, please be aware that when answering questions you need to actually explain how it solves the problem. This answer, as it is, is only a code dump and should have at least a few sentences explaining why and/or how it is usefull. Thanks! – Félix Adriyel Gagnon-Grenier Dec 15 '14 at 19:58