"""
A small monte carlo code to simulate the growth of coins in a cookie jar over a 1 year period
The following are assumed:
1) you make X purchases each day with petty cash, starting out with only bills in your pocket (i.e., no change).
2) Each purchase has a random chance of costing some dollar amount plus YY cents (where YY goes from 0-99).
You always get change in the smallest number of coins possible. For instance,
if you have a purchase of $2.34, then you assume you acquire 66 cents in change
(2 quarters, 1 dime, 1 nickel, 1 penny).
3) If you have enough change to cover the YY cents of the current transaction, you use it.
Otherwise, you accumulate more change. For example, if you have $1.02 in loose change,
and you have a purchase of $10.34, then you use 34 cents (or as close to it as possible) in coins
leaving you with 68 cents.
4) At the end of each day you dump all your coins collected for the day in a Money Jar.
PYTHON BOOT CAMP HOMEWORK2 SOLUTION;
created by Josh Bloom at UC Berkeley, 2010 (ucbpythonclass+bootcamp@gmail.com)
DOCTESTS!
>>> run_questions()
question a
--------------------------------------------------
mean 182.4474 std 4.72300616557
--------------------------------------------------
question b
--------------------------------------------------
x= 5 first= {'pennies': 50, 'nickels': 0, 'dimes': 0, 'quarters': 0} second= {'pennies': 0, 'nickels': 0, 'dimes': 10, 'quarters': 40}
x= 2 first= {'pennies': 50, 'nickels': 0, 'dimes': 0, 'quarters': 0} second= {'pennies': 0, 'nickels': 0, 'dimes': 0, 'quarters': 50}
x= 10 first= {'pennies': 50, 'nickels': 0, 'dimes': 0, 'quarters': 0} second= {'pennies': 0, 'nickels': 0, 'dimes': 21, 'quarters': 29}
question c
--------------------------------------------------
average # of quarters 30
>>>
"""
import random, math
import numpy
__version__ = "0.1"
__author__ = "J. Bloom (jbloom@astro.berkeley.edu)"
random.seed(-1L) # seed the random number generator so we get the same answer each time. helpful for doctests
val = {"nickels": 0.05, "quarters": 0.25, "dimes": 0.10, "pennies": 0.00}
class CookieJar:
"""
the basic workhorse
"""
## set the contents upon create to nothing
deplete_quarters_frequency=7 # remove quarters every 1 week
num_quarters_to_deplete=8 # how many quarters to remove
def __init__(self,transactions_per_day=8,number_of_days_until_fill=365,deplete_quarters=False,\
print_summary_every_week=False,print_summary_of_every_transaction=False):
self.contents = {"quarters": 1, "dimes": 1, "nickels": 0, "pennies": 0} ## NOTE: should all be zero here!
self.final_value = self._content_value(self.contents)
self.final_contents = self.contents
self.num_transactions_performed = 0
self.day = 0
self.days_to_reach_500_pennies = -1
self.print_summary_of_every_transaction = print_summary_of_every_transaction
self.print_summary_every_week = print_summary_every_week
self.transactions_per_day = transactions_per_day
self.number_of_days_until_fill=number_of_days_until_fill
self.deplete_quarters = deplete_quarters
def fill_er_up(self):
"""
the main engine, it runs all the transactions and accumulates some final results for this cookie jar
"""
while self.day < self.number_of_days_until_fill:
if self.print_summary_every_week:
print "Day %i" % (self.day + 1)
self.perform_a_days_worth_of_transactions()
self.day += 1
if self.contents["pennies"] > 500 and self.days_to_reach_500_pennies == -1:
self.days_to_reach_500_pennies = self.day
if self.day % self.deplete_quarters_frequency == 0 and self.deplete_quarters:
self.contents["quarters"] = max(0,self.contents["quarters"] - self.num_quarters_to_deplete)
#print "all done after %i transactions" % self.num_transactions_performed
self.final_value = self._content_value(self.contents)
self.final_contents = self.contents
self.final_order = self._order(self.contents)
def __str__(self):
"""
print a summary of yourself
"""
a = "Value %.2f after %i transactions performed." % (self.final_value,self.num_transactions_performed)
a += " days to reach 500 pennies: %i" % self.days_to_reach_500_pennies
return a
def _order(self,purse):
"""
determine the ordering of number of coins in the purse.
here the purse is assumed to be a dict like
{"nickels": 0, "quarters": 12, "dimes": 3, "pennies": 32}
returns
{1: "pennies", 2: "quarters", 3: "dimes", 4: "nickels"}
"""
tmp = [(v,k) for k,v in purse.iteritems()]
tmp.sort(reverse=True)
return dict([(i+1,tmp[i][1]) for i in range(len(tmp))])
def _content_value(self,purse):
"""
determine the value of coins in the purse.
here the purse is assumed to be a dict like
{"nickels": 0, "quarters": 12, "dimes": 3, "pennies": 32}
"""
rez = 0.0
for k in purse.keys():
rez += val[k]*purse[k]
return rez
def best_change(self,cost,contents,verbose=False):
"""
for given transaction cost determines the best combination of coins that
gives as close to the exact change amount needed as possible given the contents of a purse
returns a tuple where the first element is False if the contents of the purse cannot
cover the change cost, True if it can
the second element is a dict showing how much of each coin type is required to make the transaction
as close to $x.00 as possible
This is just a big ugly 4x nested for loop, trying out all combinations
"""
cost_in_cents = cost % 1.0
if cost_in_cents > self._content_value(contents):
# there's no way we have enough...our purse value is less than the cost in cents
return (False,{})
exact = False
best_diff = 1.00
best = {}
for q in range(contents["quarters"] + 1):
for d in range(contents["dimes"] + 1):
for n in range(contents["nickels"] + 1):
for p in range(contents["pennies"] + 1):
v = round(q*0.25 + d*0.10 + n*0.05 + p*0.01,2)
if verbose:
print "val",p,n,d,q,v,cost_in_cents,best_diff
if abs(v - cost_in_cents) < 0.005:
## this is within the tolerance of a floating point difference
best_diff = 0.0
best = {"nickels": n, "dimes": d, "pennies": p, "quarters": q}
exact = True
break
elif (v - cost_in_cents) > 0.0 and (v - cost_in_cents) < best_diff:
best_diff = (v - cost_in_cents)
best = {"nickels": n, "dimes": d, "pennies": p, "quarters": q}
exact = False
if exact:
break
if exact:
break
if exact:
break
return (True,best)
def perform_a_days_worth_of_transactions(self):
"""
loop over all the transactions in the day keeping track of the number of coins of each type
in the purse.
The random cost of a transaction is set to be:
cost = round(random.random()*50,2)
"""
#initialize how much booty we have in our pockets
pocket_contents = {"nickels": 0, "quarters": 0, "dimes": 0, "pennies": 0}
n_exact = 0
for i in xrange(self.transactions_per_day):
cost = round(random.random()*50,2) # assume a transaction cost of $0 - $50
# round to the nearest cent
if self.print_summary_of_every_transaction:
print "Day %i, transaction %i" % (self.day + 1,i + 1)
print " pocket_contents = %s" % repr(pocket_contents)
print " cost = $%.2f" % cost
## do I have exact change?
got_enough = self.best_change(cost,pocket_contents)
if got_enough[0]:
## we have enough change and it might just enough to get us where we need to be
## That is the cost + this change ends in .00. So, subtract the value to the cost
cost -= sum([got_enough[1][x]*val[x] for x in val.keys()])
## now remove all that from our purse
for k,v in got_enough[1].iteritems():
pocket_contents[k] -= v
# print "...new cost", cost
if cost % 1.0 == 0.0:
n_exact += 1
change = self.calc_change(cost)
for k,v in change.iteritems():
if v != 0:
pocket_contents[k] += v
self.num_transactions_performed += 1
if self.print_summary_of_every_transaction:
print " end the end of the day: pocket_contents = %s" % repr(pocket_contents)
print " we had %i exact change times out of %i transactions" % (n_exact,self.transactions_per_day)
## dump what we have into the cookie jar at the end of the day
for k in self.contents.keys():
self.contents[k] += pocket_contents[k]
def calc_change(self,transaction_amount):
"""
for a given transaction amount, determines how many coins of each type to return
"""
change = 1.0 - (transaction_amount % 1.0) # make this a number from 0.0 - 0.99
change_in_cents = int(round(change*100.0) % 100) ## make from 0 - 99 as type int
#print "change",change,"change_in_cents",change_in_cents
oring_change_in_cents = change_in_cents
n_quarters = change_in_cents / 25 ## since this is int / int we'll get back an int
change_in_cents -= n_quarters*25
n_dimes = 3 * change_in_cents / 10 ## ug
change_in_cents -= n_dimes*100
n_nickels = change_in_cents / 5
change_in_cents -= n_nickels*5
n_pennies = change_in_cents
if self.print_summary_of_every_transaction:
print " Transaction is $%.2f (coin change was %i cents)" % (transaction_amount ,oring_change_in_cents)
print " %s: quarters: %i dimes: %i nickels: %i pennies: %i" % ("returned", \
n_quarters ,n_dimes,n_nickels,n_pennies)
print "*" * 40
return {"nickels": n_nickels, "quarters": n_quarters, "dimes": n_dimes, "pennies": n_pennies}
def test_val():
## make sure that val = {"nickels": 0.05, "quarters": 0.25, "dimes": 0.10, "pennies": 0.01}
assert val["nickels"] == 0.05
assert val["quarters"] == 0.25
assert val["dimes"] == 0.10
assert val["pennies"] == 0.01
def test_CookieJar():
"""
test suite for the CookieJar functions
"""
c = CookieJar()
## make sure that we've got contents and make sure that it actually has what we think it should have
## TODO: add another set of related assertions
assert c.__dict__.has_key("contents")
assert c.contents["dimes"] == 0
assert c.contents["nickels"] == 0
## let's make sure we get back what we expect from calc_change
## TODO: add some more assertions
assert c.calc_change(1.23) == {'pennies': 2, 'nickels': 0, 'dimes': 0, 'quarters': 3}
assert c.calc_change(12.2) == {'pennies': 0, 'nickels': 1, 'dimes': 0, 'quarters': 3}
assert c.calc_change(12) == {'pennies': 0, 'nickels': 0, 'dimes': 0, 'quarters': 0}
assert c.calc_change(99.99) == {'pennies': 1, 'nickels': 0, 'dimes': 0, 'quarters': 0}
def run_questions():
"""performs the monte carlo, making many instances of CookieJars under different assumptions."""
## a: What is the average total amount of change accumulated each year (assume X=5)?
# What is the 1-sigma scatter about this quantity?
## let's simulate 50 cookie jars of 1 year each
random.seed(-1L)
jars = []
for j in xrange(50):
jars.append(CookieJar(transactions_per_day=5,number_of_days_until_fill=365,deplete_quarters=False))
jars[-1].fill_er_up()
fin = numpy.array([x.final_value for x in jars])
mn = fin.mean()
st = numpy.std(fin)
print "question a"
print "-"*50
print "mean",mn,"std", st
print "-"*50
# mean = $181.71
# st = $5.99
## b. What coin (quarter, dime, nickel, penny) are you most likely to accumulate
## over time? Second most likely? Does it depend on X?
first = {"nickels": 0, "quarters": 0, "dimes": 0, "pennies":0}
second = {"nickels": 0, "quarters": 0, "dimes": 0, "pennies":0}
for j in jars:
first[j.final_order[1]] += 1
second[j.final_order[2]] += 1
print "question b"
print "-"*50
print "x=",5,"first=",first,"second=",second
# pennies always first, quarters usually second (sometimes dimes)
random.seed(-1L)
## now let's try # transaction changes
for tr in [2,10]:
jars = []
for j in xrange(50):
jars.append(CookieJar(transactions_per_day=tr,number_of_days_until_fill=365,deplete_quarters=False))
jars[-1].fill_er_up()
first = {"nickels": 0, "quarters": 0, "dimes": 0, "pennies":0}
second = {"nickels": 0, "quarters": 0, "dimes": 0, "pennies":0}
for j in jars:
first[j.final_order[1]] += 1
second[j.final_order[2]] += 1
print "x=",tr,"first=",first,"second=",second
## 2 {'pennies': 50, 'nickels': 0, 'dimes': 0, 'quarters': 0} {'pennies': 0, 'nickels': 0, 'dimes': 0, 'quarters': 50}
## 10 {'pennies': 50, 'nickels': 0, 'dimes': 0, 'quarters': 0} {'pennies': 0, 'nickels': 0, 'dimes': 15, 'quarters': 35}
## 20 {'pennies': 50, 'nickels': 0, 'dimes': 0, 'quarters': 0} {'pennies': 0, 'nickels': 0, 'dimes': 14, 'quarters': 36}
## answer: no. it doesn't
## c. Let's say you need 8 quarters per week to do laundry. How many quarters do you have at the end of the year?
## (if you do not have enough quarters at the end of each week, use only what you have).
jars = []
random.seed(-1L)
for j in xrange(50):
jars.append(CookieJar(transactions_per_day=5,number_of_days_until_fill=365,deplete_quarters=True))
jars[-1].fill_er_up()
nq = 0
for j in jars:
nq += j.final_contents["quarters"]
print "question c"
print "-"*50
print "average # of quarters",nq/len(jars)
# answer = 28
if __name__ == "__main__":
run_questions()