[1]
import numpy as np
from scipy import optimize
from scipy import interpolate

import matplotlib.pyplot as plt
plt.style.use('seaborn-whitegrid')
colors = [x['color'] for x in plt.style.library['seaborn']['axes.prop_cycle']]
from matplotlib import cm

Income process estimation

Function for simulation:

[2]
def simulate(T,N,sigma_psi,sigma_xi,pi,seed):
    """ simulate income process
    
    Args:
        
        T (int): number of periods
        N (int): number of persons
        sigma_psi (float): std. of permanent shocks
        sigma_xi (float): std. of transitory shocks
        pi (float): unemployment risk  
        seed (int): seed for random numbers
        
    Returns:
    
        Y (np.ndarray): income
        
    """
    
    # a. allocate
    Y = np.nan*np.ones((T,N))
    P = np.nan*np.ones((T,N))
    
    # b. draw random shocks
    np.random.seed(seed)
    psi = np.random.lognormal(-0.5*sigma_psi**2,sigma_psi,size=(T,N))
    xi = np.random.lognormal(-0.5*sigma_xi**2,sigma_xi,size=(T,N))
    mu = np.random.uniform(size=(T,N))
    
    # b. simulate
    for t in range(T):
        
        # i. previous period
        if t == 0:
            Plag = 1
        else:
            Plag = P[t-1,:]
        
        # ii. permanent and transitory shocks
        P[t,:] = Plag*psi[t,:]
        Y[t,:] = P[t,:]*xi[t,:]
        
        # iii. unemployment
        I = mu[t,:] < pi
        Y[t,I] = 0
    
    return Y

Create data:

[3]
# a. settings
data_T = 20
data_N = 50_000
data_seed = 1917
true_sigma_psi = 0.10
true_sigma_xi = 0.15
true_pi = 0.05

# b. simulate
dataY = simulate(data_T,data_N,true_sigma_psi,true_sigma_xi,true_pi,data_seed)

# c. save
with open('dataY.npy', 'wb') as f:
    np.save(f,dataY)

Question 1

[4]
dataY = np.load('dataY.npy')
[5]
def growth_rate(Y):    
    """ simulate income process
    
    Args:
        
        Y (np.ndarray): income      
        
    Returns:
    
        dlogY (np.ndarray): log-change in income
        
    """

    T,N = Y.shape
    dlogY = np.nan*np.ones((T,N))
    for t in range(T):
        I = (Y[t,:] > 0) & (Y[t-1,:] > 0)
        dlogY[t,I] = np.log(Y[t,I])-np.log(Y[t-1,I])
        
    return dlogY

dlogdataY = growth_rate(dataY)

Question 2

[6]
def calculate_statistics(Y,dlogY):
    """ calculate statistics
    
    Args:
        
        Y (np.ndarray): income      
        dlogY (np.ndarray): log-change in income
        
    Returns:
        
        s1 (float): share of observations with zero
        s2 (float): variance of income growth
        s3 (float): co-variance of income growth
        
    """
    
    # a. s1
    mean_Yzero = np.mean(Y == 0)
    
    # b. s2
    I = ~np.isnan(dlogY)
    var_dlogY = np.var(dlogY[I])
    
    # c. s3
    dlogY_now = dlogY[1:,:]
    dlogY_lag = dlogY[:-1,:]
    I = (~np.isnan(dlogY_now)) & (~np.isnan(dlogY_lag))
    cov_dlogY = np.cov(dlogY_now[I],dlogY_lag[I])[0,1]
    
    return mean_Yzero,var_dlogY,cov_dlogY

data_mean_Yzero,data_var_dlogY,data_cov_dlogY = calculate_statistics(dataY,dlogdataY)

Question 3

[7]
# a. choices
T = 20
N = 100_000
seed = 1986
sigma_psi = 0.05
sigma_xi = 0.10
pi = 0.04

# a. simulate
simY = simulate(T,N,sigma_psi,sigma_xi,pi,seed)

# b. calculate statistics
dlogsimY = growth_rate(simY)
mean_Yzero,var_dlogY,cov_dlogY = calculate_statistics(simY,dlogsimY)

# c. compare with data statistics
print(f'mean_Yzero: {mean_Yzero:7.4f} (sim) vs. {data_mean_Yzero:7.4f} (data)')
print(f' var_dlogY: {var_dlogY:7.4f} (sim) vs. {data_var_dlogY:7.4f} (data)')
print(f' cov_dlogY: {cov_dlogY:7.4f} (sim) vs. {data_cov_dlogY:7.4f} (data)')
mean_Yzero: 0.0400 (sim) vs. 0.0499 (data) var_dlogY: 0.0248 (sim) vs. 0.0645 (data) cov_dlogY: -0.0101 (sim) vs. -0.0230 (data)

Question 4

