How to round a number to significant figures in Python
Asked Answered
N

27

215

I need to round a float to be displayed in a UI. e.g, to one significant figure:

1234 -> 1000

0.12 -> 0.1

0.012 -> 0.01

0.062 -> 0.06

6253 -> 6000

1999 -> 2000

Is there a nice way to do this using the Python library, or do I have to write it myself?

Nesselrode answered 5/8, 2010 at 0:48 Comment(4)
Are you just formatting the output? Are you asking about this? docs.python.org/library/stdtypes.html#string-formatting or this? docs.python.org/library/string.html#string-formattingOutpour
what output do you expect for 0.062 and 6253?Gainly
The package to-precision now does this. My posted answer details how this applies.Loop
The answer by @Falken gives the requested results, which are correct. Nearly all the others give results like 1000.0 with trailing decimal points and/or zeros, which in standard practice indicate far more precision.Contingence
S
204

You can use negative numbers to round integers:

>>> round(1234, -3)
1000.0

Thus if you need only most significant digit:

>>> from math import log10, floor
>>> def round_to_1(x):
...   return round(x, -int(floor(log10(abs(x)))))
... 
>>> round_to_1(0.0232)
0.02
>>> round_to_1(1234243)
1000000.0
>>> round_to_1(13)
10.0
>>> round_to_1(4)
4.0
>>> round_to_1(19)
20.0

You'll probably have to take care of turning float to integer if it's bigger than 1.

Spilt answered 5/8, 2010 at 2:57 Comment(13)
This is the correct solution. Using log10 is the only proper way to determine how to round it.Cephalad
How would you adapt this to allow the user to choose an arbitrary number of significant figures? This solution is only good for 1 sig fig.Leaden
round_to_n = lambda x, n: round(x, -int(floor(log10(x))) + (n - 1))Jinja
You should use log10(abs(x)), otherwise negative numbers will fail (And treat x == 0 separately of course)Fulk
Also, be aware of the usual rounding error problem, see hereFulk
@TobiasKienzler it works with negative numbers -- try it. It was actually very engineous what Evgeny did there.Sike
@TobiasKienzler, ah, I stand corrected, sorry... I don't know what I was thinking... I have decimal numbers (with "negative" expoent), and I guess I was thinking/trying those...Sike
@RicardoCruz No worries :D I took the liberty of editing the answer, since Evgeny hasn't been around ever since before I left that comment...Fulk
As @TobiasKienzler says, be aware of the known limitations of round(). For example, this function fails to properly round up the float 0.075 (it returns 0.07).Leapfrog
I have created a package that does this now and is probably easier and more robust than this one. Post Link, Repo Link. Hope this helps!Loop
round_to_n = lambda x, n: x if x == 0 else round(x, -int(math.floor(math.log10(abs(x)))) + (n - 1)) protects against x==0 and x<0 Thank you @RoyHyunjinHan and @TobiasKienzler . Doesn't protected against undefined like math.inf, or garbage like None etcSecateurs
@Leapfrog that's because 0.075 is actually 0.07499999999999999722444243843710864894092082977294921875. See Is floating point math broken?.Dorcasdorcea
This gives the wrong answer. 1000.0 is not the same as 1000, since in standard practice the former indicates 5 digits of precision. The answer by @Falken should be accepted instead since it gives the requested results.Contingence
N
134

%g in string formatting will format a float rounded to some number of significant figures. It will sometimes use 'e' scientific notation, so convert the rounded string back to a float then through %s string formatting.

>>> '%s' % float('%.1g' % 1234)
'1000'
>>> '%s' % float('%.1g' % 0.12)
'0.1'
>>> '%s' % float('%.1g' % 0.012)
'0.01'
>>> '%s' % float('%.1g' % 0.062)
'0.06'
>>> '%s' % float('%.1g' % 6253)
'6000.0'
>>> '%s' % float('%.1g' % 1999)
'2000.0'
Nesselrode answered 5/8, 2010 at 4:24 Comment(10)
For me it was: >>> '%s' % float('%.1g' % 1234) '1000.0'Spilt
The OP's requirement was for 1999 to be formatted as '2000', not as '2000.0'. I can't see a trivial way to change your method to achieve this.Armageddon
Note that the behaviour of %g is not always correct. In particular it always trims trailing zeros even if they are significant. The number 1.23400 has 6 significant digits, but "%.6g" %(1.23400) will result in "1.234" which is incorrect. More details in this blog post: randlet.com/blog/python-significant-figures-formatCharla
The float trick doesn't work here, we have to use the Decimal class instead, to convert scientific back to normal: '%s' % Decimal('%.1g' % .000000999) (Which, of cooourse, doesn't work with: '%.1g' % 10 -> '1e+01', so we use different methods for > 1 and < 1...)Fishbowl
Bingo! Solved it for me and no imports. Thank you Peter Graham.Bathelda
Just like the method in Evgeny's answer, this fails to correctly round 0.075 to 0.08. It returns 0.07 instead.Leapfrog
round_sig = lambda f,p: float(('%.' + str(p) + 'e') % f) allows you to adjust the number of significant digits!Gaddi
As @TimMartin notes this answer gives the wrong results. See Falken's clever answer for the right results.Contingence
"{1999:.1g}" or "{:.1g}".format(6253) in newer versions of PythonCornett
@Leapfrog That is in fact expected rounding behaviour in Python3, in an attempt to counter the rounding bias. More about Python rounding here.En
O
91
f'{float(f"{i:.1g}"):g}'
# Or with Python <3.6,
'{:g}'.format(float('{:.1g}'.format(i)))

