web-dev-qa-db-fra.com

Comment puis-je effectuer une interpolation bidimensionnelle à l'aide de scipy?

Ce Q & A est conçu comme un canonique (-ish) concernant l’interpolation bidimensionnelle (et multidimensionnelle) à l’aide de scipy. Il existe souvent des questions concernant la syntaxe de base de diverses méthodes d’interpolation multidimensionnelle. .

J'ai un ensemble de points de données bidimensionnels dispersés, et j'aimerais les tracer comme une surface de Nice, en utilisant de préférence quelque chose comme contourf ou plot_surface dans matplotlib.pyplot. Comment interpoler mes données bidimensionnelles ou multidimensionnelles sur un maillage à l'aide de scipy?

J'ai trouvé le scipy.interpolate sous-paquet, mais je continue à avoir des erreurs en utilisant interp2d ou bisplrep ou griddata ou rbf. Quelle est la syntaxe appropriée de ces méthodes?

98
Andras Deak

Disclaimer: J'écris principalement ce billet avec des considérations syntaxiques et un comportement général à l'esprit. Je ne connais pas les aspects de la mémoire et du processeur des méthodes décrites, et je vise cette réponse uniquement pour ceux qui disposent d'un ensemble de données raisonnablement réduit, de sorte que la qualité de l'interpolation puisse être l'aspect principal à prendre en compte. Je suis conscient que, lorsque vous travaillez avec de très grands ensembles de données, les méthodes les plus performantes (notamment griddata et Rbf) risquent de ne pas être réalisables.

Je vais comparer trois types de méthodes d’interpolation multidimensionnelle ( interp2d /splines, griddata et Rbf ). Je vais les soumettre à deux types de tâches d’interpolation et à deux types de fonctions sous-jacentes (points à partir desquels des points doivent être interpolés). Les exemples spécifiques démontreront une interpolation bidimensionnelle, mais les méthodes viables sont applicables dans des dimensions arbitraires. Chaque méthode fournit différents types d'interpolation; dans tous les cas, j'utiliserai une interpolation cubique (ou quelque chose de proche1). Il est important de noter que chaque fois que vous utilisez une interpolation, vous introduisez un biais par rapport à vos données brutes, et les méthodes spécifiques utilisées affectent les artefacts avec lesquels vous allez vous retrouver. Soyez toujours conscient de cela et interpolez de manière responsable.

Les deux tâches d’interpolation seront

  1. suréchantillonnage (les données d'entrée sont sur une grille rectangulaire, les données de sortie sont sur une grille plus dense)
  2. interpolation de données dispersées sur une grille régulière

Les deux fonctions (sur le domaine [x,y] in [-1,1]x[-1,1]) Seront

  1. une fonction douce et conviviale: cos(pi*x)*sin(pi*y); plage dans [-1, 1]
  2. une fonction maléfique (et en particulier non continue): x*y/(x^2+y^2) avec une valeur de 0,5 proche de l'origine; plage dans [-0.5, 0.5]

Voici à quoi ils ressemblent:

fig1: test functions

Je vais d'abord démontrer comment les trois méthodes se comportent sous ces quatre tests, puis je détaillerai la syntaxe des trois. Si vous savez ce que vous devez attendre d'une méthode, vous ne voudrez peut-être pas perdre votre temps à apprendre sa syntaxe (vous regarder, interp2d).

Données de test

Par souci de clarté, voici le code avec lequel j'ai généré les données d'entrée. Bien que dans ce cas particulier, je suis évidemment conscient de la fonction sous-jacente aux données, je ne l'utiliserai que pour générer des entrées pour les méthodes d'interpolation. J'utilise numpy pour plus de commodité (et surtout pour générer les données), mais scipy seul suffirait aussi.

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)

Fonction lisse et suréchantillonnage

Commençons par la tâche la plus facile. Voici comment un suréchantillonnage d'un maillage de forme [6,7] Vers l'un des [20,21] Fonctionne pour la fonction de test de lissage:

fig2: smooth upsampling

Même s'il s'agit d'une tâche simple, il existe déjà des différences subtiles entre les résultats. À première vue, les trois résultats sont raisonnables. Il existe deux caractéristiques à noter, basées sur notre connaissance préalable de la fonction sous-jacente: le cas moyen de griddata déforme le plus les données. Notez la limite y==-1 Du tracé (la plus proche de l'étiquette x): la fonction doit être strictement égale à zéro (puisque y==-1 Est une ligne nodale pour la fonction smooth), n'est pas le cas pour griddata. Notez également la limite x==-1 Des tracés (en arrière, à gauche): la fonction sous-jacente a un maximum local (impliquant un gradient nul près de la limite) à [-1, -0.5], Mais le griddata _ la sortie montre clairement un gradient non nul dans cette région. L'effet est subtil, mais c'est néanmoins un parti pris. (La fidélité de Rbf est encore meilleure avec le choix par défaut de fonctions radiales, surnommé multiquadratic.)

Mauvais fonctionnement et suréchantillonnage

Une tâche un peu plus difficile consiste à surévaluer notre fonction perverse:

fig3: evil upsampling

