NBER Heterogeneous-Agent Macro Workshop
Matthew Rognlie
Spring 2022
Four steps, middle two the hardest:
Before we start, we need to import the Python libraries that we'll be using:
import numpy as np
import numba
from scipy import optimize
import matplotlib.pyplot as plt
# some useful plot defaults
plt.rcParams.update({'font.size' : 20, 'lines.linewidth' : 3.5, 'figure.figsize' : (13,7)})
Uniform points $u_i$ on $[0,\bar{u}]$, construct $a_i$ as
$$ a_i = \underline{a} + e^{e^{u_i}-1}-1 \tag{4} $$This gives min of $\underline{a}$, max of $e^{e^{\bar{u}}-1}-1$. Equating this max with $\overline{a}$ gives
$$ \bar{u} = \log(1+\log(1+ \overline{a} - \underline{a})) $$Implement in function below:
def discretize_assets(amin, amax, n_a):
# find maximum ubar of uniform grid corresponding to desired maximum amax of asset grid
ubar = np.log(1 + np.log(1 + amax - amin))
# make uniform grid
u_grid = np.linspace(0, ubar, n_a)
# double-exponentiate uniform grid and add amin to get grid from amin to amax
return amin + np.exp(np.exp(u_grid) - 1) - 1
Example of 50 points between 0 and 10,000. (Normally we want more than this, but easier to visualize.)
a_grid = discretize_assets(0, 10_000, 50)
with np.printoptions(precision=2):
print(a_grid)
Note vastly greater concentration near the borrowing constraint of 0: out of 50, first 12 points are less than 1, next 10 points are less than 5
Many variations on this (e.g. could add scalar parameters to double exponential, or try triple exponential as suggested by Carroll)
Can show that the Markov transition matrix $\Pi_n$ over values of $s$ obeys the recursion
$$ \Pi_{n}=p\begin{bmatrix}\Pi_{n-1} & \mathbf{0}\\ \mathbf{0}' & 0 \end{bmatrix}+(1-p)\begin{bmatrix}\mathbf{0} & \Pi_{n-1}\\ 0 & \mathbf{0}' \end{bmatrix}+(1-p)\begin{bmatrix}\mathbf{0}' & 0\\ \Pi_{n-1} & \mathbf{0} \end{bmatrix}+p\begin{bmatrix}0 & \mathbf{0}'\\ \mathbf{0} & \Pi_{n-1} \end{bmatrix} \tag{6} $$with base case
$$ \Pi_{2}=\begin{bmatrix}p & 1-p\\ 1-p & p \end{bmatrix} \tag{7} $$Implement:
def rouwenhorst_Pi(N, p):
# base case Pi_2
Pi = np.array([[p, 1 - p],
[1 - p, p]])
# recursion to build up from Pi_2 to Pi_N
for n in range(3, N + 1):
Pi_old = Pi
Pi = np.zeros((n, n))
Pi[:-1, :-1] += p * Pi_old
Pi[:-1, 1:] += (1 - p) * Pi_old
Pi[1:, :-1] += (1 - p) * Pi_old
Pi[1:, 1:] += p * Pi_old
Pi[1:-1, :] /= 2
return Pi
Given transition matrix $\Pi$, simple and robust way to find stationary distribution is to repeatedly apply transition matrix, updating distribution $\pi$ to $\Pi'\pi$, until convergence up to some tolerance:
def stationary_markov(Pi, tol=1E-14):
# start with uniform distribution over all states
n = Pi.shape[0]
pi = np.full(n, 1/n)
# update distribution using Pi until successive iterations differ by less than tol
for _ in range(10_000):
pi_new = Pi.T @ pi
if np.max(np.abs(pi_new - pi)) < tol:
return pi_new
pi = pi_new
Given a target for persistence $\rho$, cross-sectional standard deviation $\sigma$, and the number of discrete states $n_s$, this function gives us the $y(s)$ for each state $s=0,\ldots,n_s-1$ (normalized to have mean of 1), the stationary distribution $\pi(s)$ over states, and Markov transition matrix $\Pi(s,s')$ between states.
def discretize_income(rho, sigma, n_s):
# choose inner-switching probability p to match persistence rho
p = (1+rho)/2
# start with states from 0 to n_s-1, scale by alpha to match standard deviation sigma
s = np.arange(n_s)
alpha = 2*sigma/np.sqrt(n_s-1)
s = alpha*s
# obtain Markov transition matrix Pi and its stationary distribution
Pi = rouwenhorst_Pi(n_s, p)
pi = stationary_markov(Pi)
# s is log income, get income y and scale so that mean is 1
y = np.exp(s)
y /= np.vdot(pi, y)
return y, pi, Pi
Let's try $\rho=0.975$, $\sigma=0.7$, and $n_s=7$. The former two values roughly match some estimates of the quarterly persistence, and the standard deviation, of the persistent component of log income in the US.
y, pi, Pi = discretize_income(0.975, 0.7, 7)
y, pi
Verify mean of $y$ is 1 as desired:
np.vdot(pi, y)
Verify standard deviation is $\sigma=0.7$:
mean_log_y = np.vdot(pi, np.log(y))
sd_log_y = np.sqrt(np.vdot(pi, (np.log(y) - mean_log_y)**2))
sd_log_y
Back to standard incomplete markets model, with $t$ subscripts added:
$$ \begin{gather} V_t(s,a)=\max_{c,a'} u(c)+\beta\mathbb{E}[V_{t+1}(s',a')|s] \tag{1}\\ \text{s.t. }a'+c=(1+r_t)a+y_t(s) \tag{2}\\ a'\geq \underline{a} \tag{3} \\ \end{gather} $$Optimal policy characterized by two conditions, which only require derivative of value function $V_a$ with respect to assets ("marginal value function"), not value function itself.
Envelope condition: $$ V_{a,t}(s,a)=(1+r_t)u'(c_t(s,a)) \tag{8} $$
First-order condition (inequality binds unless borrowing constraint binds):
$$ u'(c_t(s,a)) \geq \beta \mathbb{E}[V_{a,t+1}(s',a_t'(s,a))|s] \tag{9} $$More efficient and accurate to use (8) and (9) to iterate directly on $V_a$ than to do standard value function iteration (Carroll 2006, "endogenous gridpoints method")
Backward iteration in time: take tomorrow's $V_{a,t+1}$ on right in (9), solve for today's policies $a_t'(s,a)$ and $c_t(s,a)$ and marginal value function $V_{a,t}$
Define "end-of-period" value function $W_t$ as
$$ W_t(s,a') \equiv \beta \mathbb{E}[V_{t+1}(s',a')|s] \tag{10} $$so that (9) simplifies to
$$ u'(c_t(s,a)) \geq W_{a,t}(s,a_t'(s,a)) \tag{11} $$Take expectations in (10) by multiplying by Markov matrix $\Pi$, so if $V_{a,t+1}(s,a)$ is in $n_s\times n_a$ array Va
, get $n_s\times n_a$ array Wa
of $W_{a,t}$ by writing
Wa = (beta * Pi) @ Va
eis
in the code), so that $(u')^{-1}(x) = x^{-\sigma}$:c_endog = Wa**(-eis)
coh = y[:, np.newaxis] + (1+r)*a_grid
Turns out an equivalent (thanks to linearity) and slightly more convenient alternative is to think of (13) as giving a collection of sample points $(coh(s,a_t(s,a')), a')$ from the function mapping cash on hand to asset choice $a'$, then linearly interpolate to evaluate $a'$ for cash-on-hand $coh_t(s,a)$ on the actual grid of $a$, giving us $a_t'(s,a)$ on our grid of $a$
Need to do this separately for each $s$, so code iterates over them:
a = np.empty_like(coh)
for s in range(len(y)):
a[s, :] = np.interp(coh[s, :], c_endog[s, :] + a_grid, a_grid)
a = np.maximum(a, a_grid[0])
c = coh - a
Implement envelope condition (8), again using parametrization of $u$ where $u'(c)=c^{-1/\sigma}$, and $\sigma$ is eis
in code:
Va = (1+r) * c**(-1/eis)
Combine the 9 lines of code we wrote for steps 1 to 4 into the body of a single backward iteration function
def backward_iteration(Va, Pi, a_grid, y, r, beta, eis):
# step 1: discounting and expectations
Wa = (beta * Pi) @ Va
# step 2: solving for asset policy using the first-order condition
c_endog = Wa**(-eis)
coh = y[:, np.newaxis] + (1+r)*a_grid
a = np.empty_like(coh)
for s in range(len(y)):
a[s, :] = np.interp(coh[s, :], c_endog[s, :] + a_grid, a_grid)
# step 3: enforcing the borrowing constraint and backing out consumption
a = np.maximum(a, a_grid[0])
c = coh - a
# step 4: using the envelope condition to recover the derivative of the value function
Va = (1+r) * c**(-1/eis)
return Va, a, c
This function takes in an $n_s\times n_a$ array Va
containing tomorrow's marginal value function $V_{a,t+1}$ at every point on the grid. It also takes in the $n_s\times n_s$ Markov matrix Pi
($\Pi$), the length-$n_a$ asset grid a
, the length-$n_s$ array y
of incomes $y_t(s)$, and the scalars r
($r_t$), beta
($\beta$), and eis
($\sigma$).
It outputs $n_s\times n_a$ arrays containing today's marginal value function $V_{a,t}$ and the policies $a'_t$ and $c_t$ at every point on the grid.
To obtain steady-state $V_a$ and policies $a'$ and $c$, we need:
backward_iteration
until convergence criterion hitFor now we try:
def policy_ss(Pi, a_grid, y, r, beta, eis, tol=1E-9):
# initial guess for Va: assume consumption 5% of cash-on-hand, then get Va from envelope condition
coh = y[:, np.newaxis] + (1+r)*a_grid
c = 0.05*coh
Va = (1+r) * c**(-1/eis)
# iterate until maximum distance between two iterations falls below tol, fail-safe max of 10,000 iterations
for it in range(10_000):
Va, a, c = backward_iteration(Va, Pi, a_grid, y, r, beta, eis)
# after iteration 0, can compare new policy function to old one
if it > 0 and np.max(np.abs(a - a_old)) < tol:
return Va, a, c
a_old = a
Make grids for assets and income, with 500 points and 7 points respectively, and a borrowing constraint of 0. Use same calibration for income as we did above:
a_grid = discretize_assets(0, 10_000, 500)
y, pi, Pi = discretize_income(0.975, 0.7, 7)
Choose some values for the other parameters, with the idea that this is a quarterly calibration:
r = 0.01/4
beta = 1-0.08/4
eis = 1
Solve for steady-state policies:
Va, a, c = policy_ss(Pi, a_grid, y, r, beta, eis)
Looking at entire function, it seems linear, because at high assets it becomes asymptotically linear, and our grid includes really high asset levels to make sure we don't miss any rich households:
for s, ys in enumerate(y):
plt.plot(a_grid, c[s, :], label=f'y={ys:.2f}')
plt.legend()
plt.xlabel('assets')
plt.ylabel('consumption');
Looking only at lower assets, we see a more interesting picture, with some of the well-known concavity of the consumption function for lower income states:
for s, ys in enumerate(y):
plt.plot(a_grid[:120], c[s, :120], label=f'y={ys:.2f}')
plt.legend()
plt.xlabel('assets')
plt.ylabel('consumption');
It's even more visible if we zoom in on only the lowest income states:
for s, ys in enumerate(y[:4]):
plt.plot(a_grid[:120], c[s, :120], label=f'y={ys:.2f}')
plt.legend()
plt.xlabel('assets')
plt.ylabel('consumption');
It's also interesting to plot net saving—the increase or decrease in assets. For each income state, there is a "target" level of assets where net saving is zero. For lower incomes, this is zero (the borrowing constraint). For higher incomes, people are net savers until they reach a very high level of assets. You can see how this might produce a right-skewed distribution of assets!
for s, ys in enumerate(y):
plt.plot(a_grid[:300], a[s, :300] - a_grid[:300], label=f'y={ys:.2f}')
plt.axhline(0, linestyle='--', color='k')
plt.legend()
plt.xlabel('assets')
plt.ylabel('savings');
Another important concept is the marginal propensity to consume (MPC) out of a one-time, unexpected increase in cash on hand. This equals the derivative of consumption function with respect to assets, divided by asset income $1+r$.
Calculating it by taking differences of the consumption function to approximate the derivative:
mpcs = np.empty_like(c)
# symmetric differences away from boundaries
mpcs[:, 1:-1] = (c[:, 2:] - c[:, 0:-2]) / (a_grid[2:] - a_grid[:-2]) / (1+r)
# asymmetric first differences at boundaries
mpcs[:, 0] = (c[:, 1] - c[:, 0]) / (a_grid[1] - a_grid[0]) / (1+r)
mpcs[:, -1] = (c[:, -1] - c[:, -2]) / (a_grid[-1] - a_grid[-2]) / (1+r)
# special case of constrained
mpcs[a == a_grid[0]] = 1
Since the consumption function is concave, MPCs are declining in assets.
We see here that it seems to jump downward, e.g. from 1 to around 0.5 as households go from constrained to "not constrained, but probably constrained tomorrow, so we'll smooth any resources at the margin between 2 periods". These stark jumps exist in discrete time as long as we have discretized income states.
for s in range(7):
plt.plot(a_grid[:50], mpcs[s, :50], linewidth=3, label=f'y = {y[s]:.2f}')
plt.xlabel('Assets')
plt.ylabel('MPC')
plt.title('Quarterly marginal propensities to consume by income state y(s)')
plt.legend();
a[5, 0]
This turns out to be in between points 66 and 67 on the grid:
a_grid[60:70]
Where should we assume this mass of households goes?
For any individual policy $a'$, we can (1) identify bracketing gridpoints $a_i$ and $a_{i+1}$ and (2) calculate $\pi$ from (14). Following code takes in $a'$ as a
and returns $i$ and $\pi$ as a_i
and a_pi
.
def get_lottery(a, a_grid):
# step 1: find the i such that a' lies between gridpoints a_i and a_(i+1)
a_i = np.searchsorted(a_grid, a) - 1
# step 2: obtain lottery probabilities pi
a_pi = (a_grid[a_i+1] - a)/(a_grid[a_i+1] - a_grid[a_i])
return a_i, a_pi
Try it for single point a[5,0]
we saw earlier, which was between gridpoints 66 and 67
get_lottery(a[5,0], a_grid)
This gives us $i=66$ and $\pi\approx 0.62$, with latter indicating that a[5,0]
is closer to gridpoint 66 than 67, so it gets higher weight in lottery.
This function also works (NumPy "broadcasts" it) if we apply it to entire policy function a
, with outputs having same $n_s\times n_a$ dimensionality as a
:
a_i, a_pi = get_lottery(a, a_grid)
a_i.shape, a_i.dtype, a_pi.shape, a_pi.dtype
@numba.njit
decorator to accelerate with Numba's just-in-time compilation@numba.njit
def forward_policy(D, a_i, a_pi):
Dend = np.zeros_like(D)
for s in range(a_i.shape[0]):
for a in range(a_i.shape[1]):
# send pi(s,a) of the mass to gridpoint i(s,a)
Dend[s, a_i[s,a]] += a_pi[s,a]*D[s,a]
# send 1-pi(s,a) of the mass to gridpoint i(s,a)+1
Dend[s, a_i[s,a]+1] += (1-a_pi[s,a])*D[s,a]
return Dend
stationary_markov
earlier, multiply by transpose $\Pi'$ of Markov matrix $\Pi(s,s')$backward_iteration
, where we multiply by $\Pi$ to take expectationsforward_policy
function to get complete forward iteration from $D_t(s,a)$ to $D_{t+1}(s',a')$def forward_iteration(D, Pi, a_i, a_pi):
Dend = forward_policy(D, a_i, a_pi)
return Pi.T @ Dend
def distribution_ss(Pi, a, a_grid, tol=1E-10):
a_i, a_pi = get_lottery(a, a_grid)
# as initial D, use stationary distribution for s, plus uniform over a
pi = stationary_markov(Pi)
D = pi[:, np.newaxis] * np.ones_like(a_grid) / len(a_grid)
# now iterate until convergence to acceptable threshold
for _ in range(10_000):
D_new = forward_iteration(D, Pi, a_i, a_pi)
if np.max(np.abs(D_new - D)) < tol:
return D_new
D = D_new
We get an $n_s\times n_a$ array giving mass at each $D(s,a)$
D = distribution_ss(Pi, a, a_grid)
D.shape
How to visualize? Can try plotting CDF of assets, obtained by summing D
across $s$ and taking cumulative sum across $a$.
Turns out not to be so informative, since virtually everyone has assets far below the top of the distribution (we made grid maximum higher than necessary):
plt.plot(a_grid, D.sum(axis=0).cumsum());
i = np.argmax(a_grid > 20) # first gridpoint above 20
plt.plot(a_grid[:i], D.sum(axis=0)[:i].cumsum())
plt.hlines(1,0, a_grid[i-1], colors='k', linestyles='dotted');
i = np.argmax(a_grid > 2) # first gridpoint above 2
plt.plot(a_grid[:150], D.sum(axis=0)[:150].cumsum())
plt.hlines(1,0, a_grid[149], colors='k', linestyles='dotted');
for s, ys in enumerate(y):
plt.plot(a_grid[:150], D[s][:150].cumsum()/pi[s], label=f'y={ys:.2f}')
plt.legend();
Let's get a sense of extent to which higher incomes hold far higher assets, even relative to income:
for s, ys in enumerate(y):
print(f'Ave assets at y={ys:.2f}: {np.vdot(a_grid, D[s, :]) / pi[s]:.2f}')
Higher income states hold a lot more, but rarer. Still, top few hold most assets:
for s, ys in enumerate(y):
print(f'Total assets at y={ys:.2f} ({100*pi[s]:.1f}% of hh)\t: {np.vdot(a_grid, D[s, :]):.2f}')
Sum of these is total assets held in economy:
np.sum(a_grid*D) # use array broadcasting since a_grid missing 's' dimension
which should equal (up to numerical error) total assets chosen for tomorrow:
np.vdot(a, D)
Pi
, a_grid
, y
, r
, beta
, eis
), the individual level policies and marginal value function (a
, c
, Va
), the distribution (D
), and aggregate policies (A
and C
)def steady_state(Pi, a_grid, y, r, beta, eis):
Va, a, c = policy_ss(Pi, a_grid, y, r, beta, eis)
D = distribution_ss(Pi, a, a_grid)
return dict(D=D, Va=Va,
a=a, c=c,
A=np.vdot(a, D), C=np.vdot(c, D), # aggregation
Pi=Pi, a_grid=a_grid, y=y, r=r, beta=beta, eis=eis)
ss = steady_state(Pi, a_grid, y, r, beta, eis)
ss.keys()
Steady-state assets slope sharply upward as we increase the steady-state real interest rate $r$, and indeed asymptote to infinity as we approach $r=\beta^{-1}-1$, which in this case is just above 0.02:
rs = r + np.linspace(-0.02, 0.015, 15)
As = [steady_state(Pi, a_grid, y, r, beta, eis)['A'] for r in rs]
plt.plot(rs, As)
plt.xlabel('Real interest rate')
plt.ylabel('Aggregate assets over quarterly income');
Steady-state assets also slope strongly upward in income risk, which we'll manipulate below by calibrating to different standard deviations $\sigma$ of cross-sectional log income. (This turns out to change only the vector y
of incomes, making them more dispersed, not the Markov matrix Pi
.)
sigmas = np.linspace(0.3, 1.2, 8) # our benchmark was sigma=0.7
As = []
for sigma in sigmas:
y_new, pi_new, Pi_new = discretize_income(0.975, sigma, 7)
As.append(steady_state(Pi_new, a_grid, y_new, r, beta, eis)['A'])
plt.plot(sigmas, As)
plt.xlabel('Cross-sectional standard deviation of income')
plt.ylabel('Assets over quarterly income');
eis_vec = np.linspace(0.4, 2, 10) # our benchmark was eis=1
As = [steady_state(Pi, a_grid, y, r, beta, eis)['A'] for eis in eis_vec]
plt.plot(eis_vec, As)
plt.xlabel('Elasticity of intertemporal substitution')
plt.ylabel('Assets over quarterly income');
A
must be $4\times 1.4=5.6$beta_calib = optimize.brentq(
lambda beta: steady_state(Pi, a_grid, y, r, beta, eis)['A'] - 5.6,
0.98, 0.995)
beta_calib
Get entire steady state and verify we have right assets:
ss_calib = steady_state(Pi, a_grid, y, r, beta_calib, eis)
ss_calib['A'], ss_calib['C']
Check aggregate steady-state budget balance $C=1+rA$ (up to numerical error from our steady-state tolerance):
ss_calib['C'] - (1 + ss_calib['r']*ss_calib['A'])
Calibrate similar to before:
B = 5.6 # annual bonds/GDP is 140%, so quarterly is 560%, and quarterly GDP is 1
tau = r*B # labor tax needed to balance steady-state government budget
e = y # use our previous y, which had mean 1, for the labor endowment process
Because of the low real interest rate, $\tau$ is quite small:
tau
Now calibrate $\beta$ to be consistent with steady-state asset market clearing $A=B$, using $(1-\tau)e(s)$ as income:
beta_ge = optimize.brentq(
lambda beta: steady_state(Pi, a_grid, (1-tau)*e, r, beta, eis)['A'] - B,
0.98, 0.995)
beta_ge
Compute full steady state with beta_ge
, check asset market and goods market clearing:
ss_ge = steady_state(Pi, a_grid, (1-tau)*e, r, beta_ge, eis)
ss_ge['A'] - B, ss_ge['C'] - 1 # asset mkt clearing, goods mkt clearing
sigmas = np.linspace(0.3, 1.2, 8) # our benchmark was sigma=0.7
rs = []
for sigma in sigmas:
e_new, pi_new, Pi_new = discretize_income(0.975, sigma, 7)
rs.append(optimize.brentq(
lambda r: steady_state(Pi_new, a_grid, (1-r*B)*e_new, r, beta_ge, eis)['A'] - B,
-0.02, 0.015))
plt.plot(sigmas, 4*np.array(rs))
plt.xlabel('Cross-sectional standard deviation of income')
plt.ylabel('Equilibrium real interest rate (annualized)');
Recall that given a_i
and a_pi
describing the lottery form of policy, we updated the beginning-of-period distribution $D_t(s,a)$ to an end-of-period distribution $D_t^{end}(s, a')$ using:
@numba.njit
def forward_policy(D, a_i, a_pi):
Dend = np.zeros_like(D)
for s in range(a_i.shape[0]):
for a in range(a_i.shape[1]):
# send pi(s,a) of the mass to gridpoint i(s,a)
Dend[s, a_i[s,a]] += a_pi[s,a]*D[s,a]
# send 1-pi(s,a) of the mass to gridpoint i(s,a)+1
Dend[s, a_i[s,a]+1] += (1-a_pi[s,a])*D[s,a]
return Dend
Here, we need to go in the opposite direction: start with some end-of-period variable $X_t^{end}(s,a')$, and obtain its beginning-of-period expectation $X_t(s,a) = \pi(s,a)*X_t^{end}(s,i(s,a)) + (1-\pi(s,a))*X_t^{end}(s,i(s,a)+1)$. Though we don't write it explicitly as a matrix, one can verify that this is actually the transpose of the linear mapping in forward_policy
:
@numba.njit
def expectation_policy(Xend, a_i, a_pi):
X = np.zeros_like(Xend)
for s in range(a_i.shape[0]):
for a in range(a_i.shape[1]):
# expectation is pi(s,a)*Xend(s,i(s,a)) + (1-pi(s,a))*Xend(s,i(s,a)+1)
X[s, a] = a_pi[s, a]*Xend[s, a_i[s, a]] + (1-a_pi[s, a])*Xend[s, a_i[s, a]+1]
return X
Recall that for the distribution, we updated $D_t^{end}(s,a')$ to $D_{t+1}^{end}(s',a')$ by multiplying by the transpose of the Markov matrix, then combined this with the update using the lottery:
def forward_iteration(D, Pi, a_i, a_pi):
Dend = forward_policy(D, a_i, a_pi)
return Pi.T @ Dend
Similarly, here we need to obtain $X_t^{end}(s,a') = \mathbb{E}_t[X_{t+1}(s',a') | s]$ by taking expectations with respect to $s'$, then combine this with the code we already wrote to obtain $X_t(s,a)$ from $X_t^{end}(s,a')$. Altogether, this gives us a function that iterates
def expectation_iteration(X, a_i, a_pi):
Xend = Pi @ X
return expectation_policy(Xend, a_i, a_pi)
Note that taking expectations goes in the opposite order (first take expectations with respect to $s'$, then $a'$) as updating the distribution, because it's going backward in time rather than forward.
X
) and calculates expectation vectors $\mathcal{E}_0,\ldots,\mathcal{E}_{T-1}$ up to some horizon $T$.expectation_iteration
to evaluate right side of (15):def expectation_vectors(X, a_i, a_pi, T):
# set up array of curlyEs and fill in first row with base case
curlyE = np.empty((T, ) + X.shape)
curlyE[0] = X
# recursively apply law of iterated expectations
for j in range(1, T):
curlyE[j] = expectation_iteration(curlyE[j-1], a_i, a_pi)
return curlyE
Let's try this for all $j$ up to $T=40$:
T = 40
ctilde = c - np.vdot(D, c) # demeaned consumption
E_ctilde = expectation_vectors(ctilde, a_i, a_pi, T) # expectation vectors for 0, ... , T-1
Need to take product of ctilde
with the $j$th row of E_ctilde
, then aggregate by steady-state distribution D
.
Autocov_c = np.empty(T)
for j in range(T):
Autocov_c[j] = np.vdot(D, ctilde * E_ctilde[j])
Autocorrelation is autocovariance divided by variance, and variance is autocovariance at a lag of zero, so can write:
Autocorr_c = Autocov_c / Autocov_c[0]
Python trickery side note: not necessary here for speed, but can calculate Autocov_c
above without a for
loop by flattening state space $(s,a)$ into one dimension using ravel
(for ctilde*D
) and reshape
(for E_ctilde
) and then doing matrix multiplication:
Autocov_c_alt = E_ctilde.reshape((T, -1)) @ (ctilde*D).ravel()
assert np.max(np.abs(Autocov_c_alt - Autocov_c)) < 1E-15 # verify this is right
plt.plot(Autocorr_c)
plt.title('Autocorrelation of consumption')
plt.xlabel('Horizon in quarters')
plt.ylabel('Correlation');