Como posso realizar a interpolação bidimensional usando o scipy?

105

Este Q&A pretende ser um canônico (-ish) sobre interpolação bidimensional (e multidimensional) usando scipy. Freqüentemente, há questões relativas à sintaxe básica de vários métodos de interpolação multidimensional, espero esclarecê-las também.

Eu tenho um conjunto de pontos de dados bidimensionais espalhados e gostaria de representá-los como uma boa superfície, de preferência usando algo como contourfou plot_surfaceem matplotlib.pyplot. Como posso interpolar meus dados bidimensionais ou multidimensionais em uma malha usando o scipy?

Encontrei o scipy.interpolatesubpacote, mas continuo recebendo erros ao usar interp2dou bisplrepou griddataou rbf. Qual é a sintaxe adequada desses métodos?

Andras Deak
fonte

Respostas:

163

Isenção de responsabilidade: estou escrevendo este post principalmente com considerações sintáticas e comportamento geral em mente. Não estou familiarizado com o aspecto de memória e CPU dos métodos descritos, e pretendo esta resposta para aqueles que têm conjuntos de dados razoavelmente pequenos, de forma que a qualidade da interpolação pode ser o principal aspecto a ser considerado. Estou ciente de que, ao trabalhar com conjuntos de dados muito grandes, os métodos de melhor desempenho (ou seja, griddatae Rbf) podem não ser viáveis.

Vou comparar três tipos de métodos de interpolação multidimensional ( interp2d/ splines griddatae Rbf). Vou submetê-los a dois tipos de tarefas de interpolação e dois tipos de funções subjacentes (pontos a partir dos quais devem ser interpolados). Os exemplos específicos irão demonstrar a interpolação bidimensional, mas os métodos viáveis ​​são aplicáveis ​​em dimensões arbitrárias. Cada método fornece vários tipos de interpolação; em todos os casos, usarei interpolação cúbica (ou algo próximo a 1 ). É importante observar que sempre que você usa a interpolação, você introduz um viés em comparação com seus dados brutos, e os métodos específicos usados ​​afetam os artefatos que você terá. Esteja sempre ciente disso e interpole com responsabilidade.

As duas tarefas de interpolação serão

  1. upsampling (os dados de entrada estão em uma grade retangular, os dados de saída estão em uma grade mais densa)
  2. interpolação de dados dispersos em uma grade regular

As duas funções (sobre o domínio [x,y] in [-1,1]x[-1,1]) serão

  1. a suavizar e função friendly: cos(pi*x)*sin(pi*y); alcance em[-1, 1]
  2. uma função má (e em particular, não contínua): x*y/(x^2+y^2)com um valor de 0,5 próximo à origem; alcance em[-0.5, 0.5]

Veja como eles são:

fig1: funções de teste

Primeiro demonstrarei como os três métodos se comportam nesses quatro testes e, em seguida, detalharei a sintaxe de todos os três. Se você sabe o que deve esperar de um método, talvez não queira perder seu tempo aprendendo sua sintaxe (olhando para você interp2d).

Dados de teste

Para fins de clareza, aqui está o código com o qual gerei os dados de entrada. Embora, neste caso específico, esteja obviamente ciente da função subjacente aos dados, usarei isso apenas para gerar entrada para os métodos de interpolação. Eu uso o numpy por conveniência (e principalmente para gerar os dados), mas o scipy sozinho também seria suficiente.

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)

Função suave e upsampling

Vamos começar com a tarefa mais fácil. Veja como um upsampling de uma malha de forma [6,7]para outra [20,21]funciona para a função de teste suave:

fig2: upsampling suave

Embora seja uma tarefa simples, já existem diferenças sutis entre os resultados. À primeira vista, todas as três saídas são razoáveis. Há dois recursos a serem observados, com base em nosso conhecimento prévio da função subjacente: o caso do meio de griddatadistorce mais os dados. Observe o y==-1limite do gráfico (mais próximo do xrótulo): a função deve ser estritamente zero (uma vez que y==-1é uma linha nodal para a função suave), mas este não é o caso griddata. Observe também o x==-1limite dos gráficos (atrás, à esquerda): a função subjacente tem um máximo local (implicando em gradiente zero próximo ao limite) [-1, -0.5], mas a griddatasaída mostra um gradiente claramente diferente de zero nesta região. O efeito é sutil, mas ainda assim é um viés. (A fidelidade deRbfé ainda melhor com a escolha padrão de funções radiais, dublado multiquadratic.)

Função do mal e upsampling

Uma tarefa um pouco mais difícil é realizar upsampling em nossa função maligna:

fig3: evil upsampling

Diferenças claras estão começando a aparecer entre os três métodos. Olhando para os gráficos de superfície, há nítidos extremos espúrios aparecendo na saída de interp2d(observe as duas saliências no lado direito da superfície plotada). Embora griddatae Rbfpareça produzir resultados semelhantes à primeira vista, o último parece produzir um mínimo mais profundo próximo [0.4, -0.4]que está ausente da função subjacente.

