In [1]:
from __future__ import division
import numpy as np
import quantecon as qe

Economic Dynamics, Chapter 4: Exercises - Suggested Solutions

Exercises using Hamilton's Markov chain:


In [2]:
P = [[0.971, 0.029, 0    ],
     [0.145, 0.778, 0.077],
     [0    , 0.508, 0.492]]
mc_H = qe.MarkovChain(P)

In [3]:
mc_H.P


Out[3]:
array([[ 0.971,  0.029,  0.   ],
       [ 0.145,  0.778,  0.077],
       [ 0.   ,  0.508,  0.492]])

In [4]:
states = {'NG': 0, 'MR': 1, 'SR': 2}

4.2 Finite State Markov Chains

4.2.2 Marginal Distributions

Exercise 4.2.3

Let us use the following function from the previous exercise set:


In [5]:
def cross_sectional_dist(mc, init, T, num_reps=100):
    """
    Return a distribution of visits at time T across num_reps sample paths
    of mc with an initial state init.
    
    """
    x = np.empty(num_reps, dtype=int)
    for i in range(num_reps):
        x[i] = mc.simulate(init=init, sample_size=T+1)[-1]
    bins = np.arange(mc.n+1)
    hist, bin_edges = np.histogram(x, bins=bins)
    dist = hist/len(x)
    return dist

In [6]:
psi = [0, 0, 1]
psi_10 = cross_sectional_dist(mc_H, init=psi, T=10)
print psi_10


[ 0.62  0.33  0.05]

In [7]:
psi_10[states['NG']]


Out[7]:
0.62

4.2.3 Other Identities

Exercise 4.2.12


In [8]:
profits = [1000, 0, -1000]

In [9]:
t = 5
exp_profits_vec = np.linalg.matrix_power(mc_H.P, t).dot(profits)
print exp_profits_vec


[ 885.34763268  388.68267371  217.74304877]

The expected profit when the economy starts in NG:


In [10]:
exp_profits_vec[states['NG']]


Out[10]:
885.34763267632297

The change in profits when the economy starts in SR:


In [11]:
exp_profits_vec[states['SR']] - exp_profits_vec[states['NG']]


Out[11]:
-667.60458391024304

Exercise 4.2.13


In [12]:
t = 1000
exp_profits_vec_4213 = np.linalg.matrix_power(mc_H.P, t).dot(profits)
print exp_profits_vec_4213


[ 788.16  788.16  788.16]

Exercise 4.2.14


In [13]:
psi = np.array([0.2, 0.2, 0.6])
t = 5
exp_profits = psi.dot(np.linalg.matrix_power(mc_H.P, t)).dot(profits)
print exp_profits


385.451890538

4.2.4 Constructing Joint Distributions

Exercise 4.2.15


In [14]:
def path_prob(P, psi, X):
    prob = psi[X[0]]
    for t in range(len(X)-1):
        prob *= P[X[t], X[t+1]]
    return prob

In [15]:
psi = [0.2, 0.2, 0.6]
X = [states['NG'], states['MR'], states['NG']]
path_prob(mc_H.P, psi, X)


Out[15]:
0.00084100000000000006

Exercise 4.2.16

A recursion version of the path_prob function:


In [16]:
def path_prob_recursion(P, psi, X):
    if len(X) == 1:
        return psi[X[0]]

    return path_prob_recursion(P, psi, X[:-1]) * P[X[-2], X[-1]]

Note that, given a list X = [x^0, ..., x^{T-1}, x^T], the path [x^0, ..., x^{T-1}] can be obtained by X[:-1] (or equivalently, X[0:-1]).


In [17]:
path_prob_recursion(mc_H.P, psi, X)


Out[17]:
0.00084100000000000006

Exercise 4.2.18


In [18]:
recession_states = [states[s] for s in states.keys() if s not in ['NG']]
print recession_states


[2, 1]

We want to obtain the Cartesian product recession_states x recession_states x recession_states.

One way is to use cartesian from quantecon:


In [19]:
from quantecon.cartesian import cartesian

paths = cartesian([recession_states for t in range(3)])
print paths


