SymPy rounding behaviour - python

I was investigating different rounding method using Python built-in solution and some other external libraries such SymPy and while doing so I stumbled upon some cases that I need help with understanding the reason behind it.
Ex-1:
print(round(1.0065,3))
output:
1.006
In the first case, using the Python built-in rounding function the output was 1.006 instead of 1.007 and I can understand that this is not a mistake as Python rounds to the nearest even and that's known as Bankers rounding.
And this is why I from the beginning started searching for another way to control the rounding behaviour. With a quick search, I've found decimal.Decimal module which can easily handle decimal values and efficiently round is using quantize() as in this example:
from decimal import Decimal, getcontext, ROUND_HALF_UP
context= getcontext()
context.rounding='ROUND_HALF_UP'
print(Decimal('1.0065').quantize(Decimal('.001')))
output:1.007
This is a very good solution but the only problem is it is not easy to be hardcoded in long math expressions as I'll need to convert every number to string then after using decimal I will pass it the precession as in the form of "0.001" instead of writing '3' directly as in the case of built-in round.
While searching for another solution I found that SymPy, which I already use a lot in my scripts, offers some very powerful functions that might help but when I tried it the output was not as I expected.
Ex-1 using SymPy sympify():
print(sympify(1.0065).evalf(3))
output: 1.01
Ex-2 using SymPy N (normalize):
print(N(1.0065,3))
output: 1.01
Af first the output was a little bit weird but after investigating I realized that N and sympify already performing round right but rounding to significant figures, not to decimal places.
And here the questions come:
As I can use with Decimal objects getcontext().rounding='ROUND_HALF_UP' to change the rounding behaviour, is there a way to change the N and sympify rounding behaviour to decimal places instead of significant figures?

Instead of re-implementing decimal rounding in SymPy, perhaps use decimal to do the rounding, but hide the calculation in a utility function:
import sympy as sym
import decimal
from decimal import Decimal as D
def dround(d, ndigits, rounding=decimal.ROUND_HALF_UP):
result = D(str(d)).quantize(D('0.1')**ndigits, rounding=rounding)
# result = sym.sympify(result) # if you want a SymPy Float
return result
for x in [0.0065, 1.0065, 10.0065, 100.0065]:
print(dround(x, 3))
prints
0.007
1.007
10.007
100.007

The n of evalf gives the first n significant digits of x (measured from the left). If you use x.round(3) it will round x to the nth digit from the decimal point and can be positive (right of decimal pt) or negative (left of decimal pt).
>>> for x in '0.0065, 1.0065, 10.0065, 100.0065'.split(', '):
... print S(x).round(3)
0.006
1.006
10.007
100.007
>>> int(S(12345).round(-2))
12300

First of all, N and evalf are essentially the same thing; N(x, n) amounts to sympify(x).evalf(n). In your case, since x is a Python float, it's easier to use N because it sympifies the input.
To get three digits after decimal dot, use N(x, 3 + log(x, 10) + 1). The adjustment log(x, 10) + 1 is 0 when x is between 0.1 and 1; in this case the number of significant digits is the same as the number of digits after the decimal dot. If x is larger, we get more significant digits.
Example:
for x in [0.0065, 1.0065, 10.0065, 100.0065]:
print(N(x, 3 + log(x, 10) + 1))
prints
0.006
1.007
10.007
100.007
The transition from 6 to 7 is curious, but not entirely surprising. These numbers are not exactly represented in binary system, so the truncation to nearest double-precision float may be a factor here. I've made a few additional observation on this effect on my blog.

Related

The precision of Decimal library in python

From the documentation page of Decimal, I thought that once we use decimal to compute, it'll be a correct result without any floating error.
But when I try this equation
from decimal import Decimal, getcontext
getcontext().prec = 250
a = Decimal('6')
b = Decimal('500000')
b = a ** b
print('prec: ' + str(getcontext().prec) + ', ', end='')
print(b.ln() / a.ln())
It gives me different result!
I want to calculate the digit of 6**500000 in base-6 representation, so my expect result would be int(b.ln() / a.ln()) + 1, which should be 500001. However, when I set the prec to 250, it gives me the wrong result. How can I solve this?
Also, if I want to output the result without the scientific notation (i.e. 5E+5), what should I do?
The Documentation clearly show the parameters of getcontext()
when you simply execute getcontext() it can show its built-in parameters.
Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[], traps=[InvalidOperation, DivisionByZero, Overflow])
When you can change getcontext().prec = 250 then it can only overwrite prec value.
the main thing which can effect the result is rounding parameter right after the prec, It is built-in parameter rounding=ROUND_HALF_EVEN. I want to change this to None but it can show an error.
so it can clarify that no matter what we can do it must change even slightly because of rounding parameter.
Note: your result may also effect because of other built-in parameters.
If you're after very precise or symbolic math and/or being bitten by IEEE 754 float aliasing, check out SymPy
>>> from sympy import log, N
>>> expr = log(6**50000) / log(6)
>>> N(expr)
50000.0000000000
The documentation for Decimal.ln() states:
Return the natural (base e) logarithm of the operand. The result is correctly rounded using the ROUND_HALF_EVEN rounding mode.
When you changed the precision, more digits of the numbers were calculated and they are then rounded down instead of up. The number needs to be rounded because it is not necessarily within the specified precision, or even rational at all.
For outputting results in scientific notation, see this question.

