Bisect Search to choose best savings rate - python

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.

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.

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?

Related

What is the expected value of a coin-toss that doubles in value if heads and why is it different in practice? [closed]

Closed. This question is not about programming or software development. It is not currently accepting answers.
This question does not appear to be about a specific programming problem, a software algorithm, or software tools primarily used by programmers. If you believe the question would be on-topic on another Stack Exchange site, you can leave a comment to explain where the question may be able to be answered.
Closed 3 months ago.
Improve this question
Here's the thought experiment: say I have a coin that is worth 1$. Everytime I toss it, if it lands on head, it will double in value. If it lands on tail, it will be forever stuck with the latest value. What is the expected final value of the coin?
Here is how I am thinking about it:
ExpectedValue = 1 * 0.5 + (1 * 2) * (0.5 * 0.5) + (1 * 2 * 2) * (0.5 * 0.5 * 0.5) ...
=0.5 + 0.5 + 0.5 ...
= Infinity
Assuming my Math is correct, the expected value should be infinity. However, when I do try to simulate it out on code, the expected value comes out very different. Here's the code below:
import random
def test(iterations):
total = 0
max = 0
for i in range(iterations):
coin = False
val = 1
while coin == False:
coin = random.choice([True, False])
val *= 2
total += val
if val > max:
max = val
ave = total/iterations
print(ave)
test(10000000) # returns 38.736616
I assume that the sample size of 10000000 should be statistically significant enough. However, the final expected value returned is 38.736616, which is nowhere near Infinity. Either my Math is wrong or my code is wrong. Which is it?
The average value of the process over infinitely many trials is infinite. However, you did not perform infinitely many trials; you only performed 10,000,000, which falls short of infinity by approximately infinity.
Suppose we have a fair coin. In four flips, the average number of heads that come up is two. So, I do 100 trials: 100 times, I flip the coin four times, and I count the heads. I got 2.11, not two. Why?
My 100 trials are only 100 samples from the population. They are not distributed the same way as the population. Your 10,000,000 trials are only 10,000,000 samples from an infinite population. None of your samples happened to include the streak of a hundred heads in a row, which would have made the value for that sample 299, and would have made the average for your 10,000,000 trials more than 299/10,000,000 = 6.338•1022, which is a huge number (but still not infinity).
If you increase the number of trials, you will tend to see increasing averages, because increasing the number of trials tends to move your samples toward the full population distribution. For the process you describe, you need to move infinitely far to get to the full distribution. So you need infinitely many trials.
(Also, there is a bug in your code. If the trial starts with False, representing tails, it still doubles the value. This means the values for 0, 1, 2, 3… heads are taken as 2, 4, 8, 16,… The process you describe in the question would have them as 1, 2, 4, 8,…)
Another way of looking at this is to conduct just one trial. The average value for just one trial is infinite. However, half the time you start one trial, you stop after one coin flip, since you got a tail immediately. One quarter of the time you stop after one or two flips. One-eighth of the time, you stop after three flips. Most of the time, you will get a small number as the answer. In every trial, you will get a finite number as the answer; every trial will always end with getting a tail, and the value at that point will be finite. It is impossible to do a finite number of trials and ever end up with an infinite value. Yet there is no finite number that is greater than the expected value: If you do more and more trials, the average will tend to grow and grow, and, eventually, it will exceed any finite target.

How does the python interpreter know that only specific variables in the equation are to be multiplied by the number of days?

I am working on practice examples on edabit using python. One of the examples I am stuck on is as follows:
Create a function that takes the number of daily average recovered cases recovers, daily average new_cases, current active_cases, and returns the number of days it will take to reach zero cases.
def end_corona(recovers, new_cases, active_cases):
end_corona(4000, 2000, 77000) ➞ 39
end_corona(3000, 2000, 50699) ➞ 51
end_corona(30000, 25000, 390205) ➞ 79
Below is one of the correct solutions that I am not quite sure how the interpreter knows that only "new_cases" and "recovers" are supposed to be multiplied by the number of days (d). Somehow this solution does work but can someone explain to me why and how?
def end_corona(recovers, new_cases, active_cases):
days = 0
while active_cases > 0:
days += 1
active_cases = active_cases-recovers+new_cases
return days
The function is running in a loop. So in each iteration it subtracts the daily average recovered cases from the active cases and adds the daily average new cases and increase the number of days by one. It runs until the active cases are not zero and returns the total number of days it would take to get to zero cases.

