pythonmatplotliberrorbarlmfit

Showing the error of lmfit-model center in plot


I'm working with fitting of data with the python lmfit package. I am looking at several peaks and fitting them with a pseudo-Voigt + constant model. I then extract the center position of the fitted peak and compile about a hundred of these peak positions. The hundred positions are then plotted and fitted with a sine-wave model, also with lmfit. My issue, is that I am not able to find the information I need to show the uncertainty of the peak positions extracted from the pseudo-Voigt fitting. In many similar questions, I encounter the eval_uncertainty function also included in the lmfit package, but I am not able to use this to extract the confidence intervals of the center position, only uncertainties in the amplitude and sigma value of the peak.

The ideal result would look like the following image: The ideal case I'm trying to achieve

And my current graph looks like this: Current graph

Obviously I need to switch from a scatter plot to an errorbar plot, but I do not have any error-intervals to work with at the moment.

The following is the most simple workable "mini"-case I could construct, while still including my original fit-models. My original data is around 100 datapoints instead of four, and the sine-wave in this smaller case is therefore not "smooth", but this should not affect the method of introduction of errorbars.

import numpy as np
import matplotlib.pyplot as plt
from lmfit.models import PseudoVoigtModel, ConstantModel, SineModel

def Voigt_app(x,y,a,b):
    
    psvo_mod = PseudoVoigtModel(prefix='psvo_')
    pars = psvo_mod.guess(y,x=x)
    
    con_mod = ConstantModel(prefix='con_')
    pars = pars.update(con_mod.make_params(c = (y[0]+y[1])/2))
    
    mod = psvo_mod + con_mod

    out = mod.fit(y, pars, x=x)
    
    details = out.fit_report(min_correl=0.25)
    
    return out, details

def Sin_app(x,y):
    sin_mod = SineModel(prefix='sin_')
    pars = sin_mod.guess(y,x=x)
    
    con_mod = ConstantModel(prefix='con_')
    pars = pars.update(con_mod.make_params(c = np.mean(y)))
    
    mod = sin_mod + con_mod

    out = mod.fit(y,pars,x=x)
    details = out.fit_report(min_correl=0.25)
                       
    return out, details

data = np.asarray(
      [[14407.0 , 26989.0 , 31026.0 , 31194.0 , 31302.0 , 91996.0 , 112250.0 , 112204.0 , 113431.0 , 75097.0 , 55942.0 , 55826.0 , 56181.0 , 28266.0 , 14687.0],
       [12842.0 , 13275.0 , 13581.0 , 13943.0 , 14914.0 , 20463.0 , 21132.0 , 21457.0 , 23308.0 , 32017.0 , 30808.0 , 30927.0 , 30337.0 , 21025.0 , 15216.0],
       [15770.0 , 17677.0 , 19008.0 , 20911.0 , 25958.0 , 27984.0 , 28164.0 , 31161.0 , 33517.0 , 29430.0 , 28673.0 , 32725.0 , 28495.0 , 24527.0 , 25173.0],
       [16299.0 , 20067.0 , 25102.0 , 34968.0 , 37757.0 , 44871.0 , 51347.0 , 60154.0 , 54985.0 , 53383.0 , 45776.0 , 40836.0 , 30816.0 , 27922.0 , 26066.0]])
a,b = 1932, 1947
centers = []

colors = ['red','black','green','blue']
fig, ax = plt.subplots()
for i in range(4):
    ax.scatter(np.arange(15),data[i,:],label=f'data {i}',color=colors[i])
    out, details = Voigt_app(x,data[i,:],a,b)
    ax.plot(out.best_fit,label=f'fit {i}',color=colors[i])
    centers.append(out.values['psvo_center'])
ax.set_title('Data points and pseudo-Voigt fits')
ax.legend()


fig, ax = plt.subplots()
ax.scatter(np.arange(4),centers,color='red',label='data')

