Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Approximation (curve fitting)

Faculty of Mechanical Engineering, University of Ljubljana

Introduction

In engineering practice we often encounter a table of data that may be affected by measurement or numerical errors.

Let us look at an example of measuring a (linear) spring (xx is the displacement, yy is the measured force):

Let us look at the data in a figure; first we import the required packages:

<Figure size 640x480 with 1 Axes>

For this particular example, given our knowledge of the physical background of a linear spring, it would be appropriate to try to describe the measurements with a linear function:

f(x)=a0 x+a1f(x) = a_0\,x+ a_1

We know a table of nn data points xi,yix_i, y_i for i=0,1,…,n−1i=0,1,\dots,n-1; there are more of them than we need to determine the two constants a0a_0 and a1a_1, so we have an overdetermined system of linear equations:

yi=a0 xi+a1zai=0,1,…,n−1.y_i=a_0\,x_i+a_1\quad\textrm{za}\quad i=0,1,\dots,n-1.

We are looking for values of the constants a0a_0 and a1a_1 such that the function f(x)f(x) best matches yiy_i at the known points xix_i.

So first we need a criterion for the best match.

For the values xi,yix_i, y_i from the table we could look for values of a0a_0 and a1a_1 at which the sum of the absolute values of the deviations PP is the smallest:

P(a0,a1)=∑i=0n−1∣yi−(a0 xi+a1)∣.P(a_0, a_1) = \sum_{i=0}^{n-1} |y_i - (a_0\,x_i+a_1)|.

However, since such a function P(a0,a1)P(a_0, a_1) is not continuously differentiable, we prefer to use the least-squares method:

S(a0,a1)=∑i=0n−1(yi−(a0 xi+a1))2.S(a_0, a_1) = \sum_{i=0}^{n-1} \left(y_i - (a_0\,x_i+a_1)\right)^2.

Such a function S(a0,a1)S(a_0, a_1) is continuous and continuously differentiable. By taking partial derivatives with respect to the parameters a0a_0 and a1a_1 we can find a stationary point (the partial derivatives are equal to 0). We will go through the procedure for a linear function in the next section.

Least-squares method for a linear function

Loading...

We need to find the constants a0a_0, a1a_1 such that the sum of the squares of the differences between the function and the tabulated value (xi,yix_i, y_i, where i=0,1,…,n−1i=0,1,\dots, n-1 and nn is the number of tabulated data points):

S(a0,a1)=∑i=0n−1(yi−(a0 xi+a1))2S(a_0, a_1) = \sum_{i=0}^{n-1} \left(y_i - (a_0\,x_i+a_1)\right)^2

is the smallest. The value will be smallest at the stationary point, which we determine by taking the partial derivative with respect to the parameters a0a_0 and a1a_1.

First we take the partial derivative with respect to the parameter a0a_0:

∂S(a0,a1)∂a0=2 ∑i=0n−1(yi−a0 xi−a1) (−xi)\frac{\partial S(a_0, a_1)}{\partial a_0} = 2\,\sum_{i=0}^{n-1} \left(y_i - a_0\,x_i-a_1\right)\,(-x_i)

We rearrange the expression:

∂S(a0,a1)∂a0=−2(∑i=0n−1yi xi−a0 ∑i=0n−1xi2−a1 ∑i=0n−1xi)\frac{\partial S(a_0, a_1)}{\partial a_0} =-2\left(\sum_{i=0}^{n-1} y_i\,x_i -a_0\,\sum_{i=0}^{n-1} x_i^2- a_1\,\sum_{i=0}^{n-1} x_i\right)

We proceed similarly for a1a_1:

∂S(a0,a1)∂a1=2 ∑i=0n−1(yi−a0 xi−a1) (−1)\frac{\partial S(a_0, a_1)}{\partial a_1} = 2\,\sum_{i=0}^{n-1} \left(y_i - a_0\,x_i- a_1\right)\,(-1)
∂S(a0,a1)∂a1=−2(∑i=0n−1yi−a0 ∑i=0n−1xi−a1 ∑i=0n−11)\frac{\partial S(a_0, a_1)}{\partial a_1} = -2\left(\sum_{i=0}^{n-1} y_i -a_0\,\sum_{i=0}^{n-1} x_i- a_1\,\sum_{i=0}^{n-1}1\right)

Since at the stationary point ∂S(a0,a1)/∂a0=0\partial S(a_0, a_1)/\partial a_0=0 and ∂S(a0,a1)/∂a1=0\partial S(a_0, a_1)/\partial a_1=0, from the expressions above we derive:

a0 ∑i=0n−1xi2+a1 ∑i=0n−1xi=∑i=0n−1yi xia_0\,\sum_{i=0}^{n-1} x_i^2 + a_1\,\sum_{i=0}^{n-1} x_i=\sum_{i=0}^{n-1} y_i\,x_i

