views:

78

answers:

2

Does anyone know a scipy/numpy module which will allow to fit exponential decay to data?

Google search returned a few blog posts, for example - http://exnumerus.blogspot.com/2010/04/how-to-fit-exponential-decay-example-in.html , but that solution requires y-offset to be pre-specified, which is not always possible

EDIT:

curve_fit works, but it can fail quite miserably with no initial guess for parameters, and that is sometimes needed. The code I'm working with is

#!/usr/bin/env python
import numpy as np
import scipy as sp
import pylab as pl
from scipy.optimize.minpack import curve_fit

x = np.array([  50.,  110.,  170.,  230.,  290.,  350.,  410.,  470.,  
530.,  590.])
y = np.array([ 3173.,  2391.,  1726.,  1388.,  1057.,   786.,   598.,   
443.,   339.,   263.])

smoothx = np.linspace(x[0], x[-1], 20)

guess_a, guess_b, guess_c = 4000, -0.005, 100
guess = [guess_a, guess_b, guess_c]

exp_decay = lambda x, A, t, y0: A * np.exp(x * t) + y0

params, cov = curve_fit(exp_decay, x, y, p0=guess)

A, t, y0 = params

print "A = %s\nt = %s\ny0 = %s\n" % (A, t, y0)

pl.clf()
best_fit = lambda x: A * np.exp(t * x) + y0

pl.plot(x, y, 'b.')
pl.plot(smoothx, best_fit(smoothx), 'r-')
pl.show()

which works, but if we remove "p0=guess", it fails miserably.

+2  A: 

I would use the scipy.optimize.curve_fit function. The doc string for it even has an example of fitting an exponential decay in it which I'll copy here:

>>> import numpy as np
>>> from scipy.optimize import curve_fit
>>> def func(x, a, b, c):
...     return a*np.exp(-b*x) + c

>>> x = np.linspace(0,4,50)
>>> y = func(x, 2.5, 1.3, 0.5)
>>> yn = y + 0.2*np.random.normal(size=len(x))

>>> popt, pcov = curve_fit(func, x, yn)

The fitted parameters will vary because of the random noise added in, but I got 2.47990495, 1.40709306, 0.53753635 as a, b, and c so that's not so bad with the noise in there. If I fit to y instead of yn I get the exact a, b, and c values.

Justin Peel
You beat me to it! That's what I get for taking so long to type up an example.! :) I'll leave mine up, as well, though, as it elaborates a bit on the pros and cons...
Joe Kington
+3  A: 

