golu
golu

Reputation: 849

Round to significant figures in python

I want to round like this 42949672 -> 42949700, 2147483647 -> 2147480000, 4252017622 -> 4252020000 etc.

I tried to use following , but only works for the first one. How can I make a more general one? thanks

round(42949672, -2)

Upvotes: 6

Views: 20266

Answers (7)

Jagerber48
Jagerber48

Reputation: 965

I've developed a PyPi packaged called sciform which is designed to do this type of rounding. sciform is principally designed to round and format numbers into strings according to many options. For this use case sciform can be used to round a number to a certain number of sig figs and then the string can be converted back to a float (or int in this case).

from sciform import Formatter

sform = Formatter(
    round_mode="sig_fig",  # This is the default behavior
    ndigits=6,
)

for num in [42949672, 2147483647, 4252017622]:
    result = int(sform(num))
    print(f'{num} -> {result}')

# 42949672 -> 42949700
# 2147483647 -> 2147480000
# 4252017622 -> 4252020000

Upvotes: 0

Don H
Don H

Reputation: 41

These work for me, for ints and floats, both positive and negative. If desired they may be modified to preserve the original int or float type by applying isinstance() to x and then return int(float()) or float().

Note that format beyond around 14 digits can give unexpected results.

# Option 1: use math to find number of digits left of decimal
import math
def round_sig(x, sigdig=14):
    """Round x to sigdig significant digits"""
    if x == 0: return 0   # can't log10(0)
    # n = digits left of decimal, can be negative
    n = math.floor(math.log10(abs(x))) + 1
    return round(x, sigdig - n)

# Option 2: use format to round
def round_sig2(x, sigdig=14):
    """Round x to sigdig significant digits"""
    f = f'{{:.{sigdig - 1}E}}'
    s = f.format(x)
    return float(s)

# Option 3: Use format to find number of digits left of decimal
def round_sig3(x, sigdig=14):
    """Round x to sigdig significant digits"""
    s = '{:.0E}'.format(x)
    # n = digits left of decimal, can be negative
    n = int( s[s.index('E') + 1 :] ) + 1
    return round(x, sigdig - n)

# Bonus: use math to truncate
import math
def trunc_sig(x, sigdig=14):
    """Truncate x to sigdig significant digits"""
    if x == 0: return 0   # can't log10(0)
    # n = digits left of decimal, can be negative
    n = math.floor(math.log10(abs(x))) + 1
    shift = 10**(sigdig - n)
    return int(x * shift) / shift

# Unexpected format results with large number of significant digits
>>> '{:.20E}'.format(10**-1)
'1.00000000000000005551E-01'

>>> '{:.20E}'.format(10**-10)
'1.00000000000000003643E-10'

>>> '{:.20E}'.format(10**-20)
'9.99999999999999945153E-21'

>>> n = .123456789012345678901234567890
>>> '{:.20E}'.format(n)
'1.23456789012345677370E-01'

Upvotes: 0

Uladzimir Treihis
Uladzimir Treihis

Reputation: 33

Expanded on @david-joy's function. Indeed, it did not work properly in the else clause, and actually not all cases were covered in the if clause. So here is my function.

def round_to_nsf(value, nsf=2):
    """
    Rounds the number to the provided number of significant figures.
    """

    integer_part = math.floor(value)
    if integer_part > 0:
        integer_part_len = len(str(integer_part))
        return round(value, nsf-integer_part_len)
    else:
        str_value = str(value)
        #if of the form "8e-05"
        if '-' in str_value:
            index = int(str_value[str_value.find('-')+1:]) - 1
        else:
            st = {'1', '2', '3', '4', '5', '6', '7', '8', '9'}
            index = next((i for i, ch in enumerate(str(value)) if ch in st), None) - 2
        return round(value, index+nsf)

Upvotes: 0

David Joy
David Joy

Reputation: 31

To expand upon Autumn's answer, the reason that the accepted answer fails in the general case is that round is rounding to decimal places and not (necessarily) to significant figures.

The following function addresses the case when there are significant figures both before and after the decimal place:

import math
def round_to_nsf(number, nsf=6):
    integer_part = math.floor(number)
    return round(number, nsf - len(str(integer_part)))

although it doesn't address the case for numbers like 0.0000012345678, which rounded to 6 s.f. should be 0.00000123457.

The following code almost works - but is very inefficient, risks stack overflow through its use of recursion and won't always return the correct answer due to the fact that computers use binary arithmetic instead of decimal:

def round_to_nsf(number, nsf=6):
    integer_part = math.floor(number)
    if integer_part > 0:
        return round(number, nsf - len(str(integer_part)))
    else:
        return round_to_nsf(10 * number, nsf) / 10

But I think that the else clause could be modified by transforming the number into a string, using the main function in a single recursive step, then constructing the output from strings in order to obtain a non-recurring decimal.

Upvotes: 3

Autumn
Autumn

Reputation: 3756

The accepted answer has limitations and does not produce technically correct significant figures in the general case.

numpy.format_float_positional supports the desired behaviour directly. The following fragment returns the float x formatted to 4 significant figures, with scientific notation suppressed.

import numpy as np
x=12345.6
np.format_float_positional(x, precision=4, unique=False, fractional=False, trim='k')
> 12340.

Upvotes: 10

pilatipus
pilatipus

Reputation: 91

To round to any digits that you are targeting, you can simply put the digits as a negative number in your round function and then convert it to integer.

digits = 4
num = 2147483647
rounded_num = int(round(num, -digits))

If you are trying to keep 6 significant figures always, that's the pattern that I saw in your question you would want to do this:

def sf_round(num, sig_fig):
    return int(round(num, -(len(str(num))-sig_fig)))

Upvotes: 0

Reinstate Monica
Reinstate Monica

Reputation: 4723

I assume you want to round to 6 significant figures. If you want to round ints, you can use

def round_sf(number, significant):
    return round(number, significant - len(str(number)))

print(round_sf(4252017622, 6))

Result:

4252020000

Upvotes: 1

Related Questions