0

Hi I would like some help with this questions set as one of the problems on the MIT OCW Computer Science and Python Course. I know people have asked similar questions and I have found useful posts such as Bisection search code doesnt work but I am still stuck!

I have struggled with this problem for many days and tried to tackle it in different ways and failed in all ways. If at all possible, could somebody just hint at where I am going wrong, rather than telling me the answer. I would like to solve this problem for myself with bit of help.

For reference, the question is part C, here: https://ocw.mit.edu/courses/electrical-engineering-and-computer-science/6-0001-introduction-to-computer-science-and-programming-in-python-fall-2016/assignments/MIT6_0001F16_ps1.pdf

As I have been struggling, I have broken down this task into an overall aim and then into steps to solve the problem.

Aim: try to find the best rate of savings to achieve a down payment on a $1M house in 36 months ##Steps to solve the problem:
1) make a guess on the savings rate, which is the average of the 0 and 1000
2) calculate how that would grow over 36 months
3a) if amount reached goes over 25% of $1m within 36 months, then a lower savings rate should be the new guess
...max=guess (old guess) and min=0 and update the guess (average of high and low)
...run the calculation in step 2 with the new guess
3b) if amount does not reach 25% of $1m within 36 months, then a higher savings rate should be the new guess
...min=guess (old guess) and update the guess (average of high and low) ...run the calculation in step 2 with the new guess
3c) if amount reaches 25% of $1m within the 36th month deadline then quit and record the savings rate as the best guess.
For simplicity: assume no interest and assume wages remain same

So here's my code with the current effort at solving this. (it leads to the "guess" variable tending to 0 and then infinitely looping)

total_cost=1000000 #cost of house
portion_down_payment=0.25 #fraction of cost needed for downpayment on house
downpayment=total_cost*portion_down_payment

starting_annual_salary=float(input("Enter the starting salary: "))
low=0
high=1000
bisect_steps=0
month=1 #set as 1 because the first calculation will occur in month 1 
guess=(low+high)//2
current_savings=0

def calSavings(current_savings,monthly_salary,guess,month):
    while month<37:
        monthly_savings=monthly_salary*(guess/1000)
        current_savings+=monthly_savings
        month+=1
    return(current_savings) 

current_savings=calSavings(current_savings,monthly_salary,guess,1)
while True:
    current_savings=calSavings(current_savings,monthly_salary,guess,1)
    if current_savings>downpayment and month<=35: #if amount reached goes over 25% of $1m within 36 months
        #a lower savings rate should be the new guess
        high=guess #max=guess (old guess) and min=0 and update the guess
        bisect_steps+=1
        guess=(low+high)//2
        print("The guess was too high, so the new lower guess is",guess," This is bisect step",bisect_steps)
        continue #send new guess up to beginning of while loop to calculate 
    elif current_savings<downpayment and month>=36: #if amount does not reach 25% of $1m within 36 months
        low=guess
        bisect_steps=+1
        guess=(low+high)//2
        print("The guess was too low, so the new higher guess is",guess," This is bisect step",bisect_steps)
        continue #send new guess up to beginning of while loop to calculate 
    elif current_savings>=downpayment and month==36: #if amount reaches 25% of $1m in the 36th months then quit
        # record the savings rate as the best guess
        print("The best savings rate is ",guess/100,"%, the amount saved was ",current_savings," in ",month," months")
        break #break out of while loop

I know other people have asked similar questions (I have looked at those answers and still not solved my problem) but more than an answer I want help on HOW to solve this problem.

Westworld
  • 190
  • 1
  • 2
  • 14

2 Answers2

1

Update

The reason why your loop isn't stopping is because you aren't giving it enough time. What you are forgetting is that you're dealing with the decimal type. Using the == with decimal values is always dangerous. The decimal type is accurate (by default) to 28 places, which means you're trying to find an extremely good approximation for this problem, because only when it's correct to 28 decimals will (current_savings>downpayment or current_savings<downpayment) evaluate to False invoking your exit condition.

Basically, the issue that's causing your problem is that even when you eventually get the estimate of $1,000,000.0000000001, python says this is not equal to $1,000,000.0000000000, so it keeps going until it gets the next 0, but then it just adds another zero and so on. This will continue for a very very long time and in rare cases might never stop due to the fact that not all decimal numbers can be stored as binary numbers (1,000,000 is not among those cases).

So, how do we solve this? There are two options. The easiest one is to ignore cents and just cast your comparison values to int, this will make sure that any value off by a fraction of a dollar will be accepted. The other options is to create a range of accepted answers. Say for example, I'd like to save EXACTLY $1 million in those 36 months, but that isn't likely to happen. So, instead, I'll settle for any amount in the range $1,000,000.00 - $1,000,010.00 (for example). This way, we ensure that any guess that is way too high will get rejected and only a very limited amount of guesses are accepted.

Regardless of which route you go, it is generally a good idea to put the exit condition of an infinite loop to the top, this way you guarantee that it will always be evaluated.

My suggestion would be to write a function like this and use that for your condition to exit the loop (which you would place at the top):

def are_decimals_equal(a, b):
    accuracy = 0.0001
    return abs(a-b) < accuracy

This will consider 0.00009 (and all decimals less than that) to be equal to 0.0000.

Original

First off, just as a note, what you're doing is not called bisection, it's called binary search.

Now to the problem, you are never altering the value of month in your main loop. This means, as soon as current_savings>downpayment evaluates to False, your program will go into an infinite loop as none of the conditions after it could evaluate to True as month>=36 will always be False.