This solution is different from all of the others because:

  1. it exactly solves the OP question
  2. it does not need any extra package
  3. it does not need any user-defined auxiliary function or mathematical operation

For an arbitrary number n of significant figures, you can use:

print('{:g}'.format(float('{:.{p}g}'.format(i, p=n))))

Test:

a = [1234, 0.12, 0.012, 0.062, 6253, 1999, -3.14, 0., -48.01, 0.75]
b = ['{:g}'.format(float('{:.1g}'.format(i))) for i in a]
# b == ['1000', '0.1', '0.01', '0.06', '6000', '2000', '-3', '0', '-50', '0.8']

Note: with this solution, it is not possible to adapt the number of significant figures dynamically from the input because there is no standard way to distinguish numbers with different numbers of trailing zeros (3.14 == 3.1400). If you need to do so, then non-standard functions like the ones provided in the to-precision package are needed.

Oxidase answered 15/2, 2018 at 17:10 Comment(10)
FYI: I found this solution independently from eddygeek while I was trying to solve the very same problem in one of my code. Now I realize that my solution is, obviously, almost identical to his (I just noticed the erroneous output and didn't bother to read the code, my mistake). Probably a short comment beneath his answer would have been enough instead of a new answer... The only (key) difference is the double use of the :g formatter which preserve integers.Oxidase
Wow, your answer needs to be really read from top to bottom ;) This double-cast trick is dirty, but neat. (Note that 1999 formatted as 2000.0 suggests 5 significant digits, so it has to go through {:g} again.) In general, integers with trailing zeros are ambiguous with regard to significant figures, unless some technique (like overline above last significant) is used.Displace
Do you mind to just walk me through how this works? What are :.1g and :g I dont think ive come across either before, and havent found anything in the docs eitherAerugo
@Aerugo There is an explanation in the doc here for g. The first formatting {:.1g} keeps one significant digit but it is not fully satisfying if you provide a float because you may end up with something like 1000.0. The second {:g} ensures that a float with only 0s after the decimal point becomes an integer (1000).Oxidase
Sorry, my previous comment was not really thorough, let's try again: {:.1g} keeps one significant digit but may return a number in exponent notation. To avoid that, we use float(). But then we get 1000.0 which is not the correct notation for a number with one significant figure. Instead, we want 1000. To ensure that, we use another {:g}. As per the doc: 'the decimal point is also removed if there are no remaining digits following it'. I'm pretty sure that part was missing from the doc when I wrote this answer, not sure how I found that, it was such a long time ago! ;-)Oxidase
although your answer doesn't use any auxiliary function, it makes the resulting code very write-once-read-none, in my opinion. I think it might be good to make an auxiliary function, if such a thing is possible in this scenario.Ledaledah
For some reason, this solution fails for longer precisions. '{:g}'.format(float('{:.{p}g}'.format(0.123456789012, p=20))) results in 0.123457.Sammiesammons
@TomSwirly The reason why you are getting a maximum precision of 6 is for the first '{:g}' (based on the documentation: "With no precision given, uses a precision of 6 significant digits"). If you pass the precision in that '{:g}' too, you will get the correct result. The code would be: '{:.{p}g}'.format(float('{:.{p}g}'.format(0.123456789012, p=12)),p=12) resulting in 0.123456789012Depredate
Your answer seems perfectly correct. Thanks for the clarification!Sammiesammons
Is there a reason that you suggest f-strings for 1 significant digit, but .format for an arbitrary number n of significant digits? It seems that this works too: f'{float(f"{i:.{n}g}"):g}'Hoelscher
L
74

If you want to have other than 1 significant decimal (otherwise the same as Evgeny):

