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

105

Ce Q&A est conçu comme un canonique (-ish) concernant l'interpolation bidimensionnelle (et multidimensionnelle) à l'aide de scipy. Il y a souvent des questions concernant la syntaxe de base de diverses méthodes d'interpolation multidimensionnelle, j'espère les redresser aussi.

J'ai un ensemble de points de données bidimensionnels dispersés, et je voudrais les tracer comme une belle surface, de préférence en utilisant quelque chose comme contourfou plot_surfacedans matplotlib.pyplot. Comment puis-je interpoler mes données bidimensionnelles ou multidimensionnelles dans un maillage à l'aide de scipy?

J'ai trouvé le scipy.interpolatesous-package, mais j'obtiens des erreurs lors de l'utilisation de interp2dou bisplrepou griddataou rbf. Quelle est la syntaxe appropriée de ces méthodes?

Andras Deak
la source

Réponses:

163

Avis de non-responsabilité: J'écris principalement cet article avec des considérations syntaxiques et un comportement général à l'esprit. Je ne suis pas familier avec l'aspect mémoire et CPU des méthodes décrites, et je vise cette réponse à ceux qui ont des ensembles de données raisonnablement petits, de sorte que la qualité de l'interpolation puisse être le principal aspect à considérer. Je suis conscient que lorsque vous travaillez avec de très grands ensembles de données, les méthodes les plus performantes (à savoir griddataet Rbf) peuvent ne pas être réalisables.

Je vais comparer trois types de méthodes d'interpolation multidimensionnelle ( interp2d/ splines griddataet Rbf). Je les soumet à deux types de tâches d'interpolation et à deux types de fonctions sous-jacentes (points à partir desquels il faut interpoler). 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 proche 1 ). Il est important de noter que chaque fois que vous utilisez l'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 vous retrouverez. 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 surface lisse et la fonction amicale: cos(pi*x)*sin(pi*y); gamme dans[-1, 1]
  2. une fonction mauvaise (et en particulier non continue): x*y/(x^2+y^2)avec une valeur de 0,5 près de l'origine; gamme dans[-0.5, 0.5]

Voici à quoi ils ressemblent:

fig1: fonctions de test

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

Données de test

Par souci d'explicitation, voici le code avec lequel j'ai généré les données d'entrée. Bien que dans ce cas précis, je connaisse évidemment 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 fluide et suréchantillonnage

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

fig2: suréchantillonnage en douceur

