Numpy / Scipy中的快速线性插值“沿路径”

时间:2015-10-11 19:44:34

标签: python numpy scipy interpolation

假设我有一个山上3个(已知)高度的气象站的数据。具体而言,每个站每分钟记录一次温度测量。我有两种插值我想要执行。而且我希望能够快速完成每一项工作。

所以让我们设置一些数据:

import numpy as np
from scipy.interpolate import interp1d
import pandas as pd
import seaborn as sns

np.random.seed(0)
N, sigma = 1000., 5

basetemps = 70 + (np.random.randn(N) * sigma)
midtemps = 50 + (np.random.randn(N) * sigma)
toptemps = 40 + (np.random.randn(N) * sigma)
alltemps = np.array([basetemps, midtemps, toptemps]).T # note transpose!
trend = np.sin(4 / N * np.arange(N)) * 30
trend = trend[:, np.newaxis]

altitudes = np.array([500, 1500, 4000]).astype(float)

finaltemps = pd.DataFrame(alltemps + trend, columns=altitudes)
finaltemps.index.names, finaltemps.columns.names = ['Time'], ['Altitude']
finaltemps.plot()

很好,所以我们的温度看起来像这样: Raw Temperature Data

按相同高度插入所有时间:

我认为这个非常简单。假设我想每次将温度控制在1000海拔高度。我可以使用内置的scipy插值方法:

interping_function = interp1d(altitudes, finaltemps.values)
interped_to_1000 = interping_function(1000)

fig, ax = plt.subplots(1, 1, figsize=(8, 5))
finaltemps.plot(ax=ax, alpha=0.15)
ax.plot(interped_to_1000, label='Interped')
ax.legend(loc='best', title=finaltemps.columns.name)

Temperature with static interp

这很好用。让我们来看看速度:

%%timeit
res = interp1d(altitudes, finaltemps.values)(1000)
#-> 1000 loops, best of 3: 207 µs per loop

沿着路径插值:

所以现在我有第二个相关的问题。假设我知道徒步旅行的高度是时间的函数,我想通过随时间线性插入我的数据来计算他们(移动)位置的温度。 特别是,我知道远足派对位置的时间是相同次,我知道气象站的温度。我也可以这样做努力:

location = np.linspace(altitudes[0], altitudes[-1], N)
interped_along_path = np.array([interp1d(altitudes, finaltemps.values[i, :])(loc) 
                                             for i, loc in enumerate(location)])

fig, ax = plt.subplots(1, 1, figsize=(8, 5))
finaltemps.plot(ax=ax, alpha=0.15)
ax.plot(interped_along_path, label='Interped')
ax.legend(loc='best', title=finaltemps.columns.name)

Temperature with moving interp

所以这很好用,但重要的是要注意上面的关键是使用列表理解来隐藏大量的工作。在前一种情况下,scipy正在为我们创建单个插值函数,并在大量数据上对其进行一次评估。在这种情况下,scipy实际上构建N个别插值函数,并在少量数据上评估每一次。这感觉本身效率低下。这里潜伏着一个for循环(在列表理解中)而且,这只是感觉很松弛。

毫不奇怪,这比前一种情况要慢得多:

%%timeit
res = np.array([interp1d(altitudes, finaltemps.values[i, :])(loc) 
                            for i, loc in enumerate(location)])
#-> 10 loops, best of 3: 145 ms per loop

所以第二个例子的运行速度比第一个慢1000。即与繁重提升是“进行线性插值函数”步骤的想法一致......在第二个例子中发生了1000次,而在第一个例子中只发生了一次。

所以,问题是:是否有更好的方法来解决第二个问题?例如,是否有一种很好的方法来设置它采用2维插值(也许可以处理这种情况已知远足队地点的时间温度采样的时间)?还是有一种特别灵巧的方式来处理时间排队的事情?还是其他?

3 个答案:

答案 0 :(得分:11)

关于点y1,位置y2x1的两个值x2xi之间的线性插值只是:

yi = y1 + (y2-y1) * (xi-x1) / (x2-x1)

使用一些矢量化Numpy表达式,我们可以从数据集中选择相关点并应用上述函数:

I = np.searchsorted(altitudes, location)

x1 = altitudes[I-1]
x2 = altitudes[I]

time = np.arange(len(alltemps))
y1 = alltemps[time,I-1]
y2 = alltemps[time,I]

xI = location

yI = y1 + (y2-y1) * (xI-x1) / (x2-x1)

问题在于某些点位于已知范围的边界(或甚至在已知范围之外),应该将其考虑在内:

I = np.searchsorted(altitudes, location)
same = (location == altitudes.take(I, mode='clip'))
out_of_range = ~same & ((I == 0) | (I == altitudes.size))
I[out_of_range] = 1  # Prevent index-errors

x1 = altitudes[I-1]
x2 = altitudes[I]

time = np.arange(len(alltemps))
y1 = alltemps[time,I-1]
y2 = alltemps[time,I]

xI = location

yI = y1 + (y2-y1) * (xI-x1) / (x2-x1)
yI[out_of_range] = np.nan

幸运的是,Scipy已经提供了ND插值,这也很容易处理不匹配的时间,例如:

from scipy.interpolate import interpn

time = np.arange(len(alltemps))

M = 150
hiketime = np.linspace(time[0], time[-1], M)
location = np.linspace(altitudes[0], altitudes[-1], M)
xI = np.column_stack((hiketime, location))

yI = interpn((time, altitudes), alltemps, xI)

这是一个基准代码(实际上没有任何pandas,我确实包含了其他答案的解决方案):

import numpy as np
from scipy.interpolate import interp1d, interpn

def original():
    return np.array([interp1d(altitudes, alltemps[i, :])(loc)
                                for i, loc in enumerate(location)])

def OP_self_answer():
    return np.diagonal(interp1d(altitudes, alltemps)(location))

def interp_checked():
    I = np.searchsorted(altitudes, location)
    same = (location == altitudes.take(I, mode='clip'))
    out_of_range = ~same & ((I == 0) | (I == altitudes.size))
    I[out_of_range] = 1  # Prevent index-errors

    x1 = altitudes[I-1]
    x2 = altitudes[I]

    time = np.arange(len(alltemps))
    y1 = alltemps[time,I-1]
    y2 = alltemps[time,I]

    xI = location

    yI = y1 + (y2-y1) * (xI-x1) / (x2-x1)
    yI[out_of_range] = np.nan

    return yI

def scipy_interpn():
    time = np.arange(len(alltemps))
    xI = np.column_stack((time, location))
    yI = interpn((time, altitudes), alltemps, xI)
    return yI

N, sigma = 1000., 5

basetemps = 70 + (np.random.randn(N) * sigma)
midtemps = 50 + (np.random.randn(N) * sigma)
toptemps = 40 + (np.random.randn(N) * sigma)
trend = np.sin(4 / N * np.arange(N)) * 30
trend = trend[:, np.newaxis]
alltemps = np.array([basetemps, midtemps, toptemps]).T + trend
altitudes = np.array([500, 1500, 4000], dtype=float)
location = np.linspace(altitudes[0], altitudes[-1], N)

funcs = [original, interp_checked, scipy_interpn]
for func in funcs:
    print(func.func_name)
    %timeit func()

from itertools import combinations
outs = [func() for func in funcs]
print('Output allclose:')
print([np.allclose(out1, out2) for out1, out2 in combinations(outs, 2)])

在我的系统上显示以下结果:

original
10 loops, best of 3: 184 ms per loop
OP_self_answer
10 loops, best of 3: 89.3 ms per loop
interp_checked
1000 loops, best of 3: 224 µs per loop
scipy_interpn
1000 loops, best of 3: 1.36 ms per loop
Output allclose:
[True, True, True, True, True, True]

与最快的方法相比,Scipy的interpn在速度方面有所不同,但由于它的通用性和易用性,它绝对是最佳选择。

答案 1 :(得分:6)

对于固定时间点,您可以使用以下插值函数:

g(a) = cc[0]*abs(a-aa[0]) + cc[1]*abs(a-aa[1]) + cc[2]*abs(a-aa[2])