How can I find out the number of outputs in a loop?

I am a beginner at python and I'm struggling with one of my (simple) college assignments. I have been given the following instructions:
A bank is offering a savings account where a yearly fee is charged. Write
a program that lets the user enter
An initial investment.
The yearly interest rate in percent.
The yearly fee.
the program should then calculate the time it takes
for the investment to double. The interest is added on once per year.
An example run of the program:
Enter the investment: 1000
Enter the interest rate: 10
Enter the fee: 10
The investment doubles after 7 years.
I have formulated the following code but am receiving an error message with regards to t. I would really appreciate if I could get some help, thanks!:
t=0
p=float(input("Enter the investment:"))
a=float(input("Enter the interest rate:"))
m=float(input("Enter the fee:"))
i=(float(a/100))
f=p
while f<=(2*p):
f=(float(f*((1+i)**t)-m)
t=t+1
print("The investment doubles after",t,"years")
I tried to write this in a way that was very easy to follow and understand. I edited it with comments to explain what is happening line by line. I would recommend using more descriptive variables. t/p/a/m/f may make a lot of sense to you, but going back to this program 6 months from now, you may have issues trying to understand what you were trying to accomplish. NOTE You should use input instead of raw_input in my example if using Python 3+. I use 2.7 so I use raw_input.
#first we define our main function
def main():
#investment is a variable equal to user input. it is converted to a float so that the user may enter numbers with decimal places for cents
investment = float(raw_input("Starting Investment: "))
#interest is the variable for interest rate. it is entered as a percentage so 5.5 would equate to 5.5%
interest = float(raw_input("Interest Rate as %, ex: 5.5 "))
#annual_fee is a variable that will hold the value for the annual fee.
annual_fee = float(raw_input("Annual Fee: "))
#years is a variable that we will use with a while loop, adding 1 to each year (but we wait until within the loop to do this)
years = 1
#we use a while loop as opposed to a for loop because we do not know how many times we will have to iterate through this loop to achieve a result. while true is always true, so this segment is going to run without conditions
while True:
#this is a variable that holds the value of our total money per year, this is equal to the initial investment + investment * interest percentage - our annual fee per year
#I actually had to try a few different things to get this to work, a regular expression may have been more suited to achieve an interest % that would be easier to work with. do some research on regular expressions in python as you will sooner or later need it.
total_per_year = investment + (years * (investment * (interest / 100))) - (annual_fee * years)
#now we start adding 1 to our years variable, since this is a while loop, this will recalculate the value of total_per_year variable
years += 1
#the conditional statement for when our total_per_year becomes equal to double our initial investment
if total_per_year >= 2 * investment:
#print years value (at time condition is met, so it will be 5 if it takes 5 years) and the string ' Years to Double Investment'
print years,' Years to Double Investment'
#prints 'You will have $' string and then the value our variable total_per_year
print 'You will have $', total_per_year
#this will break our while loop so that it does not run endlessly
break
#here is error handling for if the fee is larger than investment + interest
if (years * annual_fee) >= (years * (investment * (interest / 100))):
print('Annual Fee Exceeds Interest - Losing Money')
break
#initial call of our main function/begins loop
main()

Python Blackjack Game how to carry a variable in a while loop

I'm still deep into the making of my Blackjack game in Python, my aim is to allow the computer to 'twist' (Take another card) until the total value is greater than 15. From then onwards I want it so that as the total of the cards gets higher, the chance of the computer taking another card gets smaller just like a human (taking a risk if the number isn't TOO big).The following code occurs after the computer has been dealt two values.
if (computerCardValueTotal < 15):
print ("The computer has decided to twist")
ranCompCard3 = random.choice(availableCards)
computerCardValueTotal = ranCompCard + ranCompCard2 + ranCompCard3
if (computerCardValueTotal < 15):
print ("The computer has chosen to twist again")
My aim is to have this piece of code loop if the total value is less than 15 so that the computer twists until it is over 15. I have considered using a while loop but I'm unsure on how to carry the current total to the start of the loop so that the next card value is added to the current total. Does anyone have a solution they could help me out with?
Also, secondly, it's not the biggest of the two issues in this question, but how would you recommend doing the computer so that the chance of it twisting again is smaller as the total value of cards get bigger? For example, for if the value of the cards totals at 17, there's a 1/10 chance of the computer twisting but if the total value of the cards is 19 the chance of the computer twisting is 1/20.
All valuable help will be voted up and as always, thanks in advance! :)
You have hobbled yourself by making e.g. ranCompCard, ranCompCard2, ranCompCard3, ... when a list would make life much easier:
compCards = [ranCompCard, ranCompCard2]
while sum(compCards) < 15:
compCards.append(random.choice(availableCards))
For adjusting the probability of picking another card, you could do something like:
while True:
gap = 21 - sum(compCards)
if gap > 0 and random.random() < (1 / (40 / float(gap))):
compCards.append(random.choice(availableCards))
else:
break
Note that this is a linear relationship between gap and the probability of picking another card:
>>> for card_sum in range(15, 21):
print card_sum, 1 / (40 / float(21 - card_sum))
15 0.15
16 0.125
17 0.1
18 0.075
19 0.05
20 0.025
but you can adjust the equation as needed.
While you're editing your code, take a look at (and consider following) the official style guide; variable names should be lowercase_with_underscores.
Simply reassign the value when it changes.
while computerCardValueTotal < 15:
# other code
computerCardValueTotal = <new value>