Des différences claires commencent à apparaître entre les trois méthodes. En regardant les tracés de surface, il y a des extrema parasites clairs apparaissant dans la sortie de interp2d (Notez les deux bosses sur le côté droit de la surface tracée). Alors que griddata et Rbf semblent produire des résultats similaires au premier abord, ce dernier semble produire un minimum plus profond près de [0.4, -0.4] Qui est absent de la fonction sous-jacente.

Cependant, il existe un aspect crucial pour lequel Rbf est de loin supérieur: il respecte la symétrie de la fonction sous-jacente (ce qui est bien entendu également possible grâce à la symétrie du maillage de l’échantillon). La sortie de griddata rompt la symétrie des points d’échantillon, qui est déjà faiblement visible dans le cas lisse.

Fonction lisse et données dispersées

Le plus souvent, on souhaite effectuer une interpolation sur des données dispersées. Pour cette raison, je m'attends à ce que ces tests soient plus importants. Comme indiqué ci-dessus, les points d'échantillonnage ont été choisis pseudo-uniformément dans le domaine d'intérêt. Dans des scénarios réalistes, chaque mesure peut générer du bruit supplémentaire et vous devez déterminer s'il est judicieux d'interpoler vos données brutes.

Sortie pour la fonction de lissage:

fig4: smooth scattered interpolation

Maintenant, il y a déjà un petit spectacle d'horreur en cours. J'ai coupé la sortie de interp2d À [-1, 1] Exclusivement à des fins de traçage, afin de conserver au moins une quantité minimale d'informations. Il est clair que, si une partie de la forme sous-jacente est présente, il existe d’énormes régions bruyantes où la méthode s’effondre complètement. Le second cas de griddata reproduit assez bien la forme, mais notez les régions blanches situées à la frontière du tracé du contour. Ceci est dû au fait que griddata ne fonctionne que dans la coque convexe des points de données en entrée (en d’autres termes, il n’effectue aucune extrapolation ). J'ai conservé la valeur par défaut de NaN pour les points de sortie situés à l'extérieur de la coque convexe.2 Compte tenu de ces caractéristiques, Rbf semble fonctionner mieux.

Fonction diabolique et données dispersées

Et au moment que nous attendions tous:

fig5: evil scattered interpolation

Ce n'est pas une grosse surprise que interp2d Abandonne. En fait, lors de l'appel à interp2d, Vous devez vous attendre à ce que certains RuntimeWarning amis se plaignent de l'impossibilité de construire la spline. Comme pour les deux autres méthodes, Rbf semble produire la meilleure sortie, même près des limites du domaine où le résultat est extrapolé.


