Python SciPy Leastsq: Solve Non-Linear Least Squares Problems

I was working on a data analysis project where I needed to fit a complex exponential decay model to experimental measurements. The challenge was that my model was non-linear, making traditional linear regression methods unsuitable. This is where SciPy’s leastsq function came to the rescue.

In this article, I’ll share how to use SciPy’s leastsq function to solve nonlinear least squares problems in Python. Whether you’re fitting complex models to experimental data or optimizing parameters in scientific computing, this useful tool can help you get accurate results with minimal effort.

So let’s start..

SciPy’s Leastsq Function

SciPy’s leastsq function is part of the scipy.optimize module and provides a way to solve nonlinear least squares problems. It’s essentially used to find the parameters that minimize the sum of squares of a set of functions.

In simpler terms, when you have a model with unknown parameters and you want to find the parameter values that best fit your data, leastsq can help you do that, even when your model is non-linear.

Basic Usage of Leastsq

Let’s start with a simple example of how to use the leastsq function:

import numpy as np
from scipy.optimize import leastsq
import matplotlib.pyplot as plt

# Generate some sample data with noise
def true_func(x):
    return 2.5 * np.exp(-x * 0.5) + 0.5

# Create noisy data
x_data = np.linspace(0, 5, 50)
y_true = true_func(x_data)
y_noise = 0.2 * np.random.normal(size=len(x_data))
y_data = y_true + y_noise

# Define the function to minimize (residuals)
def residuals(params, x, y):
    a, b, c = params
    model = a * np.exp(-b * x) + c
    return model - y

# Initial guess for parameters
initial_guess = [1.0, 1.0, 1.0]

# Perform the fit
result, success = leastsq(residuals, initial_guess, args=(x_data, y_data))

# Extract the optimized parameters
a_opt, b_opt, c_opt = result

# Plot the results
plt.figure(figsize=(10, 6))
plt.scatter(x_data, y_data, label='Noisy data')
plt.plot(x_data, true_func(x_data), 'r-', label='True function')
plt.plot(x_data, a_opt * np.exp(-b_opt * x_data) + c_opt, 'g--', label='Fitted function')
plt.legend()
plt.xlabel('X')
plt.ylabel('Y')
plt.title('Exponential Decay Fitting with Leastsq')
plt.show()

print(f"Optimized parameters: a={a_opt:.4f}, b={b_opt:.4f}, c={c_opt:.4f}")
print(f"True parameters: a=2.5000, b=0.5000, c=0.5000")

You can see the output in the screenshot below.

scipy optimize least squares

In this example, we:

  1. Generate synthetic data following an exponential decay with added noise
  2. Define a residual function that computes the difference between our model and the data
  3. Provide initial parameter guesses
  4. Use leastsq to find the optimal parameters
  5. Plot the results to visualize the fit

Read Python SciPy Exponential

Advanced Usage: Working with Full Jacobian

For more complex problems, you might want to provide the Jacobian matrix of the residual function. This can significantly improve the performance and accuracy of the fitting process.

import numpy as np
from scipy.optimize import leastsq
import matplotlib.pyplot as plt

# Generate data for a damped oscillation
def true_func(x, params):
    a, w, phi, c = params
    return a * np.exp(-0.1 * x) * np.cos(w * x + phi) + c

# True parameters: amplitude, frequency, phase, offset
true_params = [2.0, 1.5, 0.3, 0.5]

# Create sample data with noise
x_data = np.linspace(0, 10, 100)
y_true = true_func(x_data, true_params)
y_noise = 0.2 * np.random.normal(size=len(x_data))
y_data = y_true + y_noise

# Define the residual function
def residuals(params, x, y):
    a, w, phi, c = params
    model = a * np.exp(-0.1 * x) * np.cos(w * x + phi) + c
    return model - y

# Define the Jacobian (partial derivatives with respect to each parameter)
def jacobian(params, x, y):
    a, w, phi, c = params
    exp_term = np.exp(-0.1 * x)
    cos_term = np.cos(w * x + phi)
    sin_term = np.sin(w * x + phi)

    # Partial derivatives
    da = exp_term * cos_term
    dw = -a * exp_term * x * sin_term
    dphi = -a * exp_term * sin_term
    dc = np.ones_like(x)

    return np.vstack([da, dw, dphi, dc]).T

# Initial guess
initial_guess = [1.0, 1.0, 0.0, 0.0]

# Perform the fit with Jacobian
result, success = leastsq(residuals, initial_guess, args=(x_data, y_data), 
                         Dfun=jacobian, col_deriv=0, full_output=1)

# Extract the optimized parameters
params_opt = result[0]
a_opt, w_opt, phi_opt, c_opt = params_opt

