⇦ Back

1 Rounding Off to a Whole Number

Python has a built-in function round() that will, as the name suggests, round a number off:

unrounded = 5.99
rounded = round(unrounded)

print(rounded)
## 6

Note that, by default, this uses the round half to even rule for tie-breaking multiples of 0.5. In other words, any number that ends in “.5” gets rounded to the nearest EVEN whole number:

# 2.5 gets rounded DOWN
print(round(2.5))
## 2
# 3.5 gets rounded UP
print(round(3.5))
## 4

This is true even for negative numbers:

print(round(-2.5))
## -2

This method of rounding off is known as statistician’s rounding and avoids both a positive/negative bias and a bias towards/away from zero

1.1 Rounding Down

The built-in math library contains the floor() function that will round numbers ‘towards the floor’ (ie towards negative infinity):

import math

negative_example = math.floor(-2.9)
positive_example = math.floor(2.9)

print(negative_example, positive_example, sep=', ')
## -3, 2

1.2 Rounding Up

The complement to floor() is ceil() which will round numbers ‘towards the ceiling’ (ie towards positive infinity):

negative_example = math.ceil(-2.9)
positive_example = math.ceil(2.9)

print(negative_example, positive_example, sep=', ')
## -2, 3

1.3 Rounding Towards Zero

The trunc() function truncates a number, which means that it throws away the part after the decimal point. This is equivalent to rounding towards zero:

negative_example = math.trunc(-2.9)
positive_example = math.trunc(2.9)

print(negative_example, positive_example, sep=', ')
## -2, 2

1.4 Rounding Away from Zero

There is no single function to do this in math (not even in numpy)! However, here’s a quick custom function you can use:

def round_away_from_zero(x):
    """Round a number x away from 0."""
    return math.ceil(abs(x)) * math.copysign(1, x)


negative_example = round_away_from_zero(-2.9)
positive_example = round_away_from_zero(2.9)

print(negative_example, positive_example, sep=', ')
## -3.0, 3.0

The abs() function takes the absolute value of the un-rounded number (ie makes it positive if it’s negative), rounds it up with ceil() then multiplies that answer by either -1 or 1 using the copysign() function to copy the sign of the original number.

2 Rounding Off to a Number of Decimal Places

The round() function can take a second argument, namely the number of decimal places you want to round off to:

unrounded = 6.283185
rounded = round(unrounded, 3)

print(rounded)
## 6.283

Again, this uses statistician’s rounding:

# Round DOWN to 6.28318
print(round(6.283185, 5))
## 6.28318

2.1 Rounding Using String Formatting

Another method for rounding off is to use the .xf notation with formatted strings (usually these will be ‘f-strings’ - see this page for more info - but you can also use this notation with the older .format() method or %-sign notation). .xf will round a floating-point number off to x places after the decimal point and it can be used with f-strings as follows:

unrounded = 6.283185
rounded = f'The number rounded to 3 decimal places is: {unrounded:.3f}'

print(rounded)
## The number rounded to 3 decimal places is: 6.283

Note that the result is a string not a number! So you can’t do any further calculation with it when using this method.

3 Rounding Off to a Number of Significant Figures

Instead of rounding off to a certain number of decimal places we often want to round off to a certain number of significant figures - digits excluding leading zeroes. A useful library that will do this is sigfig which can be installed from the terminal with:

# If you have a version of Python that isn't 3.9 use those version numbers instead
python3.9 -m pip install sigfig

This includes a new implementation of the round() function that overwrites the built-in one. It can round off to a given number of decimal places using the decimals keyword argument like so:

import sigfig

unrounded = 1.2345
rounded = sigfig.round(unrounded, decimals=3)

print(f'{1.2345} becomes {rounded}')
## 1.2345 becomes 1.235

Note that this function rounds half away from zero, also known as commercial rounding: 1.2345 gets rounded to 1.235 as opposed to statistician’s rounding which would make it 1.234

This implementation of round() can also round off to a given number of significant figures via the sigfigs keyword argument:

unrounded = 1.2345
rounded = sigfig.round(unrounded, sigfigs=3)

print(f'{1.2345} becomes {rounded}')
## 1.2345 becomes 1.23

Note that exponential notation is preserved unless you convert to a string before invoking the function:

unrounded = 1.2345e-7
exponential = sigfig.round(unrounded, sigfigs=3)
floating = sigfig.round(str(unrounded), sigfigs=3)

print(f'{1.2345e-7} becomes {exponential} or {floating}')
## 1.2345e-07 becomes 1.23e-07 or 0.000000123

This function can also incorporate uncertainty using the uncertainty keyword:

unrounded = 1.2345
rounded = sigfig.round(unrounded, uncertainty=0.0002)

print(f'{1.2345} becomes {rounded}')
## 1.2345 becomes 1.2345 ± 0.0002

And, finally, it can use different representations of uncertainty:

# Estimate Newton's gravitational constant
print('G =', sigfig.round('6.67430', '0.00015', format='Drake'), '×10^−11 m3⋅kg−1⋅s−2')
## G = 6.674 30(15) ×10^−11 m3⋅kg−1⋅s−2

3.1 Customising Sig Fig Notation

If you are absolutely set on using statistician’s rounding (or if you want to make any other modifications to how the sigfig package does its rounding), or if you only want to use built-in packages instead of relying on external dependencies, you can re-create the function yourself. This has been done below:

from decimal import Decimal, getcontext


def round_to_sig_figs(x, sigfigs):
    """Round a number off to a given number of significant figures."""
    # A special case is if the number is 0
    if float(x) == 0.0:
        output = '0' * sigfigs
        # Insert a decimal point
        output = output[0] + '.' + output[1:]

        return output

    # A special case is if the number is 'nan'
    if np.isnan(x):

        return 'None'

    # Set the precision
    getcontext().prec = sigfigs
    rounded = Decimal(x)
    # The precision only kicks in when an arithmetic operation is performed
    rounded = rounded * 1
    # Remove scientific notation
    # (if the order of magnitude of the number is larger than the number of
    # significant figures, the number will have been converted to scientific
    # notation)
    rounded = float(rounded)
    # Convert to string
    output = str(rounded)

    # Count the number of significant figures
    sigfigs_now = len(output.replace('-', '').replace('.', '').lstrip('0'))

    # Remove trailing zero if one exists and it is not necessary
    if ((sigfigs_now > sigfigs) and (output.endswith('.0'))):
        output = output.removesuffix('.0')

    # Add trailing zeroes if necessary
    if sigfigs_now < sigfigs:
        discrepancy = sigfigs - sigfigs_now
        # Append a decimal point if necessary
        if '.' not in output:
            output = output + '.'
        # Add trailing zeroes
        output = output + '0' * discrepancy

    return output

Let’s test it:

unrounded = 1.2345
rounded = round_to_sig_figs(unrounded, 4)

print(f'{1.2345} becomes {rounded}')
## 1.2345 becomes 1.234

4 Rounding Using Engineering Notation

Another useful library to have is engineering_notation. It can be installed from the terminal with:

# If you have a version of Python that isn't 3.9 use those version numbers instead
python3.9 -m pip install engineering_notation

This library contains the EngNumber() function which converts a number into ‘engineering notation’ (ie it represents it as a multiple of \(10^x\) or of \(1/10^x\) where \(x\) is a multiple of 3) using the usual SI units. It then rounds off to a given precision (number of decimal place; 2 by default) or to a given level of significance:

from engineering_notation import EngNumber

# Represent a large number in engineering notation with a unit
# Use default rounding (to 2 decimal places)
raw = 12345
formatted = EngNumber(raw)

print(f'{raw} becomes {formatted}')
## 12345 becomes 12.34k
# Represent a small number in engineering notation with a unit
# Round to 1 decimal place
raw = 0.012345
formatted = EngNumber(raw, precision=1)

print(f'{raw} becomes {formatted}')
## 0.012345 becomes 12.3m
# Represent a number in engineering notation
# Round to 4 significant figures
raw = 6.283185
formatted = EngNumber(raw, significant=4)

print(f'{raw} becomes {formatted}')
## 6.283185 becomes 6.283

4.1 Customising Engineering Notation

If you want to tweak the output of EngNumber() you can copy the function’s source code from GitHub and edit it yourself. Here’s an example of exactly that: the EngNumber() function re-moulded into a new function round_engineering() that represents the output in a variant of exponential notation:

from decimal import Decimal


def round_engineering(value, precision=2, significant=0):
    """
    Round and convert to an exponential format that is a multiple of 3.

    :return: A string representing the engineering number
    """
    number = Decimal(str(value))

    # Since the Decimal class only really converts numbers that are very small
    # into engineering notation we will simply make all numbers small numbers.
    # We can then take advantage of the Decimal class
    num_str = number * Decimal('10e-25')
    num_str = num_str.to_eng_string().lower()

    base, exponent = num_str.split('e')

    if significant > 0:
        if abs(Decimal(base)) >= 100.0:
            base = str(round(Decimal(base), significant - 3))
        elif abs(Decimal(base)) >= 10.0:
            base = str(round(Decimal(base), significant - 2))
        else:
            base = str(round(Decimal(base), significant - 1))
    else:
        base = str(round(Decimal(base), precision))

    if 'e' in base.lower():
        base = str(int(Decimal(base)))

    # Remove trailing decimals
    # https://stackoverflow.com/questions/3410976/how-to-round-a-number-to-significant-figures-in-python
    # https://stackoverflow.com/questions/11227620/drop-trailing-zeros-from-decimal
    if '.' in base:
        base = base.rstrip('.')

    # Remove trailing .00 in precision 2
    if precision == 2 and significant == 0:
        if '.00' in base:
            base = base[:-3]

    # Scale the exponent back to normal
    exponent = str(int(exponent) + 24)
    # Format the exponent
    if exponent == '0':
        exponent_str = ''
    else:
        exponent_str = '×10^' + exponent

    return base + exponent_str

Here’s how it’s used:

numbers = [
    1,
    12,
    123,
    1234,
    12345,
    123456,
    0.1234,
    1.234,
    12.34,
    123.4,
    1234,
]
for number in numbers:
    print(f'{number:>7} becomes {round_engineering(number, significant=3)}')
##       1 becomes 1.00
##      12 becomes 12.0
##     123 becomes 123
##    1234 becomes 1.23×10^3
##   12345 becomes 12.3×10^3
##  123456 becomes 123×10^3
##  0.1234 becomes 123×10^-3
##   1.234 becomes 1.23
##   12.34 becomes 12.3
##   123.4 becomes 123
##    1234 becomes 1.23×10^3

⇦ Back