[[ 2.  2.  2.]
 [ 2.  2.  1.]
 [ 2.  1.  2.]
 [ 2.  1.  1.]
 [ 1.  2.  2.]
 [ 1.  2.  1.]
 [ 1.  1.  2.]
 [ 1.  1.  1.]]

(Refer to this issue.)

Another is to use product from itertools:


In [20]:
from itertools import product

paths = product(*[recession_states for t in range(3)])
for path in paths:
    print path


(2, 2, 2)
(2, 2, 1)
(2, 1, 2)
(2, 1, 1)
(1, 2, 2)
(1, 2, 1)
(1, 1, 2)
(1, 1, 1)

In [21]:
paths = cartesian([recession_states for t in range(3)]).astype(int)
# paths = product(*[recession_states for t in range(3)])
prob = 0
for path in paths:
    prob += path_prob(mc_H.P, psi, path)

print prob


0.704242

Exercise 4.2.19

Suppose that the path was [2, 0, 1]:


In [22]:
path = [2, 0, 1]

You can determine whether the path contains state NG by the not in operator:


In [23]:
states['NG'] not in path


Out[23]:
False

Count the number of True's out of 10,000 observations:


In [24]:
sample_size = 3
num_obs = 10000
is_recession = np.empty(num_obs, dtype=bool)
for i in range(num_obs):
    path = mc_H.simulate(init=psi, sample_size=sample_size)
    is_recession[i] = (states['NG'] not in path)

frequency = is_recession.sum()/num_obs
print frequency


0.7037

You may use the mean method instead:


In [25]:
is_recession.mean()


Out[25]:
0.70369999999999999

Exercise 4.2.20

Recall:


In [26]:
profits


Out[26]:
[1000, 0, -1000]

In [27]:
psi


Out[27]:
[0.2, 0.2, 0.6]

In [28]:
def discounted_profit(h, r, X):
    profit = 0
    for t in range(len(X)):
        profit += (1/(1+r)**t) * h[X[t]]

    return profit

In [29]:
T = 2
r = 0.05
paths = cartesian([states.values() for t in range(T+1)]).astype(int)

NPV = 0
for path in paths:
    NPV += discounted_profit(profits, r, path) * path_prob(mc_H.P, psi, path)

print NPV


-396.513741497

Exercise 4.2.22


In [30]:
num_obs = 10000
discounted_profit_values = np.empty(num_obs)
for i in range(num_obs):
    path = mc_H.simulate(init=psi, sample_size=sample_size)
    discounted_profit_values[i] = discounted_profit(profits, r, path)

avg_profit = discounted_profit_values.sum()/num_obs
print avg_profit


-391.004988662

You may use the mean method instead:


In [31]:
discounted_profit_values.mean()


Out[31]:
-391.00498866213155

Exercise 4.2.23


In [32]:
def NPV_matrix_power(P, psi, h, r, T):
    psi = np.asarray(psi)  # To use the method dot from NumPy

    NPV = 0
    values = np.empty(T+1)
    for t in range(T+1):
        values[t] = \
            (1/(1+r)**t) * psi.dot(np.linalg.matrix_power(P, t)).dot(h)

    return values.sum()

In [33]:
NPV_matrix_power(mc_H.P, psi, profits, r, T)


Out[33]:
-396.51374149659864

Another implementation starategy is to use recursion:


In [34]:
def NPV_recursion(P, psi, h, r, T):
    if T == 0:
        return np.dot(psi, h)
    NPV = np.dot(psi, h) + \
        (1/(1+r)) * NPV_recursion(P, np.dot(psi, P), h, r, T-1)
    return NPV

In [35]:
NPV_recursion(mc_H.P, psi, profits, r, T)


Out[35]:
-396.51374149659858

Plot NPVs against T:


In [36]:
%matplotlib inline
import matplotlib.pyplot as plt

In [37]:
ts = 7
NPV_values = np.empty(ts)
for t in range(ts):
    NPV_values[t] = NPV_recursion(mc_H.P, psi, profits, r, t)