How to disable rounding in Decimal python?

When calculating, I get the wrong last digit of the number. At first, I just calculated with an accuracy of one digit more than I needed, and then I just removed the last rounded digit with a slice. But then I noticed that sometimes Decimal rounds more than one digit. Is it possible to calculate without rounding?
For example
from decimal import Decimal as dec, Context, setcontext, ROUND_DOWN
from math import log
def sqr(x):
return x*x
def pi(n):
getcontext().prec=n+1
a=p=1
b=dec(1)/dec(2).sqrt()
t=dec(1)/dec(4)
for _ in range(int(log(n,2))):
an=(a+b)/2
b=(a*b).sqrt()
t-=p*sqr(a-an)
p*=2
a=an
return sqr(a+b)/(4*t)
If I try pi (12) I get "3.141592653591" (the last 2 digits are wrong), but if I try pi(13), they both change to the correct ones - "3.1415926535899".
It's called Roundoff Error and is unavoidable when working with Floating-Point Arithmetic. You can write the following code in your Python REPL and should get, interestingly, False.
0.2 + 0.1 == 0.3 # False
It's because the last bits of float numbers are, actually, garbage. One way you can work around this is by using more terms in your series and, then, rounding the result to the wanted precision.
If you want to understand this deeper, you can read these two links I've attached and, maybe, some Numerical Computing textbook.

In python, how do I preserve decimal places in numbers? [duplicate]

This question already has answers here:
How can I force division to be floating point? Division keeps rounding down to 0?
(11 answers)
Closed 9 years ago.
I'd like to pass numbers around between functions, while preserving the decimal places for the numbers.
I've discovered that if I pass a float like '10.00' in to a function, then the decimal places don't get used. This messes an operation like calculating percentages.
For example, x * (10 / 100) will always return 0.
But if I manage to preserve the decimal places, I end up doing x * (10.00 / 100). This returns an accurate result.
I'd like to have a technique that enables consistency when I'm working with numbers that decimal places that can hold zeroes.
When you write
10 / 100
you are performing integer division. That's because both operands are integers. The result is 0.
If you want to perform floating point division, make one of the operands be a floating point value. For instance:
10.0 / 100
or
float(10) / 100
Do beware also that
10.0 / 100
results in a binary floating point value and binary floating data types cannot represent the true result value of 0.1. So if you want to represent the result accurately you may need to use a decimal data type. The decimal module has the functionality needed for that.
Division in python for float and int works differently, take a look at this question and it's answers: Python division.
Moreover, if you are looking for a solution to format a decimal floating point of your figures into string, you might need to use %f.
Python
# '1.000000'
"%f" % (1.0)
# '1.00'
"%.2f" % (1.0)
# ' 1.00'
"%6.2f" % (1.0)
Python 2.x will use integer division when dividing two integers unless you explicitly tell it to do otherwise. Two integers in --> one integer out.
Python 3 onwards will return, to quote PEP 238 http://www.python.org/dev/peps/pep-0238/ a reasonable approximation of the result of the division approximation, i.e. it will perform a floating point division and return the result without rounding.
To enable this behaviour in earlier version of Python you can use:
from __future__ import division
At the very top of the module, this should get you the consistent results you want.
You should use the decimal module. Each number knows how many significant digits it has.
If you're trying to preserve significant digits, the decimal module is has everything you need. Example:
>>> from decimal import Decimal
>>> num = Decimal('10.00')
>>> num
Decimal('10.00')
>>> num / 10
Decimal('1.00')

Arbitrary precision of square roots

