如何使用scipy执行二维插值?

时间:2016-06-17 02:27:49

标签: python scipy interpolation

  

此Q& A旨在作为使用scipy进行二维(和多维)插值的规范(-ish)。关于各种多维插值方法的基本语法经常有问题,我希望也能直接设置这些。

我有一组分散的二维数据点,我想将它们绘制成一个漂亮的表面,最好使用contourf中的plot_surfacematplotlib.pyplot。如何使用scipy将我的二维或多维数据插入网格?

我找到了scipy.interpolate子包,但在使用interp2dbisplrepgriddatarbf时,我一直收到错误消息。这些方法的正确语法是什么?

1 个答案:

答案 0 :(得分:140)

免责声明:我主要是在写这篇文章时考虑到语法因素和一般行为。我不熟悉所描述方法的内存和CPU方面,我将这个答案的目标对象是那些拥有相当小的数据集的人,这样插值的质量可能是需要考虑的主要方面。我知道,在处理非常大的数据集时,性能更好的方法(即griddataRbf)可能不可行。

我将比较三种多维插值方法(interp2d / splines,griddataRbf)。我将使它们受到两种插值任务和两种底层函数(要插入的点)。具体的例子将展示二维插值,但可行的方法适用于任意维度。每种方法都提供各种插值;在所有情况下,我将使用三次插值(或接近 1 的东西)。重要的是要注意,无论何时使用插值,都会引入与原始数据相比的偏差,并且所使用的特定方法会影响您最终会得到的工件。始终注意这一点,并负责任地插入。