In [38]:
fig, ax = plt.subplots()
ax.plot(NPV_values)
ax.set_xlabel(r'$T$')
ax.set_ylabel('NPV')
ax.axhline(color='k', linewidth=0.5)
ax.grid(True)
plt.show()


4.3 Stability of Finite State MCs

4.3.1 Stationary Distributions

Exercise 4.3.9


In [39]:
mc_H.num_recurrent_classes


Out[39]:
1

mc_H has only one recurrent class, and hence has only one stationary distribution.


In [40]:
psi_star = mc_H.stationary_distributions[0]

In [41]:
print psi_star


[ 0.8128   0.16256  0.02464]

In [42]:
extected_profit_stationary = psi_star.dot(profits)
print extected_profit_stationary


788.16

Recall from Exercise 4.2.13:


In [43]:
print exp_profits_vec_4213


[ 788.16  788.16  788.16]

Exercise 4.3.10


In [44]:
mc_H.is_aperiodic


Out[44]:
True

mc_H is aperiodic, so that the marginal distribution converges to a stationary distribution, which is unique.


In [45]:
psi_20 = cross_sectional_dist(mc_H, init=psi_star, T=20, num_reps=1000)
print psi_20


[ 0.82   0.155  0.025]

4.3.3 Stability

Exercise 4.3.27


In [46]:
def sample_return_time(mc, state):
    t = 0
    x = state

    while True:
        t += 1
        x = mc.simulate(init=x, sample_size=2)[-1]
        if x == state:
            break

    return t

Or, alternatively:


In [47]:
def sample_return_time2(mc, state):
    t = 1
    x = mc.simulate(init=state, sample_size=2)[-1]

    while (x != state):
        t += 1
        x = mc.simulate(init=x, sample_size=2)[-1]

    return t

In [48]:
def simulated_mean_return_time(mc, state, num_reps):
    return_times = np.empty(num_reps)

    for i in range(num_reps):
        return_times[i] = sample_return_time(mc, state)

    return return_times.mean()

In [49]:
mc_H.is_irreducible


Out[49]:
True

mc_H is irreducible, so that every state is recurrent, and therefore for any initial state, the chain returns to it in finite time with probability one.


In [50]:
nums = [10**i for i in range(1, 5)]

mean_return_time_vec = np.empty(mc_H.n)

for num_reps in nums:
    for state in range(mc_H.n):
        mean_return_time_vec[state] = \
            simulated_mean_return_time(mc_H, state, num_reps)
    print 'num_reps = {0}:'.format(num_reps)
    print '  mean return times = {0}'.format(mean_return_time_vec)


num_reps = 10:
  mean return times = [  1.    6.   38.5]
num_reps = 100:
  mean return times = [  1.04   6.39  44.65]
num_reps = 1000:
  mean return times = [  1.208   7.013  39.465]
num_reps = 10000:
  mean return times = [  1.2587   5.9449  41.0213]

Compare:


In [51]:
psi_star ** (-1)


Out[51]:
array([  1.23031496,   6.1515748 ,  40.58441558])

4.3.4 The Law of Large Numbers

Exercise 4.3.34


In [52]:
h = np.zeros(mc_H.n)
h[states['NG']] = 1
print h


[ 1.  0.  0.]

In [53]:
def h_mean(mc, init, h, sample_size):
    path = mc.simulate(init=init, sample_size=sample_size)

    h_sum = 0
    for t in range(sample_size):
        h_sum += h[path[t]]

    return h_sum/sample_size

In [54]:
sample_size = 50000
init = 0

h_mean(mc_H, init=init, h=h, sample_size=sample_size)


Out[54]:
0.81503999999999999

In [55]:
psi_star.dot(h)


Out[55]:
0.81279999999999997

Exercise 4.3.36

Recall:


In [56]:
profits


Out[56]:
[1000, 0, -1000]

In [57]:
h_mean(mc_H, init=init, h=profits, sample_size=sample_size)


Out[57]:
794.3

Recall from Exercise 4.3.9:


In [58]:
extected_profit_stationary


Out[58]:
788.15999999999997

In [58]: