Optimisation and curve fitting
minimize for general problems, curve_fit for a model fitted to data, and how to tell a real solution from a local one.
Minimising an objective
import numpy as np
from scipy.optimize import minimize, Bounds, LinearConstraint
def rosen(x):
return np.sum(100.0 * (x[1:] - x[:-1]**2.0)**2.0 + (1.0 - x[:-1])**2.0)
res = minimize(rosen, x0=np.array([-1.2, 1.0]), method="BFGS")
print(res.x) # close to [1.0, 1.0]
print(res.fun) # near zero
print(res.success, res.nit, res.message)# constrained problems use the modern bounds/constraints objects, not lambdas
res = minimize(rosen, x0=[0.5, 0.5], method="SLSQP",
bounds=Bounds([0.0, 0.0], [1.5, 1.5]),
constraints=LinearConstraint([[1.0, 1.0]], 1.0, np.inf))
print(res.x, res.message)- The objective returns one scalar and takes one 1-D array. Flatten inside the function if your parameters are conceptually a matrix.
- Supply
jacwhen you can compute the gradient; otherwise the finite-difference default costs an extra function evaluation per parameter. - Match the method to the constraints: L-BFGS-B for bounds, SLSQP or trust-constr for general constraints, BFGS for a smooth unconstrained problem, Nelder-Mead only when the objective is noisy or non-differentiable.
- Read
res.successandres.messageevery time. A plausible-lookingres.xfrom a failed run is the most common silent bug in this API. - Scale the variables. Optimisers compare step sizes, and a parameter measured in millions next to one measured in fractions will not converge sensibly.
Fitting a model to data
import numpy as np
from scipy.optimize import curve_fit
def decay(t, a, tau, c):
return a * np.exp(-t / tau) + c
rng = np.random.default_rng(0)
t = np.linspace(0.0, 5.0, 50)
y = decay(t, 2.5, 1.2, 0.1) + rng.normal(0.0, 0.05, t.size)
popt, pcov = curve_fit(decay, t, y, p0=[1.0, 1.0, 0.0])
perr = np.sqrt(np.diag(pcov))
print(popt) # roughly 2.5, 1.2, 0.1
print(perr) # one standard error per parameter
print(perr / popt) # relative error; near 1 means poorly constrained| Function | You supply | You get back | Reach for it when |
|---|---|---|---|
| minimize | A cost function | The optimum x | Only the parameters matter, not the residuals |
| curve_fit | A model f(x, *params) and data | popt, pcov | Least squares with a model you can write down |
| least_squares | A residual vector | Full result, robust losses, sparse Jacobian | You need bounds, robust loss, or a large problem |
| root_scalar | f(x) plus a bracket or guess | A root and convergence info | You are solving an equation, not minimising |
| linprog | Linear objective and constraints | An LP or MILP optimum | The problem is genuinely linear |
⚠️
curve_fit is a local optimiser: a poor p0 gives a wrong answer without raising anything. Plot the fitted curve over the data, try several starting points, and remember that with absolute_sigma=False (the default) pcov is scaled by the residual variance, so the errors are relative rather than absolute.FAQ
When is least_squares better than curve_fit?
When you need bounds, a robust loss such as
soft_l1 to tolerate outliers, or a sparse Jacobian for a large problem. curve_fit is a thin convenience wrapper over the same machinery for the ordinary case.The fit looks good but the parameters are nonsense.
The model is probably unidentifiable: two parameters trade off against each other, so many combinations fit equally well. Check the off-diagonal terms of
pcov for strong correlation and re-parameterise in terms of a quantity the data actually determines.Related
Vectorised maths and broadcasting Machine learning in one page
Last refreshed 2026-09-18.