No entanto, há um aspecto crucial em que Rbfé muito superior: ele respeita a simetria da função subjacente (o que, obviamente, também é possível pela simetria da malha da amostra). A saída de griddataquebra a simetria dos pontos de amostra, que já é fracamente visível no caso suave.

Função suave e dados dispersos

Na maioria das vezes, deseja-se realizar a interpolação em dados dispersos. Por esse motivo, espero que esses testes sejam mais importantes. Conforme mostrado acima, os pontos da amostra foram escolhidos de forma pseudo-uniformemente no domínio de interesse. Em cenários realistas, você pode ter ruído adicional com cada medição e deve considerar se faz sentido interpolar seus dados brutos para começar.

Saída para a função suave:

fig4: interpolação espalhada suave

Agora já está acontecendo um pouco de um show de terror. Cortei a saída de interp2dpara entre [-1, 1]exclusivamente para plotagem, a fim de preservar pelo menos uma quantidade mínima de informações. É claro que, embora algumas das formas subjacentes estejam presentes, há grandes regiões ruidosas onde o método falha completamente. O segundo caso de griddatareproduz a forma muito bem, mas observe as regiões brancas na borda do gráfico de contorno. Isso se deve ao fato de que griddatasó funciona dentro do casco convexo dos pontos de dados de entrada (ou seja, não realiza nenhuma extrapolação ). Eu mantive o valor NaN padrão para pontos de saída situados fora do casco convexo. 2 Considerando esses recursos, Rbfparece ter o melhor desempenho.

Função do mal e dados dispersos

E o momento que todos nós estávamos esperando:

fig5: interpolação mal espalhada

Não é nenhuma grande surpresa que interp2ddesista. Na verdade, durante a ligação, interp2dvocê deve esperar alguns amigos RuntimeWarningreclamando da impossibilidade de construção do spline. Quanto aos outros dois métodos, Rbfparece produzir o melhor resultado, mesmo próximo às fronteiras do domínio onde o resultado é extrapolado.


Portanto, deixe-me dizer algumas palavras sobre os três métodos, em ordem decrescente de preferência (de modo que o pior seja o menos provável de ser lido por alguém).

scipy.interpolate.Rbf

A Rbfclasse significa "funções de base radial". Para ser honesto, nunca considerei essa abordagem até começar a pesquisar para este post, mas tenho certeza de que vou usá-la no futuro.

Assim como os métodos baseados em spline (veja mais adiante), o uso vem em duas etapas: primeiro, uma cria uma Rbfinstância de classe que pode ser chamada com base nos dados de entrada e, em seguida, chama esse objeto para uma determinada malha de saída para obter o resultado interpolado. Exemplo do teste de upsampling suave:

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

Observe que os pontos de entrada e saída eram matrizes 2d neste caso, e a saída z_dense_smooth_rbftem o mesmo formato x_densee y_densesem nenhum esforço. Observe também que Rbfsuporta dimensões arbitrárias para interpolação.

Assim, scipy.interpolate.Rbf

  • produz uma saída bem comportada, mesmo para dados de entrada malucos
  • suporta interpolação em dimensões superiores
  • extrapola fora do casco convexo dos pontos de entrada (é claro que a extrapolação é sempre uma aposta, e você geralmente não deve confiar nela)
  • cria um interpolador como uma primeira etapa, portanto, avaliá-lo em vários pontos de saída é menos esforço adicional
  • pode ter pontos de saída de forma arbitrária (em oposição a ser restrito a malhas retangulares, veja mais tarde)
  • propenso a preservar a simetria dos dados de entrada
  • suporta vários tipos de funções radiais para palavra-chave function: multiquadric, inverse, gaussian, linear, cubic, quintic, thin_platee arbitrário definido pelo usuário

scipy.interpolate.griddata

Meu antigo favorito, griddataé um burro de carga geral para interpolação em dimensões arbitrárias. Ele não executa extrapolação além de definir um único valor predefinido para pontos fora do casco convexo dos pontos nodais, mas como a extrapolação é uma coisa muito instável e perigosa, isso não é necessariamente um trapaceiro. Exemplo de uso:

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

Observe a sintaxe um pouco confusa. Os pontos de entrada devem ser especificados em uma matriz de formas [N, D]em Ddimensões. Para isso, primeiro temos que nivelar nossos arrays de coordenadas 2d (usando ravel), depois concatenar os arrays e transpor o resultado. Existem várias maneiras de fazer isso, mas todas parecem ser volumosas. Os zdados de entrada também devem ser achatados. Temos um pouco mais de liberdade quando se trata dos pontos de saída: por algum motivo, eles também podem ser especificados como uma tupla de matrizes multidimensionais. Observe que o helpde griddataé enganoso, pois sugere que o mesmo é verdadeiro para os pontos de entrada (pelo menos para a versão 0.17.0):

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.

