Curve fitting How-to

by W. Garrett Mitchener

This worksheet goes over traditional linear and non-linear least squares curve fitting and different ways to do it in Mathematica. It also goes over maximum likelihood curve fitting.  Along the way, it shows different functions for finding maxima and minima of expressions.

Least squares and linear regression

Let's say you have some data {((Subscript[x, 1]), (Subscript[y, 1])),  ((Subscript[x, 2]), (Subscript[y, 2])) ... } and you want to fit a curve to it so that you can say y = f(x) + noise.

To illustrate, let's create some noisy data:

data = 
      Table[{x, 
          2 x + 3 + 
            Random[Real, {-1, 1}]}, 
{x, -5, 5, 
          0.25}]

pointPlot = 
      ListPlot[data, PlotStyle -> PointSize[0.025]]

In Mathematica, the Fit function takes a list of points, a list of expressions, and a list of independent variables, and determines which linear combination of the given expressions produces the best fit to the data.  If you want to fit a line to the data, your list of expressions should consist of 1 and x, since a line is a linear combination of a constant and a multiple of x:

lineFit = Fit[data, {1, x}, x]

linePlot = Plot[lineFit, {x, -5, 5}]

Show[pointPlot, linePlot]

And you can see that we've more or less recovered the 3 + 2  x that we used to create the data in the first place.

You can also do more interesting things:

data2 = 
      Table[{x, 
          Sin[x] + 
            Random[Real, {-0.25, 0.25}]}, 
{x, -5, 
          5, 0.25}]

pointPlot2 = 
      ListPlot[data2, PlotStyle -> PointSize[0.025]]

For instance, this fits a second degree polynomial to the data:

poly2fit = Fit[data2, {1, x, x^2}, x]

Plot[poly2fit, {x, -5, 5}]

Not a very good fit, is it?  It doesn't have nearly enough bumps.  Let's try something higher degree.

poly5fit = Fit[data2, {1, x, x^2, x^3, x^4, x^5}, x]

poly5plot = Plot[poly5fit, {x, -5, 5}]

That looks a lot better.  I knew to guess 5 for the degree because the data goes through four extrema.

Show[poly5plot, pointPlot2]

But don't get carried away.  If you give it too many degrees of freedom, it will start to fit the noise, as in this example:

powerTable = Table[x^n, {n, 0, 25}]

poly25fit = Fit[data2, powerTable, x]

Plot[poly25fit, {x, -5, 5}]

You can also do more exotic things:

cos3fit = Fit[data2, {1, Cos[x], Cos[2  x], Cos[3  x]}, x]

Plot[cos3fit, {x, -5, 5}]

but that doesn't look right somehow.  In fact, I get vastly different plots every time I run this worksheet, which indicates that the random noise added to the data is having a huge impact on the cosine fit, which isn't the case for the fifth degree polynomial.  That indicates that these cosines are not a good way to fit this data.  (Think about it: Why?)  The first step to getting a good fit is to know what functions to include.

Details on how Fit works.

The way Fit works is called least squares, because it minimizes this:

(∑_(j = 1)^n)(((f[(Subscript[x, j])] - (Subscript[y, j])))^2)

In Mathematica notation

LeastSquaresError[data_, f_] := 
      Sum[(f[data[[j, 1]]] - data[[j, 2]])^2, {j, 1, 
          Length[data]}]

Let's return to our linear data.

data

Here's how to define a general line function:

lineFunction[x_] = a x  +  b

To fit this line to the data, we need to determine a and b such that the error is minimized.

Mathematica has several functions for finding the minimum of an expression.  They all work a little differently.  The  Minimize  function works algebraically:

minSolution = 
      Minimize[LeastSquaresError[data, lineFunction], {a, b}]

The result is a list {min,  args} where min is the minimum value it found, and args is a rule table.  Here's how to use a rule table.  First, just so we're clear on what's going on, we'll unpack the list returned by Minimize.

{min, args} = minSolution

That defined min to be the minimum value, and args to be the rule table giving the values of a and b:

args

This was the function we were trying to fit:

lineFunction[x]

And we can connect the general function to the specific values of a and b by using the /.
operator.  (You can also use the ReplaceAll function.  The /. is short-hand for ReplaceAll.)

lineFunction[x] /. args

ReplaceAll[lineFunction[x], args]

If you want to define a function representing the fit:

fittedFunction[x_] = lineFunction[x] /. args

fittedFunction[3.5]

The Minimize function works algebraically, which means it sometimes doesn't do quite what you'd like.  It generally works well on polynomial problems, but if you give it a nasty enough trancendental problem, you're out of luck.

Minimize[Exp[-x^2] + Exp[(x - 2)^2], x]

So, sometimes you get better results working just numerically.  For that, try NMinimize .

NMinimize[(E^((((-2 + x))^2))) + (E^(-(x^2))), x]

Here's our linear least squares fit again:

