From: Robin Becker on
A client wants to know why his db number -9.85 gets displayed by some simple
code as -9.8

I looked at the number and see that

>>> -9.85
-9.8499999999999996

ie I expect simple rounding to produce the observed result and indeed

>>> '%.1f' % -9.85
'-9.8'

however, when I use round I get an unexpected result ie
>>> round(-9.85,1)
-9.9000000000000004

according to its definition

> round(x[, n])�
> Return the floating point value x rounded to n digits after the decimal point.
> If n is omitted, it defaults to zero. The result is a floating point number.
> Values are rounded to the closest multiple of 10 to the power minus n;
> if two multiples are equally close, rounding is done away from 0 (so. for example,
> round(0.5) is 1.0 and round(-0.5) is -1.0).

so looking at the absolute differences I see

>>> abs(-9.9 - -9.85)
0.050000000000000711
>>> abs(-9.8 - -9.85)
0.049999999999998934

ie the -9.8 value appears closer and at least to a primitive test

>>> abs(-9.9 - -9.85) > abs(-9.8 - -9.85)
True

the distance from the -9.9 result is larger, however, that may be because the
model numbers for -9.8 & -9.9 differ in distance from the true 10**-n values eg

>>> -9.9
-9.9000000000000004
>>> -9.8
-9.8000000000000007

What value should round(-9.85,1) return? Is the result explainable in python (ie
without resort to the internal FP representations etc etc)?
--
Robin Becker

From: Mark Dickinson on
On Jul 12, 10:52 am, Robin Becker <ro...(a)reportlab.com> wrote:
> What value should round(-9.85,1) return? Is the result explainable in python (ie
> without resort to the internal FP representations etc etc)?

As you observe, the closest float to -9.85 is actually just a little
smaller (i.e., closer to 0) than -9.85:

Python 2.7 (r27:82500, Jul 11 2010, 22:38:53)
[GCC 4.2.1 (Apple Inc. build 5659)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import decimal
>>> decimal.Decimal(-9.85)
Decimal('-9.8499999999999996447286321199499070644378662109375')

So you're right: round(-9.85, 1) *should* return -9.8. The 2.x (for x
<= 6) version of round is a bit deficient in that respect.
Internally, it's doing the obvious thing: namely, multiplying by
10.0, rounding to the nearest integer, then dividing by 10.0. The
problem is that this not-quite-9.85 value, when multiplied by 10,
becomes (as a result of rounding error) *exactly* 98.5, which then
gets rounded *up* to 99 instead of down to 98.

This is fixed in Python 2.7, and in Python 3.x. (The code that was
introduced for the new short float repr made it easy to fix.)

That said, if your client really *means* -9.85 (rather than some
binary approximation to it), and wants it to round in a predictable
manner, the right way to fix this would be to use Decimal instead of
float to represent the number. That way, you can also specify what
rounding mode you want (instead of relying on the default round-half-
away-from-zero in 2.x or round-half-to-even in 3.x.)

>>> decimal.Decimal('-9.85').quantize(decimal.Decimal('0.1'), rounding=decimal.ROUND_HALF_UP)
Decimal('-9.9')
>>> decimal.Decimal('-9.85').quantize(decimal.Decimal('0.1'), rounding=decimal.ROUND_HALF_EVEN)
Decimal('-9.8')

--
Mark
From: Gary Herron on
On 07/12/2010 02:52 AM, Robin Becker wrote:
> A client wants to know why his db number -9.85 gets displayed by some
> simple code as -9.8
>
> I looked at the number and see that
>
> >>> -9.85
> -9.8499999999999996
>
> ie I expect simple rounding to produce the observed result and indeed
>
> >>> '%.1f' % -9.85
> '-9.8'
>
> however, when I use round I get an unexpected result ie
> >>> round(-9.85,1)
> -9.9000000000000004
>
> according to its definition
>
>> round(x[, n])¶
>> Return the floating point value x rounded to n digits after the
>> decimal point.
>> If n is omitted, it defaults to zero. The result is a floating
>> point number.
>> Values are rounded to the closest multiple of 10 to the power
>> minus n;
>> if two multiples are equally close, rounding is done away from 0
>> (so. for example,
>> round(0.5) is 1.0 and round(-0.5) is -1.0).
>
> so looking at the absolute differences I see
>
> >>> abs(-9.9 - -9.85)
> 0.050000000000000711
> >>> abs(-9.8 - -9.85)
> 0.049999999999998934