Même s'il s'agit d'une tâche simple, il existe déjà des différences subtiles entre les sorties. À première vue, les trois sorties sont raisonnables. Il y a deux caractéristiques à noter, basées sur notre connaissance préalable de la fonction sous-jacente: le cas du milieu de griddatadéforme le plus les données. Notez la y==-1limite du tracé (la plus proche de l' xétiquette): la fonction doit être strictement nulle (puisqu'il y==-1s'agit d'une ligne nodale pour la fonction lisse), mais ce n'est pas le cas pour griddata. Notez également la x==-1limite des tracés (derrière, à gauche): la fonction sous-jacente a un maximum local (impliquant un gradient nul près de la limite) à [-1, -0.5], mais la griddatasortie montre clairement un gradient non nul dans cette région. L'effet est subtil, mais c'est néanmoins un biais. (La fidélité deRbfest encore mieux avec le choix par défaut des fonctions radiales, doublées multiquadratic.)

Fonction maléfique et suréchantillonnage

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

fig3: suréchantillonnage maléfique

De nettes différences 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 griddataet Rbfsemblent produire des résultats similaires à première vue, ce dernier semble produire un minimum plus profond proche [0.4, -0.4]qui est absent de la fonction sous-jacente.

Cependant, il y a un aspect crucial dans lequel il Rbfest de loin supérieur: il respecte la symétrie de la fonction sous-jacente (qui est bien sûr également rendue possible par la symétrie du maillage de l'échantillon). La sortie de griddatarompt la symétrie des points d'échantillonnage, qui est déjà faiblement visible dans le cas lisse.

Fonction fluide 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 de manière pseudo-uniforme dans le domaine d'intérêt. Dans des scénarios réalistes, vous pouvez avoir du bruit supplémentaire avec chaque mesure et vous devez vous demander s'il est judicieux d'interpoler vos données brutes pour commencer.

Sortie pour la fonction lisse:

fig4: interpolation dispersée lisse

Maintenant, il y a déjà un peu de spectacle d'horreur en cours. J'ai coupé la sortie de interp2dà entre [-1, 1]exclusivement pour le 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 deuxième cas de griddatareproduit assez bien la forme, mais notez les régions blanches à la limite du tracé de contour. Cela est dû au fait que cela griddatane fonctionne qu'à l'intérieur de la coque convexe des points de données d'entrée (en d'autres termes, il n'effectue aucune extrapolation ). J'ai conservé la valeur NaN par défaut pour les points de sortie situés à l'extérieur de la coque convexe. 2 Compte tenu de ces caractéristiques, Rbfsemble fonctionner le mieux.

Fonction maléfique et données dispersées

Et le moment que nous attendions tous:

fig5: interpolation dispersée maléfique

Ce n'est pas une énorme surprise qui interp2dabandonne. En fait, pendant l'appel, interp2dvous devriez vous attendre à ce que des amis se RuntimeWarningplaignent de l'impossibilité de construire la spline. Quant aux deux autres méthodes, Rbfsemble produire le meilleur résultat, même près des frontières du domaine où le résultat est extrapolé.


Permettez-moi donc de dire quelques mots sur les trois méthodes, par ordre décroissant de préférence (afin que la pire soit la moins susceptible d'être lue par quiconque).

scipy.interpolate.Rbf

La Rbfclasse signifie "fonctions de base radiale". Pour être honnête, je n'ai jamais envisagé cette approche jusqu'à ce que j'ai commencé à faire des recherches pour cet article, mais je suis presque sûr que je les utiliserai à 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 Rbfinstance de classe appelable basée sur les données d'entrée, puis appelle cet objet pour un maillage de sortie donné pour obtenir le résultat interpolé. Exemple tiré du test de suréchantillonnage progressif:

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_rbfa la même forme que x_denseet y_densesans aucun effort. Notez également que Rbfprend en charge les dimensions arbitraires pour l'interpolation.

Alors, 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 des dimensions plus élevées
  • extrapole en dehors 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 y fier du tout)
  • crée un interpolateur dans un premier temps, donc l'évaluer dans divers points de sortie est moins d'effort supplémentaire
  • peut avoir des points de sortie de forme arbitraire (par opposition à être contraints à des maillages rectangulaires, voir plus loin)
  • enclin à préserver la symétrie des données d'entrée
  • prend en charge de multiples types de fonctions radiales pour le mot clé function: multiquadric, inverse, gaussian, linear, cubic, quintic, thin_plateet arbitraire définie par l' utilisateur

scipy.interpolate.griddata

Mon ancien favori, griddataest un bourreau de travail général pour l'interpolation dans des dimensions arbitraires. Il n'effectue pas d'extrapolation au-delà de la définition d'une seule valeur prédéfinie pour les points en dehors de la coque convexe des points nodaux, mais comme l'extrapolation est une chose très inconstante et dangereuse, ce n'est pas nécessairement un con. 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 formes [N, D]en Ddimensions. 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 le faire, mais elles semblent toutes volumineuses. Les zdonnées d' entrée 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 comme un tuple de tableaux multidimensionnels. Notez que le helpde griddataest trompeur, car il suggère que la même chose est vraie pour les points d' entrée (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 des dimensions plus élevées
  • n'effectue pas d'extrapolation, une seule valeur 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 à partir de 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 en 1d et 2d. Utilisation d'interpolation de voisin le plus proche et linéaire NearestNDInterpolatoret LinearNDInterpolatorsous le capot, respectivement. L'interpolation cubique 1d utilise une spline, l'interpolation cubique 2D utilise CloughTocher2DInterpolatorpour construire un interpolateur cubique par morceaux différentiable en continu.
  • pourrait violer la symétrie des données d'entrée

scipy.interpolate.interp2d/scipy.interpolate.bisplrep

La seule raison pour laquelle je parle interp2det ses proches est qu'il a un nom trompeur et que les gens essaieront probablement de l'utiliser. Alerte spoiler: ne l'utilisez pas (à partir de la version scipy 0.17.0). C'est déjà plus spécial que les sujets précédents en ce sens qu'il est spécifiquement utilisé pour l'interpolation bidimensionnelle, mais je soupçonne que c'est de loin le cas le plus courant pour l'interpolation multivariée.

En ce qui concerne la syntaxe, interp2dest similaire à Rbfen ce qu'elle doit 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 hic: les points de sortie doivent être situés sur un maillage rectangulaire, donc les entrées entrant dans l'appel à l'interpolateur doivent être des vecteurs 1d qui couvrent la grille de sortie, comme si 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 interp2dest de placer vos maillages 2d complets dans l'appel d'interpolation, ce qui conduit à une consommation de mémoire explosive et, espérons-le, à une précipitation MemoryError.

Maintenant, le plus gros problème avec interp2dest que cela ne fonctionne souvent pas. Pour comprendre cela, nous devons regarder sous le capot. Il s'avère que interp2dc'est un wrapper pour les fonctions de niveau inférieur bisplrep+ bisplev, qui sont à leur tour des 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 le problème interp2d: (dans la version scipy 0.17.0) il y a un joli commentaireinterpolate/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, bisplrepil 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 interp2d- jacentes ne sont pas vraiment destinées à effectuer une interpolation. Ils peuvent suffire pour des données suffisamment bien comportées, 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 bivariés (bien qu'il y ait des limites interpnpour les points d'entrée définis sur une grille)
  • effectue une extrapolation
  • crée un interpolateur dans un premier temps, donc l'évaluer dans divers points de sortie est moins d'effort supplémentaire
  • ne peut produire une sortie que sur une grille rectangulaire, pour une sortie dispersée, vous devrez 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

1 Je suis assez certain que le cubicet lineartype de fonctions de base de Rbfne correspondent pas exactement aux autres interpolateurs du même nom.
2 Ces NaN sont aussi la raison pour laquelle le tracé de la surface semble si étrange: matplotlib a historiquement des difficultés à tracer des objets 3D complexes avec des informations de profondeur. Les valeurs NaN dans les données confondent le moteur de rendu, de sorte que les parties de la surface qui devraient être à l'arrière sont tracées pour être à l'avant. Il s'agit d'un problème de visualisation et non d'interpolation.

Andras Deak
la source
2
Rbf peut consommer plus de mémoire que les griddata en fonction du nombre de points de données et du nombre de dimensions. Griddata a également l'objet sous-jacent LinearNDInterpolator qui peut être utilisé comme Rbf en 2 étapes.
denfromufa
1
L'interpolation cubique de Griddata est limitée à 2 dimensions (?). Pour des dimensions plus élevées, les grilles clairsemées de smolyak basées sur chebfun méritent d'être considérées.
denfromufa
1
laissez-moi terminer mes commentaires avec ce lien, où j'ai recherché toutes les options d'interpolation dispersées: scicomp.stackexchange.com/questions/19137/…
denfromufa
4
l'interpolation linéaire griddata est locale, l'interpolation cubique griddata est globale. L'extrapolation n'est pas prise en charge, car je n'ai pas eu le temps de comprendre comment préserver la continuité / la différentiabilité. Rbf convient pour les petits ensembles de données, mais pour interpoler n points de données, il faut inverser la matrice nxn, ce qui devient finalement impossible après n> 5000. Rbf peut également être sensible à la distribution des données et vous devrez peut-être affiner ses paramètres à la main. Il est possible de faire Rbf pour les grands ensembles de données, mais cela n'est pas implémenté dans scipy.
pv.
1
voici rbf pour les grands ensembles de données: github.com/scipy/scipy/issues/5180
denfromufa