其中a是徒步旅行者的高度,aa具有3个度量altitudescc的向量是具有系数的向量。有三点需要注意:

  1. 对于与alltemps对应的给定温度(aa),可以使用cc求解线性矩阵方程来确定np.linalg.solve()
  2. g(a)很容易为(N,)维a和(N,3)维cc(分别包括np.linalg.solve())进行矢量化。
  3. g(a)被称为一阶单变量样条内核(对于三个点)。使用abs(a-aa[i])**(2*d-1)会将样条线顺序更改为d。这种方法可以解释为Gaussian Process in Machine Learning的简化版本。
  4. 所以代码是:

    import matplotlib.pyplot as plt
    import numpy as np
    import seaborn as sns
    
    # generate temperatures
    np.random.seed(0)
    N, sigma = 1000, 5
    trend = np.sin(4 / N * np.arange(N)) * 30
    alltemps = np.array([tmp0 + trend + sigma*np.random.randn(N)
                         for tmp0 in [70, 50, 40]])
    
    # generate attitudes:
    altitudes = np.array([500, 1500, 4000]).astype(float)
    location = np.linspace(altitudes[0], altitudes[-1], N)
    
    
    def doit():
        """ do the interpolation, improved version for speed """
        AA = np.vstack([np.abs(altitudes-a_i) for a_i in altitudes])
        # This is slighty faster than np.linalg.solve(), because AA is small:
        cc = np.dot(np.linalg.inv(AA), alltemps)
    
        return (cc[0]*np.abs(location-altitudes[0]) +
                cc[1]*np.abs(location-altitudes[1]) +
                cc[2]*np.abs(location-altitudes[2]))
    
    
    t_loc = doit()  # call interpolator
    
    # do the plotting:
    fg, ax = plt.subplots(num=1)
    for alt, t in zip(altitudes, alltemps):
        ax.plot(t, label="%d feet" % alt, alpha=.5)
    ax.plot(t_loc, label="Interpolation")
    ax.legend(loc="best", title="Altitude:")
    ax.set_xlabel("Time")
    ax.set_ylabel("Temperature")
    fg.canvas.draw()
    

    测量时间给出:

    In [2]: %timeit doit()
    10000 loops, best of 3: 107 µs per loop
    

    更新:我替换了doit()中的原始列表推导 导入速度提高30%(N=1000)。

    此外,根据要求进行比较,@ moarningsun在我的机器上的基准代码块:

    10 loops, best of 3: 110 ms per loop  
    interp_checked
    10000 loops, best of 3: 83.9 µs per loop
    scipy_interpn
    1000 loops, best of 3: 678 µs per loop
    Output allclose:
    [True, True, True]
    

    请注意N=1000是一个相对较小的数字。使用N=100000生成结果:

    interp_checked
    100 loops, best of 3: 8.37 ms per loop
    
    %timeit doit()
    100 loops, best of 3: 5.31 ms per loop
    

    这表明此方法比N方法更适合大interp_checked

答案 2 :(得分:1)

我会提供一点进步。在第二种情况下(沿着路径“插值”),我们正在进行许多不同的插值函数。我们可以尝试的一件事就是只做一个插值函数(一个在高度维度中进行插值,如上面的第一种情况一样)并反复评估该函数(以矢量化方式)。这将为我们提供比我们想要的更多数据(它将为我们提供1,000 x 1,000矩阵而不是1,000元素向量)。但那时我们的目标结果就是沿着对角线。所以问题是,在调用单个函数的过程中,更复杂的参数运行得比制作许多函数并使用简单参数调用它们更快吗?

答案是肯定的!

关键是scipy.interpolate.interp1d返回的插值函数能够接受numpy.ndarray作为输入。因此,您可以通过输入矢量输入以C速度多次有效地调用插值函数。即这种方式比编写一个for循环更快,它在标量输入上一遍又一遍地调用插值函数。因此,当我们计算出许多我们最终丢弃的数据点时,我们通过不构建我们几乎不使用的许多不同的插值函数来节省更多的时间。

old_way = interped_along_path = np.array([interp1d(altitudes, finaltemps.values[i, :])(loc) 
                                                      for i, loc in enumerate(location)])
# look ma, no for loops!
new_way = np.diagonal(interp1d(altitudes, finaltemps.values)(location)) 
# note, `location` is a vector!
abs(old_way - new_way).max()
#-> 0.0

然而:

%%timeit
res = np.diagonal(interp1d(altitudes, finaltemps.values)(location))
#-> 100 loops, best of 3: 16.7 ms per loop

所以这种方法让我们更好!谁能做得更好?或者提出一种完全不同的方法?