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 jac when 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.success and res.message every time. A plausible-looking res.x from 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
FunctionYou supplyYou get backReach for it when
minimizeA cost functionThe optimum xOnly the parameters matter, not the residuals
curve_fitA model f(x, *params) and datapopt, pcovLeast squares with a model you can write down
least_squaresA residual vectorFull result, robust losses, sparse JacobianYou need bounds, robust loss, or a large problem
root_scalarf(x) plus a bracket or guessA root and convergence infoYou are solving an equation, not minimising
linprogLinear objective and constraintsAn LP or MILP optimumThe 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.

Vectorised maths and broadcasting Machine learning in one page

Last refreshed 2026-09-18.