Em poucas palavras, scipy.interpolate.griddata

  • produz uma saída bem comportada, mesmo para dados de entrada malucos
  • suporta interpolação em dimensões superiores
  • não executa extrapolação, um único valor pode ser definido para a saída fora do casco convexo dos pontos de entrada (consulte fill_value)
  • calcula os valores interpolados em uma única chamada, portanto, a sondagem de vários conjuntos de pontos de saída começa do zero
  • pode ter pontos de saída de forma arbitrária
  • suporta vizinho mais próximo e interpolação linear em dimensões arbitrárias, cúbico em 1d e 2d. O vizinho mais próximo e a interpolação linear usam NearestNDInterpolatore LinearNDInterpolatorsob o capô, respectivamente. A interpolação 1d cúbica usa um spline, a interpolação 2d cúbica usa CloughTocher2DInterpolatorpara construir um interpolador cúbico por partes continuamente diferenciável.
  • pode violar a simetria dos dados de entrada

scipy.interpolate.interp2d/scipy.interpolate.bisplrep

A única razão pela qual estou discutindo interp2de seus parentes é que ele tem um nome enganoso e é provável que as pessoas tentem usá-lo. Alerta de spoiler: não o use (a partir da versão 0.17.0 do scipy). Já é mais especial do que os assuntos anteriores, pois é especificamente usado para interpolação bidimensional, mas suspeito que este seja de longe o caso mais comum para interpolação multivariada.

No que diz respeito à sintaxe, interp2dé semelhante a Rbfporque primeiro precisa construir uma instância de interpolação, que pode ser chamada para fornecer os valores interpolados reais. Há um porém: os pontos de saída devem estar localizados em uma malha retangular, então as entradas que vão para a chamada ao interpolador devem ser 1d vetores que abrangem a grade de saída, como se fossem de numpy.meshgrid:

# 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

Um dos erros mais comuns ao usar interp2dé colocar suas malhas 2D completas na chamada de interpolação, o que leva a um consumo explosivo de memória e, com sorte, a um apressado MemoryError.

Agora, o maior problema com interp2dé que muitas vezes não funciona. Para entender isso, temos que examinar os bastidores. Acontece que interp2dé um invólucro para as funções de nível inferior bisplrep+ bisplev, que por sua vez são invólucros para rotinas FITPACK (escritas em Fortran). A chamada equivalente ao exemplo anterior seria

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

Agora, aqui está o seguinte interp2d: (na versão scipy 0.17.0) há um bom comentáriointerpolate/interpolate.py para 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)

e, de fato interpolate/fitpack.py, bisplrephá alguma configuração e, finalmente,

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)                 

E é isso. As rotinas subjacentes interp2dnão têm o objetivo de realizar interpolação. Eles podem ser suficientes para dados suficientemente bem comportados, mas em circunstâncias realistas você provavelmente desejará usar outra coisa.

Só para concluir, interpolate.interp2d

  • pode levar a artefatos, mesmo com dados bem temperados
  • é especificamente para problemas bivariados (embora haja o limite interpnpara pontos de entrada definidos em uma grade)
  • realiza extrapolação
  • cria um interpolador como uma primeira etapa, portanto, avaliá-lo em vários pontos de saída é menos esforço adicional
  • só pode produzir saída em uma grade retangular, para saída espalhada você teria que chamar o interpolador em um loop
  • suporta interpolação linear, cúbica e quíntica
  • pode violar a simetria dos dados de entrada

1 Estou bastante certo de que o cubice lineartipo de funções de base de Rbfnão correspondem exatamente aos outros interpoladores com o mesmo nome.
2 Esses NaNs também são a razão pela qual a plotagem de superfície parece tão estranha: matplotlib historicamente tem dificuldades em plotar objetos 3d complexos com informações de profundidade adequadas. Os valores NaN nos dados confundem o renderizador, de modo que partes da superfície que deveriam estar na parte de trás são plotadas para ficar na frente. Este é um problema de visualização, não de interpolação.

Andras Deak
fonte
2
Rbf pode consumir mais memória do que griddata dependendo do número de pontos de dados e do número de dimensões. Também griddata tem objeto LinearNDInterpolator subjacente que pode ser usado como Rbf em 2 etapas.
denfromufa
1
A interpolação cúbica de Griddata é limitada a 2 (?) Dimensões. Para dimensões mais altas, grades esparsas de smolyak baseadas em chebfun devem ser consideradas.
denfromufa
1
deixe-me terminar meus comentários com este link, onde pesquisei todas as opções de interpolação dispersas: scicomp.stackexchange.com/questions/19137/…
denfromufa
4
a interpolação linear griddata é local, a interpolação cúbica griddata é global. Extrapolação não é suportada, porque eu não tive tempo para descobrir como preservar a continuidade / diferenciabilidade. Rbf é bom para pequenos conjuntos de dados, mas para interpolar n pontos de dados, ele precisa inverter a matriz nxn, o que eventualmente se torna impossível após n> 5000. O Rbf também pode ser sensível à distribuição dos dados e você pode precisar ajustar seus parâmetros manualmente. É possível fazer Rbf para grandes conjuntos de dados, mas isso não é implementado no scipy.
pv.
1
aqui está o rbf para grandes conjuntos de dados: github.com/scipy/scipy/issues/5180
denfromufa