Do you *seriously* need to ask a computer if -9.85 is half-way between
-9.9 and -9.8. Just look at the numbers man! Of course it's equally
close to both, so rounding to -9.9 is correct according to the definition.



Do you actually *believe* that -9.9 - -9.85 = 0.050000000000000711.
Of course you know it's really 0.05. All you've done here is
demonstrate the fallible nature of (the last several digits of) floating
point arithmetic on a computer. Don't let that distract you from using
your common sense.


Gary Herron




> ie the -9.8 value appears closer and at least to a primitive test
>
> >>> abs(-9.9 - -9.85) > abs(-9.8 - -9.85)
> True
>
> the distance from the -9.9 result is larger, however, that may be
> because the model numbers for -9.8 & -9.9 differ in distance from the
> true 10**-n values eg
>
> >>> -9.9
> -9.9000000000000004
> >>> -9.8
> -9.8000000000000007
>
> What value should round(-9.85,1) return? Is the result explainable in
> python (ie without resort to the internal FP representations etc etc)?


From: Emile van Sebille on
On 7/12/2010 2:52 AM Robin Becker said...
<snip>
> What value should round(-9.85,1) return?

Per round's definition, -9.9. String interpolation for %n.mf doesn't
appear to define it's rounding behavior, so a peek at the source would
answer what's being done.

It does look inconsistent however, and it seems to me rounding and
interpolation should behave similarly. So I'd call it a bug.

Emile



>>> def display(val):
.... print "%.1f" % val
.... print round(val,1)
....
>>> display(1.45)
1.5
1.5
>>> display(-1.45)
-1.5
-1.5
>>> display(-1.85)
-1.9
-1.9
>>> display(1.85)
1.9
1.9
>>> display(-7.85)
-7.8
-7.9
>>> display(-9.85)
-9.8
-9.9
>>>



From: Mark Dickinson on
Emile van Sebille <emile <at> fenx.com> writes:

>
> On 7/12/2010 2:52 AM Robin Becker said...
> <snip>
> > What value should round(-9.85,1) return?
>
> Per round's definition, -9.9.

No. The float that's represented by the literal '-9.85' *isn't*
exactly -9.85, for all the usual binary floating-point reasons.
The value that gets stored is a tiny amount smaller (i.e., closer
to zero) than -9.85, so according to the definition it should
round *down*, to -9.8. (Or rather, to a float that's very close
to, but not exactly equal to, -9.8.)

> String interpolation for %n.mf doesn't
> appear to define it's rounding behavior, so a peek at the source would
> answer what's being done.

In Python 2.6, the string interpolation delegates to the system,
so it does whatever C string formatting does. Usually that's
rounding to nearest, with exact halfway cases rounded to
even. In Python 2.7 and 3.x, Python has its own code for
string formatting, and again it rounds to nearest, rounding
ties to even.

> It does look inconsistent however, and it seems to me rounding and
> interpolation should behave similarly.

Agreed. In this case it's a minor bug that round(-9.85, 1)
produces -9.9 instead of -9.8; both string formatting
and round should give -9.8. This bug is fixed in Python
2.7 and in Python 3.x.

Note that in 2.7 there's still a legitimate difference: round
rounds halfway cases away from 0, while string formatting
rounds them to even. So the following results are correct:

Python 2.7 (r27:82500, Jul 11 2010, 22:38:53)
[GCC 4.2.1 (Apple Inc. build 5659)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> round(1.25, 1)
1.3
>>> '%.1f' % 1.25
'1.2'

(1.25 *is* an exact halfway case, since it's exactly
representable as a binary float.)

In Python 3.x, round always does round-half-to-even, so
string formatting and round should agree (and if they don't,
it's definitely a bug: please report it!)

With all this said, asking for *decimal* rounding of
*binary* approximations to *decimal* halfway cases to give
the results you expect is ... optimistic, to say the least.
Use the decimal module if you care about which way
your (almost) halfway cases get rounded.

[I already replied to this earlier through Google groups, but
I'm not sure whether it went through properly. Apologies
for the duplication, if so.]

--
Mark