Error Propagation with Python
When you compute a result from measured quantities, the uncertainties in those measurements carry through to the result. This page covers doing that propagation two ways: by hand (so you understand what’s happening) and with the uncertainties package (so you don’t have to do it by hand every time).
1 The Formula
For a function \(f(x, y, \dots)\) of independent quantities with uncertainties \(\sigma_x, \sigma_y, \dots\):
\[\sigma_f = \sqrt{\left(\frac{\partial f}{\partial x}\right)^2 \sigma_x^2 + \left(\frac{\partial f}{\partial y}\right)^2 \sigma_y^2 + \cdots}\]
Each term is a partial derivative (how much \(f\) changes when that variable changes) times that variable’s uncertainty. The terms add in quadrature. This assumes the uncertainties are independent — if two inputs are correlated, this formula needs an extra cross term.
1.1 Common Cases
Working the partials out for the combinations that come up most often gives the rules below. Each one is written to give \(\sigma_f\) directly, so they all combine the same way; \(a\), \(b\), and \(n\) are exact constants.
| \(f\) | \(\sigma_f\) |
|---|---|
| \(ax\) | \(\lvert a \rvert \, \sigma_x\) |
| \(ax \pm by\) | \(\sqrt{a^2 \sigma_x^2 + b^2 \sigma_y^2}\) |
| \(xy\) or \(x/y\) | \(\lvert f \rvert \sqrt{(\sigma_x/x)^2 + (\sigma_y/y)^2}\) |
| \(x^n\) | \(\lvert f \rvert \cdot \lvert n \rvert \, \sigma_x / x\) |
| \(\ln x\) | \(\sigma_x / x\) |
| \(e^x\) | \(\lvert f \rvert \, \sigma_x\) |
| \(g(x)\), any single variable | \(\lvert dg/dx \rvert \, \sigma_x\) |
Products, quotients, and powers are easiest to remember in relative form: for \(xy\) and \(x/y\) the relative uncertainties add in quadrature, and for \(x^n\) the relative uncertainty is multiplied by \(\lvert n \rvert\). Squaring a quantity doubles its relative uncertainty; taking a square root halves it.
Two places these rules mislead:
- Subtracting two close numbers. The absolute uncertainty of \(x - y\) is unremarkable, but the result is small, so the relative uncertainty can be enormous. A difference of two 1% measurements that nearly cancel can easily carry 100% uncertainty. If a measurement plan depends on a small difference of large numbers, that is worth catching before taking data rather than after.
- A variable appearing more than once. Every rule above assumes the inputs are independent. \(x \cdot x\) is not the product rule — \(x\) is perfectly correlated with itself, and the correct answer is the power rule for \(x^2\). Rewrite the expression so each measured quantity appears once, or use the package below, which tracks this automatically.
2 By Hand
For a product \(f = x \cdot y\), the partials are \(\partial f/\partial x = y\) and \(\partial f/\partial y = x\):
import numpy as np
x, sigma_x = 5.0, 0.1
y, sigma_y = 3.0, 0.2
f = x * y
sigma_f = np.sqrt((y * sigma_x) ** 2 + (x * sigma_y) ** 2)
print(f"f = {f:.2f} ± {sigma_f:.2f}")Doing it by hand is worth it once or twice to build intuition — for instance, that for a product the relative uncertainties add in quadrature. For routine work, let the library do it.
3 With the uncertainties Package
The uncertainties package attaches an uncertainty to a number and propagates it automatically through arithmetic and math functions.
from uncertainties import ufloat
x = ufloat(5.0, 0.1) # 5.0 ± 0.1
y = ufloat(3.0, 0.2) # 3.0 ± 0.2
print(f"x * y = {x * y}") # 15.0+/-1.1
print(f"x + y = {x + y}") # 8.0+/-0.22
print(f"x / y = {x / y}") # 1.67+/-0.12Access the pieces directly with .nominal_value and .std_dev:
result = x * y
print(result.nominal_value, result.std_dev)3.2 Math Functions
Use uncertainties.umath in place of math for functions like sin, cos, exp, log:
from uncertainties import ufloat
import uncertainties.umath as umath
angle = ufloat(0.5, 0.01) # radians
print(f"sin = {umath.sin(angle)}")
print(f"cos = {umath.cos(angle)}")
value = ufloat(2.0, 0.1)
print(f"exp = {umath.exp(value)}")
print(f"log = {umath.log(value)}")3.3 Arrays of Uncertain Values
unumpy extends this to arrays, so you can propagate uncertainties element-wise:
import numpy as np
from uncertainties import unumpy
nominal = np.array([1.0, 2.0, 3.0])
errors = np.array([0.1, 0.15, 0.2])
values = unumpy.uarray(nominal, errors)
squared = values ** 2
print("nominal:", unumpy.nominal_values(squared))
print("std dev:", unumpy.std_devs(squared))4 Worked Example
Combining several measured quantities, each with its own uncertainty, into one derived result — the propagation happens automatically:
from uncertainties import ufloat
import uncertainties.umath as umath
a = ufloat(0.50, 0.02)
b = ufloat(0.75, 0.03)
c = ufloat(100.0, 1.0)
result = umath.sqrt(a * b) / c
print(f"result = {result}") # value with propagated uncertaintyCompared to doing this by hand — three partial derivatives, squared, summed, square-rooted — the package is both faster and less error-prone. Reach for the by-hand formula when you need to understand which input dominates the uncertainty; reach for the package when you just need the number.
5 Statistical vs. Systematic Uncertainty
Propagation handles the uncertainties you feed it — but make sure you’re feeding it the right ones. Measurement uncertainty comes in two flavors:
- Statistical (random): fluctuates from measurement to measurement and averages down with more measurements (\(\sigma\) of the mean shrinks like \(1/\sqrt{N}\)). Example: voltage noise on a reading.
- Systematic: a consistent bias that affects every measurement the same way and does not average out. Example: a position axis that reads 0.1 mm high on every point.
Curve-fit uncertainties and repeated-measurement statistics capture the statistical part. Systematic uncertainties have to be identified and folded in separately, then propagated alongside the statistical ones. A complete uncertainty budget accounts for both.