1

I have a rounding issue in Python 2.7 leading to unexpected output. I'm trying to get combinations of p1 and p2 that sum up to 0.6 or less in total.

from itertools import product
P = []
p1 = [0.0,0.2,0.4,0.6]
p2 = [0.0,0.2,0.4,0.6] 
for p11,p22 in product(p1,p2):
    if p11+p22 <= max(p1):    
        P.append((p11,p22))

However, when I run this, it does not include all values for which p11+p22 = 0.6:

[(0.0, 0.0),
 (0.0, 0.2),
 (0.0, 0.4),
 (0.0, 0.6),
 (0.2, 0.0),
 (0.2, 0.2),
 (0.4, 0.0),
 (0.6, 0.0)]

It does work correctly when I set p11+p22 <= max(p1)+0.01. For different p1 and p2 the problem may or may not occur. I find this behavior extremely strange, leading to very unreliable results.

It is probably related to floating precision issues. In my opinion this behaviour shouldn't exist in Python, since R and Matlab do not have this behaviour either. Are there any simple ways around this?

Martin Thoma
  • 124,992
  • 159
  • 614
  • 958
Forzaa
  • 1,465
  • 4
  • 15
  • 27
  • 4
    This is a general problem: [Floating Point Arithmetic: Issues and Limitations](http://docs.python.org/3/tutorial/floatingpoint.html). R and Matlab may implicitly convert comparisons for equality into comparisons for "equality +/- epsilon", but in Python, you need to do this explicitly. – Tim Pietzcker Nov 21 '15 at 15:58
  • 1
    "*I find this behavior extremely strange*" Why so? It is the expected behavior of almost every general purpose language with IEEE floating point support. The general lesson is: Be extra careful when comparing floats. – dhke Nov 21 '15 at 16:06
  • 1
    FWIW `(0.4 + 0.2) <= 0.6` gives `FALSE` in R and `0` in Octave for me, just like Python, as you'd expect – DSM Nov 21 '15 at 16:10
  • @dhke Because it is different than what you would expect from mathematics. – Forzaa Nov 21 '15 at 16:10
  • 1
    @Forzaa Only if you're naively trying to apply your mathematical knowledge of adding small decimals. If you convert to binary and look up how floating-point numbers are stored in computers, the math on this (and the problems with it) becomes perfectly clear. – Two-Bit Alchemist Nov 21 '15 at 16:16
  • 1
    @Forzaa That I can understand. Unfortunately, abstract numbers with infinite precision are somewhat hard to represent in the real world. Welcome to the wonderful science of *numerics*. – dhke Nov 21 '15 at 16:16
  • And btw I only meant this topic is not unapproachable from a mathematical standpoint. It is simply another topic in mathematics many people have not considered. – Two-Bit Alchemist Nov 21 '15 at 16:20
  • I don't think any language can naturally solve this problem. [See here] ( http://matlabgeeks.com/tips-tutorials/floating-point-comparisons-in-matlab/). – B. M. Nov 21 '15 at 17:08

4 Answers4

5

What is happening?

Computers have an internal representation of numbers. In most cases, those representations have a fixed number of bits. This leads to only a fixed amount of numbers being representable. For example, you might know that languages Like C have a maximum value for integers.

Similar, you can't store the exact representation of some floating point numbers. As the computer uses base two, there are some numbers in base 10 which have a short, finite representation but the binary one is long. For more details, see IEEE 754.

How can it be "fixed"?

There is nothing to be fixed here as everything is working like it was specified. But you have to know about these types of problems. When you are aware of the fact that there is a problem, then there are two strategies to get around it.

Either use epsilons (-> don't compare with exact numbers, but check if the number is within a very small interval around the number. The length of this interval is often called "epsilon") or use arbitrary precision representations (see fractions. The second only works when you can influence how the number is put into the program, e.g.

from itertools import product
from fractions import Fraction
P = []
p1 = [Fraction(0.0), Fraction(2, 10), Fraction(4, 10), Fraction(6, 10)]
p2 = [Fraction(0.0), Fraction(2, 10), Fraction(4, 10), Fraction(6, 10)]
for p11, p22 in product(p1, p2):
    if p11+p22 <= max(p1):
        P.append((p11, p22))

See also

Community
  • 1
  • 1
Martin Thoma
  • 124,992
  • 159
  • 614
  • 958
  • 1
    "Computers have an internal representation of numbers" ... it's not per definition a problem for *computers*. The result of `1/3` cannot be represented exactly in decimal, similar to the result of `1/10` in floating point binary (which is what computers happen to use internall). – Jongware Nov 21 '15 at 16:48
  • 1
    @Jongware Correct (+1). I thought about adding exactly this example, but I wanted to keep the answer short. If somebody is interested in details, I can recommend the last link ("What Every Computer Scientist Should Know About Floating-Point Arithmetic") – Martin Thoma Nov 21 '15 at 16:51
  • 1
    Absolutely agree - that particular link should pop up automatically for *every* question that states "my computer is broken" :) I wanted to point out that it is not limited to a 'computer' problem. – Jongware Nov 21 '15 at 16:58
1

Because the limitations on fixed bit width floating point, you need to use an arbitrary precision floating point package or explicitly compare with +/- an epsilon amount.

Python includes decimal (for 'arithmetic that works in the same way as the arithmetic that people learn at school'):

from itertools import product
import decimal

P = []
p1 = map(decimal.Decimal, ['0.0','0.2','0.4','0.6'])
p2 = map(decimal.Decimal, ['0.0','0.2','0.4','0.6'])
for p11,p22 in product(p1,p2):
    if p11+p22 <= max(p1):    
        P.append((p11,p22))
dawg
  • 98,345
  • 23
  • 131
  • 206
1

If you want to compare with matlab or R, or have performance issues , here is a numpy approach, with np.isclose() as workaround.

p1 = [0.0,0.2,0.4,0.6]
p2 = [0.0,0.2,0.4,0.6] 
sums=np.add.outer(p1,p2)
P1,P2=np.meshgrid(p1,p2)
compare = (sums<0.6) | np.isclose(sums,0.6)
print(np.dstack((P1,P2))[compare])

which gives :

[[ 0.   0. ]
 [ 0.2  0. ]
 [ 0.4  0. ]
 [ 0.6  0. ]
 [ 0.   0.2]
 [ 0.2  0.2]
 [ 0.4  0.2]
 [ 0.   0.4]
 [ 0.2  0.4]
 [ 0.   0.6]]
B. M.
  • 18,243
  • 2
  • 35
  • 54
-1

One way around it would be to define a different inequality comparison function,

function less_or_nearly_equal(a,b)
    return (a <= b + .000001)

and use that instead of <= in your comparison

for p11,p22 in product(p1,p2):
    if less_or_nearly_equal(p11+p22,max(p1)):    
        P.append((p11,p22))

(I hope my syntax is correct in the function definition.)

Thinkadoodle
  • 512
  • 1
  • 4
  • 11