>>> from math import log10, floor
>>> def round_sig(x, sig=2):
...   return round(x, sig-int(floor(log10(abs(x))))-1)
... 
>>> round_sig(0.0232)
0.023
>>> round_sig(0.0232, 1)
0.02
>>> round_sig(1234243, 3)
1230000.0
Lindeberg answered 5/8, 2010 at 9:49 Comment(8)
round_sig(-0.0232) -> math domain error, you may want to add an abs() in there ;)Ormazd
Just like the methods in Evgeny's and Peter Graham's answers, this fails to correctly round 0.075 to 0.08. It returns 0.07 instead.Leapfrog
Also it fails for round_sig(0).Vampirism
@Leapfrog That is a built in "feature" of python running on your computer, and manifest itself in its behavior of the function round. docs.python.org/2/tutorial/floatingpoint.html#tut-fp-issuesRapparee
@Leapfrog I've added an answer that explains why you should expect to get 0.7 back from rounding "0.075"! see https://mcmap.net/q/66816/-how-to-round-a-number-to-significant-figures-in-pythonNeoimpressionism
Thanks Sam, I've commented your answer.Leapfrog
For those of us working with large data sets, this function fails for x = 0. You will need to add if x == 0: return x to this. Or, if you are using it in a looping style like I am, if x[i][j]==0: continue.Abram
Python 3 note: int() can be omitted, because math.floor returns integer.Koren
N
16

To directly answer the question, here's my version using naming from the R function:

import math

def signif(x, digits=6):
    if x == 0 or not math.isfinite(x):
        return x
    digits -= math.ceil(math.log10(abs(x)))
    return round(x, digits)

My main reason for posting this answer are the comments complaining that "0.075" rounds to 0.07 rather than 0.08. This is due, as pointed out by "Novice C", to a combination of floating point arithmetic having both finite precision and a base-2 representation. The nearest number to 0.075 that can actually be represented is slightly smaller, hence rounding comes out differently than you might naively expect.

Also note that this applies to any use of non-decimal floating point arithmetic, e.g. C and Java both have the same issue.

To show in more detail, we ask Python to format the number in "hex" format:

0.075.hex()

which gives us: 0x1.3333333333333p-4. The reason for doing this is that the normal decimal representation often involves rounding and hence is not how the computer actually "sees" the number. If you're not used to this format, a couple of useful references are the Python docs and the C standard.

To show how these numbers work a bit, we can get back to our starting point by doing:

0x13333333333333 / 16**13 * 2**-4

which should should print out 0.075. 16**13 is because there are 13 hexadecimal digits after the decimal point, and 2**-4 is because hex exponents are base-2.

Now we have some idea of how floats are represented we can use the decimal module to give us some more precision, showing us what's going on:

from decimal import Decimal

Decimal(0x13333333333333) / 16**13 / 2**4

giving: 0.07499999999999999722444243844 and hopefully explaining why round(0.075, 2) evaluates to 0.07

Neoimpressionism answered 10/7, 2019 at 16:21 Comment(8)
This is a great explanation of why 0.075 is rounded down to 0.07 at the code level, but we (in the physical sciences) have been taught to always round up not down. So the expected behaviour is actually to have 0.08 as a result, floating point precision issues notwithstanding.Leapfrog
I'm unsure where your confusion is: when you enter 0.075 you're actually entering ~0.07499 (as above), which rounds according to normal maths rules. if you were using a data type (like decimal floating point) that could represent 0.075 then it should indeed round to 0.08Neoimpressionism
I'm not confused. When I enter 0.075 I'm actually entering 0.075. Whatever happens in the floating point math inside the code I don't care.Leapfrog
@Gabriel: And if you had deliberately entered 0.074999999999999999, what would you expect to get in that case?Hoskinson
@MarkDickinson that depends. One significant figure: 0.07, two: 0.075.Leapfrog
+1 to Gabriel and +1 to Sam. You're coming at this from different angles. @Leapfrog unfortunately you ought to know that a 0.075 float is just a shorthand for 0.074999999999999999, and not and that representation is not an "implementation detail". If you don't want that, use Decimal.Killingsworth
You in the physical sciences are also taught to treat cows as frictionless spheres in perfect vacuum. At some point you have to come down and mix with us peasantry and care about how your model applies to reality. In this case, you can't use floating point. How much you care about floating point conversion is irrelevant, you have chosen the wrong tool for the job, and it's on you to fix it.Ernestinaernestine
I quite enjoyed this. =)Vile
L
13

I have created the package to-precision that does what you want. It allows you to give your numbers more or less significant figures.

It also outputs standard, scientific, and engineering notation with a specified number of significant figures.

In the accepted answer there is the line

>>> round_to_1(1234243)
1000000.0

That actually specifies 8 sig figs. For the number 1234243 my library only displays one significant figure:

>>> from to_precision import to_precision
>>> to_precision(1234243, 1, 'std')
'1000000'
>>> to_precision(1234243, 1, 'sci')
'1e6'
>>> to_precision(1234243, 1, 'eng')
'1e6'

It will also round the last significant figure and can automatically choose what notation to use if a notation isn't specified:

