4

I would like to calculate the sum of reciprocals of a list of integers (and see if it is larger or equal to 1):

enter image description here

I want to work with integers to avoid floating-point rounding issues. To do so, I want to work it out like this:

enter image description here

I have done this:

import numpy as np

my_list = [2, 3, 5, 7]
numerator = 0
for i in range(len(my_list)):
    numerator += np.product(my_list[:i] + my_list[i+1 :])
denominator = np.product(my_list)
result = numerator>=denominator

but I feel like there should be a one-liner for that. Is there a function to calculate the sum of reciprocals as fractions? Or perhaps a function to calculate the numerator from a list?

Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153
Rémi Baudoux
  • 542
  • 3
  • 16
  • 1
    I don't understand how `numerator>=denominator` is intended to give a useful or correct result. Surely you mean `numerator / denominator`? "I want to work with integers to avoid the famous 0.999999999999998 >= 1 -> False" Instead, why not just work with a rational number class, such as the standard library `fractions.Fraction`? Doing arithmetic is not a justification for busting out Numpy. – Karl Knechtel Mar 21 '22 at 20:42
  • "but I feel like there is a one-liner for that" Are you familiar with the built-in `sum` function? – Karl Knechtel Mar 21 '22 at 20:43
  • `numerator>=denominator` is just the the same as `numerator / denominator>=1` but without rounding issue. Yes, I will have a look at `fractions` – Rémi Baudoux Mar 21 '22 at 20:52
  • Ah, I missed the "and see if it is larger or equal to 1" part of the question initially. – Karl Knechtel Mar 21 '22 at 20:53

3 Answers3

6

The Fraction type can do this easily and exactly:

>>> from fractions import Fraction
>>> bottoms = [2, 3, 5, 7]
>>> total = sum(Fraction(1, d) for d in bottoms)
>>> total
Fraction(247, 210)
>>> total > 1
True
Tim Peters
  • 67,464
  • 13
  • 126
  • 132
  • I was going to post essentially this, but I was annoyed by not finding an *elegant* way to simplify the fraction. I see a note in the documentation: *Changed in version 3.9: The math.gcd() function is now used to normalize the numerator and denominator.* I don't have that version installed. Does this imply that e.g. `Fraction(total)` will normalize the result? Because it doesn't in 3.8. – Karl Knechtel Mar 21 '22 at 20:51
  • 1
    Sorry, I don't know what that comment is trying to say. You can see from the source (https://github.com/python/cpython/blob/3.8/Lib/fractions.py) that even in 3.8 fractions were _always_ reduced to lowest terms. But before 3.9, sometimes the module used its own implementation (of `gcd()`), for historical reasons lost in time. As to your specific `Fraction(total)`, no, it is (and always has been) assumed that an _incoming_ `Fraction` is always in lowest terms. Under 3.10.1, `f = Fraction(3, 3, _normalize=False); Fraction(f)` displays `Fraction(3, 3)`. `_normalize` is really for internal use. – Tim Peters Mar 21 '22 at 21:05
  • Pardon; for some reason I imagined for a moment that 247 and 210 weren't relatively prime :) – Karl Knechtel Mar 21 '22 at 21:15
  • 1
    No problem - at first glance, it was obvious to me they were both divisible by 3 ;-) – Tim Peters Mar 21 '22 at 21:15
  • My answer's benchmarks make this look somewhat slow, but it depends on the list. I used the first two things that came to my mind. Let me know if you think something else would be better/fairer. – Kelly Bundy Mar 22 '22 at 17:28
  • The OP didn't mention speed - they asked about exactness, and some measure of clarity/succinctness. On those measures, using `Fraction()` appears unbeatable. If it turns out they _do_ care about speed, and above all else, then it's their job to tell us about the expected distribution of inputs. – Tim Peters Mar 22 '22 at 17:35
  • Yeah yeah, I agree. Yours is certainly the nicest and what I would've done if you hadn't already. That's why I was worried I might improperly make it look bad in a way (and one reason I wasn't worried about Pranav's, as that isn't as nice (the other reason being that I expect it to always be slower)). – Kelly Bundy Mar 22 '22 at 17:40
2

If you want to do this by cross-multiplying integers without using division, you could use the itertools.combinations() function.

from itertools import combinations

def product(iterable):
    prod = 1
    for item in iterable: prod *= item
    return prod

my_list = [2, 3, 5, 7]
n = len(my_list)

numerator = sum(product(combo) for combo in combinations(my_list, n-1))
denominator = product(my_list)

Then, you can compare numerator >= denominator instead of fraction >= 1

Note that I defined the product() function to give the product of an iterable the same way sum() gives the sum of the elements of an iterable.

Pranav Hosangadi
  • 23,755
  • 7
  • 44
  • 70
  • 3
    Note that in a recent Python (>= 3.8) you can use `math.prod()` instead of defining your own `product()`. – Tim Peters Mar 21 '22 at 21:22
1

Using math.prod and computing the numerator by dividing the numbers out of the denominator:

den = prod(my_list)
num = sum(den // i for i in my_list)
print(num >= den)

Benchmark with three lists of 1000 random ints from 2 to 8000 (which appears to have a ~50% chance of the sum reaching 1):

 36.4 ms   35.5 ms   34.8 ms  Tim_Fractions
333.9 ms  322.5 ms  326.3 ms  Pranav_Combinations
  6.0 ms    5.9 ms    5.9 ms  Kelly_Divide

Benchmark with the prime numbers up to 8000 (just because your example does something like this, although 1/2+1/3+1/5 already exceeds 1):

123.9 ms  123.8 ms  126.0 ms  Tim_Fractions
304.4 ms  313.6 ms  298.2 ms  Pranav_Combinations
  5.9 ms    5.9 ms    5.9 ms  Kelly_Divide

If you insist on a oneliner:

(d := prod(my_list)) <= sum(d // i for i in my_list)

Possible optimization idea: Sort the numbers and don't blindly compute the whole sum, instead stop as soon as you reach 1.

Benchmark code:

def Tim_Fractions(bottoms):
    return sum(Fraction(1, d) for d in bottoms) >= 1

def Pranav_Combinations(my_list):
    def product(iterable):
        prod = 1
        for item in iterable: prod *= item
        return prod
    n = len(my_list)
    numerator = sum(product(combo) for combo in combinations(my_list, n-1))
    denominator = product(my_list)    
    return numerator >= denominator

def Kelly_Divide(my_list):
    den = prod(my_list)
    num = sum(den // i for i in my_list)
    return num >= den

funcs = [
    Tim_Fractions,
    Pranav_Combinations,
    Kelly_Divide,
]

from timeit import repeat
from fractions import Fraction
from itertools import combinations
from math import prod
import random

my_list = [i for i in range(2, 8000) if all(i % d for d in range(2, i))]
tss = [[] for _ in funcs]
for _ in range(3):
    # remove the next line if you want to benchmark with the primes instead
    my_list = random.sample(range(2, 8000), 1000)
    for func, ts in zip(funcs, tss):
        t = min(repeat(lambda: func(my_list), number=1))
        ts.append(t)
        print(*('%5.1f ms ' % (t * 1e3) for t in ts), func.__name__)
    print()
Kelly Bundy
  • 23,480
  • 7
  • 29
  • 65