and

a0 ∑i=0n−1xi+a1 n=∑i=0n−1yi.a_0\,\sum_{i=0}^{n-1} x_i+ a_1\,n=\sum_{i=0}^{n-1} y_i.

We have obtained a system of two linear equations for the unknowns a0a_0 and a1a_1, which we know how to solve. We call it the normal system (the number of equations equals the number of unknowns).

Let us write the normal system in matrix form:

A: [[30.8725 10.35  ]
 [10.35    5.    ]]
b: [764.7085 243.92  ]

Now we need to solve the linear system:

A a=b,\mathbf{A}\,\mathbf{a}=\mathbf{b},

Note: here we have written the vector of unknowns as a=(a0,a1)\mathbf{a}=(a_0, a_1).

We solve the system:

(np.float64(27.497258679085512), np.float64(-8.135325465707014))

Let us also check the condition number:

np.float64(25.20071350870242)

Now we will look at the result. First we prepare a figure that will also contain information about the sum of squares of the deviation of f(xi)f(x_i) from the tabulated values yiy_i.

Loading...

Using the pseudo-inverse matrix

We can reach the same result using the pseudo-inverse matrix. We look for y(x)=a0 x+a1y(x)=a_0\,x+a_1 and set up the overdetermined system A a=y\mathbf{A}\,\mathbf{a}=\mathbf{y}, where the coefficient matrix A\mathbf{A} is defined according to the values xix_i (i=0,1,…,n−1i=0,1,\dots,n-1):

array([[0.1 , 1. ], [1.1 , 1. ], [2.05, 1. ], [3.2 , 1. ], [3.9 , 1. ]])

We have denoted the vector of constants by b\mathbf{b}, but in our case this is simply the vector of values y\mathbf{y} with elements yiy_i (i=0,1,2,…,n−1i=0,1,2,\dots,n-1):

array([ 0.22, 18.15, 44.33, 75.59, 105.63])

Let us check the rank of the coefficient matrix:

np.int64(2)

And also the rank of the augmented matrix:

array([[1.0000e-01, 1.0000e+00, 2.2000e-01], [1.1000e+00, 1.0000e+00, 1.8150e+01], [2.0500e+00, 1.0000e+00, 4.4330e+01], [3.2000e+00, 1.0000e+00, 7.5590e+01], [3.9000e+00, 1.0000e+00, 1.0563e+02]])
np.int64(3)

Since we are solving a system of mm linear equations with nn unknowns, and m>nm>n holds and the rank of the augmented matrix is n+1n+1, we have an overdetermined system.

We determine the vector of unknown parameters a\mathbf{a} using the pseudo-inverse matrix:

a=A+ y\mathbf{a}=\mathbf{A}^{+}\,\mathbf{y}
array([27.49725868, -8.13532547])

Least-squares method for an arbitrary polynomial

We will generalize the linear approximation presented above to an arbitrary polynomial of degree mm:

f(a0,a1,…,am,x)=∑s=0mas xm−s⏟fs(x),f(a_0,a_1,\dots,a_{m}, x) = \sum_{s=0}^{m}a_s\,\underbrace{x^{m-s}}_{f_s(x)},

where fs(x)=xm−sf_s(x)=x^{m-s} is called a basis function (s=0,1,2,…,ms=0,1,2,\dots,m).

Let the data table be defined by xi,yix_i, y_i, where i=0,1,2,…,n−1i=0,1,2,\dots,n-1.

Note: for compactness of notation we will write the constants aa in vector form a=[a0,a1,…,am]\mathbf{a}=[a_0, a_1,\dots,a_m].

We use the least-squares method:

S(a)=∑i=0n−1(yi−f(a,xi))2=∑i=0n−1(yi−∑s=0mas xim−s)2.S(\mathbf{a}) = \sum_{i=0}^{n-1} \left(y_i - f(\mathbf{a}, x_i)\right)^2= \sum_{i=0}^{n-1} \left(y_i - \sum_{s=0}^{m}a_s\,x_i^{m-s}\right)^2.

The necessary condition for an extremum of a function of m+1m+1 independent variables is that we find a stationary point for each ava_v, so we look for ∂S(a)/∂av=0\partial S(\mathbf{a})/\partial a_v=0 (instead of ss we have used the index vv).

First we determine the partial derivative for the chosen ava_v:

∂S(a)∂av=∑i=0n−1−2 (yi−∑s=0mas xim−s) xim−v\frac{\partial S(\mathbf{a})}{\partial a_v} = \sum_{i=0}^{n-1} - 2\,\left(y_i - \sum_{s=0}^{m}a_s\,x_i^{m-s}\right)\,x_i^{m-v}

