%matplotlib notebook
import random
import math
import numpy as np
import matplotlib.pyplot as plt
import time
from sklearn.utils.extmath import cartesian
Hello there, If you have found this through reddit or social media sharing sites, you should be aware that this iPython Notebook is a companion piece to a talk I gave at Sydney Python. The full write-up can be found here and the slides that precede and succeed this simulation can be found here. I recommend reading in this order:
Also, come caveats: I have purposely omitted discussions of stuff such as the effect of money supply and demand on wealth. It should be quite immediately obvious the moment you start thinking of money as a good as well. However, I have found through previous experience of teaching this to people, that it's not a good way to help them understand how money is created
Welcome to the great nation of Dystopianistan. The eternal leader is Kimberly Il-Smith. Here in Dystopianistan, things are done slightly differently from how you know them to be. But there are some quite familiar things. We'll start with one of them: the idea of Bank
s
Bank
s provide a place for the citizens of Dystopianistan to store their money. It also provides loans to any citizen who needs it. Loans in Dystopianistan are rather simple - there is no concept of compound interest. Also, all loans are set for a fixed period of time
Bank
s loan money with special rules - they can loan more than they actually have. If a Bank
holds $200 in its holdings, it can loan up to $1000. The reason is because through a lot of study, it's been found that the citizens of Dystopianistan are generally good debtors - they pay back whatever they owe, plus interest, most of the time. This allows the banks to count on future returns, and so they're allowed to lend out more money than they have in store.
interest_rate = 0.1
reserve_rate = 0.2
class Bank(object):
def __init__(self, interest_rate, reserve_rate):
self.interest_rate = interest_rate
self.reserve_rate = reserve_rate
self.loans = []
self.unrecovered = 0.0
self.holding = 0.0
def can_lend(self, amount):
# check if there is enough money in holdings
if self.unrecovered+amount > 1/self.reserve_rate*self.holding:
return False # cannot loan any more money
return True
def lend(self, loan):
self.unrecovered += loan.amount
def repay(self, loan):
self.unrecovered -= loan.repay_amount()
class Loan(object):
def __init__(self, lender, borrower, amount, repay_at, interest_rate):
self.lender = lender
self.borrower = borrower
self.amount = amount
self.repay_at = repay_at
self.interest_rate = interest_rate
def repay_amount(self):
return self.amount * (1 + self.interest_rate)
def __repr__(self):
return "{} | {}".format(self.repay_amount(), self.repay_at)
central_bank = Bank(interest_rate, reserve_rate)
In Dystopianistan, citizens are also able to make investments. Investments are also simple here. At the end of 7 periods, citizens get a return on investment by means of upgrading their skills - they simply become more productive depending on how much investment they put in.
class Investment(object):
def __init__(self, principle, return_rate, return_at):
self.principle = principle
self.return_rate = return_rate
self.return_at = return_at
self.p = abs(random.gauss(0, 0.01)) # half normal distribution
def __repr__(self):
return "{} | {}".format(self.principle, self.return_at)
The citizens of Dystopianistan are very different from what you normally know as people. They are born as full adults, with the capacity to produce things. Right from the moment they are born, they are thrust into the economic machine, producing and consuming goods. Due to their highly efficient nature, the citizens of Dystopianistan only consume goods once every period. Likewise, they produce goods only once every period.
Despite all that, the citizens of Dystopianistan are not robots. They are quite varied. Each citizen has different likes and dislikes (we call those preferences
), and each citizen has different skills in producing goods (we call those productivities
). Also, each citizen of Dystopianistan have different preferences for risk - some like to save more money now (savings_pref
), some like to invest more money for future benefit (investment_pref
).
Consumption of goods make Dystopianistanians happy - a citizen may be good at producing oranges, but she may like apples more than oranges. Therefore she does the logical thing - sell her oranges and buy apples instead. However, sometimes, the prices of apples may be too high for her consumption habit, so she has to either cut down her consumption of apples, or take out a loan.
Consequently, every Dystopianistanian also has a likelihood of taking out a loan to fuel his/her spending habits (we call that consumerism
).
In determining if a citizen is qualified for a loan, we'd have to calculate their creditworthiness
.
DEFAULT_THRESHOLD = 4
class Person(object):
def __init__(self, bank, endowment, productivities, preferences, investment_pref, savings_pref):
'''
:param bank: which Bank does this Person banks with?
:param endowment: how much savings does a person initially start with? rich families = high endowment
:param productivities: a list of numbers the size of `basket`.
This represents how many goods Person can produce per period.
:param preferences: a list of floats the size of `basket`
:param investment_pref: a float between [0,1). 0 = person doesn't care about his/her future earnings
'''
self.bank = bank
self.savings = endowment
self.bank.holding += self.savings
self.productivities = productivities
self.preferences = preferences
self.investment_pref = investment_pref
self.savings_pref = savings_pref
self.loans = []
self.investments = []
self.last_income = endowment
self.nonpayment = 0
self.defaulted = False
self.consumerism = np.random.beta(3,5)
def utility(self, q, investment):
'''
utility represents a utility a Person has when presented with an allocation of goods and investment.
:param q: a list of quantities
:param investment: a number
'''
u = 1
for g, p in zip(q, self.preferences):
u *= math.pow(g, p)
u *= math.pow(investment, self.investment_pref)
return u
def normalized_preferences(self):
return [pref/(sum(self.preferences)+self.investment_pref+self.savings_pref) for pref in self.preferences]
def normalized_productivities(self):
return [prod/sum(self.productivities) for prod in self.productivities]
def produce(self, priceMatrix, current):
'''
produce simulates the production capactity of a Person.
'''
normal_prod = np.array(self.normalized_productivities())
budget = self.budget(current)
quantities = normal_prod * priceMatrix
return quantities
def consume(self, priceMatrix, current):
'''
Returns the demand given the prices. Demand is inelastick, because these Persons are turdballs.
'''
normal_pref = np.array(self.normalized_preferences())
budget = self.budget(current)
quantities = normal_pref * budget / priceMatrix
return quantities
def budget(self, current):
'''
The budget forms the constraint for a Person in his/her consumption
'''
repayments = 0.0
for loan in self.loans:
if current == loan.repay_at:
repayments += loan.repay_amount()
return self.creditworthiness()*self.consumerism + self.savings - (1-self.consumerism) * repayments
def creditworthiness(self):
'''
creditworthiness determines how much a Person can borrow from the bank
'''
if self.savings > 0:
return self.last_income + (self.last_income * interest_rate) + self.savings
elif self.last_income > 0:
return self.last_income + (self.last_income * interest_rate)
return 0 # NO CREDIT FOR YOU!
def debt(self):
'''
How much debt does a Person have?
'''
amount = 0.0
for loan in self.loans:
amount += loan.repay_amount()
return amount
def spend(self, amount, current):
if amount < self.savings:
self.savings -= amount
return
elif amount > self.savings:
if self.bank.can_lend(amount):
loan = Loan(self.bank, self, self.creditworthiness(), current+5, interest_rate)
self.loans.append(loan)
self.bank.lend(loan)
else:
self.savings = 0
def earn(self, income):
self.savings += income
self.last_income = income
def invest(self, amount, current):
investment = Investment(amount, interest_rate, current+7)
self.investments.append(investment)
def step(self, prices, current):
production = self.produce(prices, current)
income = sum([p*q for p, q in zip(prices, production)])
consumption = self.consume(prices, current)
inv_pref = self.investment_pref/(sum(self.preferences)+self.investment_pref)
inv_amt = inv_pref * self.budget(current)
norm_prod = self.normalized_productivities()
prod_cost = [p*q for p, q in zip(prices, production)]
prod_cost = sum(n*c for n, c in zip(norm_prod, prod_cost))
cost = sum([p*q for p, q in zip(prices, consumption)]) + inv_amt + prod_cost
self.earn(income)
self.spend(cost, current)
if self.savings > inv_amt:
self.invest(inv_amt, current)
# remove loans that are up
remove = []
cantpay = False
for loan in self.loans:
if loan.repay_at <= current:
if self.savings >= loan.repay_amount():
self.savings -= loan.repay_amount()
self.bank.repay(loan)
remove.append(loan)
else:
cantpay = True
for loan in remove:
self.loans.remove(loan)
if cantpay:
self.nonpayment += 1
# realize investments
remove = []
for investment in self.investments:
if investment.return_at == current:
self.savings += investment.principle*(1+investment.return_rate)
self.productivities = [prod *(1+investment.p) for prod in self.productivities]
remove.append(investment)
for investment in remove:
self.investments.remove(investment)
# check for defaults
if self.nonpayment >= DEFAULT_THRESHOLD:
self.defaulted = True
default_amt = 0.0
for loan in self.loans:
default_amt += loan.amount
self.bank.unrecovered -= default_amt
basket = 3
endowment = random.gauss(50,10)
preferences = np.random.randint(0,10, basket)
skills = np.random.randint(0,10, basket)
investment_pref = np.random.randint(0,10)
savings_pref = np.random.randint(0,10)
A = Person(central_bank,endowment, skills, preferences, investment_pref, savings_pref)
for i in range(2):
prices = np.random.beta(3,2, (3,basket)) * 10
production = A.produce(prices,i)
income = sum([p*q for p, q in zip(prices, production)])
consumption = A.consume(prices,i)
consumes = sum([p*q for p,q in zip(prices, consumption)])
A.step(prices[0], i)
print("Step {}".format(i))
print("Price: \n{}".format(prices))
print("Budget {}".format(A.budget(i)))
print("Productivity \n{}".format(production))
print("Consumption \n{}".format(consumption))
print("Income: {} {}".format(A.last_income, income))
print("Savings: {}".format(A.savings))
print("Cost: {}".format(consumes))
print("Debt: {}".format(A.debt()))
print("Creditworthiness {}".format(A.creditworthiness()))
print("Loans: {}".format(A.loans))
print("Investments: {}".format(A.investments))
print("\n\n")
Step 0 Price: [[ 5.60191322 7.01336885 7.12751418] [ 8.66811908 4.56525387 7.09286451] [ 7.99165882 8.03726514 3.87125812]] Budget 189.01043990690778 Productivity [[ 2.40081995 0. 4.07286525] [ 3.71490818 0. 4.05306543] [ 3.42499664 0. 2.2121475 ]] Consumption [[ 0. 4.57357645 6.00044237] [ 0. 7.0261544 6.0297554 ] [ 0. 3.99093199 11.04763277]] Income: 42.47858983516505 [ 73.02185606 0. 66.34104276] Savings: 97.47168364658936 Cost: [ 0. 96.22853572 128.3047143 ] Debt: 158.61794571179803 Creditworthiness 144.19813246527093 Loans: [158.61794571179803 | 5] Investments: [16.038089286975342 | 7] Step 1 Price: [[ 7.63988269 4.41222354 8.21191414] [ 8.87513079 4.20549432 2.89029569] [ 3.12333561 5.97389287 6.57323107]] Budget 307.61531531020654 Productivity [[ 3.27423544 0. 4.69252237] [ 3.80362748 0. 1.65159754] [ 1.3385724 0. 3.75613204]] Consumption [[ 0. 10.70947777 7.67220393] [ 0. 11.2359229 21.79828181] [ 0. 7.90985225 9.58485702]] Income: 63.549365420926776 [ 62.95327685 0. 67.99811986] Savings: 161.02104906751612 Cost: [ 0. 141.75782993 189.01043991] Debt: 158.61794571179803 Creditworthiness 230.92535103053558 Loans: [158.61794571179803 | 5] Investments: [16.038089286975342 | 7, 23.626304988363472 | 8]
Consider a small subset of the population of Dystopianistan where there are two citizens, Alice and Bob, and they only produce two goods: apples and oranges.
This is what they produced:
Who | Apples Produced | Oranges Produced |
---|---|---|
Alice | 5 | 10 |
Bob | 12 | 5 |
This is what each of them prefer to have:
Who | Apples Prefered | Oranges Prefered |
---|---|---|
Alice | 8 | 2 |
Bob | 9 | 13 |
This can be solved quite easily with basic algebra:
$$3A - 8O = 0$$$$A = \frac{8}{3}O$$While it was simple to solve for Alice and Bob, the process of solving it for everyone take $\mathcal{O}(n^n)$. Instead, the geniuses at Dystopianistan came up with a novel idea:
Through meticulous study, they have discovered that this method is indeed the most fair method of redistributing the goods - at least in theory. The end result was Pareto optimal - where no one can be made better off without making someone worse off.
So, back to the example with Alice and Bob. Let's say the fair price for Apples are $3, and the fair price for oranges are $8.
First, they both sell all their goods to InvisbleHandatron 2000:
Who | Income From Apples | Income from Oranges | Total Income |
---|---|---|---|
Alice | \$15 | \$80 | \$95 |
Bob | \$36 | \$40 | $76 |
They then buy back however much they prefer from InvisibleHandatron 2000:
Who | Cost of buying Apples | Cost of buying Oranges | Total Cost |
---|---|---|---|
Alice | \$24 | \$20 | \$45 |
Bob | \$27 | \$104 | \$131 |
Here you might notice Bob doesn't make quite enough money to buy what he prefers. But who knows? He may crack into his savings. Or he might take a loan to fund his fruit-related addiction. Or he may choose to eat fewer fruits and go to the toilet less. Either way, the story has ended.
class Market(object):
current = 0
def __init__(self, thickness, basket, bank, max_price, steps):
self.basket = basket
self.participants = []
self.bank = bank
self.max_price = max_price # government intervention! This is the price ceiling that the government sets
self.steps = steps # how many steps to break down from 0 to max price
for i in range(thickness):
endowment = random.gauss(50,10)
preferences = np.random.randint(1,10, self.basket)
skills = np.random.randint(1,10, self.basket)
investment_pref = np.random.randint(0, 10)
savings_pref = np.random.randint(0,10)
participant = Person(bank,endowment, skills, preferences, investment_pref, savings_pref)
self.participants.append(participant)
def demand(self, priceMatrix):
quantities = np.zeros((priceMatrix.shape[0], priceMatrix.shape[1]))
for participant in self.participants:
quantities += participant.consume(priceMatrix, self.current)
return quantities
def supply(self, priceMatrix):
quantities = np.zeros((priceMatrix.shape[0], priceMatrix.shape[1]))
for participant in self.participants:
q = participant.produce(priceMatrix, self.current)
quantities += q
return quantities
def market_price(self):
prices = (np.arange(self.steps) + 1) * self.max_price / float(self.steps)
priceMatrix = np.tile(prices.reshape(self.steps,1), (1, self.basket))
# priceMatrix = cartesian([prices for i in range(m.basket)])
supply = self.supply(priceMatrix)
demand = self.demand(priceMatrix)
surplus = supply - demand
min_surplus = np.argmin(np.abs(surplus),0)
eq_prices = []
for i in range(self.basket):
eq_prices.append(priceMatrix[min_surplus[i]][i])
return np.array(eq_prices)
def step(self):
eq_prices = self.market_price()
for participant in self.participants:
if not participant.defaulted:
central_bank.holding -= participant.savings
participant.step(eq_prices, m.current)
central_bank.holding += participant.savings
m.current += 1
def m1(self):
money = 0.0
for participant in self.participants:
if not participant.defaulted and participant.savings > 0:
money += participant.savings
return money
def m2(self):
money = 0.0
for participant in self.participants:
if not participant.defaulted:
for loan in participant.loans:
money += loan.amount * (1+loan.interest_rate)
return money
def actives(self):
return sum([1 for p in self.participants if not p.defaulted])
The market is quite self explanatory - at every time period, it calculates the equilibrium price. Remember, the citizens of Dystopianistan are price takers with InvisbleHandatron being the price maker.
Here we see how the price is calculated:
basket = 4
m = Market(100, basket, central_bank, 20, 20)
prices = (np.arange(m.steps) + 1) * m.max_price / float(m.steps)
# priceMatrix = cartesian([prices for i in range(m.basket)])
priceMatrix = np.tile(prices.reshape(m.steps,1), ( 1, basket))
supply = m.supply(priceMatrix)
demand = m.demand(priceMatrix)
market_price = m.market_price()
print("Market Price: {}".format(market_price))
fig, ax = plt.subplots(1,1)
ax.set_xlabel("Quantity")
ax.set_ylabel("Price")
ax.plot(demand.T[0],priceMatrix.T[0], c='r')
ax.plot(supply.T[0],priceMatrix.T[0], c='b')
fig.canvas.draw()
Market Price: [ 7. 8. 8. 8.]
It should come to no surprise that the chart looks very close to the traditional supply-demand charts you see in economics text books. Afterall, the citizens of Dystopianistan are generally quite rational.
def plotmoney(ax, steps, m1s, m2s):
ax.lines[0].set_xdata(steps)
ax.lines[1].set_xdata(steps)
ax.lines[0].set_ydata(m1s)
ax.lines[1].set_ydata(m2s)
def plotpeople(ax, steps, people):
ax.lines[0].set_xdata(steps)
ax.lines[0].set_ydata(people)
def plotprices(ax, steps, goods):
for i, g in enumerate(goods):
ax.lines[i].set_xdata(steps)
ax.lines[i].set_ydata(g)
And now, the simulation of how things work in Dystopianistan. Recall earlier that Dystopianistan has discovrered that the citizens are generally good payers-back of loans? Well, here's a how's-the-sausage-is-made reasoning for that: they simply throuw debtors who have failed to pay back their loans DEFAULT_THRESHOLD
times, into the gulag, removing them from the economy entirely.
It would make sense too. If you analyze their behaviour, the defaulters generally have high consumerism
preferences. They typicallly spend more than they can make. Therefore in her Great Knowledge, the Eternal Glorious Leader of Dystopianistan has decided to cast those who default into little boats along the river (the Eternal Glorious Leader has plans with those boats obviously - to send them to explore Terra Australis Incognita in about 10 years time)
#reset in case the below case "no loans allowed" is run previously
def budget(self, current):
'''
The budget forms the constraint for a Person in his/her consumption
'''
repayments = 0.0
for loan in self.loans:
if current == loan.repay_at:
repayments += loan.repay_amount()
return self.creditworthiness()*self.consumerism + self.savings - (1-self.consumerism) * repayments
Person.budget = budget
central_bank = Bank(interest_rate, reserve_rate)
n_steps = 60
participant_count = 1000
max_price = 100
price_steps = 200
basket = 7
m = Market(participant_count, basket, central_bank, max_price, price_steps)
print(central_bank.holding)
print(len(m.participants))
mp = m.market_price()
fig,(ax1,ax2, ax3) = plt.subplots(3,1, sharex=True)
ax1.set_xlabel('Time')
ax1.set_ylabel('Money Supply')
ax1.set_xlim(0,n_steps)
ax1.set_ylim(0,700000)
ax2.set_xlabel('Time')
ax2.set_ylabel('Prices')
ax2.set_xlim(0,n_steps)
ax2.set_ylim(0,35)
ax3.set_ylabel("Participants")
ax3.set_ylim(0,participant_count*1.1)
steps =[0]
m1s = [m.m1()]
m2s = [m.m2()]
participants = [participant_count]
prices = [[p] for p in mp]
colors = ['r', 'b', 'g', 'c', 'm', 'y','k']
ax1.plot(steps, m1s, c='r')
ax1.plot(steps, m2s, c='b')
ax3.plot(steps, participants)
for i, p in enumerate(prices):
ax2.plot(steps, p, c=colors[i])
fig.canvas.draw()
for i in range(n_steps):
mp = m.market_price()
m.step()
steps.append(i)
m1 = m.m1()
m2 = m.m2()
# plotting shit
m1s.append(m1)
m2s.append(m2)
for i, p in enumerate(mp):
prices[i].append(p)
participants.append(m.actives())
maxY1 = max(m1s+m2s)*1.1
maxY2 = max([p for pl in prices for p in pl ])*1.1
ax1.set_ylim(0, maxY1)
ax2.set_ylim(0, maxY2)
plotmoney(ax1, steps, m1s, m2s)
plotprices(ax2, steps, prices)
plotpeople(ax3, steps, participants)
fig.canvas.draw()
# print("Time {}. M1: {}, M2: {}, Total: {}".format(i, m1, m2, m1+m2))
50361.26007423992 1000
What happens if loans cannot be made?
import types
central_bank2 = Bank(interest_rate, reserve_rate)
central_bank2.can_lend = types.MethodType(lambda bank, amount: False, central_bank2) # never do this on prod servers!
Person.budget = lambda self, cuurent: self.savings
As you'll note - nothing interesting happens if there are no loans allowed. The economy stagnates. A stagnating economy usually spells civil unrest
m = Market(participant_count, basket, central_bank2, max_price, price_steps)
print(central_bank2.holding)
print(len(m.participants))
mp = m.market_price()
fig,(ax1,ax2, ax3) = plt.subplots(3,1, sharex=True)
ax1.set_xlabel('Time')
ax1.set_ylabel('Money Supply')
ax1.set_xlim(0,n_steps)
ax1.set_ylim(0,700000)
ax2.set_xlabel('Time')
ax2.set_ylabel('Prices')
ax2.set_xlim(0,n_steps)
ax2.set_ylim(0,35)
ax3.set_ylabel("Participants")
ax3.set_ylim(0,participant_count*1.1)
steps =[0]
m1s = [m.m1()]
m2s = [m.m2()]
participants = [participant_count]
prices = [[p] for p in mp]
colors = ['r', 'b', 'g', 'c', 'm', 'y','k']
ax1.plot(steps, m1s, c='r')
ax1.plot(steps, m2s, c='b')
ax3.plot(steps, participants)
for i, p in enumerate(prices):
ax2.plot(steps, p, c=colors[i])
fig.canvas.draw()
for i in range(n_steps):
mp = m.market_price()
m.step()
steps.append(i)
m1 = m.m1()
m2 = m.m2()
# plotting shit
m1s.append(m1)
m2s.append(m2)
for i, p in enumerate(mp):
prices[i].append(p)
participants.append(m.actives())
maxY1 = max(m1s+m2s)*1.1
maxY2 = max([p for pl in prices for p in pl ])*1.1
ax1.set_ylim(0, maxY1)
ax2.set_ylim(0, maxY2)
plotmoney(ax1, steps, m1s, m2s)
plotprices(ax2, steps, prices)
plotpeople(ax3, steps, participants)
fig.canvas.draw()
# print("Time {}. M1: {}, M2: {}, Total: {}".format(i, m1, m2, m1+m2))
50120.42960338321 1000