Checking if number is within more or less range in Python

I'm new to Python as part of an exercise I'm making a simple number guessing game. I've gotten the basics covered, but I'm trying to implement a manner which tells the user if their guess is close or far from the right number. I can't guess within a range because the number to be guessed is randomly selected each time. My exercise tells me to look into abs(), but that doesn't bring me much sense. It also mentions something about within(), which also doesn't do much for me. Just need a push in the right direction if anyone can help. Thanks.
You have to come up with values that mean close or far.
It might look something like this:
diff = abs(guess - random_number)
if diff >= 50:
print("Really cold!")
elif diff >= 40:
print("Cold.")
...
elif diff >= 5:
print("Getting really hot!")
You could have a function return a number which represents how hot or cold it is (0 through 10 for example), then have a dict that looks like this:
hot_str = {
0: "You guessed it!",
1: "Extremely hot!",
10: "Frozen cold!",
}
print(hot_str[heat(diff)])
You could use operator chaining and test against both extremes of your range:
if lower <= number <= upper:
This test matches if number is in the range [lower, upper] (both ends inclusive).
If you wanted to see how close a value is to something else, you could use abs(target - guess):
distance = abs(target - guess)
if 10 <= distance <= 20:
print('You are getting closer now!')
A whole series of such tests are going to be tedious; you can print messages based on how close they are with a sequence of tests:
messages = (
(100, 'Way, way off!'),
(80, 'So cold, are you not freezing?'),
(60, 'Is there a light on the horizon?'),
(40, 'Where there is warmth, there is hope!'),
(30, 'You are getting warmer now..'),
(20, 'Is it just me, or is it getting hot in here?'),
(10, 'You are about to burn yourself!'),
(0, 'Fire, ta-cha-ta, burning desire, ta-cha-ta!')
)
distance = abs(target - guess)
if distance == 0:
print("You guessed it right!")
else:
for target, message in messages:
if distance > target:
print(message)
break
It depends on how you define close and far. For now I assume let d be parameter and if abs(guess-right_num) is less than d then it is close otherwise it is far
so the code can be:
if abs(guess-right_num) < d:
print("close")
else:
print("far")
You can't use a range to directly compare your number and the randomly generated number, but you can use a range to directly compare their differences.
abs() can be used to return the positive version of a number whether it is positive or negative.
abs(10)
10
abs(-10)
10
You can use this to ensure your random number is always positive.
You must be misreading about within(); This is either something accessible only to you and your class, or you misread what it meant, because within() is not a build-in function.
Now, on solving your problem. This takes a bit of math and logic.
You can't work within a range because the number is randomly generated. But you can always get a percentage of them and work based on that.
As an example, consider this number line, where the random number is 10 (denoted by |).
[(0)---------|---------(20)]
You will always be able to divide the number you guess by the random number and get a number that will range from (in this case) (0/10) and (10/20). In all cases, when number_guessed == number_generated, the number calculated in the division operation will be equal to 1.
I leave you to figure out the implementation of this, but the basic concept is that you can figure out how close you are to the number based on how far away you are from the number 1.
It's not perfect either, but it's an interested and clever way that might get a professor's notice, if you do it right.

Categories

Resources