Non-linear regression
Contents
Non-linear regression#
Recap on linear regression#
Last class, we talked about how we could turn linear regression into a linear algebra problem
You can calculate this yourself
You can also do linear regression using statistical packages like statsmodel
Today we will discuss two ways of solving non-linear regression problems
Turn a non-linear problem into a linear one and solve
Non-linear curve fitting
Turn a non-linear regression problem into a linear regression problem#
Rate constants and reaction orders are determined by using models that are fit to experimental data
A common case is to monitor concentration vs. time in a constant volume, batch reactor
We consider the disappearance of \(A\)
From the mole balance we know:
Let us assume the rate law is of the form: \(r_A = k C_A^\alpha\) and a constant volume so that:
Let us be loose with mathematics, rearrange the equation, and take the log of both sides.
By loose I mean we take logs of quantities that are not dimensionless
This suggests that if we could numerically compute \(\frac{dC_A}{dt}\) from our data of \(C_A(t)\) then a plot of the log of the negative derivative vs the log of concentration would have
an intercept equal to the log of the rate constant, \(k\)
and a slope equal to the reaction order \(\alpha\)
Given the following data, determine the reaction order in A and the rate constant with 95% confidence intervals.
time (min) | C\_A (mol/L) |
---|---|
0 | 0.0500 |
50 | 0.0380 |
100 | 0.0306 |
150 | 0.0256 |
200 | 0.0222 |
250 | 0.0195 |
300 | 0.0174 |
We can get the derivatives by first fitting a spline through the data. The spline is essentially just a smoothing function
We will use the
splev
function to numerically compute derivatives from spline fits of the function.This works best when the \(x\) points are evenly spaced, and they should be monotically increasing or decreasing
import numpy as np
import matplotlib.pyplot as plt
data=np.array([[0,0.05],
[50,.038],
[100,.0306],
[150,.0256],
[200,.0222],
[250,.0195],
[300,.0174]])
plt.plot(data[:,0],data[:,1],'o')
plt.xlabel('Time [min]')
plt.ylabel('Conc. [mol/L]')
Text(0, 0.5, 'Conc. [mol/L]')
So, we need to convert the list of numbers to a numpy array so we can do the analysis.
import numpy as np
import matplotlib.pyplot as plt
from scipy import interpolate
# data will be a 2d list, which we convert to an array here
data = np.array(data)
t = data[:, 0] # column 0
Ca = data[:, 1] # column 1
# calculate a spline through the data
tck = interpolate.splrep(t, Ca)
t_eval = np.linspace(0,300)
Ca_spline = interpolate.splev(t_eval, tck)
plt.plot(data[:,0],data[:,1],'o', label='Exp Data')
plt.plot(t_eval, Ca_spline,'--.',label='Spline Fit')
plt.xlabel('Time [min]')
plt.ylabel('Conc. [mol/L]')
plt.legend()
<matplotlib.legend.Legend at 0x7f001ee04cd0>
import numpy as np
import matplotlib.pyplot as plt
from scipy import interpolate
import statsmodels.api as sm
# data will be a 2d list, which we convert to an array here
data = np.array(data)
t = data[:, 0] # column 0
Ca = data[:, 1] # column 1
# calculate numerical derivatives
tck = interpolate.splrep(t, Ca)
dCadt = interpolate.splev(t, tck, der=1)
# do the transformation
x = np.log(Ca)
y = np.log(-dCadt)
# setup and do the regression
# column of ones and x: y = b + mx
X = np.column_stack([x, x**0])
mod = sm.OLS(y, X)
res = mod.fit()
intercept = res.params[1]
alpha = res.params[0]
confidence_intervals = res.conf_int(0.05)
intercept_error = confidence_intervals[1]
alpha_error = confidence_intervals[0]
print('alpha = {0}, conf interval {1}'.format(alpha, alpha_error))
print('k = {0}, conf interval {1}'.format(np.exp(intercept),
np.exp(intercept_error)))
# always visually inspect the fit
plt.plot(x, y,'o')
plt.plot(x, res.predict(X))
plt.xlabel('$\ln(C_A)$')
plt.ylabel('$\ln(-dC_A/dt)$')
plt.show()
alpha = 2.0354816446001145, conf interval [1.92418422 2.14677907]
k = 0.1402128334966662, conf interval [0.09372748 0.20975319]
res.summary()
/home/runner/micromamba-root/envs/buildenv/lib/python3.10/site-packages/statsmodels/stats/stattools.py:74: ValueWarning: omni_normtest is not valid with less than 8 observations; 7 samples were given.
warn("omni_normtest is not valid with less than 8 observations; %i "
Dep. Variable: | y | R-squared: | 0.998 |
---|---|---|---|
Model: | OLS | Adj. R-squared: | 0.997 |
Method: | Least Squares | F-statistic: | 2210. |
Date: | Wed, 19 Oct 2022 | Prob (F-statistic): | 8.22e-08 |
Time: | 19:29:39 | Log-Likelihood: | 13.785 |
No. Observations: | 7 | AIC: | -23.57 |
Df Residuals: | 5 | BIC: | -23.68 |
Df Model: | 1 | ||
Covariance Type: | nonrobust |
coef | std err | t | P>|t| | [0.025 | 0.975] | |
---|---|---|---|---|---|---|
x1 | 2.0355 | 0.043 | 47.013 | 0.000 | 1.924 | 2.147 |
const | -1.9646 | 0.157 | -12.539 | 0.000 | -2.367 | -1.562 |
Omnibus: | nan | Durbin-Watson: | 2.377 |
---|---|---|---|
Prob(Omnibus): | nan | Jarque-Bera (JB): | 0.735 |
Skew: | -0.181 | Prob(JB): | 0.692 |
Kurtosis: | 1.454 | Cond. No. | 40.4 |
Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
You can see there is a reasonably large range of values for the rate constant and reaction order (although the confidence interval does not contain zero)
The fit looks ok, but you can see the errors are not exactly random
There seems to be systematic trends in a sigmoidal shape of the data
That suggests small inadequacy in the model
Let us examine some methods of evaluating the quality of fit
First we examine the residuals, or the errors between the data and the model.
In a good fit, these will be randomly distributed
In a less good fit, there will be trends
residuals = y - res.predict(X)
# always visually inspect the fit
plt.plot(x, residuals, 'o-')
plt.xlabel('$\ln(C_A)$')
plt.ylabel('residuals')
plt.show()
You can see there are trends in this data
That means the model may not be complete
There is uncertainty in the data
In each concentration measurement there is uncertainty in the time and value of concentration
You need more data to reduce the uncertainty
You may also need better data to reduce the uncertainty
Derivatives tend to magnify errors in data
The method we used to fit the data contributed to the uncertainty
We also nonlinearly transformed the errors by taking logs and exp of the data and results, which may have skewed the confidence limits
Nonlinear regression#
Nonlinear models are abundant in reaction engineering
\(r = k C_A^n \) is linear in the \(k\) parameter, and nonlinear in \(n\)
Nonlinear fitting is essentially a non-linear optimization problem
Unlike linear regression, where we directly compute the parameters using matrix algebra, we have to provide an initial guess and iterate to the solution
Similar to using fsolve, we must define a function of the model
The function takes an independent variable, and parameters, f(x,a,b,…)
The function should return a value of \(y\) for every value of \(x\)
i.e. it should be vectorized
It is possible to formulate these problems as nonlinear minimization of summed squared errors. See this example.
The function scipy.optimize.curve_fit provides nonlinear fitting of models (functions) to data.
Let’s say we want to fit some other data to the function $\(y=ax/(b+x)\)$
import numpy as np
x = np.array([0.5, 0.387, 0.24, 0.136, 0.04, 0.011])
y = np.array([1.255, 1.25, 1.189, 1.124, 0.783, 0.402])
plt.plot(x,y,'o')
plt.xlabel('x')
plt.ylabel('y')
Text(0, 0.5, 'y')
from scipy.optimize import curve_fit
def func(x, a, b):
'nonlinear function in a and b to fit to data'
return a * x / (b + x)
popt, pcov = curve_fit(func, x, y, p0=(3,3))
xrange = np.linspace(0,0.5)
fitted_y = func(xrange, *popt)
plt.plot(x,y,'o')
plt.plot(xrange,fitted_y)
plt.xlabel('x')
plt.ylabel('y')
Text(0, 0.5, 'y')
print(popt)
[1.32753142 0.02646156]
We also need to estimate uncertainties in nonlinear parameters
lmfit
provides a nice way to do this
Read the lmfit documentation to see how the confidence intervals are computed
Here is an example usage of lmfit.
!pip install lmfit
Requirement already satisfied: lmfit in /home/runner/micromamba-root/envs/buildenv/lib/python3.10/site-packages (1.0.3)
Requirement already satisfied: uncertainties>=3.0.1 in /home/runner/micromamba-root/envs/buildenv/lib/python3.10/site-packages (from lmfit) (3.1.7)
Requirement already satisfied: scipy>=1.4 in /home/runner/micromamba-root/envs/buildenv/lib/python3.10/site-packages (from lmfit) (1.9.2)
Requirement already satisfied: asteval>=0.9.22 in /home/runner/micromamba-root/envs/buildenv/lib/python3.10/site-packages (from lmfit) (0.9.27)
Requirement already satisfied: numpy>=1.18 in /home/runner/micromamba-root/envs/buildenv/lib/python3.10/site-packages (from lmfit) (1.23.4)
Requirement already satisfied: future in /home/runner/micromamba-root/envs/buildenv/lib/python3.10/site-packages (from uncertainties>=3.0.1->lmfit) (0.18.2)
from lmfit import Model
gmodel = Model(func, independent_vars=['x'],param_names=['a','b'])
params = gmodel.make_params(a=2., b=1.0)
result = gmodel.fit(y, params, x=x)
print(result.fit_report())
xrange = np.linspace(0,0.5)
fitted_y = result.eval(x=xrange)
plt.plot(x,y,'o')
plt.plot(xrange,fitted_y)
plt.xlabel('x')
plt.ylabel('y')
[[Model]]
Model(func)
[[Fit Statistics]]
# fitting method = leastsq
# function evals = 36
# data points = 6
# variables = 2
chi-square = 6.9885e-04
reduced chi-square = 1.7471e-04
Akaike info crit = -50.3470350
Bayesian info crit = -50.7635160
[[Variables]]
a: 1.32753139 +/- 0.00972276 (0.73%) (init = 2)
b: 0.02646155 +/- 0.00102789 (3.88%) (init = 1)
[[Correlations]] (unreported correlations are < 0.100)
C(a, b) = 0.711
Text(0, 0.5, 'y')
Here the two intervals are relatively small, and do not include zero, suggesting both parameters are significant.
More importantly, the errors are not skewed by a nonlinear transformation.
Note you have to provide an initial guess.
This will not always be easy to guess.
There may be more than one minimum in the fit also, so different guesses may give different parameters.