# Plot the results
plt.figure(figsize=(12, 6))
plt.scatter(x_data, y_data, alpha=0.6, label='Noisy data')
plt.plot(x_data, true_func(x_data, true_params), 'r-', label='True function')
plt.plot(x_data, true_func(x_data, params_opt), 'g--', label='Fitted function')
plt.legend()
plt.xlabel('Time (s)')
plt.ylabel('Amplitude')
plt.title('Damped Oscillation Fitting with Jacobian')
plt.grid(True)
plt.show()

print("True parameters:", true_params)
print("Fitted parameters:", [round(p, 4) for p in params_opt])

You can see the output in the screenshot below.

scipy least squares

By providing the Jacobian, we give the algorithm analytical derivatives instead of having it estimate them numerically. This makes the fitting process more accurate and efficient, especially for complex models.

Check out Python SciPy Confidence Interval

Common Challenges Faced While Working with SciPy’s Leastsq Function and Solutions

Now, I will explain to you the common challenges that can be faced while working with SciPy’s Leastsq Function.

1. Convergence Issues

If your fitting process doesn’t converge, try these approaches:

# Improve convergence with better initial guesses
# Instead of:
initial_guess = [1.0, 1.0, 1.0]

# Try a more informed guess based on data characteristics:
amplitude_guess = np.max(y_data) - np.min(y_data)
offset_guess = np.mean(y_data)
decay_guess = 0.5  # Domain knowledge
initial_guess = [amplitude_guess, decay_guess, offset_guess]

# You can also try different method parameters
result, success = leastsq(residuals, initial_guess, args=(x_data, y_data),
                         ftol=1e-10, xtol=1e-10, maxfev=10000)

2. Parameter Bounds

The leastsq function doesn’t directly support parameter bounds. If you need bounded optimization, consider using scipy.optimize.least_squares (a newer alternative):

from scipy.optimize import least_squares

# Define bounds (lower and upper bounds for each parameter)
lower_bounds = [0, 0, 0]  # All parameters must be positive
upper_bounds = [10, 5, 2]  # Upper limits for each parameter

# Use least_squares instead of leastsq
result = least_squares(residuals, initial_guess, 
                      args=(x_data, y_data),
                      bounds=(lower_bounds, upper_bounds))

# Extract optimized parameters
params_opt = result.x

Read Python SciPy Minimize

3. Weight Data Points

Sometimes, not all data points are equally reliable. You can add weights to give more importance to certain measurements:

# Define weights (higher weight = more important)
weights = 1.0 / np.array([0.1, 0.2, 0.1, 0.3, 0.2, 0.1, 0.2, 0.3, 0.2, 0.1])
# Modify the residual function to include weights
def weighted_residuals(params, x, y, weights):
    model = some_model_function(x, params)
    return weights * (model - y)

# Use the weighted residuals in leastsq
result, success = leastsq(weighted_residuals, initial_guess, 
                         args=(x_data, y_data, weights))

Check out Python SciPy Freqz

Real-World Example: Temperature Sensor Calibration

Let’s look at a more practical example. Imagine you’re working for a weather station in New York City, and you need to calibrate temperature sensors. You have measurements from a reference sensor and your new sensor that needs calibration.

import numpy as np
from scipy.optimize import leastsq
import matplotlib.pyplot as plt

# Reference temperature readings (°F) in NYC throughout a day
reference_temps = np.array([55.2, 57.8, 60.5, 65.3, 72.1, 75.6, 78.4, 
                           79.2, 77.5, 74.3, 70.1, 65.8, 60.2])

# Your uncalibrated sensor readings for the same times
uncalibrated_temps = np.array([54.0, 56.3, 58.9, 63.8, 70.5, 73.9, 76.5, 
                              77.0, 75.2, 72.1, 68.0, 63.5, 58.1])

# Define the calibration model: reference = a * uncalibrated + b
def residuals(params, uncalibrated, reference):
    a, b = params
    return a * uncalibrated + b - reference

# Initial guess for calibration parameters
initial_guess = [1.0, 0.0]  # Start with no adjustment

# Perform the calibration
result, success = leastsq(residuals, initial_guess, args=(uncalibrated_temps, reference_temps))

# Extract the optimized parameters
a_opt, b_opt = result

# Apply calibration to the uncalibrated data
calibrated_temps = a_opt * uncalibrated_temps + b_opt

# Plot the results
plt.figure(figsize=(10, 6))
plt.scatter(uncalibrated_temps, reference_temps, label='Uncalibrated vs Reference')
plt.plot([min(uncalibrated_temps), max(uncalibrated_temps)], 
         [min(uncalibrated_temps), max(uncalibrated_temps)], 'k--', label='Perfect calibration')
