-1

I'm trying to create a function that will divide a number into categories based on a specified percent (okay, a budget.)

I've taken the divided floats, grabbed a test truncate function off the web, and truncated everything after the hundredth place. This is to prevent the sum of all the categories being greater than the initial amount. Then I subtract the total sum of the categories from the initial amount to get a "remainder," which in my example should be 0.03, or 3 cents.

def budget_calc(amount):

    budget = {"t":0.10,
            "c":0.50,
            "s":0.20,
            "e":0.05,
            "c/m":0.05,
            "tr":0.05,
            "o":0.03,
            "g/d":0.02}

    def truncate(x, d):

        return int(x*(10.0**d))/(10.0**d)


    def multp(key):

        cate = truncate(amount * budget.get(key), 2)

        return cate


    new_amounts = {'t': multp('t'),
              'c': multp('c'),
              's': multp('s'),
              'e': multp('e'),
              'c/m': multp('c/m'),
              'tr': multp('tr'),
              'o': multp('o'),
              'g/d': multp('g/d')}

    remainder = amount - sum(new_amounts.values())
    new_amounts.update(remainder = remainder)

    return new_amounts

This is what I'm getting:

budget_calc(148.72)

     {'t': 14.87,
     'c': 74.36,
     's': 29.74,
     'e': 7.43,
     'c/m': 7.43,
     'tr': 7.43,
     'o': 4.46,
     'g/d': 2.97,
     'remainder': 0.029999999999972715} #<-- this number should only contain two decimal points

So somewhere along the line the "truncate" function is only displaying the truncated number, not actually getting rid of the excess digits? The math in there is wrong as well, 148.72 - the sum of all the truncated numbers (148.69) should be 0.03. That's what it seems like, an additional diagnosis would be great.

  • You may want to read this https://stackoverflow.com/questions/588004/is-floating-point-math-broken to get a basic understanding of (binary) floating-point arithmetics. You could use the `decimal` package to achieve what you want. – GZ0 Aug 13 '19 at 00:25

2 Answers2

1

You never applied truncate to the remainder calculation, you just summed the truncated numbers and subtracted them. Every addition in the sum includes a possibility of a small precision issue, and the subtraction introduces another, and truncating the results will cause serious problems (because an error that reduces the value even a little will cause a huge error when you truncate). If you want to round it consistently, you have to round it every time. I'd also recommend using math.fsum to reduce the error on your summation:

>>> d = {'t': 14.87,
         'c': 74.36,
         's': 29.74,
         'e': 7.43,
         'c/m': 7.43,
         'tr': 7.43,
         'o': 4.46,
         'g/d': 2.97}
>>> import math
>>> sum(d.values())        # Loses precision
148.69000000000003
>>> math.fsum(d.values())  # More accurate!
148.69
>>> 148.72 - sum(d.values())  # Already lost precision, so result wrong
0.029999999999972715
>>> 148.72 - math.fsum(d.values())  # Sadly, even with accurate fsum, final subtraction is "wrong"
0.030000000000001137
>>> round(148.72 - math.fsum(d.values()), 2)  # But we can always round it off
0.03

All that said, trying to do this with floats is inherently error-prone; float precision is expressed in binary, and you want decimal precision. If you need decimal precision, I'd strongly recommend you use the decimal module which has a million knobs for making it behave exactly the way you want it to.

ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
1

This is due to imprecision in float calculations, just like

0.1 + 0.2 = 0.30000000000000004

in javascript.

You can find more information about that here Why does adding two decimals in Javascript produce a wrong result?

A solution would be to round instead of truncating, and also round your reminder.

Sunny Pelletier
  • 329
  • 2
  • 13