out, details = Sin_app(np.arange(4),np.asarray(centers))
ax.plot(np.arange(4),out.best_fit,color='black',label='fit')
ax.set_title('Peak positions and sine-wave fit')
ax.legend()

From this I would like to switch out the scatter plot in the second graph with an errorbar plot with the confidence intervals of the pseudo-Voigt peak positions ("out.values['psvo_center']").


Solution

  • If I understand your question correctly, you want to extract not only the best-fit value for the 'psvo_center' parameter for each of your fits, but also the standard error for 'psvo_center', and then use those as the data for fitting to a sinusoidal oscillation.

    If that is correct, I think that what you want to do is to save the stderr attribute, by changing you code to

    centers = []
    stderrs = []  # < new
    
    colors = ['red','black','green','blue']
    fig, ax = plt.subplots()
    for i in range(4):
        ax.scatter(np.arange(15),data[i,:],label=f'data {i}',color=colors[i])
        out, details = Voigt_app(np.arange(15), data[i,:], a, b)
        ax.plot(out.best_fit,label=f'fit {i}',color=colors[i])
        centers.append(out.params['psvo_center'].value)       # < new 
        stderrs.append(out.params['psvo_center'].stderr)      # < new
    
    

    You can then use that to make and error-bar plot with

    ax.errorbar(np.arange(4), centers, stderrs, color='red',label='data')
    

    You might also want to use those stderrs to weight the fit to the sinusoidal function, perhaps modifying your Sin_App to be

    
    def Sin_app(x, y, dely):       # < new
        sin_mod = SineModel(prefix='sin_')
        pars = sin_mod.guess(y,x=x)
    
        con_mod = ConstantModel(prefix='con_')
        pars = pars.update(con_mod.make_params(c = np.mean(y)))
    
        mod = sin_mod + con_mod
    
        out = mod.fit(y, pars, x=x, weights=1./dely)    # < new
        details = out.fit_report(min_correl=0.25)
        return out, details
    

    and calling that with

    out, details = Sin_app(np.arange(4), np.asarray(centers), np.asarray(stderrs))
    

    for the 4 example datasets you give, the variation in the standard-error isn't very big, so it does not make a big difference. But as you add more datasets, it might become more important. You might want to add a check that stderr is valid (like, not None and not 0) before using this "in production".

    For "one more thing", you might also want to calculate the predicted uncertainty in the sinusoidal function and plot that range too. Your example has 4 variables for the sinusoidal fit and only 4 values, which results for this case in a near-perfect fit with absurdly small uncertainties on the fitted sine function. So to illustrate the approach, I fix the con_c parameter in the fit below. If fitting the results of more than 4 peak profiles, that should not be necessary. But, for illustration, changing your Sin_app to be

    def Sin_app(x, y, dely):
        sin_mod = SineModel(prefix='sin_')
        pars = sin_mod.guess(y,x=x)
    
        con_mod = ConstantModel(prefix='con_')
        pars = pars.update(con_mod.make_params(c = np.mean(y)))
    
        pars['con_c'].vary = False  # needed with only 4 (x,y) values
    
        mod = sin_mod + con_mod
    
        out = mod.fit(y, pars, x=x, weights=1./dely)
        details = out.fit_report(min_correl=0.25)
    
        delta_fit = out.eval_uncertainty(sigma=1)   # < new
        return out, details, delta_fit
    

    and then plotting the results of using that with

    
    out, details, delta_fit = Sin_app(np.arange(4), np.asarray(centers), np.asarray(stderrs))
    
    ax.plot(np.arange(4),out.best_fit, 'o', color='black',label='fit')
    
    ax.fill_between(np.arange(4), 
                    out.best_fit-delta_fit, out.best_fit+delta_fit,
                    color="#D7D7D7")
    
    ax.set_title('Peak positions and sine-wave fit')
    ax.legend()
    
    

    This leads to the final plot of:

    plot of final fit of peak centers to sinusoidal function

    showing the "data" for the peak centers with error bars, the fitted values, and the 1-sigma range of the fit.