plt.plot(uncalibrated_temps, calibrated_temps, 'r-', label='Calibrated')
plt.legend()
plt.xlabel('Sensor Temperature (°F)')
plt.ylabel('Reference Temperature (°F)')
plt.title('Temperature Sensor Calibration')
plt.grid(True)
plt.show()

print(f"Calibration parameters: a={a_opt:.4f}, b={b_opt:.4f}")
print(f"This means: Reference = {a_opt:.4f} × Sensor + {b_opt:.4f}")

You can see the output in the screenshot below.

scipy optimize leastsq

In this example, we calibrated a temperature sensor by finding the linear relationship between the uncalibrated readings and reference values, which is a common task in instrumentation and measurement science.

Read Python SciPy Stats Multivariate_Normal

Leastsq vs. Curve_fit: Which One to Choose?

SciPy provides another function called curve_fit that’s built on top of leastsq. It’s often easier to use for common curve fitting tasks:

from scipy.optimize import curve_fit

# Define your model function
def model_func(x, a, b, c):
    return a * np.exp(-b * x) + c

# Fit using curve_fit
params_opt, params_cov = curve_fit(model_func, x_data, y_data)

# params_opt contains the optimized parameters
# params_cov gives the covariance matrix (useful for error estimation)

So, when should you use each?

  • Use curve_fit when:
    • You have a simple model function
    • You want parameter error estimates
    • You prefer a simpler interface
  • Use leastsq when:
    • You need more control over the optimization process
    • Your residual function is complex
    • You want to provide an analytical Jacobian
    • You’re solving a system of equations, not just fitting a curve

Error Analysis with Leastsq

Understanding the uncertainty in your fitted parameters is crucial for scientific work:

import numpy as np
from scipy.optimize import leastsq
from scipy import stats

# After running leastsq with full_output=True
result = leastsq(residuals, initial_guess, args=(x_data, y_data), full_output=True)
params_opt, cov_x, infodict, mesg, ier = result

# Number of data points and parameters
n = len(x_data)
p = len(initial_guess)

# Residual sum of squares
residual = residuals(params_opt, x_data, y_data)
ss_res = np.sum(residual**2)

# Degrees of freedom
dof = max(0, n - p)

# Compute reduced chi-squared
chi_squared = ss_res / dof

# Scale covariance matrix
if cov_x is not None:
    cov_x = cov_x * chi_squared
    
    # Standard errors
    param_errors = np.sqrt(np.diag(cov_x))
    
    # Confidence intervals (95%)
    t_value = stats.t.ppf(0.975, dof)
    conf_intervals = np.array([(params_opt[i] - t_value * param_errors[i], 
                               params_opt[i] + t_value * param_errors[i]) 
                               for i in range(p)])
    
    print("Parameter values and 95% confidence intervals:")
    for i, (param, error, interval) in enumerate(zip(params_opt, param_errors, conf_intervals)):
        print(f"p{i} = {param:.4f} ± {error:.4f} ({interval[0]:.4f}, {interval[1]:.4f})")

This code calculates standard errors and confidence intervals for your fitted parameters, giving you a measure of how reliable your results are.

Read Python SciPy Stats Mode

Best Practices for Using Leastsq

Based on my years of experience working with SciPy’s optimization tools, here are some best practices:

  1. Start with reasonable initial guesses: The closer your initial guess is to the solution, the faster and more reliably leastsq will converge.
  2. Normalize your data: If your x and y variables have very different scales, normalize them before fitting to improve numerical stability.
  3. Provide analytical Jacobians when possible: This improves both speed and accuracy, especially for complex models.
  4. Check the return status: The second output from leastsq tells you if the optimization was successful.
  5. Plot your results: Always visualize your fits to ensure they make sense.
  6. Experiment with different models: Sometimes, a simpler model can give more robust results than a complex one.
  7. Use bounds when needed: If you need parameter constraints, consider using least_squares instead of leastsq.

Check out Python SciPy Eigenvalues

Conclusion

SciPy’s leastsq function is a useful tool for solving nonlinear least squares problems in Python. I’ve used it extensively in my work, from fitting complex physical models to calibrating sensors and analyzing experimental data.

While it may take some practice to master, the flexibility and power it offers make it worth the effort, especially for scientific and engineering applications where accurate parameter estimation is crucial.

You may also read other SciPy-related articles.

51 Python Programs

51 PYTHON PROGRAMS PDF FREE

Download a FREE PDF (112 Pages) Containing 51 Useful Python Programs.

pyython developer roadmap

Aspiring to be a Python developer?

Download a FREE PDF on how to become a Python developer.

Let’s be friends

Be the first to know about sales and special discounts.