>>> to_precision(599, 2)
'600'
>>> to_precision(1164, 2)
'1.2e3'
Loop answered 23/5, 2017 at 11:59 Comment(7)
Now I'm looking for the same but applied to a pandas dfDoorstone
@Doorstone you can probably use pandas map with a lambda. lambda x: to_precision(x, 2)Loop
Add this to (PyPI)[pypi.org/]. There is nothing like this that exists on there, as far as I can tell.Caucasia
this is a great package but I think most of the features are now in the sigfig moduleBarnebas
it has a bug: std_notation(9.999999999999999e-05, 3) gives: '0.00010' which is only 2 significant digitsVince
The sigfig module appears to have a similar bug: round(9.999999999999999e-05, sigfigs=3) gives 0.0001. Notably, though, round('9.999999999999999e-05', sigfigs=3) does give '0.000100'.Phyte
Please submit pr for fix. Would be very helpful thanks.Loop
M
6

The posted answer was the best available when given, but it has a number of limitations and does not produce technically correct significant figures.

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.
Madea answered 21/10, 2019 at 17:1 Comment(1)
The documentation (moved to numpy.org/doc/stable/reference/generated/…) states that this function implements the Dragon4 algorithm (of Steele &White 1990, dl.acm.org/doi/pdf/10.1145/93542.93559). It produces annoying results, e.g. print(*[''.join([np.format_float_positional(.01*a*n,precision=2,unique=False,fractional=False,trim='k',pad_right=5) for a in [.99, .999, 1.001]]) for n in [8,9,10,11,12,19,20,21]],sep='\n'). I didn't check Dragon4 itself.Spitsbergen
S
5
def round_to_n(x, n):
    if not x: return 0
    power = -int(math.floor(math.log10(abs(x)))) + (n - 1)
    factor = (10 ** power)
    return round(x * factor) / factor

round_to_n(0.075, 1)      # 0.08
round_to_n(0, 1)          # 0
round_to_n(-1e15 - 1, 16) # 1000000000000001.0

Hopefully taking the best of all the answers above (minus being able to put it as a one line lambda ;) ). Haven't explored yet, feel free to edit this answer:

round_to_n(1e15 + 1, 11)  # 999999999999999.9
Secateurs answered 5/8, 2010 at 0:48 Comment(0)
D
5

To round an integer to 1 significant figure the basic idea is to convert it to a floating point with 1 digit before the point and round that, then convert it back to its original integer size.

To do this we need to know the largest power of 10 less than the integer. We can use floor of the log 10 function for this.

from math import log10, floor
def round_int(i,places):
    if i == 0:
        return 0
    isign = i/abs(i)
    i = abs(i)
    if i < 1:
        return 0
    max10exp = floor(log10(i))
    if max10exp+1 < places:
        return i
    sig10pow = 10**(max10exp-places+1)
    floated = i*1.0/sig10pow
    defloated = round(floated)*sig10pow
    return int(defloated*isign)
Dateless answered 4/3, 2012 at 18:18 Comment(0)
S
4

I modified indgar's solution to handle negative numbers and small numbers (including zero).

from math import log10, floor
def round_sig(x, sig=6, small_value=1.0e-9):
    return round(x, sig - int(floor(log10(max(abs(x), abs(small_value))))) - 1)
Sailplane answered 26/3, 2016 at 19:50 Comment(4)
Why not just test whether x == 0? If you love a one-liner, just return 0 if x==0 else round(...).Ease
@pjvandehaar, you are correct for the general case and I should have put that in. In addition, for the numerical calculations I need to perform we occasionally get numbers like 1e-15. In our application we want a comparison of two small numbers (one of which might be zero) to be considered equal. Also some people want to round small numbers (it could be 1e-9, 1e-15, or even 1e-300) to zero.Sailplane
Interesting. Thanks for explaining that. In that case, I really like this solution.Ease
@Caucasia This is an interesting and difficult problem. As you pointed out, the printed value does not show the 3 significant digits, but the value is correct (e.g. 0.970 == 0.97). I think you could use some of the other print solutions like f'{round_sig(0.9701, sig=3):0.3f}' if you want the zero printed.Sailplane
I
3

If you want to round without involving strings, the link I found buried in the comments above:

http://code.activestate.com/lists/python-tutor/70739/

strikes me as best. Then when you print with any string formatting descriptors, you get a reasonable output, and you can use the numeric representation for other calculation purposes.

The code at the link is a three liner: def, doc, and return. It has a bug: you need to check for exploding logarithms. That is easy. Compare the input to sys.float_info.min. The complete solution is:

import sys,math

def tidy(x, n):
"""Return 'x' rounded to 'n' significant digits."""
y=abs(x)
if y <= sys.float_info.min: return 0.0
return round( x, int( n-math.ceil(math.log10(y)) ) )

It works for any scalar numeric value, and n can be a float if you need to shift the response for some reason. You can actually push the limit to:

sys.float_info.min*sys.float_info.epsilon

without provoking an error, if for some reason you are working with miniscule values.