[8]
def objective(x,data_mean_Yzero,data_var_dlogY,data_cov_dlogY,T,N,seed):
    
    # a. unpack
    sigma_psi = x[0]
    sigma_xi = x[1]
    pi = x[2]
    
    # b. simulate
    simY = simulate(T,N,sigma_psi,sigma_xi,pi,seed)
    
    # c. calculate moments
    dlogsimY = growth_rate(simY)
    mean_Yzero,var_dlogY,cov_dlogY = calculate_statistics(simY,dlogsimY)
    
    # d. calculate objective
    obj = (mean_Yzero-data_mean_Yzero)**2 + (var_dlogY-data_var_dlogY)**2 + (cov_dlogY-data_cov_dlogY)**2
    return obj 
[9]
x = [sigma_psi,sigma_xi,pi]
res = optimize.minimize(objective,x,method='L-BFGS-B',bounds=((0,None),(0,None),(0,1)),args=(data_mean_Yzero,data_var_dlogY,data_cov_dlogY,T,N,seed),
                        options={'eps':1e-4})
assert res.success
[10]
print(f'sigma_psi: {res.x[0]:.4f} [true: {true_sigma_psi:.4f}]')
print(f' sigma_xi: {res.x[1]:.4f} [true: {true_sigma_xi:.4f}]')
print(f'       pi: {res.x[2]:.4f} [true: {true_pi:.4f}]')
sigma_psi: 0.1001 [true: 0.1000] sigma_xi: 0.1499 [true: 0.1500] pi: 0.0498 [true: 0.0500]

See optimizer results:

[11]
res
      fun: 4.533508167430575e-09
 hess_inv: <3x3 LbfgsInvHessProduct with dtype=float64>
      jac: array([-6.46516131e-08, -9.21598742e-09, -1.41513704e-06])
  message: b'CONVERGENCE: NORM_OF_PROJECTED_GRADIENT_<=_PGTOL'
     nfev: 60
      nit: 13
     njev: 15
   status: 0
  success: True
        x: array([0.10013867, 0.14989204, 0.04976324])

Compare statistics:

[12]
# a. unpack results
sigma_psi = res.x[0]
sigma_xi = res.x[1]
pi = res.x[2]

# b. simulate and calculate statistics
simY = simulate(T,N,sigma_psi,sigma_xi,pi,seed)
dlogsimY = growth_rate(simY)
mean_Yzero,var_dlogY,cov_dlogY = calculate_statistics(simY,dlogsimY)

# c. compare withd ata
print(f'mean_Yzero: {mean_Yzero:7.4f} (sim) vs. {data_mean_Yzero:7.4f} (data)')
print(f' var_dlogY: {var_dlogY:7.4f} (sim) vs. {data_var_dlogY:7.4f} (data)')
print(f' cov_dlogY: {cov_dlogY:7.4f} (sim) vs. {data_cov_dlogY:7.4f} (data)')
mean_Yzero: 0.0499 (sim) vs. 0.0499 (data) var_dlogY: 0.0644 (sim) vs. 0.0645 (data) cov_dlogY: -0.0230 (sim) vs. -0.0230 (data)

Wealth in the utility function

[13]
# a. parameters
rho = 2.0
sigma = 1.2
kappa = 0.6
a_ubar = 2.0
r = 0.04
y = 1.0

# b. grids
a_lag_vec = np.linspace(0,300,300)
[14]
beta = 0.97
T = 20

Question 1

[15]
def solve(a_lag_vec,beta,rho,sigma,kappa,a_ubar,r,y,interp_next_v=None):

    # a. grids
    v_vec = np.empty(a_lag_vec.size)
    c_vec = np.empty(a_lag_vec.size)

    # b. solve for each a_lag in grid
    for i,a_lag in enumerate(a_lag_vec):

        # i. objective
        m = (1+r)*a_lag + y
        def obj(c):
            a = m - c
            c_utility = c**(1-rho)/(1-rho)
            a_utility = kappa*(a+a_ubar)**(1-sigma)/(1-sigma)
            utility = c_utility + a_utility
            if not interp_next_v is None:
                utility += beta*interp_next_v(a)
            return -utility           

        # ii. optimizer
        result = optimize.minimize(obj,[0.01*m],method='L-BFGS-B',bounds=((1e-8,m),))
        
        # iv. save
        v_vec[i] = -result.fun
        c_vec[i] = result.x
        
    return v_vec,c_vec
[16]
# solve
vT_vec,cT_vec = solve(a_lag_vec,beta,rho,sigma,kappa,a_ubar,r,y)

# illustration
fig = plt.figure(figsize=(18,5))

ax = fig.add_subplot(1,3,1)
ax.plot(a_lag_vec,vT_vec)
ax.set_xlabel('$a_{T-1}$')
ax.set_ylabel('$v_T$')
ax.set_title('value function in period T');

ax = fig.add_subplot(1,3,2)
ax.plot(a_lag_vec,cT_vec)
ax.set_xlabel('$a_{T-1}$')
ax.set_ylabel('$c_T$')
ax.set_title('consumption function in period T')

aT_vec = (1+r)*a_lag_vec+y-cT_vec
ax = fig.add_subplot(1,3,3)
ax.plot(a_lag_vec,aT_vec)
ax.set_xlabel('$a_{T-1}$')
ax.set_ylabel('$a_T$')
ax.set_title('saving function in period T');

Questions 2+3

