4

NB: this question is about significant figures. It is not a question about "digits after the decimal point" or anything like that.

EDIT: This question is not a duplicate of Significant figures in the decimal module. The two questions are asking about entirely different problems. I want to know why the function about does not return the desired value for a specific input. None of the answers to Significant figures in the decimal module address this question.


The following function is supposed to return a string representation of a float with the specified number of significant figures:

import decimal

def to_sigfigs(value, sigfigs):
    return str(decimal.Context(prec=sigfigs).create_decimal(value))

At first glance, it seems to work:

print to_sigfigs(0.000003141592653589793, 5)
# 0.0000031416

print to_sigfigs(0.000001, 5)
# 0.0000010000

print to_sigfigs(3.141592653589793, 5)
# 3.1416

...but

print to_sigfigs(1.0, 5)
# 1

The desired output for the last expression (IOW, the 5-significant figure representation of 1.0) is the string '1.0000'. The actual output is the string '1'.

Am I misunderstanding something or is this a bug in decimal?

Community
  • 1
  • 1
kjo
  • 33,683
  • 52
  • 148
  • 265
  • Can you link to some part of the documentation that says that precision _should_ behave this way or that it is related to the concept of significant figures? It's not a bug if it only happens to behave this way sometimes if it's not intended to behave this way all the time. – Two-Bit Alchemist Dec 10 '15 at 20:54
  • Possible duplicate of [Significant figures in the decimal module](http://stackoverflow.com/questions/144218/significant-figures-in-the-decimal-module) – Two-Bit Alchemist Dec 10 '15 at 20:55
  • 2
    `0.000001` - you know, that's already got some rounding error in it, due to the conversion from decimal source code to a floating-point value. `1.0`, on the other hand, is exactly 1.0. – user2357112 Dec 10 '15 at 20:57
  • Yeah regarding what @user2357112 said you should really be initializing your Decimals with strings for full precision. – Two-Bit Alchemist Dec 10 '15 at 21:00
  • @Two-BitAlchemist but if you initialize your Decimal with a string containing trailing zeros, it seems to remember how many zeros you provided. That means it should be possible to specify the number of digits when using a float input too, the question is how. – Mark Ransom Dec 10 '15 at 21:20
  • @MarkRansom The documentation says clearly that that has to do with the _value_ of the Decimal, which you are initializing with a string or a float (whatever you've chosen). In the scenario you're describing, it's the _float_ that's losing the precision, before it's ever passed to the Decimal. At least that's my understanding. – Two-Bit Alchemist Dec 10 '15 at 21:28
  • `decimal` does that with the string representation of all numbers that are "infinitely precise" (e.g., `Decimal('1.1')`). its more concise and the math works out okay anyway so I suppose its mostly harmless. – tdelaney Dec 10 '15 at 22:00

2 Answers2

5

The precision of a context is a maximum precision; if an operation would produce a Decimal with less digits than the context's precision, it is not padded out to the context's precision.

When you call to_sigfigs(0.000001, 5), 0.000001 already has some rounding error due to the conversion from source code to binary floating point. It's actually 9.99999999999999954748111825886258685613938723690807819366455078125E-7. Rounding that to 5 significant figures gives decimal.Decimal("0.0000010000").

On the other hand, 1 is exactly representable in binary floating point, so 1.0 is exactly 1. Since only 1 digit is needed to represent this in decimal, the context's precision doesn't require any rounding, and you get a 1-digit Decimal result.

user2357112
  • 260,549
  • 28
  • 431
  • 505
2

Is it a bug? I don't know, I don't think the documentation is tight enough to make that determination. It certainly is a surprising result.

It is possible to fix your own function with a little more logic.

def to_sigfigs(value, sigfigs):
    sign, digits, exponent = decimal.Context(prec=sigfigs).create_decimal(value).as_tuple()
    if len(digits) < sigfigs:
        missing = sigfigs - len(digits)
        digits = digits + (0,) * missing
        exponent -= missing
    return str(decimal.Decimal((sign, digits, exponent)))
Mark Ransom
  • 299,747
  • 42
  • 398
  • 622