“We fitted a Gaussian” is half of most results sections. scipy.optimize.curve_fit does the fit; the part people skip — and reviewers ask about — is the uncertainties, which come from the same call. This lesson does both.

Step 1 — Define the model

The model is a plain function: first argument x, then the parameters to fit:

import numpy as np

def gaussian(x, a, mu, sigma):
    return a * np.exp(-((x - mu) ** 2) / (2 * sigma ** 2))

Step 2 — Make some honest test data

While building the figure, synthetic data with known parameters tells you whether the fit machinery works — if it can’t recover mu = 5, it won’t recover your real peak either:

rng = np.random.default_rng(1)
x = np.linspace(0, 10, 80)
y = gaussian(x, a=1.0, mu=5.0, sigma=0.5) + rng.normal(0, 0.05, x.size)

Step 3 — Fit

from scipy.optimize import curve_fit

p0 = [1, 4, 1]                      # rough initial guesses: a, mu, sigma
popt, pcov = curve_fit(gaussian, x, y, p0=p0)
perr = np.sqrt(np.diag(pcov))       # 1-sigma uncertainties

for name, val, err in zip(["a", "mu", "sigma"], popt, perr):
    print(f"{name} = {val:.3f} ± {err:.3f}")

popt is the best-fit parameters; the diagonal of the covariance matrix gives their standard errors. The printed value ± error lines are exactly what goes in the paper.

Step 4 — Plot data and fit together

import matplotlib.pyplot as plt

xs = np.linspace(x.min(), x.max(), 400)
plt.scatter(x, y, s=10, label="data")
plt.plot(xs, gaussian(xs, *popt), linewidth=2, label="fit")
plt.xlabel("x")
plt.ylabel("signal")
plt.legend()

Evaluate the fitted curve on a fine grid (xs), not on the data points — a smooth line over discrete markers is the convention readers expect.

Step 5 — Export or insert

Export / Insert for the vector PDF or a placed figure with \label. Because the script is the saved source, the quoted parameters and the drawn curve can never drift apart — both come from the same popt.

Tip: If curve_fit returns nonsense, the initial guess p0 is almost always the culprit. Plot the model at p0 over the data once — if the starting curve is nowhere near, the optimizer has nothing to climb.