From what I can see, the second part of your conditions in the if/elif statements is unnecessary, your calSavings will always compute 36 months worth of savings, never more, never less. Thus, if you remove that condition from your if/elif statements, your program will eventually stop and at that point it should settle on the correct answer.

Lastly, the reason why you're seeing 0 as the output is your division at the end. If you do print(typeof(guess)) you will see it is an integer, 100 is also an integer, thus this division will result in some value like 0.3123 which will get truncated to 0. Change your output to float(guess/100) and this will go away.

nick
  • 449
  • 3
  • 12
  • Thanks so much for helping me! I will go back and apply what you've suggested and feed-back here. – Westworld Nov 01 '18 at 00:10
  • Just focusing on the If conditionals in the While loop here. I take the point about the calSavings running for 36 months only, so I have removed the second part of each of the If conditionals relating to month. Within the If conditionals, I have reset the month, month=1, and current savings, current_savings=0. When the program gets to 'continue', should it not then return to the start of the while loop, with then new 'guess' and recalculate current_savings ? It doesn't seem to as it stays in an infinite loop. I am really missing something on the logic here! – Westworld Nov 01 '18 at 16:19
  • (1) You don't need the month variable at all, you can just remove it. (2) Continue works as you think it does, it will run the next iteration of the (infinite) loop, though, if the code above is complete, you also don't need that as the next step would be the next iteration anyway. – nick Nov 02 '18 at 08:59
  • I'll update my answer concerning why it's not stopping. – nick Nov 02 '18 at 09:03
  • I also noticed another issue in your code. This line ```monthly_savings=monthly_salary*(guess/1000)``` should really be ```monthly_savings=monthly_salary*(float(guess)/1000.0)``` to ensure you aren't getting an integer value there. Similarly, declare ```low = 0.0```, ```high = 1000.0```, and ```current_savings = 0.0``` just to be safe. – nick Nov 02 '18 at 09:41
  • Hi Nick - again, thank you so much for your help. The update you've made is very clear and gives me a lot to work with. I'd like to explain why I used 1000 as a maximum to mean 100% (savings rate) in the "guess" variable. This is so (high+low)//2 only an integer results. Woking only with integers avoids infinite division and keeps the percentage savings to 1 decimal place, so for example 875 would be 87.5%. Working in this way was not my idea, but was suggested by the MIT lecturer who set this problem. I will work on what you've suggested there and come back with my revised code. – Westworld Nov 02 '18 at 10:36
  • ```//``` is integer division, it will never result in a float, regardless of input types. ```high + low``` will be implicitly cast to ```int``` after the addition. See https://python-reference.readthedocs.io/en/latest/docs/operators/floor_division.html – nick Nov 03 '18 at 11:04
0

I hope it's okay for me to provide an answer to my own question here - though it's not a perfect answer.

The results the code produces seem plausible.

total_cost=1000000 #cost of house

portion_down_payment=0.25 #fraction of cost needed for downpayment on house
downpayment=total_cost*portion_down_payment

starting_annual_salary=float(input("Enter the starting salary: "))
monthly_salary=starting_annual_salary/12
low=0
high=1000
binary=0
month=1 #set as 1 because the first calculation will occur in month 1
guess=(low+high)//2
current_savings=0
tolerance=500

def calSavings(current_savings,monthly_salary,guess,month):
    while month<37:
        monthly_savings=int(monthly_salary*(guess/1000))
        current_savings+=monthly_savings
        month+=1
    return(current_savings)

current_savings=calSavings(current_savings,monthly_salary,guess,1)

while True:
    if abs(current_savings-downpayment)<=tolerance: #if the difference between the current savings and downpayment is less than $500
        # record the savings rate as the best guess
        print("The best savings rate is ",guess/10,"%, the amount saved was $",current_savings," in 36 months")
        break #break out of while loop
    elif (current_savings-downpayment)>tolerance: #if amount reached goes over 25% of $1m within 36 months
        #a lower savings rate should be the new guess
        high=guess #high=guess (old guess) and low=low (stays same) and update the guess
        binary=binary+1
        guess=(low+high)//2
        print("The guess was too high, so the new lower savings rate is",guess/10,"%. This is binary-search step",binary)
        current_savings=calSavings(0,monthly_salary,guess,1)
        continue #send new guess up to beginning of while loop to check if conditionals
    elif (downpayment-current_savings)>tolerance: #if amount does not come to within tolerance amount of 25% of $1m within 36 months
        low=guess #to make the guess higher, make low=guess (old guess) and high stay the same
        binary=binary+1
        guess=(low+high)//2
        print("guess is ",guess)
        if guess>=990: #check if the savings rate guess is getting too high
            print("Your wages are too low. You can't save up enough")
            break #exit the while loop because conditions will never be met
        print("The guess was too low, so the new higher savings rate is",guess/10,"%. This is binary-search step",binary)
        current_savings=calSavings(0,monthly_salary,guess,1)
        continue #send new guess up to beginning of while loop to check over the conditionals

The tolerance for an acceptable answer is within $500, but if I lower that to $50, I end up in a seemingly infinite loop again, where the guess and the low end up being the same. I am happy that I've made some apparent progress, but baffled that I can't lower the tolerance without it going haywire again.

BTW, I didn't want to seem like I ignored Nick's comments about making the variables into floats, but I explained why I worked in integers in my comment - does that seem correct?

Westworld
  • 190
  • 1
  • 2
  • 14