NMinimize[LeastSquaresError[data, lineFunction], {a, b}]

Another numerical function is FindMinimum, which uses a different numerical algorithm.  You have to specify a starting point for each unknown variable.

FindMinimum[
      LeastSquaresError[data, lineFunction], {{a, 1}, {b, 1}}]

The Fit function does exactly this same minimization conceptually, but it only works if the fit function looks like

f[x] = (Subscript[a, 1]) (Subscript[f, 1])[x] + (Subscript[a, 2]) (Subscript[f, 2])[x] +  ...  + (Subscript[a, n]) 
          (Subscript[f, n])[x]

and the (Subscript[a, 1]),  (Subscript[a, 2]), ... (Subscript[a, n]) are the only unknowns.  This is the traditional method of curve fitting (predating modern computers that can do more powerful techniques almost as fast) because if f has this form, you can take a short cut from linear algebra and do the computation very quickly.  Otherwise, the minimization can be computationally intensive and may get stuck at a local minimum instead of finding the global minimum.

Non-linear least squares

The linearization method

To do non-linear curve fitting with least squares, there are a couple of alternatives.  One is to linearize the data first, then proceed using Fit.

As before, let's make up some noisy data to play with.  It's basically 4  (x^2.2) with noise added to the coefficient and the exponent.

nonlinearData = 
      Table[{x, (4 + Random[Real, {-1, 1}]) 
            x^(2.2 + 
                  Random[Real, {-0.05, 0.05}])}, 
{x, 
          1, 10, 0.25}]

nonlinearPointPlot = 
      ListPlot[nonlinearData, PlotStyle -> PointSize[0.025]]

We'd like to fit this to a power function and find a and b.

powerFunction[x_] = a x^b

But we can't use Fit, because the unknowns aren't in the right place.  So, we linearize the data first.  Assuming the power function is correct for our data, we can take the logarithm, and get something in the right form for Fit:

Log[
        y] = Log[a (x^b)] = Log[a] + Log[(x^b)] = 
          Log[a] + b Log[x]

So even though our (x, y) data is not in the right form for Fit, it turns out that (Log[x], Log[y]) is in the right form.  Here's an incantation to linearize the data.  The trick is that /. can apply rules that involve patterns (see the Mathematica book 2.5 ), so this next command looks at nonlinearData and replaces anything that looks like {x, y} with {Log[x], Log[y]}.  As in function definitions, the _ on the x and y indicates that these are pattern variables.  Without the _, Mathematica will think you mean to replace only stuff with the symbols x and y.  And you don't use the _ on the right hand side of the rule or in a function definition, only on the left.

linearizedData = 
      nonlinearData /. {x_, y_} -> {Log[x], Log[y]}

linearizedPointPlot = 
      ListPlot[linearizedData, PlotStyle -> PointSize[0.025]]

Now we can use Fit:

logFit = Fit[linearizedData, {1, logx}, logx]

logFitPlot = Plot[logFit, {logx, 0, 4}]

Show[logFitPlot, linearizedPointPlot]

And you can see that this is pretty close to 4  (x^2.2):

nonlinearFit[x_] = Exp[logFit /. logx -> Log[x]]

nonlinearFitPlot = Plot[nonlinearFit[x], {x, 0, 10}]

Show[nonlinearFitPlot, nonlinearPointPlot]

The direct least squares method

Since Mathematica can directly minimize various expressions, we can also skip the linearization step and minimize the error directly:

directFitSolution = 
      Minimize[LeastSquaresError[nonlinearData, powerFunction], {a, 
          b}]

directFit[x_] = 
      powerFunction[x] /. directFitSolution[[2]]

directFitPlot = 
      Plot[directFit[x], {x, 0, 10}, 
PlotStyle -> 
          Dashing[{0.05, 0.05}]]

Show[directFitPlot, nonlinearFitPlot, nonlinearPointPlot]

And you should be able to see that the function found by the linearization method isn't quite the same as the one found by the direct method:

{nonlinearFit[x], directFit[x]}

Both functions are actually the optimum fit to the data, but under different notions of distance.  And notice that neither one is exact compared to what we started with.  For example, here's what I got on one run of this worksheet:

{3.5498 (x^2.27148), 3.0792 (x^2.34363)}

Since the data set is constructed with random noise, the results will be a little different each time you run it.  But neither of these gives back 4 (x^2.2)exactly.  (Think about it: Should they?)  And which curve is "better"?

This contention between the results illustrates the fundamental conceptual problems in curve fitting: (1) Guess the appropriate form of the function. (2) Determine an appropriate notion of "distance" between the curve and the data to minimize.

Maximum likelihood

