import numpy as np
from numpy import polynomial as P
from scipy import interpolate
from scipy import linalg
import matplotlib as mpl
import matplotlib.pyplot as plt
Appendix F — Interpolation
\(~\)
Interpolation is a mathematical method for constructing a function from a discrete set of data points. To perform interpolation in python, we use the polynomial
module from numpy
and the interpolate
module from scipy
F.1 Importing modules
F.2 Interpolation
One-dimensional interpolation can be formulated as follows:
For a given set of \(n\) data point \(\{ (x_i, y_i) \}_{i=1}^n\), find a function \(f(x)\) such that \(f(x_i) = y_i\) for \(i \in [1, n]\). The function \(f(x)\) is known as the interpolant, and it is not unique
In fact, there are an infinite number of functions that satisfy the interpolation criteria. Typically we can write the interpolant as a linear combination of some basis functions \(\phi_j(x)\), such that
\[ f(x) = \sum_{j=1}^n c_j \phi_j(x) \]
where \(c_j\) are unknown coefficients
Substituting the given data points into this linear combination results in a linear equation system for the unknown coefficients: \(\sum_{j=1}^n c_j \phi_j(x_i) = y_i\)
\[ \begin{pmatrix} \phi_1(x_1) & \phi_2(x_1) & \cdots & \phi_n(x_1) \\ \phi_1(x_2) & \phi_2(x_2) & \cdots & \phi_n(x_2) \\ \vdots & \vdots & \ddots & \vdots\\ \phi_1(x_n) & \phi_2(x_n) & \cdots & \phi_n(x_n) \\ \end{pmatrix} \begin{pmatrix} c_1 \\ c_2 \\ \vdots \\c_n \end{pmatrix} = \begin{pmatrix} y_1 \\ y_2 \\ \vdots \\y_n \end{pmatrix} \]
or in a more compact implicit matrix form as \(\Phi(\mathbf{x}) \mathbf{c} = \mathbf{y}\), where the elements of the matrix \(\Phi(\mathbf{x})\) are \(\Phi(\mathbf{x})_{ij} = \phi_j(x_i)\)
Common choices of basis functions for interpolation are various types of polynomials, for example, the power basis \(\phi_j(x_i) = x_i^{j-1}\), or orthogonal polynomials such as Legendre polynomials \(\phi_j(x_i) = P_{j-1}(x_i)\), Chebyshev polynomials \(\phi_j(x_i) = T_{j-1}(x_i)\), or piecewise polynomials
Note that in general \(f(x)\) is not unique, but for \(n\) data points there is a unique interpolating polynomial of order \(n-1\), regardless of which polynomial basis we use. The structure of the matrix \(\Phi(\mathbf{x})\) is different for different polynomial bases, and its condition number and the computational cost of solving the interpolation problem varies correspondingly
F.3 Polynomials
The
numpy
library contains the submodulepolynomial
(here imported asP
), which provides functions and classes for working with polynomials. In particular, it provides implementations of many standard orthogonal polynomialsFor example, we can create a representation of the polynomial
\[1 +2x +3x^2\]
by passing the list
[1, 2, 3]
to thePolynomial
class:
= P.Polynomial([1,2,3])
p1 p1
\(x \mapsto \text{1.0} + \text{2.0}\,x + \text{3.0}\,x^{2}\)
- In the examples above, the first of the lists
coef
is the coefficient array and the second and third lists are thedomain
andwindow
attributes
p1.coef, p1.domain, p1.window
(array([1., 2., 3.]), array([-1, 1]), array([-1, 1]))
- Alternatively, we can also initialize a polynomial by specifying its roots using the class method
P.Polynomial.fromroots
. The polynomial with roots at \(x=-1\) and \(x=1\), for example, can be created using:
= P.Polynomial.fromroots([-1, 1])
p2 p2
\(x \mapsto \text{-1.0}\color{LightGray}{ + \text{0.0}\,x} + \text{1.0}\,x^{2}\)
- The roots of a polynomial can be computed using the
roots
method
p1.roots()
array([-0.33333333-0.47140452j, -0.33333333+0.47140452j])
p2.roots()
array([-1., 1.])
- To evaluate the polynomial
p1
at the points \(x=[1.5, 2.5, 3.5]\), we simply call thep1
class instance with a list of \(x\) values as this argument:
1.5, 2.5, 3.5])) p1(np.array([
array([10.75, 24.75, 44.75])
- Consider addition, subtraction, multiplication, and division of the polynomial \(p_1(x) = (x -3)(x -2)(x -1)\) with the polynomial \(p_2(x) = (x -2)\)
= P.Polynomial.fromroots([1, 2, 3])
p1 = P.Polynomial.fromroots([2]) p2
= p1 + p2
p3 p3
\(x \mapsto \text{-8.0} + \text{12.0}\,x - \text{6.0}\,x^{2} + \text{1.0}\,x^{3}\)
= p1 - p2
p4 p4
\(x \mapsto \text{-4.0} + \text{10.0}\,x - \text{6.0}\,x^{2} + \text{1.0}\,x^{3}\)
= p1 * p2
p5 p5
\(x \mapsto \text{12.0} - \text{28.0}\,x + \text{23.0}\,x^{2} - \text{8.0}\,x^{3} + \text{1.0}\,x^{4}\)
= p1 // p2
p6 p6
\(x \mapsto \text{3.0} - \text{4.0}\,x + \text{1.0}\,x^{2}\)
p6.roots()
array([1., 3.])
In addition to the
Polynomial
class for polynomials in the standard power basis, thepolynomial
module also has classes for representing polynomials in Chebyshev, Legendre, Laguerre and Hermite bases, with the namesChebyshev
,Legendre
,Laguerre
,Hermite
(Physicists’) andHermiteE
(Probabilists’), respectivelyFor example, the Chebyshev polynomial with coefficient list
[1, 2, 3]
, that is, the polynomial\[ T_0(x) +2T_1(x) +3T_2(x) \]
where \(T_i(x)\) is the Chebyshev polynomial of order \(i\), can be created using:
= P.Chebyshev([1, 2, 3])
c1 c1
\(x \mapsto \text{1.0}\,{T}_{0}(x) + \text{2.0}\,{T}_{1}(x) + \text{3.0}\,{T}_{2}(x)\)
c1.roots()
array([-0.76759188, 0.43425855])
= P.Chebyshev.fromroots([-1, 1])
c1 c1
\(x \mapsto \text{-0.5}\,{T}_{0}(x)\color{LightGray}{ + \text{0.0}\,{T}_{1}(x)} + \text{0.5}\,{T}_{2}(x)\)
= P.Legendre.fromroots([-1, 1])
l1 l1
\(x \mapsto \text{-0.6666666666666667}\,{P}_{0}(x)\color{LightGray}{ + \text{0.0}\,{P}_{1}(x)} + \text{0.6666666666666666}\,{P}_{2}(x)\)
-0.5, 0, 1])) c1(np.array([
array([-0.75, -1. , 0. ])
-0.5, 0, 1])) l1(np.array([
array([-0.75, -1. , 0. ])
F.4 Polynomial interpolation
To solve the interpolation problem, we need to first evaluate the matrix \(\Phi(\mathbf{x})\) for a given basis, and then solve the resulting linear equation system \(\Phi(\mathbf{x}) \mathbf{c} = \mathbf{y}\). Each of the polynomial classes in polynomial provides a function for computing the (generalized) Vandermonde matrix for the corresponding basis
For example, consider the data points \((1,1)\), \((2,3)\), \((3,5)\) and \((4,4)\)
= np.array([1, 2, 3, 4])
x = np.array([1, 3, 5, 4]) y
To interpolate a polynomial through these points, we need to use a polynomial of third degree (number of data points minus one)
For interpolation in the power basis, we seek the coefficients \(c_i\)’s such that
\[ f(x) = c_1 +c_2 x +c_3 x^2 +c_4 x^3 \]
and to find these coefficients, we evaluate the Vandermonde matrix and solve the interpolation equation system
= len(x) -1
deg = P.polynomial.polyvander(x, deg)
A A
array([[ 1., 1., 1., 1.],
[ 1., 2., 4., 8.],
[ 1., 3., 9., 27.],
[ 1., 4., 16., 64.]])
= linalg.solve(A, y)
c c
array([ 2. , -3.5, 3. , -0.5])
- The interpolation polynomial is thus \(f(x) = 2 -3.5x + 3x^2 -0.5x^3\)
= P.Polynomial(c) f1
2.5) f1(
4.1875
- To perform this polynomial interpolation in another polynomial basis, all that we need to change is the name of the function that was used to generate the Vandermonde matrix \(\mathbf{A}\) in the previous example
= P.chebyshev.chebvander(x, deg)
A A
array([[ 1., 1., 1., 1.],
[ 1., 2., 7., 26.],
[ 1., 3., 17., 99.],
[ 1., 4., 31., 244.]])
= linalg.solve(A, y)
c c
array([ 3.5 , -3.875, 1.5 , -0.125])
The interpolation polynomial in the Chebyshev basis is
\[ f(x) = 3.5 T_0(x) -3.875 T_1(x) +1.5 T_2(x) -0.125 T_3(x) \]
= P.Chebyshev(c) f2
2.5) f2(
4.1875
= np.linspace(x.min(), x.max(), 100)
xx
= plt.subplots(1, 1, figsize=(6, 4))
fig, ax
'b', lw=2, label='Power basis interp.')
ax.plot(xx, f1(xx), 'r--', lw=2, label='Chebyshev basis interp.')
ax.plot(xx, f2(xx), ='data points')
ax.scatter(x, y, label
=4)
ax.legend(loc='both', direction='in')
ax.tick_params(which0.5, 4.5)
ax.set_xlim(0, 6)
ax.set_ylim(
ax.set_xticks(x)r"$y$", fontsize=12)
ax.set_ylabel(r"$x$", fontsize=12)
ax.set_xlabel(
plt.show()
= P.Polynomial.fit(x, y, deg)
f1b f1b
\(x \mapsto \text{4.187500000000003} + \text{3.187499999999996}\,\left(\text{-1.6666666666666667} + \text{0.6666666666666666}x\right) - \text{1.687500000000004}\,\left(\text{-1.6666666666666667} + \text{0.6666666666666666}x\right)^{2} - \text{1.6874999999999973}\,\left(\text{-1.6666666666666667} + \text{0.6666666666666666}x\right)^{3}\)
= P.Chebyshev.fit(x, y, deg)
f2b f2b
\(x \mapsto \text{3.3437500000000004}\,{T}_{0}(\text{-1.6666666666666667} + \text{0.6666666666666666}x) + \text{1.9218749999999991}\,{T}_{1}(\text{-1.6666666666666667} + \text{0.6666666666666666}x) - \text{0.8437500000000021}\,{T}_{2}(\text{-1.6666666666666667} + \text{0.6666666666666666}x) - \text{0.42187499999999944}\,{T}_{3}(\text{-1.6666666666666667} + \text{0.6666666666666666}x)\)
\(~\)
- Mapping the interpolation data into the range that is most suitable for a specific basis can significantly improve the numerical stability of the interpolation. For example, using the Chebyshev basis with \(x\) values that are scaled such that \(x \in [-1, 1]\), rather than the original \(x\) values in the previous example, reduces the condition number from almost \(4660\) to about \(1.85\):
np.linalg.cond(P.chebyshev.chebvander(x, deg))
4659.738424139959
2*x -5)/3.0, deg)) np.linalg.cond(P.chebyshev.chebvander((
1.8542033440472896
- Note that with
fit
method, the domain attribute of the resulting instances are automatically set to the appropriate \(x\) values of the data points (in this example, the input range is[1, 4]
), and the coefficients are adjusted accordingly. To illustrate the problem, the values of the Chebyshev polynomials up to degree5
are plotted below
= np.linspace(-1, 1, 100)
x
= plt.figure(figsize=(5.5, 4))
fig for i in range(6):
= plt.plot(x, P.Chebyshev.basis(i)(x), lw=2,
ax =f'$T_{i}$')
label=(1.01, 1.0), loc=2)
plt.legend(bbox_to_anchor-1, 1)
plt.xlim(-1, 1)
plt.ylim( plt.show()
- In the range \(-1 \geq x \geq 1\), they are nice, equiripple functions. The same plots over the range \(-2 \geq x \geq 2\) look very different:
= np.linspace(-2, 2, 100)
x
= plt.figure(figsize=(5.5, 4))
fig for i in range(6):
= plt.plot(x, P.Chebyshev.basis(i)(x), lw=2,
ax =f'$T_{i}$')
label=(1.01, 1.0), loc=2)
plt.legend(bbox_to_anchor-2, 2)
plt.xlim(-400, 400)
plt.ylim( plt.show()
High-order polynomial interpolation can have undesirable behavior between the interpolation points. Although the interpolation is exact at the given data points, a high-order polynomial can vary wildly between the specified points
This is famously illustrated by polynomial interpolation of Runge’s function
\[f(x) = \frac{1}{1 + 25x^2}\]
using evenly spaced sample points in the interval \([-1, 1]\). The result is an interpolant that nearly diverges between the data points near the end of the interval
def runge(x):
return 1/(1 + 25*x**2)
def runge_interpolate(n):
= np.linspace(-1, 1, n +1)
x = P.Polynomial.fit(x, runge(x), deg=n)
p return x, p
= np.linspace(-1, 1, 250)
xx
= plt.subplots(1, 1, figsize=(6, 4))
fig, ax
'k', lw=2, label="Runge's function")
ax.plot(xx, runge(xx),
# 13th order interpolation of the Runge function
= 13
n = runge_interpolate(n)
x, p 'ro')
ax.plot(x, runge(x), 'r', label=f'interp. order {n}')
ax.plot(xx, p(xx),
# 14th order interpolation of the Runge function
= 14
n = runge_interpolate(n)
x, p 'go')
ax.plot(x, runge(x), 'g', label=f'interp. order {n}')
ax.plot(xx, p(xx),
=8)
ax.legend(loc-1.1, 1.1)
ax.set_xlim(-1, 2)
ax.set_ylim(-1, -0.5, 0, 0.5, 1])
ax.set_xticks([r"$x$", fontsize=12)
ax.set_xlabel(r"$y$", fontsize=12)
ax.set_ylabel(
='both', direction='in') ax.tick_params(which
F.5 Spline interpolation
For a set of \(n\) data points \(\{ x_i, y_i\}\), there are \(n-1\) subintervals \([x_i, x_{i+1}]\) in the full range of the data \([x_0, x_n]\). To interpolate the \(n\) data points using piecewise polynomials of degree \(k\) on each of the subintervals, we must determine \((k+1)(n-1)\) unknown parameters. The values at the knots give \(2(n-1)\) equations. Additional equations can be obtained by requiring also that derivatives and higher-order derivatives are continuous at the knots
A spline is a special type of piecewise polynomial interpolant: a piecewise polynomial of degree \(k\) is a spline if it is continuously differentiable \(k-1\) times. The most popular choice is the third-order spline, \(k=3\), which requires \(4(n-1)\) parameters
For this case, the continuity of two derivatives at the \(n-2\) knots gives \(2(n-2)\) additional equations, bringing the total number of equations to \(2(n-1) +2(n-2) = 4(n-1) -2\)
There are therefore two remaining undetermined parameters, which must be determined by other means. A common approach is to additionally require that the second order derivatives at the end points are zero (resulting in the natural spline). This gives two more equations, which closes the equation system
= np.linspace(-1, 1, 11)
x = np.linspace(-1, 1, 100)
xx = runge(x)
y = interpolate.interp1d(x, y, kind=3)
f_i
= plt.subplots(figsize=(6, 4))
fig, ax
'ro', label='Sample points')
ax.plot(x, y, 'k', lw=1, label="Runge's function")
ax.plot(xx, runge(xx), 'r--', lw=2, label='Spline order 3')
ax.plot(xx, f_i(xx),
ax.legend()
-1, 1)
ax.set_xlim(0, 1.2)
ax.set_ylim(-1, -0.5, 0, 0.5, 1])
ax.set_xticks([r"$x$", fontsize=12)
ax.set_xlabel(r"$y$", fontsize=12)
ax.set_ylabel(='both', direction='in')
ax.tick_params(which
plt.show()
To illustrate the effect of the order of a spline interpolation, consider the problem of interpolating the data
\[ (0,3), (1,4), (2,3.5), (3,2), (4,1), (5,1.5), (6,1.25) \text{ and } (7,0.7) \]
with splines of increasing order
= np.array([0, 1, 2, 3, 4, 5, 6, 7])
x = np.array([3, 4, 3.5, 2, 1, 1.5, 1.25, 0.7])
y
= np.linspace(x.min(), x.max(), 100)
xx
= plt.subplots(figsize=(6, 4))
fig, ax
ax.scatter(x, y)for n in [1, 2, 3, 7]:
= interpolate.interp1d(x, y, kind=n)
f ='order %d' % n)
ax.plot(xx, f(xx), label
ax.legend()r"$x$", fontsize=12)
ax.set_xlabel(r"$y$", fontsize=12)
ax.set_ylabel(
='both', direction='in') ax.tick_params(which
F.6 Multivariate interpolation
def f(x, y):
return (np.exp(-(x +0.5)**2 -2*(y +0.5)**2)
-np.exp(-(x -0.5)**2 -2*(y -0.5)**2))
= y = np.linspace(-2, 2, 20)
x = np.meshgrid(x, y)
X, Y
# simulate noisy data at fixed grid points X, Y
= f(X, Y) +0.01 *np.random.randn(*X.shape)
Z
= interpolate.RectBivariateSpline(x, y, Z)
f_i
= yy = np.linspace(x.min(), x.max(), 100)
xx = np.meshgrid(xx, yy)
XX, YY
= f_i(xx, yy) ZZi
= plt.subplots(2, 1, figsize=(6, 10), sharex=True)
fig, axes
= axes[0].contourf(XX, YY, f(XX, YY), 10, cmap=plt.cm.RdBu)
c 0].set_ylabel(r"$y$", fontsize=12)
axes[0].set_title("Exact/High sampling")
axes[= fig.colorbar(c, ax=axes[0])
cb r"$z$", fontsize=12)
cb.set_label(
= axes[1].contourf(XX, YY, ZZi, 10, cmap=plt.cm.RdBu)
c 1].set_ylim(-2, 2)
axes[1].set_xlim(-2, 2)
axes[1].set_xlabel(r"$x$", fontsize=12)
axes[1].set_ylabel(r"$y$", fontsize=12)
axes[1].set_title("Interpolation of noisy data/Low sampling")
axes[= fig.colorbar(c, ax=axes[1])
cb r"$z$", fontsize=12) cb.set_label(
For higher-dimensional problems, there is a function
interpolate.interpnd
, which is a generalization to \(n\)-dimensional problemsAnother typical situation that requires multivariate interpolation occurs when sampled data is given on an irregular coordinate grid. To be able to easily plot and analyze such data with existing tools, it may be desirable to interpolate it onto a regular coordinate grid. In
scipy
, we can use theinterpolate.griddata
for exactly this task
def f(x, y):
return np.exp(-x**2 -y**2) *np.cos(4*x) *np.sin(6*y)
= y = np.linspace(-1, 1, 100)
x = np.meshgrid(x, y)
X, Y = f(X, Y)
Z
115925231)
np.random.seed(= 500
N = np.random.uniform(-1, 1, N)
xdata = np.random.uniform(-1, 1, N)
ydata = f(xdata, ydata) zdata
= plt.subplots(figsize=(6, 5))
fig, ax
= ax.contourf(X, Y, Z, 15, cmap=plt.cm.RdBu);
c ='.')
ax.scatter(xdata, ydata, marker-1, 1)
ax.set_ylim(-1, 1)
ax.set_xlim(r"$x$", fontsize=12)
ax.set_xlabel(r"$y$", fontsize=12)
ax.set_ylabel(
= fig.colorbar(c, ax=ax)
cb r"$z$", fontsize=12) cb.set_label(
def z_interpolate(xdata, ydata, zdata):
= interpolate.griddata((xdata, ydata),
Zi_0 ='nearest')
zdata, (X, Y), method= interpolate.griddata((xdata, ydata),
Zi_1 ='linear')
zdata, (X, Y), method= interpolate.griddata((xdata, ydata),
Zi_3 ='cubic')
zdata, (X, Y), methodreturn Zi_0, Zi_1, Zi_3
= plt.subplots(3, 3, figsize=(6, 6),
fig, axes =True, sharey=True)
sharex
= [50, 150, 500]
n_vec
for idx, n in enumerate(n_vec):
= z_interpolate(xdata[:n],
Zi_0, Zi_1, Zi_3
ydata[:n],
zdata[:n])0].contourf(X, Y, Zi_0, 15, cmap=plt.cm.RdBu)
axes[idx, 0].set_ylabel(f'{n} data points\ny', fontsize=10)
axes[idx, 0].set_title("nearest", fontsize=10)
axes[idx, 1].contourf(X, Y, Zi_1, 15, cmap=plt.cm.RdBu)
axes[idx, 1].set_title("linear", fontsize=10)
axes[idx, 2].contourf(X, Y, Zi_3, 15, cmap=plt.cm.RdBu)
axes[idx, 2].set_title("cubic", fontsize=10)
axes[idx,
for m in range(len(n_vec)):
"x", fontsize=10) axes[idx, m].set_xlabel(