Imogene answered 13/4, 2018 at 6:49 Comment(0)
B
3

The sigfig package/library covers this. After installing you can do the following:

>>> from sigfig import round
>>> round(1234, 1)
1000
>>> round(0.12, 1)
0.1
>>> round(0.012, 1)
0.01
>>> round(0.062, 1)
0.06
>>> round(6253, 1)
6000
>>> round(1999, 1)
2000
Barnebas answered 30/11, 2019 at 17:32 Comment(0)
K
2

I can't think of anything that would be able to handle this out of the box. But it's fairly well handled for floating point numbers.

>>> round(1.2322, 2)
1.23

Integers are trickier. They're not stored as base 10 in memory, so significant places isn't a natural thing to do. It's fairly trivial to implement once they're a string though.

Or for integers:

def intround(n, sigfigs):
    n = str(n)
    return n[:sigfigs] + ('0' * (len(n)-sigfigs))
>>> intround(1234, 1)
'1000'
>>> intround(1234, 2)
'1200'

If you would like to create a function that handles any number, my preference would be to convert them both to strings and look for a decimal place to decide what to do:

def roundall1(n, sigfigs):
    n = str(n)
    try:
        sigfigs = n.index('.')
    except ValueError:
        pass
    return intround(n, sigfigs)

Another option is to check for type. This will be far less flexible, and will probably not play nicely with other numbers such as Decimal objects:

def roundall2(n, sigfigs):
    if type(n) is int:
        return intround(n, sigfigs)
    else:
        return round(n, sigfigs)
Kesley answered 5/8, 2010 at 1:35 Comment(3)
Just messing with strings won't round the numbers. 1999 rounded to 1 significant figure is 2000, not 1000.Nesselrode
There is a good discussion of this problem archived at ActiveState code.activestate.com/lists/python-tutor/70739Kesley
1.23 has three sig figs, not two.Grenadines
P
2

I adapted one of the answers. I like this:

def sigfiground(number:float, ndigits=3)->float:
    return float(f"{number:.{ndigits}g}")

I use it when I still want a float (I do formatting elsewhere).

Prepositive answered 17/12, 2021 at 15:21 Comment(1)
Not elegant, but very wrist friendly.Butt
P
1

Using python 2.6+ new-style formatting (as %-style is deprecated):

>>> "{0}".format(float("{0:.1g}".format(1216)))
'1000.0'
>>> "{0}".format(float("{0:.1g}".format(0.00356)))
'0.004'

In python 2.7+ you can omit the leading 0s.

Paillasse answered 8/1, 2018 at 13:34 Comment(2)
With what version of python? Python 3.6.3 |Anaconda, Inc.| (default, Oct 13 2017, 12:02:49) has the same old rounding problem. "{0}".format(float("{0:.1g}".format(0.075))) yields '0.07', not '0.08'Schrader
@DonMclachlan I've added an explanation of why this is expected in https://mcmap.net/q/66816/-how-to-round-a-number-to-significant-figures-in-pythonNeoimpressionism
K
0

I ran into this as well but I needed control over the rounding type. Thus, I wrote a quick function (see code below) that can take value, rounding type, and desired significant digits into account.

import decimal
from math import log10, floor

def myrounding(value , roundstyle='ROUND_HALF_UP',sig = 3):
    roundstyles = [ 'ROUND_05UP','ROUND_DOWN','ROUND_HALF_DOWN','ROUND_HALF_UP','ROUND_CEILING','ROUND_FLOOR','ROUND_HALF_EVEN','ROUND_UP']

    power =  -1 * floor(log10(abs(value)))
    value = '{0:f}'.format(value) #format value to string to prevent float conversion issues
    divided = Decimal(value) * (Decimal('10.0')**power) 
    roundto = Decimal('10.0')**(-sig+1)
    if roundstyle not in roundstyles:
        print('roundstyle must be in list:', roundstyles) ## Could thrown an exception here if you want.
    return_val = decimal.Decimal(divided).quantize(roundto,rounding=roundstyle)*(decimal.Decimal(10.0)**-power)
    nozero = ('{0:f}'.format(return_val)).rstrip('0').rstrip('.') # strips out trailing 0 and .
    return decimal.Decimal(nozero)


for x in list(map(float, '-1.234 1.2345 0.03 -90.25 90.34543 9123.3 111'.split())):
    print (x, 'rounded UP: ',myrounding(x,'ROUND_UP',3))
    print (x, 'rounded normal: ',myrounding(x,sig=3))
Kubetz answered 28/9, 2017 at 16:52 Comment(0)
M
0

This function does a normal round if the number is bigger than 10**(-decimal_positions), otherwise adds more decimal until the number of meaningful decimal positions is reached:

def smart_round(x, decimal_positions):
    dp = - int(math.log10(abs(x))) if x != 0.0 else int(0)
    return round(float(x), decimal_positions + dp if dp > 0 else decimal_positions)

