Fraction from float

Before reinventing the wheel, I wondered if anyone has investigated how to recover fractions from floats.

A classic example is of course
n = -16/15 == -1.0666666666666667

n.as_integer_ratio() returns (-4803839602528529, 4503599627370496) which obviously isn’t right.

likewise Fractions(str(-16/15)) returns Fraction(-10666666666666667, 10000000000000000) which isn’t right either.

Kind regards
B

1 Like

-4803839602528529/4503599627370496 is right – just not obviously so :‌)

Floats stored in binary, and the exact value of 16/15 lost when converted to float. See Python docs or more general explanations.

If you know you want a “simpler” fraction, decide on a maximum value you want in the denominator and use limit_denominator:

>>> from fractions import Fraction
>>> Fraction.from_float(-16/15).limit_denominator(1_000_000_000_000)
Fraction(-16, 15)
3 Likes

Converting to a float is lossy: the float you get will, in general, be
every slightly different from the original division.

# exact because the denominator is an exact sum of powers of 2
1/8

# inexact because the denominator is not a sum of powers of 2
1/7

So once you get that inexact float, there is no way to know what the
“true” fraction should have been.

>>> from fractions import Fraction
>>> x = 1/7
>>> Fraction(x)
Fraction(2573485501354569, 18014398509481984)

But all is not lost:

>>> Fraction(x).limit_denominator(1000)
Fraction(1, 7)


>>> Fraction(-16/15)
Fraction(-4803839602528529, 4503599627370496)
>>> Fraction(-16/15).limit_denominator(50)
Fraction(-16, 15)
1 Like

Yes - it’s the loss I’m worrying about. @encukou @steven.daprano

I’m thinking that Fractions sh/c/ould handle the case automatically where:

>>> from fractions import Fraction
>>> a,b = Fraction.from_float(-16/15), -16/15
>>> math.isclose(a,b)
True

So I’m asking myself whether letting the user setting am arbitrary limit_denominator could be avoided systematically…?

1 Like

It can – use Fraction instead of float and Fraction(a, b) instead of a/b.
Fraction can exactly express more numbers than float, but still not all of them (like π or √2). Operations on Fraction are also usually slower: most processors can do float math natively, if you go all-in on Fraction, all the exact denominators can easily grow extremely large.

2 Likes

The loss is unavoidable. Floats have no more than 64 bits of
information, and that includes 1 bit for the sign. But Fractions are
limited only by the amount of memory.

x = 1 - Fraction(1, 2)**1000000

is a fraction where both the numerator and denominator have more than
300,000 digits each. (That’s about a million bits each.)

You can’t fit 20 pounds of potatoes in a 10 pound sack, and you can’t
fit two millions bits in a 64 bit float :slight_smile:

For every finite float, there is more than one possible numerator/
denominator pair that would be rounded to that float, even if you count
only pairs in simplified form.

(Strictly speaking, there are an infinite number of such numerator/
denominator pairs. But most of them involve ludicrously big values.)

Let’s take the three adjacent floats:

0.49999999999999994 = 9007199254740991 / 18014398509481984
0.5 = 1/2
0.5000000000000001 = 4503599627370497 / 9007199254740992

So there are a whole lot of fractions that exist within the range
0.499…4 to 0.500…1 that have to be rounded to one of those three
values. For example:

>>> float(Fraction(9007199254740993, 18014398509481984))
0.5

In principle, one of your users might genuinely expect the exact
fraction 9007199254740993/18014398509481984 and be mad that it rounds to
1/2. But it isn’t very likely.

But if you really care about this, then just use Fractions all the way
through your calculations, and don’t allow any floats to creep into the
calculation. Make sure your users only enter values as numerator,
denominator pairs, and there will never be any data loss.

Or you could just use limit_denominator with a denominator of, say,
50000 or 100000 or so. Most people aren’t going to care about fractions
with larger denominators.

2 Likes

You may be interested in the simplefractions package: simplefractions · PyPI

>>> from simplefractions import simplest_from_float
>>> simplest_from_float(-16/15)
Fraction(-16, 15)
3 Likes

@mdickinson Line 145 in simplefractions:

if r-t <s = once the the difference is pointless, return.

I like that.

1 Like