Note: ∂∂av(∑s=0mas xim−s)=xim−v\frac{\partial}{\partial a_v}\left(\sum_{s=0}^{m}a_s\,x_i^{m-s}\right)=x_i^{m-v}.

Since the partial derivative at the stationary point is equal to 0, we rearrange the expression above:

∑i=0n−1(∑s=0mas xim−s) xim−v=∑i=0n−1yi xim−v\sum_{i=0}^{n-1} \left(\sum_{s=0}^{m}a_s\,x_i^{m-s}\right)\,x_i^{m-v}=\sum_{i=0}^{n-1} y_i\,x_i^{m-v}

We rearrange the expression:

∑i=0n−1∑s=0mas xi2m−s−v=∑i=0n−1yi xim−v\sum_{i=0}^{n-1} \sum_{s=0}^m a_s\,x_i^{2m-s-v}=\sum_{i=0}^{n-1} y_i\,x_i^{m-v}

We swap the order of summation and derive:

∑s=0m(as∑i=0n−1 xi2m−s−v)=∑i=0n−1yi xim−vza:v=0,1,…,m\sum_{s=0}^m \left(a_s \sum_{i=0}^{n-1} \,x_i^{2m-s-v}\right)=\sum_{i=0}^{n-1} y_i\,x_i^{m-v}\quad\textrm{za:}\quad v=0,1,\dots,m

We have derived equation vv of the system of m+1m+1 linear equations:

A a=b\mathbf{A}\,{\mathbf{a}}=\mathbf{b}

The element Av,sA_{v,s} of the coefficient matrix is:

Av,s=∑i=0n−1xi2m−v−s,A_{v,s}= \sum_{i=0}^{n-1} x_i^{2m-v-s},

The element of the vector of constants is:

bv=∑i=0n−1yi xim−vb_{v}= \sum_{i=0}^{n-1} y_i\,x_i^{m-v}

Numerical example

Let us use the data from the first problem and try to approximate it with a 2nd-degree polynomial (m=2m=2).

The data table is:

array([0.1 , 1.1 , 2.05, 3.2 , 3.9 ])
array([ 0.22, 18.15, 44.33, 75.59, 105.63])

Let us compute the coefficient matrix:

Av,s=∑i=0n−1xi2m−v−sA_{v,s}= \sum_{i=0}^{n-1} x_i^{2m-v-s}
array([[355.32690625, 102.034125 , 30.8725 ], [102.034125 , 30.8725 , 10.35 ], [ 30.8725 , 10.35 , 5. ]])

Let us also compute the vector of constants:

bv=∑i=0n−1yi xim−vb_{v}= \sum_{i=0}^{n-1} y_i\,x_i^{m-v}
array([2588.934425, 764.7085 , 243.92 ])

Let us check the condition number:

np.float64(963.2125856290679)

We solve the system:

array([ 3.17760897, 14.67380042, -1.21091344])

Given the definition of the approximating polynomial:

f(a0,a1,…,am,x)=∑v=0mav xm−vf(a_0,a_1,\dots,a_{m}, x) = \sum_{v=0}^{m}a_v\,x^{m-v}

which in this particular case is the approximating polynomial:

f(x)=3.18393375 x2+14.64106847 x−1.22621065f(x)=3.18393375\,x^2 + 14.64106847\,x -1.22621065

Let us define a numerical implementation:

We plot:

<Figure size 640x480 with 1 Axes>

Let us also look at the approximation error:

ei=yi−f(xi)e_i=y_i - f(x_i)

for i=0,1,2,…,n−1i=0,1,2,\dots,n-1.

With a correctly performed approximation, some eie_i are positive and some negative. Let us check whether this is true in our case:

array([-0.06824269, -0.62517387, 2.1057209 , -2.69396374, 1.28165939])

Note: the higher the degree of the polynomial we use, the greater the probability of poor conditioning. For this reason we do not overdo the polynomial degree (in practice we mainly use low degrees)!

Using numpy for polynomial approximation

Let us look at how to use the numpy library for polynomial approximation.

First we use the function numpy.polyfit (documentation):

polyfit(x, y, deg, rcond=None, full=False, w=None, cov=False)

which requires three parameters: x and y represent the data table (also possible as lists of vectors), and deg is the polynomial degree. The remaining parameters are optional (e.g., w to use weights in the approximation).

The polyfit function returns a list of polynomial coefficients (highest order first); the result can also be a list of lists (if the input data is a list of vectors).

Note: numpy.polyfit and numpy.poly1d belong to the older interface for polynomials; for new programs numpy recommends the class numpy.polynomial.Polynomial (documentation): p = Polynomial.fit(x, y, deg=2) returns an object that we call as a function (p(x)), and p.convert().coef gives the coefficients in increasing order of powers (exactly the opposite of polyfit).