Hope it helps.

Medicaid answered 19/7, 2018 at 9:38 Comment(0)
S
0

https://stackoverflow.com/users/1391441/gabriel, does the following address your concern about rnd(.075, 1)? Caveat: returns value as a float

def round_to_n(x, n):
    fmt = '{:1.' + str(n) + 'e}'    # gives 1.n figures
    p = fmt.format(x).split('e')    # get mantissa and exponent
                                    # round "extra" figure off mantissa
    p[0] = str(round(float(p[0]) * 10**(n-1)) / 10**(n-1))
    return float(p[0] + 'e' + p[1]) # convert str to float

>>> round_to_n(750, 2)
750.0
>>> round_to_n(750, 1)
800.0
>>> round_to_n(.0750, 2)
0.075
>>> round_to_n(.0750, 1)
0.08
>>> math.pi
3.141592653589793
>>> round_to_n(math.pi, 7)
3.141593
Schrader answered 22/2, 2019 at 18:46 Comment(0)
S
0

This returns a string, so that results without fractional parts, and small values which would otherwise appear in E notation are shown correctly:

def sigfig(x, num_sigfig):
    num_decplace = num_sigfig - int(math.floor(math.log10(abs(x)))) - 1
    return '%.*f' % (num_decplace, round(x, num_decplace))
Schindler answered 15/3, 2019 at 12:4 Comment(0)
E
0

Given a question so thoroughly answered why not add another

This suits my aesthetic a little better, though many of the above are comparable

import numpy as np

number=-456.789
significantFigures=4

roundingFactor=significantFigures - int(np.floor(np.log10(np.abs(number)))) - 1
rounded=np.round(number, roundingFactor)

string=rounded.astype(str)

print(string)

This works for individual numbers and numpy arrays, and should function fine for negative numbers.

There's one additional step we might add - np.round() returns a decimal number even if rounded is an integer (i.e. for significantFigures=2 we might expect to get back -460 but instead we get -460.0). We can add this step to correct for that:

if roundingFactor<=0:
    rounded=rounded.astype(int)

Unfortunately, this final step won't work for an array of numbers - I'll leave that to you dear reader to figure out if you need.

Elisabeth answered 3/8, 2019 at 8:59 Comment(0)
S
0
import math

  def sig_dig(x, n_sig_dig):
      num_of_digits = len(str(x).replace(".", ""))
      if n_sig_dig >= num_of_digits:
          return x
      n = math.floor(math.log10(x) + 1 - n_sig_dig)
      result = round(10 ** -n * x) * 10 ** n
      return float(str(result)[: n_sig_dig + 1])


    >>> sig_dig(1234243, 3)
    >>> sig_dig(243.3576, 5)

        1230.0
        243.36
Sidle answered 26/3, 2020 at 21:29 Comment(4)
This function is not doing what it should. sig_dig(1234243, 3) should be 1230000 and not 1230.0.Beekman
If you just return the result, then it will be fine (i.e. remove the last line of your function).Beekman
Also, you should take the absolute value math.log10(abs(x)) to deal with negative numbers. Given the two corrections, it looks like it works well and is quite fast.Beekman
It's unlikely it's fast, because it uses math.log10Sammiesammons
T
0

Most of these answers involve the math, decimal and/or numpy imports or output values as strings. Here is a simple solution in base python that handles both large and small numbers and outputs a float:

def sig_fig_round(number, digits=3):
    power = "{:e}".format(number).split('e')[1]
    return round(number, -(int(power) - digits))
Tatar answered 16/12, 2020 at 18:23 Comment(0)
C
0

A simple variant using the standard decimal library

from decimal import Decimal

def to_significant_figures(v: float, n_figures: int) -> str:
    d = Decimal(v)
    d = d.quantize(Decimal((0, (), d.adjusted() - n_figures + 1)))
    return str(d.quantize(Decimal(1)) if d == d.to_integral() else d.normalize())

Testing it

>>> to_significant_figures(1.234567, 3)
'1.23'
>>> to_significant_figures(1234567, 3)
'1230000'
>>> to_significant_figures(1.23, 7)
'1.23'
>>> to_significant_figures(123, 7)
'123'
Cochrane answered 4/9, 2021 at 21:47 Comment(0)
I
0

This function takes both positive and negative numbers and does the proper significant digit rounding.

from math import floor