[17]
def solve_full(a_lag_vec,vT_vec,beta,rho,sigma,kappa,a_ubar,r,y,T):
    
    T = 20
    v_next_vec = vT_vec
    c_vecs = np.zeros((T,a_lag_vec.size))
    for t in reversed(range(T)):
        
        print(f't = {t}')
        v_next_interp = interpolate.RegularGridInterpolator((a_lag_vec,),v_next_vec,bounds_error=False,fill_value=None)   
        v_next_vec,c_vecs[t,:] = solve(a_lag_vec,beta,rho,sigma,kappa,a_ubar,r,y,v_next_interp)
    
    return c_vecs

c_vecs = solve_full(a_lag_vec,vT_vec,beta,rho,sigma,kappa,a_ubar,r,y,T)
t = 19 t = 18 t = 17 t = 16 t = 15 t = 14 t = 13 t = 12 t = 11 t = 10 t = 9 t = 8 t = 7 t = 6 t = 5 t = 4 t = 3 t = 2 t = 1 t = 0
[18]
fig = plt.figure(figsize=(6,5))
ax = fig.add_subplot(1,1,1)
for t in range(T):
    ax.plot(a_lag_vec,c_vecs[t])
ax.set_xlabel('$a_{t-1}$')
ax.set_ylabel('$c_t$')
ax.set_title('consumption functions');
[19]
fig = plt.figure(figsize=(6,5))
ax = fig.add_subplot(1,1,1)
s_vec = (1+r)*a_lag_vec+y-c_vecs[0,:]-a_lag_vec
s_rate_vec = s_vec/(r*a_lag_vec+y)
ax.plot(a_lag_vec,s_rate_vec)
ax.set_title('saving rate');

Question 4

[20]
kappa_zero = 0.0
vT_vec,cT_vec = solve(a_lag_vec,beta,rho,sigma,kappa_zero,a_ubar,r,y)
c_vecs_kappa_zero = solve_full(a_lag_vec,vT_vec,beta,rho,sigma,kappa_zero,a_ubar,r,y,T)
t = 19 t = 18 t = 17 t = 16 t = 15 t = 14 t = 13 t = 12 t = 11 t = 10 t = 9 t = 8 t = 7 t = 6 t = 5 t = 4 t = 3 t = 2 t = 1 t = 0
[21]
fig = plt.figure(figsize=(6,5))
ax = fig.add_subplot(1,1,1)
s_vec = (1+r)*a_lag_vec+y-c_vecs_kappa_zero[0,:]-a_lag_vec
s_rate_vec = s_vec/(r*a_lag_vec+y)
ax.plot(a_lag_vec,s_rate_vec)
ax.set_title('saving rate');

Gradient descent

Question 1

[22]
def grid_search(f,x1_min,x1_max,x2_min,x2_max,N):
    
    f_best = np.inf
    for x1 in np.linspace(x1_min,x1_max,N):
        for x2 in np.linspace(x2_min,x2_max,N):
            f_now = f([x1,x2])
            if f_now < f_best:
                f_best = f_now
                x1_best = x1
                x2_best = x2
            
    return x1_best,x2_best,f_best
[23]
def refined_grid_search(f,x1_min,x1_max,x2_min,x2_max,N,K):
    
    for k in range(K):
        if k > 0:
            
            step_x1 = 3*(x1_max-x1_min)/(N-1)
            step_x2 = 3*(x2_max-x2_min)/(N-1)
            
            x1_min = np.fmax(x1_best-step_x1,x1_min)
            x2_min = np.fmax(x2_best-step_x2,x2_min)
            x1_max = np.fmin(x1_best+step_x1,x1_max)
            x2_max = np.fmin(x2_best+step_x2,x2_max)
            
        x1_best,x2_best,f_best = grid_search(f,x1_min,x1_max,x2_min,x2_max,N)
        
        print(f'{k:2d}: x = ({x1_best:.8f},{x2_best:.8f}) -> {f_best:.8f}')
        
    return x1_best,x2_best,f_best
[24]
def rosen(x):
    return (1.0-x[0])**2+2*(x[1]-x[0]**2)**2

x1_min = 0
x1_max = 5
x2_min = 0
x2_max = 4
N = 100

x1,x2,f = grid_search(rosen,x1_min,x1_max,x2_min,x2_max,N)
print(x1,x2,f)
1.0101010101010102 1.0101010101010102 0.0003102344761977566
[25]
K = 10
x1,x2,f = refined_grid_search(rosen,x1_min,x1_max,x2_min,x2_max,N,K)
0: x = (1.01010101,1.01010101) -> 0.00031023 1: x = (0.99938782,0.99908173) -> 0.00000056 2: x = (1.00003710,1.00004638) -> 0.00000000 3: x = (0.99999775,0.99999691) -> 0.00000000 4: x = (1.00000014,1.00000045) -> 0.00000000 5: x = (0.99999999,0.99999997) -> 0.00000000 6: x = (1.00000000,1.00000000) -> 0.00000000 7: x = (1.00000000,1.00000000) -> 0.00000000 8: x = (1.00000000,1.00000000) -> 0.00000000 9: x = (1.00000000,1.00000000) -> 0.00000000