You have two options:

  1. Linearize the system, and fit a line to the log of the data.
  2. Use a non-linear solver (e.g. scipy.optimize.curve_fit

The first option is by far the fastest and most robust. However, it requires that you know the y-offset a-priori, otherwise it's impossible to linearize the equation. (i.e. y = A * exp(K * t) can be linearized by fitting y = log(A * exp(K * t)) = K * t + log(A), but y = A*exp(K*t) + C can only be linearized by fitting y - C = K*t + log(A), and as y is your independent variable, C must be known beforehand for this to be a linear system.

If you use a non-linear method, it's a) not guaranteed to converge and yield a solution, b) will be much slower, c) gives a much poorer estimate of the uncertainty in your parameters, and d) is often much less precise. However, a non-linear method has one huge advantage over a linear inversion: It can solve a non-linear system of equations. In your case, this means that you don't have to know C beforehand.

Just to give an example, let's solve for y = A * exp(K * t) with some noisy data using both linear and nonlinear methods:

import numpy as np
import matplotlib.pyplot as plt
import scipy as sp
import scipy.optimize


def main():
    # Actual parameters
    A0, K0, C0 = 2.5, -4.0, 2.0

    # Generate some data based on these
    tmin, tmax = 0, 0.5
    num = 20
    t = np.linspace(tmin, tmax, num)
    y = model_func(t, A0, K0, C0)

    # Add noise
    noisy_y = y + 0.5 * (np.random.random(num) - 0.5)

    fig = plt.figure()
    ax1 = fig.add_subplot(2,1,1)
    ax2 = fig.add_subplot(2,1,2)

    # Non-linear Fit
    A, K, C = fit_exp_nonlinear(t, noisy_y)
    fit_y = model_func(t, A, K, C)
    plot(ax1, t, y, noisy_y, fit_y, (A0, K0, C0), (A, K, C0))
    ax1.set_title('Non-linear Fit')

    # Linear Fit (Note that we have to provide the y-offset ("C") value!!
    A, K = fit_exp_linear(t, y, C0)
    fit_y = model_func(t, A, K, C0)
    plot(ax2, t, y, noisy_y, fit_y, (A0, K0, C0), (A, K, 0))
    ax2.set_title('Linear Fit')

    plt.show()

def model_func(t, A, K, C):
    return A * np.exp(K * t) + C

def fit_exp_linear(t, y, C=0):
    y = y - C
    y = np.log(y)
    K, A_log = np.polyfit(t, y, 1)
    A = np.exp(A_log)
    return A, K

def fit_exp_nonlinear(t, y):
    opt_parms, parm_cov = sp.optimize.curve_fit(model_func, t, y, maxfev=1000)
    A, K, C = opt_parms
    return A, K, C

def plot(ax, t, y, noisy_y, fit_y, orig_parms, fit_parms):
    A0, K0, C0 = orig_parms
    A, K, C = fit_parms

    real_data = ax.plot(t, y, 'k--')
    obs_data = ax.plot(t, noisy_y, 'ro')
    fit_data = ax.plot(t, fit_y, 'b-')
    ax.legend((real_data, fit_data),
            ['Actual Function:\n $y = %0.2f e^{%0.2f t} + %0.2f$' % (A0, K0, C0), 
             'Fitted Function:\n $y = %0.2f e^{%0.2f t} + %0.2f$' % (A, K, C)], 
            bbox_to_anchor=(1.05, 1.1), fancybox=True, shadow=True)

if __name__ == '__main__':
    main()

Fitting exp

Note that the linear solution provides a result much closer to the actual values. However, we have to provide the y-offset value in order to use a linear solution. The non-linear solution doesn't require this a-priori knowledge.

Joe Kington
Thank you! Unfortunately, the problem with curve_fit is that it can fail miserably if no initial guess for parameters is provided
cheshire
@cheshire - That's a mathematical fact of life when using any non-linear solution. There's no "best" way around it, though some non-linear methods will work better than others for your particular problem. In your case, you can specify the Jacobian, which will help immensely in this situation. You can also use the input data to make an intelligent guess for the starting parameters.
Joe Kington
I've used QtiPlot before, and it does very good fit without any initial guessing
cheshire
Because it was doing the things I mentioned, resulting in a solver for the `exp` function, _not_ a general-purpose solver for _any_ possible function, which is what scipy's curve_fit is. _Every possible_ nonlinear method is sensitive to the initial guess to varying degrees. That's part of the definition of a non-linear system.
Joe Kington
Give me a bit, and I'll update my example to show using a manually specified Jacobian (easily derived, since we're using a simple mathematical function as a model). That should help things quite a bit...
Joe Kington
Right, you are correct, however my question was about the existence of solver for exponential-decay, not about the general-purpose nonlinear solver. Thanks for your reply, I presume fit_curve is as good as it gets inside scipy
cheshire
Example of manually specifying Jacobian would be nice, it's not mentioned anywhere in the docs.
cheshire
One possible improvement in this case would be to do a nested optimization, linear inside non-linear. Given the offset, you can directly calculate the remaining two parameters. So, in the outer optimization only the offset needs to be chosen with a non-linear optimizer. I guess that, in this case, it will be easier to find a good starting value or global optimizer.
@user333700 - how do I write an outer optimization, though?
cheshire
Whoa, hold up. "The first option is by far... most robust." Absolutely not true for exponential fitting. The LLS estimate is more sensitive to small perturbations in the observed data than the NLS estimate.
Steve