I was quite disappointed when decimal.Decimal(math.sqrt(2)) yielded
Decimal('1.4142135623730951454746218587388284504413604736328125')
and the digits after the 15th decimal place turned out wrong. (Despite happily giving you much more than 15 digits!)
How can I get the first m correct digits in the decimal expansion of sqrt(n) in Python?
Use the sqrt method on Decimal
>>> from decimal import *
>>> getcontext().prec = 100 # Change the precision
>>> Decimal(2).sqrt()
Decimal('1.414213562373095048801688724209698078569671875376948073176679737990732478462107038850387534327641573')
You can try bigfloat. Example from the project page:
from bigfloat import *
sqrt(2, precision(100)) # compute sqrt(2) with 100 bits of precision
IEEE standard double precision floating point numbers only have 16 digits of precision. Any software/hardware that uses IEEE cannot do better:
http://en.wikipedia.org/wiki/IEEE_754-2008
You'd need a special BigDecimal class implementation, with all math functions implemented to use it. Python has such a thing:
https://literateprograms.org/arbitrary-precision_elementary_mathematical_functions__python_.html
How can I get the first m correct digits in the decimal expansion of sqrt(n) in Python?
One way is to calculate integer square root of the number multiplied by required power of 10. For example, to see the first 20 decimal places of sqrt(2), you can do:
>>> from gmpy2 import isqrt
>>> num = 2
>>> prec = 20
>>> isqrt(num * 10**(2*prec)))
mpz(141421356237309504880)
The isqrt function is actually quite easy to implement yourself using the algorithm provided on the Wikipedia page.

Why does str() round up floats?

The built-in Python str() function outputs some weird results when passing in floats with many decimals. This is what happens:
>>> str(19.9999999999999999)
>>> '20.0'
I'm expecting to get:
>>> '19.9999999999999999'
Does anyone know why? and maybe workaround it?
Thanks!
It's not str() that rounds, it's the fact that you're using floats in the first place. Float types are fast, but have limited precision; in other words, they are imprecise by design. This applies to all programming languages. For more details on float quirks, please read "What Every Programmer Should Know About Floating-Point Arithmetic"
If you want to store and operate on precise numbers, use the decimal module:
>>> from decimal import Decimal
>>> str(Decimal('19.9999999999999999'))
'19.9999999999999999'
A float has 32 bits (in C at least). One of those bits is allocated for the sign, a few allocated for the mantissa, and a few allocated for the exponent. You can't fit every single decimal to an infinite number of digits into 32 bits. Therefore floating point numbers are heavily based on rounding.
If you try str(19.998), it will probably give you something at least close to 19.998 because 32 bits have enough precision to estimate that, but something like 19.999999999999999 is too precise to estimate in 32 bits, so it rounds to the nearest possible value, which happens to be 20.
Please note that this is a problem of understanding floating point (fixed-length) numbers. Most languages do exactly (or very similar to) what Python does.
Python float is IEEE 754 64-bit binary floating point. It is limited to 53 bits of precision i.e. slightly less than 16 decimal digits of precision. 19.9999999999999999 contains 18 decimal digits; it cannot be represented exactly as a float. float("19.9999999999999999") produces the nearest floating point value, which happens to be the same as float("20.0").
>>> float("19.9999999999999999") == float("20.0")
True
If by "many decimals" you mean "many digits after the decimal point", please be aware that the same "weird" results happen when there are many decimal digits before the decimal point:
>>> float("199999999999999999")
2e+17
If you want the full float precision, don't use str(), use repr():
>>> x = 1. / 3.
>>> str(x)
'0.333333333333'
>>> str(x).count('3')
12
>>> repr(x)
'0.3333333333333333'
>>> repr(x).count('3')
16
>>>
Update It's interesting how often decimal is prescribed as a cure-all for float-induced astonishment. This is often accompanied by simple examples like 0.1 + 0.1 + 0.1 != 0.3. Nobody stops to point out that decimal has its share of deficiencies e.g.
>>> (1.0 / 3.0) * 3.0
1.0
>>> (Decimal('1.0') / Decimal('3.0')) * Decimal('3.0')
Decimal('0.9999999999999999999999999999')
>>>
True, float is limited to 53 binary digits of precision. By default, decimal is limited to 28 decimal digits of precision.
>>> Decimal(2) / Decimal(3)
Decimal('0.6666666666666666666666666667')
>>>
You can change the limit, but it's still limited precision. You still need to know the characteristics of the number format to use it effectively without "astonishing" results, and the extra precision is bought by slower operation (unless you use the 3rd-party cdecimal module).
For any given binary floating point number, there is an infinite set of decimal fractions that, on input, round to that number. Python's str goes to some trouble to produce the shortest decimal fraction from this set; see GLS's paper http://kurtstephens.com/files/p372-steele.pdf for the general algorithm (IIRC they use a refinement that avoids arbitrary-precision math in most cases). You happened to input a decimal fraction that rounds to a float (IEEE double) whose shortest possible decimal fraction is not the same as the one you entered.

Categories