Reporting Values to the Right Number of Digits

NoteQuestions
  • How do I decide how many decimal places to report for a measured value?
  • How do I print a value and its uncertainty rounded consistently in Python?
NoteObjectives
  • Apply the rule: round the uncertainty to 1 significant figure, then round the value to the same decimal place.
  • Compute the required number of decimal places automatically from the uncertainty.
  • Use f-string formatting to display the rounded result, including trailing zeros.
  • Explain why round() alone is not sufficient for formatted output.

The problem: Python gives too many digits

curve_fit returns parameter values and a covariance matrix, and the numbers come out with fifteen or sixteen decimal digits — far more than any experiment justifies. Reporting all of them misleads the reader about the precision of the result.

import numpy as np
from scipy.optimize import curve_fit

rng = np.random.default_rng(42)
x = np.linspace(0, 10, 20)
y = 2.5 * x + 1.3 + rng.normal(0, 0.8, size=20)

def linear(x, m, c):
    return m * x + c

popt, pcov = curve_fit(linear, x, y)
perr = np.sqrt(np.diag(pcov))

slope, intercept = popt
slope_err, intercept_err = perr

print(f"slope     = {slope}")
print(f"slope_err = {slope_err}")
slope     = 2.538584392123583
slope_err = 0.05190574911517593

Sixteen digits imply that the slope is known to one part in 10¹⁵ — which no physics experiment ever achieves.

The reporting rule

In this course we use two conventions that are standard in experimental physics:

  1. Round the uncertainty to 1 significant figure.
  2. Round the value to the same decimal place as the rounded uncertainty.

For slope_err = 0.0519... the leading non-zero digit is in the hundredths place (10⁻²), so we round to 2 decimal places and report 0.05. The slope is then also rounded to 2 decimal places: 2.54.

The result is reported as:

\[m = 2.54 \pm 0.05\]

Computing the number of decimal places

The key step is finding which decimal place holds the leading digit of the uncertainty. numpy.floor(numpy.log10(sigma)) gives the exponent of that digit, and negating it gives the number of decimal places needed:

sigma = slope_err

exponent  = int(np.floor(np.log10(abs(sigma))))  # e.g. -2 for sigma ~ 0.05
ndigits   = -exponent                             # e.g.  2 decimal places

print(f"sigma    = {sigma}")
print(f"exponent = {exponent}")
print(f"ndigits  = {ndigits}")
sigma    = 0.05190574911517593
exponent = -2
ndigits  = 2

A few examples to build intuition:

sigma exponent ndigits rounded sigma
0.052 −2 2 0.05
0.31 −1 1 0.3
4.7 0 0 5
43 1 −1 40

Formatting with f-strings

Pass ndigits directly into an f-string format specifier to control the number of decimal places:

print(f"slope     = {slope:.{ndigits}f} ± {sigma:.{ndigits}f}")
slope     = 2.54 ± 0.05

Apply the same pattern to the intercept, computing its own ndigits:

int_exponent = int(np.floor(np.log10(abs(intercept_err))))
int_ndigits  = -int_exponent

print(f"intercept = {intercept:.{int_ndigits}f} ± {intercept_err:.{int_ndigits}f}")
intercept = 1.1 ± 0.3

For convenience, wrap this in a small helper:

def format_result(value, uncertainty):
    """Return 'value ± uncertainty' rounded to 1 sig fig on the uncertainty."""
    ndigits = -int(np.floor(np.log10(abs(uncertainty))))
    if ndigits >= 0:
        return f"{value:.{ndigits}f} ± {uncertainty:.{ndigits}f}"
    else:
        # uncertainty >= 10: round to the nearest 10, 100, …
        v = round(value,       ndigits)
        u = round(uncertainty, ndigits)
        return f"{v:.0f} ± {u:.0f}"

print("slope    ", format_result(slope,     slope_err))
print("intercept", format_result(intercept, intercept_err))
slope     2.54 ± 0.05
intercept 1.1 ± 0.3

Warning: round() drops trailing zeros

A common mistake is to round a value with round() and then print it, expecting to see the correct number of decimal places. Python floats carry no information about trailing zeros — 2.50 and 2.5 are identical objects — so whenever the rounded value ends in zero, Python prints the shorter form:

# concrete example: a value that rounds to X.X0
demo_value = 3.5049
demo_ndigits = 2

rounded = round(demo_value, demo_ndigits)
print(f"round() stored as float: {rounded}")        # prints 3.5, not 3.50
print(f"formatted at print:      {demo_value:.{demo_ndigits}f}")  # prints 3.50
round() stored as float: 3.5
formatted at print:      3.50

The same problem appears with the fit parameters: if slope happened to round to 2.50, storing it first would silently drop the trailing zero:

# wrong approach: round first, then print
x_stored = round(slope, ndigits)
print(f"stored then printed: {x_stored}")    # trailing zero may vanish

# correct: apply the format at the point of printing
print(f"formatted at print:  {slope:.{ndigits}f}")
stored then printed: 2.54
formatted at print:  2.54

The same issue affects conversion to a string: str(round(slope, 2)) may return '2.5' even when the correctly rounded value is 2.50. Always format with f"{value:.{ndigits}f}" or format(value, f'.{ndigits}f').


Note: why 1 significant figure is enough

Reporting more than 1 sig fig on an uncertainty implies a precision in the uncertainty itself that the data usually cannot support.

The standard deviation \(\sigma\) estimated from \(N\) measurements has its own statistical uncertainty of approximately \(\sigma / \sqrt{2(N-1)}\). For typical lab datasets:

for N in [5, 10, 20, 50, 100]:
    rel_unc = 1 / np.sqrt(2 * (N - 1))
    print(f"N = {N:2d}  ->  uncertainty on σ ≈ {rel_unc*100:.0f}%")
N =  5  ->  uncertainty on σ ≈ 35%
N = 10  ->  uncertainty on σ ≈ 24%
N = 20  ->  uncertainty on σ ≈ 16%
N = 50  ->  uncertainty on σ ≈ 10%
N = 100  ->  uncertainty on σ ≈ 7%

With 10 measurements the standard deviation is itself only known to about 24%. Reporting σ = 0.052 instead of σ = 0.05 implies you know the uncertainty to better than 4% — a level of precision that requires hundreds of measurements to achieve. The second digit is not meaningful.

This is why the convention “1 sig fig on the uncertainty” is so widely used. Unless you make 5,000 measurements you can’t be certain of the value of the second significant figure.

TipKey Points
  • Round the uncertainty to 1 significant figure; round the value to the same decimal place.
  • Compute decimal places with ndigits = -int(numpy.floor(numpy.log10(abs(sigma)))).
  • Format output with f"{value:.{ndigits}f} ± {sigma:.{ndigits}f}".
  • Do not rely on round() followed by print() — trailing zeros are silently dropped. Apply formatting at the point of printing.
  • 1 sig fig is statistically honest: with fewer than ~100 measurements the uncertainty on \(\sigma\) itself exceeds 10%.