An alternative notion of distance that is appropriate for modeling changing probabilities is likelihood.  This notion assumes that the data is of the form (Subscript[y, j]) = f((Subscript[x, j]), (Subscript[ω, j]); a, b,  ... ) where x is an independent variable, such as distance or time, (Subscript[ω, j]) are independent random numbers, and a, b,  ...  are the parameters that we want to find.  Then, the likelihood of the data is the probability of getting exactly the (Subscript[y, j])'s.  The curve fit procedure is to determine values of the parameters such that the likelihood of the data is maximal.

As an example, let's suppose we have a biological experiment that succeeds or fails, for example, getting bacteria to accept a fragment of DNA.  Let's suppose that the probability of success depends on temperature x.  Furthermore, let's suppose that we have some knowledge of the biochemistry involved that tells us that the probability of success is actually an exponential function: p[x] = (e^( - a (x - b))) where a and b are unknown.  We are given results from experiments run at different temperatures, and they are simply given as success or failure, and our job is to find a and b.

First, let's invent some data, using a = 0.025 and b = -5.

pSuccess[x_, a_, b_] = Exp[-a (x - b)]

Random returns a random number between 0 and 1, so the test Random[] < p returns True with probability p and False with probability 1 - p, so we can use it to simulate our experiment.  We'll also assume that this experiment is fairly expensive and time consuming, so we can't run zillions of experiments.

runExperiment[x_, a_, 
        b_] := (Random[] < pSuccess[x, a, b])

bioExampleData = 
      Table[{x, runExperiment[x, 0.025, -5]}, 
{x, 0, 
          80, 2}]

Here's a way to plot that data.  Do you see how this command works?

ListPlot[
      bioExampleData /. {True -> 1, 
          False -> 0}, 
PlotStyle -> 
        PointSize[0.025]]

The data we have isn't points (x, p[x, a, b]) which is what we'd need to do traditional curve fitting.  In other words, we want to fit a curve but we don't have points from the curve plus noise like we did in earlier examples; we have something else entirely.  Intuitively, it seems impossible for least squares to give anything useful for this type of data, so instead, we do maximum likelihood.  The experiments are independent, so the probability of getting all this data is the product of the probabilities of getting each point.  But the probability of getting each point depends on a and b.

likelihood[data_, a_, b_] := 
      Product[
If[
          data[[j, 2]], 
pSuccess[data[[j, 1]], a, 
            b], 
1 - 
            pSuccess[data[[j, 1]], a, b]], 
{j, 1, 
          Length[data]}]

So for example:

likelihood[bioExampleData, 0.001, -4]

likelihood[bioExampleData, 0.02, 2]

These are tiny numbers, and that causes trouble with the numerical maximization process, so instead of maximizing the likelihood, we maximize the log likelihood (Think about it: Why can we do this?)

We could do this, but then Mathematica will compute that tiny likelihood, then take the log.

logLikelihood1[data_, a_, b_] := Log[likelihood[data, a, b]]

A better way to compute the same thing is to expand the log of the product into a sum of logs.  I'll do this computation using different notation:

logLikelihood[data_, a_, b_] := 
      Total[
data /. {x_, success_} -> 
            If[success, 
Log[
                pSuccess[x, a, b]], 
Log[
                1 - pSuccess[x, a, b]]]]

Just to check that we did this right, these should be the same number:

Log[likelihood[bioExampleData, 0.001, -4]]

logLikelihood[bioExampleData, 0.001, -4]

Here's the first try at running the fit:

Maximize[{logLikelihood[bioExampleData, a, b], a > 0}, {a, 
        b}]

Which means it couldn't solve the problem algebraically.  So, let's try the numerical methods:

NMaximize[logLikelihood[bioExampleData, a, b], {a, b}]

You probably got an error message, either that an overflow occurred, or that it got a complex number somewhere along the way.  That means that NMaximize is trying values of a and b that yield logs of negative numbers, or perhaps log of 0.  Let's use the constraint feature of NMaximize to give it some additional hints: We ask it to maximize the log likelihood, but subject to some reasonable constraints.  We know the exponential should decrease as x increases, so we add in a > 0.

NMaximize[{logLikelihood[bioExampleData, a, b], a > 0}, {a, 
        b}]

Sometimes that works, sometimes it doesn't, depending on what random numbers appeared in our simulated experiment.  Let's try FindMaximum since it takes a starting point.

FindMaximum[
      logLikelihood[bioExampleData, a, b], {{a, 0}, {b, -1}}]

That probably didn't work because we get something like Log[0]  somewhere if we start at a = 0.

FindMaximum[
      logLikelihood[bioExampleData, a, 
        b], {{a, 0.01}, {b, -1}}]

This is pretty good, at least the time I ran it.  Sometimes you have to poke around with different starting points to get reasonable results.  There are also zillions of options, and you can spend lots of time playing with them, tweaking the numerical method, but unless you're desperate or know what all the tweaks mean, doing that is often a waste of time.

The result isn't perfect, but it has a definite interpretation:  These values for a and b are the ones such that the probability of getting our observations is maximal.  And they're reasonably close to the exact answer, which is great given how little information our data acutally contains.