python - lagrange - ¿Cómo puedo realizar una interpolación bidimensional con scipy?
signal interpolation python (1)
Este Q&A está pensado como un canónico (-ish) con respecto a la interpolación bidimensional (y multidimensional) usando scipy. A menudo hay preguntas sobre la sintaxis básica de varios métodos de interpolación multidimensional, espero aclararlos también.
Tengo un conjunto de puntos de datos bidimensionales dispersos, y me gustaría
contourf
como una superficie agradable, preferiblemente usando algo como
contourf
o
plot_surface
en
matplotlib.pyplot
.
¿Cómo puedo interpolar mis datos bidimensionales o multidimensionales a una malla usando scipy?
Encontré el
scipy.interpolate
, pero sigo recibiendo errores cuando uso
interp2d
o
bisplrep
o
griddata
o
rbf
.
¿Cuál es la sintaxis adecuada de estos métodos?
Descargo de responsabilidad: principalmente escribo esta publicación con consideraciones sintácticas y comportamiento general en mente.
No estoy familiarizado con el aspecto de la memoria y la CPU de los métodos descritos, y apunto esta respuesta a aquellos que tienen conjuntos de datos razonablemente pequeños, de modo que la calidad de la interpolación puede ser el aspecto principal a considerar.
Soy consciente de que cuando se trabaja con conjuntos de datos muy grandes, los métodos con mejor rendimiento (a saber,
griddata
y
Rbf
) podrían no ser factibles.
Voy a comparar tres tipos de métodos de interpolación multidimensionales (
interp2d
/ splines,
griddata
y
Rbf
).
Los someteré a dos tipos de tareas de interpolación y dos tipos de funciones subyacentes (puntos desde los cuales se deben interpolar).
Los ejemplos específicos demostrarán la interpolación bidimensional, pero los métodos viables son aplicables en dimensiones arbitrarias.
Cada método proporciona varios tipos de interpolación;
en todos los casos usaré interpolación cúbica (o algo parecido a
1
).
Es importante tener en cuenta que cada vez que utiliza la interpolación introduce sesgo en comparación con sus datos sin procesar, y los métodos específicos utilizados afectan los artefactos con los que terminará.
Tenga siempre en cuenta esto e interpole de manera responsable.
Las dos tareas de interpolación serán
- muestreo ascendente (los datos de entrada están en una cuadrícula rectangular, los datos de salida están en una cuadrícula más densa)
- interpolación de datos dispersos en una cuadrícula regular
Las dos funciones (sobre el dominio
[x,y] in [-1,1]x[-1,1]
) serán
-
una función suave y amigable:
cos(pi*x)*sin(pi*y)
; rango en[-1, 1]
-
una función malvada (y en particular, no continua):
x*y/(x^2+y^2)
con un valor de 0.5 cerca del origen; rango en[-0.5, 0.5]
Así es como se ven:
Primero demostraré cómo se comportan los tres métodos en estas cuatro pruebas, luego detallaré la sintaxis de los tres.
Si sabe lo que debe esperar de un método, es posible que no quiera perder el tiempo aprendiendo su sintaxis (mirándolo,
interp2d
).
Datos de prueba
En aras de lo explícito, aquí está el código con el que generé los datos de entrada. Si bien en este caso específico, obviamente, conozco la función subyacente a los datos, solo la usaré para generar información para los métodos de interpolación. Utilizo numpy por conveniencia (y principalmente para generar los datos), pero solo scipy sería 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)
Función suave y muestreo superior
Comencemos con la tarea más fácil.
Así es como un muestreo ascendente de una malla de forma
[6,7]
a una de
[20,21]
funciona para la función de prueba suave:
Aunque esta es una tarea simple, ya existen diferencias sutiles entre los resultados.
A primera vista, las tres salidas son razonables.
Hay dos características a tener en cuenta, basadas en nuestro conocimiento previo de la función subyacente: el caso medio de
griddata
distorsiona más los datos.
Tenga en cuenta el límite
y==-1
del gráfico (más cercano a la etiqueta
x
): la función debe ser estrictamente cero (ya que
y==-1
es una línea nodal para la función suave), sin embargo, este no es el caso para los
griddata
de
griddata
.
También tenga en cuenta el límite
x==-1
de las parcelas (detrás, a la izquierda): la función subyacente tiene un máximo local (lo que implica un gradiente cero cerca del límite) en
[-1, -0.5]
, pero la salida de datos de
griddata
se muestra claramente gradiente distinto de cero en esta región.
El efecto es sutil, pero no obstante es un sesgo.
(La fidelidad de
Rbf
es aún mejor con la elección predeterminada de funciones radiales, denominada
multiquadratic
).
Mala función y muestreo ascendente
Una tarea un poco más difícil es realizar un muestreo superior de nuestra función malvada:
Se empiezan a mostrar claras diferencias entre los tres métodos.
Al observar los gráficos de superficie, aparecen claros extremos espurios en la salida de
interp2d
(tenga en cuenta las dos jorobas en el lado derecho de la superficie trazada).
Mientras que
griddata
y
Rbf
parecen producir resultados similares a primera vista, este último parece producir un mínimo más profundo cerca de
[0.4, -0.4]
que está ausente de la función subyacente.
Sin embargo, hay un aspecto crucial en el que
Rbf
es muy superior: respeta la simetría de la función subyacente (que por supuesto también es posible gracias a la simetría de la malla de muestra).
La salida de
griddata
rompe la simetría de los puntos de muestra, que ya es débilmente visible en el caso liso.
Función suave y datos dispersos.
En la mayoría de los casos, se desea realizar una interpolación en datos dispersos. Por esta razón, espero que estas pruebas sean más importantes. Como se muestra arriba, los puntos de muestra fueron elegidos pseudo-uniformemente en el dominio de interés. En escenarios realistas, es posible que tenga ruido adicional con cada medición, y debe considerar si tiene sentido interpolar sus datos sin procesar para comenzar.
Salida para la función suave:
Ahora ya hay un espectáculo de terror.
interp2d
la salida de
interp2d
entre
[-1, 1]
exclusivamente para el trazado, a fin de preservar al menos una cantidad mínima de información.
Está claro que si bien parte de la forma subyacente está presente, hay grandes regiones ruidosas donde el método se descompone por completo.
El segundo caso de
griddata
reproduce la forma bastante bien, pero tenga en cuenta las regiones blancas en el borde de la gráfica de contorno.
Esto se debe al hecho de que
griddata
solo funciona dentro del casco convexo de los puntos de datos de entrada (en otras palabras, no realiza ninguna
extrapolación
).
Mantuve el valor predeterminado de NaN para los puntos de salida que se encuentran fuera del casco convexo.
2
Considerando estas características,
Rbf
parece funcionar mejor.
Mala función y datos dispersos
Y el momento que todos hemos estado esperando:
No es una gran sorpresa que
interp2d
.
De hecho, durante la llamada a
interp2d
, debe esperar algunos
RuntimeWarning
amistosos quejándose de la imposibilidad de construir la spline.
En cuanto a los otros dos métodos,
Rbf
parece producir la mejor salida, incluso cerca de los límites del dominio donde se extrapola el resultado.
Entonces, permítanme decir algunas palabras sobre los tres métodos, en orden decreciente de preferencia (de modo que lo peor es lo menos probable que alguien lo lea).
scipy.interpolate.Rbf
La clase
Rbf
significa "funciones de base radial".
Para ser sincero, nunca he considerado este enfoque hasta que comencé a investigar para esta publicación, pero estoy bastante seguro de que los usaré en el futuro.
Al igual que los métodos basados en spline (ver más adelante), el uso viene en dos pasos: primero crea una instancia de clase
Rbf
invocable basada en los datos de entrada, y luego llama a este objeto para una malla de salida dada para obtener el resultado interpolado.
Ejemplo de la prueba de muestreo 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
Tenga en cuenta que los puntos de entrada y salida eran matrices 2d en este caso, y la salida
z_dense_smooth_rbf
tiene la misma forma que
x_dense
e
y_dense
sin ningún esfuerzo.
También tenga en cuenta que
Rbf
admite dimensiones arbitrarias para la interpolación.
Entonces,
scipy.interpolate.Rbf
- produce una salida con buen comportamiento incluso para datos de entrada locos
- admite interpolación en dimensiones superiores
- extrapola fuera del casco convexo de los puntos de entrada (por supuesto, la extrapolación siempre es una apuesta y, por lo general, no debe confiar en ella)
- crea un interpolador como primer paso, por lo que evaluarlo en varios puntos de salida es menos esfuerzo adicional
- puede tener puntos de salida de forma arbitraria (en lugar de estar limitado a mallas rectangulares, ver más adelante)
- propenso a preservar la simetría de los datos de entrada
-
admite múltiples tipos de funciones radiales para la función de palabras clave:
multiquadric
,inverse
,gaussian
,linear
,cubic
,quintic
,thin_plate
y arbitraria definida por el usuario
scipy.interpolate.griddata
Mi antiguo favorito,
griddata
, es un caballo de batalla general para la interpolación en dimensiones arbitrarias.
No realiza extrapolación más allá de establecer un único valor preestablecido para puntos fuera del casco convexo de los puntos nodales, pero dado que la extrapolación es algo muy voluble y peligroso, esto no es necesariamente una estafa.
Ejemplo 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
Tenga en cuenta la sintaxis ligeramente kludgy.
Los puntos de entrada deben especificarse en una matriz de formas
[N, D]
en dimensiones
D
Para esto, primero tenemos que aplanar nuestras matrices de coordenadas 2D (usando
ravel
), luego concatenar las matrices y transponer el resultado.
Hay varias formas de hacer esto, pero todas parecen ser voluminosas.
Los datos de entrada
z
también deben ser aplanados.
Tenemos un poco más de libertad en lo que respecta a los puntos de salida: por alguna razón, estos también se pueden especificar como una tupla de matrices multidimensionales.
Tenga en cuenta que la
help
de
griddata
es engañosa, ya que sugiere que lo mismo es cierto para los puntos de
entrada
(al menos para la versión 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.
En pocas palabras,
scipy.interpolate.griddata
- produce una salida con buen comportamiento incluso para datos de entrada locos
- admite interpolación en dimensiones superiores
-
no realiza extrapolación, se puede establecer un único valor para la salida fuera del casco convexo de los puntos de entrada (consulte el valor de
fill_value
) - calcula los valores interpolados en una sola llamada, por lo que sondear múltiples conjuntos de puntos de salida comienza desde cero
- puede tener puntos de salida de forma arbitraria
-
admite la interpolación lineal más cercana y vecina en dimensiones arbitrarias, cúbicas en 1d y 2d.
La interpolación más cercana y la interpolación lineal utilizan
NearestNDInterpolator
yLinearNDInterpolator
debajo del capó, respectivamente. La interpolación cúbica 1d usa una spline, la interpolación cúbica 2d usaCloughTocher2DInterpolator
para construir un interpolador cúbico por partes continuamente diferenciable. - podría violar la simetría de los datos de entrada
scipy.interpolate.interp2d
/
scipy.interpolate.bisplrep
La única razón por la que estoy discutiendo sobre
interp2d
y sus familiares es porque tiene un nombre engañoso y es probable que las personas intenten usarlo.
Alerta de spoiler: no lo use (a partir de la versión scipy 0.17.0).
Ya es más especial que los temas anteriores, ya que se usa específicamente para la interpolación bidimensional, pero sospecho que este es, con mucho, el caso más común para la interpolación multivariante.
En cuanto a la sintaxis,
interp2d
es similar a
Rbf
en que primero necesita construir una instancia de interpolación, a la que se pueda llamar para proporcionar los valores interpolados reales.
Sin embargo, hay un problema: los puntos de salida deben ubicarse en una malla rectangular, por lo que las entradas que entran en la llamada al interpolador deben ser vectores 1d que abarcan la cuadrícula de salida, como si
numpy.meshgrid
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
Uno de los errores más comunes al usar
interp2d
es poner sus mallas 2D completas en la llamada de interpolación, lo que conduce a un consumo explosivo de memoria y, con suerte, a un
MemoryError
apresurado.
Ahora, el mayor problema con
interp2d
es que a menudo no funciona.
Para entender esto, tenemos que mirar debajo del capó.
Resulta que
interp2d
es un contenedor para las funciones de nivel inferior
bisplrep
+
bisplev
, que a su vez son contenedores para las rutinas FITPACK (escritas en Fortran).
La llamada equivalente al ejemplo anterior sería
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
Ahora, esto es lo que pasa con
interp2d
: (en la versión scipy 0.17.0) hay un buen
comentario en
interpolate/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)
y de hecho en
interpolate/fitpack.py
, en
bisplrep
hay alguna configuración y 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)
Y eso es.
Las rutinas subyacentes a
interp2d
no están realmente destinadas a realizar interpolación.
Pueden ser suficientes para obtener datos suficientemente bien comportados, pero en circunstancias realistas probablemente querrá usar otra cosa.
Solo para concluir,
interpolate.interp2d
- puede conducir a artefactos incluso con datos moderados
-
es específicamente para problemas bivariados (aunque existe una
interpn
limitada para los puntos de entrada definidos en una cuadrícula) - realiza extrapolación
- crea un interpolador como primer paso, por lo que evaluarlo en varios puntos de salida es menos esfuerzo adicional
- solo puede producir una salida en una cuadrícula rectangular, para una salida dispersa debería llamar al interpolador en un bucle
- admite interpolación lineal, cúbica y quíntica
- podría violar la simetría de los datos de entrada
1
Estoy bastante seguro de que el tipo
cubic
y
linear
de funciones
Rbf
de
Rbf
no se corresponden exactamente con los otros interpoladores del mismo nombre.
2
Estos NaN también son la razón por la cual el diagrama de superficie parece tan extraño: matplotlib históricamente tiene dificultades para trazar objetos 3D complejos con información de profundidad adecuada.
Los valores de NaN en los datos confunden al renderizador, por lo que las partes de la superficie que deberían estar en el reverso se trazan para estar en el frente.
Este es un problema con la visualización, y no con la interpolación.