def significant_arithmetic_rounding(n, d):
    '''
    This function takes a floating point number and the no. of significant digit d, perform significant digits
    arithmetic rounding and returns the floating point number after rounding
    '''
    if n == 0:
        return 0
    else:
        # Checking whether the no. is negative or positive. If it is negative we will take the absolute value of it and proceed
        neg_flag = 0
        if n < 0:
            neg_flag = 1
            n = abs(n)
        
        n1 = n   
        # Counting the no. of digits to the left of the decimal point in the no.
        ld = 0
        while(n1 >= 1):
            n1 /= 10
            ld += 1
        
        n1 = n
        # Counting the no. of zeros to the right of the decimal point and before the first significant digit in the no.
        z = 0
        if ld == 0:
            while(n1 <= 0.1):
                n1 *= 10
                z += 1
        
        n1 = n
        # No. of digits to be considered after decimal for rounding
        rd = (d - ld) + z
        n1 *= 10**rd
    
        # Increase by 0.5 and take the floor value for rounding
        n1 = floor(n1+0.5)
        # Placing the decimal point at proper position
        n1 /= 10 ** rd
        # If the original number is negative then make it negative
        if neg_flag == 1:
            n1 = 0 - n1

        return n1

Testing:

>>> significant_arithmetic_rounding(1234, 3)
1230.0
>>> significant_arithmetic_rounding(123.4, 3)
123.0
>>> significant_arithmetic_rounding(0.0012345, 3)
0.00123
>>> significant_arithmetic_rounding(-0.12345, 3)
-0.123
>>> significant_arithmetic_rounding(-30.15345, 3)
-30.2
Intercostal answered 7/12, 2021 at 15:8 Comment(0)
L
0

Easier to know an answer works for your needs when it includes examples. The following is built on previous solutions, but offers a more general function which can round to 1, 2, 3, 4, or any number of significant digits.

import math

# Given x as float or decimal, returns as string a number rounded to "sig" significant digts
# Return as string in order to control significant digits, could be a float or decimal 
def round_sig(x, sig=2):
  r = round(x, sig-int(math.floor(math.log10(abs(x))))-1)
  floatsig = "%." + str(sig) + "g"
  return "%d"%r if abs(r) >= 10**(sig-1) else '%s'%float(floatsig % r) 

>>> a = [1234, 123.4, 12.34, 1.234, 0.1234, 0.01234, 0.25, 1999, -3.14, -48.01, 0.75]
>>> [print(i, "->", round_sig(i,1), round_sig(i), round_sig(i,3), round_sig(i,4)) for i in a]

1234 -> 1000 1200 1230 1234
123.4 -> 100 120 123 123.4
12.34 -> 10 12 12.3 12.34
1.234 -> 1 1.2 1.23 1.234
0.1234 -> 0.1 0.12 0.123 0.1234
0.01234 -> 0.01 0.012 0.0123 0.01234
0.25 -> 0.2 0.25 0.25 0.25
1999 -> 2000 2000 2000 1999
-3.14 -> -3 -3.1 -3.14 -3.14
-48.01 -> -50 -48 -48.0 -48.01
0.75 -> 0.8 0.75 0.75 0.75
Longueur answered 22/4, 2022 at 20:1 Comment(0)
A
0

in very cases, the number of significant is depend on to the evaluated process, e.g. error. I wrote the some codes which returns a number according to it's error (or with some desired digits) and also in string form (which doesn't eliminate right side significant zeros)

import numpy as np

def Sig_Digit(x, *N,):
    if abs(x) < 1.0e-15:
        return(1)
    N = 1 if N ==() else N[0]
    k = int(round(abs(N)-1))-int(np.floor(np.log10(abs(x))))
    return(k);

def Sig_Format(x, *Error,):
    if abs(x) < 1.0e-15:
        return('{}')
    Error = 1 if Error ==() else abs(Error[0])
    k = int(np.floor(np.log10(abs(x))))
    z = x/10**k
    k = -Sig_Digit(Error, 1)
    m = 10**k
    y = round(x*m)/m
    if k < 0:
        k = abs(k)
        if z >= 9.5:
            FMT = '{:'+'{}'.format(1+k)+'.'+'{}'.format(k-1)+'f}'
        else:
            FMT = '{:'+'{}'.format(2+k)+'.'+'{}'.format(k)+'f}'
    elif k == 0:
        if z >= 9.5:
            FMT = '{:'+'{}'.format(1+k)+'.0e}'
        else:
            FMT = '{:'+'{}'.format(2+k)+'.0f}'
    else:
        FMT = '{:'+'{}'.format(2+k)+'.'+'{}'.format(k)+'e}'
    return(FMT)

def Sci_Format(x, *N):
    if abs(x) < 1.0e-15:
        return('{}')
    N = 1 if N ==() else N[0]
    N = int(round(abs(N)-1))
    y = abs(x)
    k = int(np.floor(np.log10(y)))
    z = x/10**k
    k = k-N
    m = 10**k
    y = round(x/m)*m
    if k < 0:
        k = abs(k)
        if z >= 9.5:
            FMT = '{:'+'{}'.format(1+k)+'.'+'{}'.format(k-1)+'f}'
        else:
            FMT = '{:'+'{}'.format(2+k)+'.'+'{}'.format(k)+'f}'
    elif k == 0:
        if z >= 9.5:
            FMT = '{:'+'{}'.format(1+k)+'.0e}'
        else:
            FMT = '{:'+'{}'.format(2+k)+'.0f}'
    else:
        FMT = '{:'+'{}'.format(2+N)+'.'+'{}'.format(N)+'e}'
    return(FMT)