Let us look at its use for the example treated previously:

array([ 3.17760897, 14.67380042, -1.21091344])
array([ 3.17760897, 14.67380042, -1.21091344])

Once we have the coefficients, we can create a polynomial object by calling numpy.poly1d (documentation):

poly1d(c_or_r, r=False, variable=None)

where c_or_r represents the list of polynomial coefficients, or the roots of the polynomial if r=True. The function returns an instance of the object, by calling which we can compute the values of the approximating polynomial at x, but we can also compute other things, such as the roots of the polynomial.

Let us look at an example:

Let us plot the values:

<Figure size 640x480 with 1 Axes>

Let us compute the roots of the polynomial:

array([-4.69897273, 0.08109792])

Approximation with an arbitrary function

In approximation we are not limited to polynomials only. We can approximate data tables:

  • with a linear combination of linearly independent basis functions, or

  • with a function in which the parameters appear in a nonlinear relationship (e.g., a0 sin⁡(a1 x+a2)a_0\,\sin(a_1\,x+a_2)).

For details, see the reference J. Petrišič: Uvod v Matlab za inženirje, Fakulteta za strojništvo 2013, p. 145.

We will focus on using the scipy package for approximation with a nonlinear function, which is based on the least-squares method.

Approximation with a harmonic function

The data table is defined as:

Let us plot the data table:

<Figure size 640x480 with 1 Axes>

We will perform the approximation with a nonlinear function using scipy.optimize.curve_fit (documentation):

curve_fit(f, xdata, ydata, p0=None, sigma=None, absolute_sigma=False, check_finite=True, bounds=(-inf, inf), method=None, jac=None, **kwargs)

which requires three parameters: f represents the definition of the Python function with which we want to approximate, and whose parameters we vary using the least-squares method. xdata and ydata represent the data table. It is also recommended to define an estimate of the sought parameters p0. The remaining parameters are optional.

The function returns two arrays: popt, which represents the parameters found, and pcov, which represents the estimated covariance of popt.

First let us define a Python function whose first parameter is the independent variable x, followed by the parameters we want to determine:

where A is the amplitude, ω the angular frequency, and ϕ the phase of the harmonic function. With the help of the figure we can guess the initial estimates: A=1, ω=1, ϕ=0

Now we import curve_fit and run the optimization procedure:

array([ 1.01461945, 1.89652046, -3.27943097])

We have computed the expected values (see above).

array([-0.0525477 , -0.99608783, -0.05612665, 1.00860622, -0.02753942, -1.01426499, 0.1110182 ])
<Figure size 640x480 with 1 Axes>

Additional

Make an .exe of your program:

Take a look at the pandas package. Pandas

Approximation with splines using SciPy

Let the data table be:

array([-3. , -2.68421053, -2.36842105, -2.05263158, -1.73684211, -1.42105263, -1.10526316, -0.78947368, -0.47368421, -0.15789474, 0.15789474, 0.47368421, 0.78947368, 1.10526316, 1.42105263, 1.73684211, 2.05263158, 2.36842105, 2.68421053, 3. ])
array([ 0.08832603, 0.02075073, 0.0526001 , 0.12684217, 0.14234432, 0.08387244, 0.34226064, 0.52862107, 0.79385312, 0.99590738, 0.98257964, 0.87172774, 0.57424082, 0.30083997, 0.15492949, 0.06565014, 0.08950146, -0.00659471, 0.01639626, -0.04258138])

Let us look at the object scipy.interpolate.UnivariateSpline (documentation), which enables both interpolation and approximation with splines:

UnivariateSpline(x, y, w=None, bbox=[None, None], k=3, s=None, ext=0, check_finite=False)

The parameters x and y represent the data table.

Note: UnivariateSpline has been marked as legacy since SciPy 1.15; the more modern equivalent for approximation (smoothing) with splines is scipy.interpolate.make_smoothing_spline (documentation) or make_splrep. The object interface below still works and is clearer for learning.

The optional parameter s sets the value that the sum of squares of the differences between the approximating spline and the approximation points must not exceed:

sum((w[i] * (y[i]-spl(x[i])))**2, axis=0) <= s

w are the weights of the individual points.

If we set s=0, we require interpolation.

The parameter k defines the degree of the polynomial spline (the default is k=3).

We perform approximation with splines by defining, in addition to the data table x and y, the parameter s as well. Let us perform interpolation:

<Figure size 640x480 with 1 Axes>

Let us also perform the approximation:

<Figure size 640x480 with 1 Axes>

The actual residual:

0.10002662427563205