两个插值任务将是

  1. 上采样(输入数据在矩形网格上,输出数据在更密集的网格上)
  2. 将散乱数据插值到规则网格上
  3. 这两个函数(通过域[x,y] in [-1,1]x[-1,1])将是

    1. 顺畅友好的功能:cos(pi*x)*sin(pi*y);范围[-1, 1]
    2. 一个邪恶的(特别是非连续的)函数:x*y/(x^2+y^2),其值在原点附近为0.5;范围[-0.5, 0.5]
    3. 以下是它们的外观:

      fig1: test functions

      我将首先演示这三种方法在这四种测试中的表现,然后我将详细介绍这三种方法的语法。如果您知道对方法的期望,您可能不想浪费时间学习它的语法(看着你,interp2d)。

      测试数据

      为了显而易见,这里是我生成输入数据的代码。虽然在这种特殊情况下,我明显意识到数据的基础功能,但我只会用它来为插值方法生成输入。我使用numpy是为了方便(主要用于生成数据),但仅仅scipy就足够了。

      import numpy as np
      import scipy.interpolate as interp
      
      # auxiliary function for mesh generation
      def gimme_mesh(n):
          minval = -1
          maxval =  1
          # produce an asymmetric shape in order to catch issues with transpositions
          return np.meshgrid(np.linspace(minval,maxval,n), np.linspace(minval,maxval,n+1))
      
      # set up underlying test functions, vectorized
      def fun_smooth(x, y):
          return np.cos(np.pi*x)*np.sin(np.pi*y)
      
      def fun_evil(x, y):
          # watch out for singular origin; function has no unique limit there
          return np.where(x**2+y**2>1e-10, x*y/(x**2+y**2), 0.5)
      
      # sparse input mesh, 6x7 in shape
      N_sparse = 6
      x_sparse,y_sparse = gimme_mesh(N_sparse)
      z_sparse_smooth = fun_smooth(x_sparse, y_sparse)
      z_sparse_evil = fun_evil(x_sparse, y_sparse)
      
      # scattered input points, 10^2 altogether (shape (100,))
      N_scattered = 10
      x_scattered,y_scattered = np.random.rand(2,N_scattered**2)*2 - 1
      z_scattered_smooth = fun_smooth(x_scattered, y_scattered)
      z_scattered_evil = fun_evil(x_scattered, y_scattered)
      
      # dense output mesh, 20x21 in shape
      N_dense = 20
      x_dense,y_dense = gimme_mesh(N_dense)
      

      平滑功能和上采样

      让我们从最简单的任务开始吧。以下是从形状[6,7]的网格到[20,21]的网格的上采样如何用于平滑测试函数:

      fig2: smooth upsampling

      尽管这是一项简单的任务,但输出之间已经存在细微差别。乍一看,所有三个输出都是合理的。基于我们对基础函数的先验知识,有两个要注意的特征:griddata的中间案例最能扭曲数据。注意绘图的y==-1边界(最接近x标签):函数应该严格为零(因为y==-1是平滑函数的节点线),但这不是griddata的案例。还要注意图的x==-1边界(后面,左边):底层函数在[-1, -0.5]处有一个局部最大值(暗示边界附近的零梯度),但是griddata输出在该区域中显示出明显的非零梯度。效果很微妙,但它仍然是一种偏见。 (Rbf的保真度甚至更好,默认选择径向函数,称为multiquadratic。)

      邪恶功能和上采样

      更难的任务是对我们的邪恶功能进行上采样:

      fig3: evil upsampling

      三种方法之间开始显示出明显的差异。观察表面图,interp2d的输出中出现明显的虚假极值(注意绘制曲面右侧的两个驼峰)。虽然griddataRbf乍一看似乎会产生类似的结果,但后者似乎会在基础函数中不存在[0.4, -0.4]附近产生更深的最小值。

      然而,有一个关键方面Rbf远远优于它:它尊重底层函数的对称性(当然也可以通过样本网格的对称性来实现)。 griddata的输出打破了采样点的对称性,这在光滑的情况下已经很弱了。

      平滑功能和分散数据

      通常,人们希望对分散的数据执行插值。出于这个原因,我希望这些测试更重要。如上所示,在感兴趣的域中伪均匀地选择样本点。在实际场景中,每次测量可能会产生额外的噪声,您应该考虑是否有必要插入原始数据。

      平滑功能的输出:

      fig4: smooth scattered interpolation

      现在已经有点恐怖表演了。我将interp2d的输出剪切到[-1, 1]之间专门用于绘图,以便保留至少最少量的信息。很明显,虽然存在一些潜在的形状,但是存在巨大的嘈杂区域,其中该方法完全破坏。 griddata的第二种情况相当好地再现了形状,但请注意等高线边界处的白色区域。这是因为griddata仅在输入数据点的凸包内部起作用(换句话说,它不执行任何外推)。我保留了位于凸包外面的输出点的默认NaN值。 2 考虑到这些功能,Rbf似乎表现最佳。

      邪恶功能和分散数据

      我们一直在等待的那一刻:

      fig5: evil scattered interpolation

      interp2d放弃并不奇怪。事实上,在致电interp2d期间,你应该期待一些友好的RuntimeWarning抱怨样条的不可能构建。至于其他两种方法,Rbf似乎产生最佳输出,甚至在推断结果的域的边界附近。

      那么,让我谈谈三种方法,按优先顺序递减(这样最差的人最不可能被任何人阅读)。

      scipy.interpolate.Rbf

      Rbf类代表"径向基函数"。说实话,在我开始研究这篇文章之前,我从未考虑过这种方法,但我很确定我将来会使用这些方法。

      就像基于样条曲线的方法(见下文)一样,用法分两步:第一步根据输入数据创建一个可调用的Rbf类实例,然后为给定的输出网格调用此对象获得插值结果。平滑上采样测试的例子:

      import scipy.interpolate as interp
      zfun_smooth_rbf = interp.Rbf(x_sparse, y_sparse, z_sparse_smooth, function='cubic', smooth=0)  # default smooth=0 for interpolation
      z_dense_smooth_rbf = zfun_smooth_rbf(x_dense, y_dense)  # not really a function, but a callable class instance
      

      请注意,在这种情况下,输入和输出点都是2d数组,输出z_dense_smooth_rbfx_densey_dense具有相同的形状,而不需要任何努力。另请注意,Rbf支持插值的任意维度。

      所以,scipy.interpolate.Rbf

      • 即使对于疯狂的输入数据也能产生良好的输出
      • 支持更高维度的插值
      • 在输入点的凸包外推断(当然外推总是赌博,你通常不应该依赖它)。
      • 创建插补器作为第一步,因此在各种输出点进行评估可以减少额外的工作量
      • 可以有任意形状的输出点(而不是被约束为矩形网格,见后面)
      • 倾向于保留输入数据的对称性
      • 支持关键字function的多种径向函数:multiquadricinversegaussianlinearcubic,{{1 },quintic和用户定义的任意

      thin_plate

      我以前最喜欢的scipy.interpolate.griddata,是任意维度插值的一般主力。除了为节点的凸包外部的点设置单个预设值之外,它不会执行外推,但由于外推是非常易变和危险的事情,因此这不一定是一个骗局。用法示例:

      griddata

      请注意略有克服的语法。必须在z_dense_smooth_griddata = interp.griddata(np.array([x_sparse.ravel(),y_sparse.ravel()]).T, z_sparse_smooth.ravel(), (x_dense,y_dense), method='cubic') # default method is linear 维度的形状[N, D]数组中指定输入点。为此,我们首先必须展平我们的2d坐标数组(使用D),然后连接数组并转置结果。有多种方法可以做到这一点,但所有这些方法看起来都很笨重。输入ravel数据也必须展平。当涉及输出点时,我们有更多的自由:由于某种原因,这些也可以被指定为多维数组的元组。请注意,z的{​​{1}}具有误导性,因为它表明输入点(至少版本为0.17.0)也是如此:

      help

      简而言之,griddata

      • 即使对于疯狂的输入数据也能产生良好的输出
      • 支持更高维度的插值
      • 不执行外推,可以为输入点的凸包外部的输出设置单个值(参见griddata(points, values, xi, method='linear', fill_value=nan, rescale=False) Interpolate unstructured D-dimensional data. Parameters ---------- points : ndarray of floats, shape (n, D) Data point coordinates. Can either be an array of shape (n, D), or a tuple of `ndim` arrays. values : ndarray of float or complex, shape (n,) Data values. xi : ndarray of float, shape (M, D) Points at which to interpolate data.
      • 在一次调用中计算插值,因此探测多组输出点从头开始
      • 可以有任意形状的输出点
      • 支持任意维度的最近邻和线性插值,1d和2d中的立方。最近邻和线性插值分别使用scipy.interpolate.griddatafill_value。 1d三次插值使用样条,2d三次插值使用NearestNDInterpolator构造连续可微分段 - 三次插值器。
      • 可能违反输入数据的对称性

      LinearNDInterpolator / CloughTocher2DInterpolator

      我讨论scipy.interpolate.interp2d及其亲属的唯一原因是它有一个欺骗性的名字,人们可能会尝试使用它。剧透警告:不要使用它(从scipy版本0.17.0开始)。它比以前的主题更特殊,因为它专门用于二维插值,但我怀疑这是多变量插值最常见的情况。

      就语法而言,scipy.interpolate.bisplrep类似于interp2d,因为它首先需要构造一个插值实例,可以调用它来提供实际的插值。然而,有一个问题:输出点必须位于矩形网格上,因此进入内插器调用的输入必须是跨越输出网格的1d向量,就好像来自interp2d

      Rbf

      使用numpy.meshgrid时最常见的错误之一就是将完整的2d网格放入插值调用,这会导致内存消耗殆尽,并希望仓促# reminder: x_sparse and y_sparse are of shape [6, 7] from numpy.meshgrid zfun_smooth_interp2d = interp.interp2d(x_sparse, y_sparse, z_sparse_smooth, kind='cubic') # default kind is 'linear' # reminder: x_dense and y_dense are of shape [20, 21] from numpy.meshgrid xvec = x_dense[0,:] # 1d array of unique x values, 20 elements yvec = y_dense[:,0] # 1d array of unique y values, 21 elements z_dense_smooth_interp2d = zfun_smooth_interp2d(xvec,yvec) # output is [20, 21]-shaped array

      现在,interp2d的最大问题在于它通常无法正常工作。为了理解这一点,我们必须深入了解。事实证明,MemoryError是较低级函数bisplrep + bisplev的包装器,它们又是FITPACK例程的包装器(用Fortran编写)。对前一个示例的等效调用将是

      interp2d

      现在,关于interp2d的问题:(在scipy版本0.17.0中)kind = 'cubic' if kind=='linear': kx=ky=1 elif kind=='cubic': kx=ky=3 elif kind=='quintic': kx=ky=5 # bisplrep constructs a spline representation, bisplev evaluates the spline at given points bisp_smooth = interp.bisplrep(x_sparse.ravel(),y_sparse.ravel(),z_sparse_smooth.ravel(),kx=kx,ky=ky,s=0) z_dense_smooth_bisplrep = interp.bisplev(xvec,yvec,bisp_smooth).T # note the transpose 有一个很好的comment in interpolate/interpolate.py

      interp2d

      确实在interp2d,在if not rectangular_grid: # TODO: surfit is really not meant for interpolation! self.tck = fitpack.bisplrep(x, y, z, kx=kx, ky=ky, s=0.0) 中有一些设置,最终

      interpolate/fitpack.py

      就是这样。 bisplrep下面的例程并不意味着执行插值。它们可能足以满足表现良好的数据,但在实际情况下,您可能希望使用其他东西。

      结论是,tx, ty, c, o = _fitpack._surfit(x, y, z, w, xb, xe, yb, ye, kx, ky, task, s, eps, tx, ty, nxest, nyest, wrk, lwrk1, lwrk2)

      • 即使有良好的数据也可能导致文物
      • 专门针对双变量问题(尽管网格上定义的输入点有限interp2d
      • 执行外推
      • 创建插补器作为第一步,因此在各种输出点进行评估可以减少额外的工作量
      • 只能在矩形网格上产生输出,对于分散输出,你必须在循环中调用插值器
      • 支持线性,立方和五次插值
      • 可能违反输入数据的对称性

      1 我相当确定interpolate.interp2d的{​​{1}}和interpn种基函数并不完全对应于其他插值器同名。
      2 这些NaN也是为什么表面图看起来如此奇怪的原因:matplotlib历史上很难用复杂的深度信息绘制复杂的3d对象。数据中的NaN值会混淆渲染器,因此应该在后面的曲面部分被绘制在前面。这是可视化问题,而不是插值问题。