def Significant(x, *Error):
    N = 0 if Error ==() else Sig_Digit(abs(Error[0]), 1)
    m = 10**N
    y = round(x*m)/m
    return(y)

def Scientific(x, *N):
    m = 10**Sig_Digit(x, *N)
    y = round(x*m)/m
    return(y)

def Scientific_Str(x, *N,): 
    FMT = Sci_Format(x, *N)
    return(FMT.format(x))

def Significant_Str(x, *Error,):    
    FMT = Sig_Format(x, *Error)
    return(FMT.format(x))

test code:

X = [19.03345607, 12.075, 360.108321344, 4325.007605343]
Error = [1.245, 0.1245, 0.0563, 0.01245, 0.001563, 0.0004603]
for x in X:
    for error in Error:
        print(x,'+/-',error, end='   \t==> ')
        print(' (',Significant_Str(x, error), '+/-', Scientific_Str(error),')')

  

print out:

19.03345607 +/- 1.245       ==>  ( 19 +/-  1 )

19.03345607 +/- 0.1245      ==>  ( 19.0 +/- 0.1 )

19.03345607 +/- 0.0563      ==>  ( 19.03 +/- 0.06 )

19.03345607 +/- 0.01245     ==>  ( 19.03 +/- 0.01 )

19.03345607 +/- 0.001563    ==>  ( 19.033 +/- 0.002 )

19.03345607 +/- 0.0004603       ==>  ( 19.0335 +/- 0.0005 )

12.075 +/- 1.245    ==>  ( 12 +/-  1 )

12.075 +/- 0.1245       ==>  ( 12.1 +/- 0.1 )

12.075 +/- 0.0563       ==>  ( 12.07 +/- 0.06 )

12.075 +/- 0.01245      ==>  ( 12.07 +/- 0.01 )

12.075 +/- 0.001563     ==>  ( 12.075 +/- 0.002 )

12.075 +/- 0.0004603    ==>  ( 12.0750 +/- 0.0005 )

360.108321344 +/- 1.245     ==>  ( 360 +/-  1 )

360.108321344 +/- 0.1245    ==>  ( 360.1 +/- 0.1 )

360.108321344 +/- 0.0563    ==>  ( 360.11 +/- 0.06 )

360.108321344 +/- 0.01245       ==>  ( 360.11 +/- 0.01 )

360.108321344 +/- 0.001563      ==>  ( 360.108 +/- 0.002 )

360.108321344 +/- 0.0004603     ==>  ( 360.1083 +/- 0.0005 )

4325.007605343 +/- 1.245    ==>  ( 4325 +/-  1 )

4325.007605343 +/- 0.1245       ==>  ( 4325.0 +/- 0.1 )

4325.007605343 +/- 0.0563       ==>  ( 4325.01 +/- 0.06 )

4325.007605343 +/- 0.01245      ==>  ( 4325.01 +/- 0.01 )

4325.007605343 +/- 0.001563     ==>  ( 4325.008 +/- 0.002 )

4325.007605343 +/- 0.0004603    ==>  ( 4325.0076 +/- 0.0005 )
Alluvial answered 17/9, 2022 at 16:11 Comment(0)
P
0

I've written a PyPi package called sciform which can easily perform this kind of rounding and supports other scientific formatting options.

from sciform import Formatter

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

num_list = [
    1234,
    0.12,
    0.012,
    0.062,
    6253,
    1999,
]

for num in num_list:
    result = sform(num)
    print(f'{num} -> {result}')

# 1234 -> 1000
# 0.12 -> 0.1
# 0.012 -> 0.01
# 0.062 -> 0.06
# 6253 -> 6000
# 1999 -> 2000

sciform was in part motivated by the fact that the python built in format specification mini-language does not always provide the exact round-to-sig-figs while also always using fixed-point desired behavior. The g option can kind of get sig figs right. If you want sig figs to work below the decimal point (that is 0.1 becomes 0.10 to 2 significant digits) you have to use the # option, but the # option means a trailing decimal point will not be removed so that 120 -> 1.e+02 to one significant digit.

See especially this comment in a discussion on Python discourse.

num_list = [
    1234,
    0.12,
    0.012,
    0.062,
    6253,
    1999,
]

for num in num_list:
    print(f'{num} -> {num:#.1g}')

# 1234 -> 1e+03
# 0.12 -> 0.1
# 0.012 -> 0.01
# 0.062 -> 0.06
# 6253 -> 6.e+03
# 1999 -> 2.e+03
Proclaim answered 12/1, 2024 at 14:57 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.