Permettez-moi donc de dire quelques mots sur les trois méthodes, par ordre de préférence décroissant (de sorte que la pire est celle qui a le moins de chances d'être lue par quiconque).

scipy.interpolate.Rbf

La classe Rbf signifie "fonctions de base radiales". Pour être honnête, je n'avais jamais envisagé cette approche avant de commencer à rechercher ce poste, mais je suis persuadé que je l'aurai à l'avenir.

Tout comme les méthodes basées sur les splines (voir plus loin), l’utilisation se fait en deux étapes: la première crée une instance appelable Rbf classe basée sur les données d’entrée, puis appelle cet objet pour un maillage de sortie donné afin d’obtenir la résultat interpolé. Exemple tiré du test de suréchantillonnage régulier:

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

Notez que les points d'entrée et de sortie étaient des tableaux 2d dans ce cas, et la sortie z_dense_smooth_rbf A la même forme que x_dense Et y_dense Sans aucun effort. Notez également que Rbf prend en charge les dimensions arbitraires pour l’interpolation.

Donc, scipy.interpolate.Rbf

  • produit une sortie bien comportée même pour des données d'entrée folles
  • prend en charge l'interpolation dans les dimensions supérieures
  • extrapoler à l'extérieur de la coque convexe des points d'entrée (bien sûr, l'extrapolation est toujours un pari, et vous ne devriez généralement pas vous en fier du tout)
  • crée un interpolateur dans un premier temps, de sorte que son évaluation en divers points de sortie demande moins d'effort supplémentaire
  • peut avoir des points de sortie de forme arbitraire (par opposition à être contraint à des mailles rectangulaires, voir plus loin)
  • enclin à préserver la symétrie des données d'entrée
  • prend en charge plusieurs types de fonctions radiales pour le mot clé function: multiquadric, inverse, gaussian, linear, cubic, quintic, thin_plate et arbitraire défini par l'utilisateur

scipy.interpolate.griddata

Mon ancien favori, griddata, est un bourreau de travail général pour l’interpolation dans des dimensions arbitraires. L'extrapolation ne consiste pas à extrapoler au-delà de la définition d'une seule valeur prédéfinie pour les points extérieurs à la coque convexe des points nodaux, mais comme l'extrapolation est une chose très instable et dangereuse, ce n'est pas nécessairement un inconvénient. Exemple d'utilisation:

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

Notez la syntaxe légèrement kludgy. Les points d'entrée doivent être spécifiés dans un tableau de forme [N, D] Dans D dimensions. Pour cela, nous devons d’abord aplatir nos tableaux de coordonnées 2D (en utilisant ravel), puis concaténer les tableaux et transposer le résultat. Il existe plusieurs façons de procéder, mais elles semblent toutes être volumineuses. Les données d'entrée z doivent également être aplaties. Nous avons un peu plus de liberté en ce qui concerne les points de sortie: pour une raison quelconque, ceux-ci peuvent également être spécifiés en tant que tuple de tableaux multidimensionnels. Notez que le help de griddata est trompeur, car il suggère qu'il en va de même pour l'entrée points ( au moins pour la version 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 un mot, scipy.interpolate.griddata

  • produit une sortie bien comportée même pour des données d'entrée folles
  • prend en charge l'interpolation dans les dimensions supérieures
  • ne réalise pas d'extrapolation, une valeur unique peut être définie pour la sortie en dehors de la coque convexe des points d'entrée (voir fill_value)
  • calcule les valeurs interpolées en un seul appel, de sorte que l'analyse de plusieurs ensembles de points de sortie commence à zéro
  • peut avoir des points de sortie de forme arbitraire
  • prend en charge l’interpolation linéaire et le plus proche voisin dans des dimensions arbitraires, cubique dans 1d et 2d. L'interpolation linéaire la plus proche et le plus proche utilise NearestNDInterpolator et LinearNDInterpolator sous le capot, respectivement. L'interpolation cubique 1d utilise une spline, l'interpolation cubique 2d utilise CloughTocher2DInterpolator Pour construire un interpolateur cubique par morceaux continuellement différentiable.
  • pourrait violer la symétrie des données d'entrée

scipy.interpolate.interp2d/scipy.interpolate.bisplrep

La seule raison pour laquelle je parle de interp2d Et de ses proches, c'est qu'il a un nom trompeur et que les gens vont probablement essayer de l'utiliser. Alerte spoiler: ne l'utilisez pas (à partir de la version 0.17.0 de scipy). C'est déjà plus spécial que les sujets précédents dans la mesure où il est spécifiquement utilisé pour l'interpolation bidimensionnelle, mais je suppose que c'est de loin le cas le plus courant pour l'interpolation multivariée.

En ce qui concerne la syntaxe, interp2d Est similaire à Rbf en ce sens qu'il faut tout d'abord construire une instance d'interpolation, qui peut être appelée pour fournir les valeurs interpolées réelles. Il y a cependant un inconvénient: les points de sortie doivent être situés sur un maillage rectangulaire, de sorte que les entrées entrant dans l'appel de l'interpolateur doivent être des vecteurs 1d qui couvrent la grille de sortie, comme si elle venait 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

L'une des erreurs les plus courantes lors de l'utilisation de interp2d Est de mettre vos maillages 2D complets dans l'appel d'interpolation, ce qui entraîne une consommation de mémoire explosive et, espérons-le, un MemoryError hâtif.

Maintenant, le plus gros problème avec interp2d Est que cela ne fonctionne souvent pas. Pour comprendre cela, nous devons regarder sous le capot. Il s’avère que interp2d Est un wrapper pour les fonctions de niveau inférieur bisplrep + bisplev , qui sont dans tourne les wrappers pour les routines FITPACK (écrites en Fortran). L’appel équivalent à l’exemple précédent serait

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

Maintenant, voici la chose à propos de interp2d: (Dans la version 0.17.0 de Scipy) il y a un Nice commentaire dans interpolate/interpolate.py pour 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)

et en effet dans interpolate/fitpack.py, dans bisplrep il y a une configuration et finalement

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)                 

Et c'est tout. Les routines sous-jacentes à interp2d Ne sont pas vraiment conçues pour effectuer une interpolation. Ils pourraient suffire à obtenir des données suffisamment fiables, mais dans des circonstances réalistes, vous voudrez probablement utiliser autre chose.

Juste pour conclure, interpolate.interp2d

  • peut conduire à des artefacts même avec des données bien tempérées
  • est spécifiquement pour les problèmes à deux variables (bien qu'il y ait le interpn limité pour les points d'entrée définis sur une grille)
  • effectue une extrapolation
  • crée un interpolateur dans un premier temps, de sorte que son évaluation en divers points de sortie demande moins d'effort supplémentaire
  • ne peut produire que la sortie sur une grille rectangulaire, pour une sortie dispersée, vous devez appeler l'interpolateur dans une boucle
  • prend en charge l'interpolation linéaire, cubique et quintique
  • pourrait violer la symétrie des données d'entrée

1Je suis à peu près certain que les fonctions cubic et linear de base de Rbf ne correspondent pas exactement aux autres interpolateurs du même nom.
2Ces NaN sont également la raison pour laquelle le tracé de surface semble si étrange: matplotlib a toujours eu du mal à tracer des objets 3D complexes avec des informations de profondeur appropriées. Les valeurs NaN dans les données confondent le rendu, de sorte que les parties de la surface qui devraient être à l'arrière sont tracées pour être à l'avant. C'est un problème de visualisation, et non d'interpolation.

149
Andras Deak