#!/usr/bin/env python
# coding: utf-8
# # Job Search Model
#
#
# **Randall Romero Aguilar, PhD**
#
# This demo is based on the original Matlab demo accompanying the Computational Economics and Finance 2001 textbook by Mario Miranda and Paul Fackler.
#
# Original (Matlab) CompEcon file: **demdp04.m**
#
# Running this file requires the Python version of CompEcon. This can be installed with pip by running
#
# !pip install compecon --upgrade
#
# Last updated: 2022-Oct-22
#
# ## About
#
# Infinitely-lived worker must decide whether to quit, if employed, or search for a job, if unemployed, given prevailing market wages.
#
# ### States
#
# - w prevailing wage
# - i unemployed (0) or employed (1) at beginning of period
#
# ### Actions
#
# - j idle (0) or active (i.e., work or search) (1) this period
#
# ### Parameters
#
# | Parameter | Meaning |
# |-----------|-------------------------|
# | $v$ | benefit of pure leisure |
# | $\bar{w}$ | long-run mean wage |
# | $\gamma$ | wage reversion rate |
# | $p_0$ | probability of finding job |
# | $p_1$ | probability of keeping job |
# | $\sigma$ | standard deviation of wage shock |
# | $\delta$ | discount factor |
#
# # Preliminary tasks
# In[1]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from compecon import BasisSpline, DPmodel, qnwnorm
# ## FORMULATION
#
# ### Worker's reward
#
# The worker's reward is:
#
# - $w$ (the prevailing wage rate), if he's employed and active (working)
# - $u=90$, if he's unemployed but active (searching)
# - $v=95$, if he's idle (quit if employed, not searching if unemployed)
# In[2]:
u = 90
v = 95
def reward(w, x, employed, active):
if active:
return w.copy() if employed else np.full_like(w, u) # the copy is critical!!! otherwise it passes a pointer to w!!
else:
return np.full_like(w, v)
# ### Model dynamics
#
# #### Stochastic Discrete State Transition Probabilities
#
# An unemployed worker who is searching for a job has a probability $p_0=0.2$ of finding it, while an employed worker who doesn't want to quit his job has a probability $p_1 = 0.9$ of keeping it. An idle worker (someone who quits or doesn't search for a job) will definitely be unemployed next period. Thus, the transition probabilities are
# \begin{align}
# q = \begin{bmatrix}1-p_0 &p_0\\1-p_1&p_1\end{bmatrix},&\qquad\text{if active} \\
# = \begin{bmatrix}1 & 0\\1 &0 \end{bmatrix},&\qquad\text{if iddle}
# \end{align}
# In[3]:
p0 = 0.20
p1 = 0.90
q = np.zeros((2, 2, 2))
q[1, 0, 1] = p0
q[1, 1, 1] = p1
q[:, :, 0] = 1 - q[:, :, 1]
# #### Stochastic Continuous State Transition
# Assuming that the wage rate $w$ follows an exogenous Markov process
# \begin{equation}
# w_{t+1} = \bar{w} + \gamma(w_t − \bar{w}) + \epsilon_{t+1}
# \end{equation}
#
# where $\bar{w}=100$ and $\gamma=0.4$.
# In[4]:
wbar = 100
ɣ = 0.40
def transition(w, x, i, j, in_, e):
return wbar + ɣ * (w - wbar) + e
# Here, $\epsilon$ is normal $(0,\sigma^2)$ wage shock, where $\sigma=5$. We discretize this distribution with the function ```qnwnorm```.
# In[5]:
σ = 5
m = 15
e, w = qnwnorm(m, 0, σ**2)
# ### Approximation Structure
#
# To discretize the continuous state variable, we use a cubic spline basis with $n=150$ nodes between $w_\text{min}=0$ and $w_\text{max}=200$.
# In[6]:
n = 150
wmin = 0
wmax = 200
basis = BasisSpline(n, wmin, wmax, labels=['wage'])
# ## SOLUTION
#
# To represent the model, we create an instance of ```DPmodel```. Here, we assume a discout factor of $\delta=0.95$.
# In[7]:
model = DPmodel(basis, reward, transition,
i =['unemployed', 'employed'],
j = ['idle', 'active'],
discount=0.95, e=e, w=w, q=q)
# Then, we call the method ```solve``` to solve the Bellman equation
# In[8]:
S = model.solve(show=True)
S.head()
# ### Compute and Print Critical Action Wages
# In[9]:
def critical(db):
wcrit = np.interp(0, db['value[active]'] - db['value[idle]'], db['wage'])
vcrit = np.interp(wcrit, db['wage'], db['value[idle]'])
return wcrit, vcrit
wcrit0, vcrit0 = critical(S.loc['unemployed'])
print(f'Critical Search Wage = {wcrit0:5.1f}')
wcrit1, vcrit1 = critical(S.loc['employed'])
print(f'Critical Quit Wage = {wcrit1:5.1f}')
# ### Plot Action-Contingent Value Function
# In[10]:
vv = ['value[idle]','value[active]']
fig1, (ax0, ax1) = plt.subplots(1,2,figsize=[16,4])
# UNEMPLOYED
ax0.set(title='Action-Contingent Value, Unemployed', xlabel='Wage', ylabel='Value')
ax0.plot(S.loc['unemployed',vv])
ax0.annotate(f'$w^*_0 = {wcrit0:.1f}$', [wcrit0, vcrit0], fontsize=12, va='top')
ax0.legend(['Do Not Search', 'Search'], loc='upper left', fontsize="x-small")
# EMPLOYED
ax1.set(title='Action-Contingent Value, Employed', xlabel='Wage', ylabel='Value')
ax1.plot(S.loc['employed',vv])
ax1.annotate(f'$w^*_0 = {wcrit1:.1f}$', [wcrit1, vcrit1], fontsize=12, va='top')
ax1.legend(['Quit', 'Work'], loc='upper left', fontsize="x-small")
# ### Plot Residual
# In[11]:
S['resid2'] = 100 * (S['resid'] / S['value'])
fig2, ax = plt.subplots()
ax.set(title='Bellman Equation Residual',
xlabel='Wage',
ylabel='Percent Residual')
ax.plot(S.loc['unemployed','resid2'])
ax.plot(S.loc['employed','resid2'])
ax.legend(model.labels.i)
# ## SIMULATION
# ### Simulate Model
#
# We simulate the model 10000 times for a time horizon $T=40$, starting with an unemployed worker ($i=0$) at the long-term wage rate mean $\bar{w}$. To be able to reproduce these results, we set the random seed at an arbitrary value of 945.
# In[12]:
T = 40
nrep = 10000
sinit = np.full((1, nrep), wbar)
iinit = 0
data = model.simulate(T, sinit, iinit, seed=945)
# In[13]:
data.head()
# ### Print Ergodic Moments
# In[14]:
ff = '\t{:12s} = {:5.2f}'
print('\nErgodic Means')
print(ff.format('Wage', data['wage'].mean()))
print(ff.format('Employment', (data['i'] == 'employed').mean()))
print('\nErgodic Standard Deviations')
print(ff.format('Wage',data['wage'].std()))
print(ff.format('Employment', (data['i'] == 'employed').std()))
# In[15]:
ergodic = pd.DataFrame({
'Ergodic Means' : [data['wage'].mean(), (data['i'] == 'employed').mean()],
'Ergodic Standard Deviations': [data['wage'].std(), (data['i'] == 'employed').std()]},
index=['Wage', 'Employment'])
ergodic.round(2)
# ### Plot Expected Discrete State Path
# In[16]:
data.head()
# In[17]:
data['ii'] = data['i'] == 'employed'
probability_of_employment = data[['ii','time']].groupby('time').mean()
fig3, ax = plt.subplots()
ax.set(title='Probability of Employment', xlabel='Period', ylabel='Probability')
ax.plot(probability_of_employment)
# ### Plot Simulated and Expected Continuous State Path
# In[31]:
subdata = data.query('_rep < 3').pivot("time", "_rep" ,'wage')
fig4, ax = plt.subplots()
ax.set(title='Simulated and Expected Wage', xlabel='Period', ylabel='Wage')
subdata.plot(ax=ax)
ax.plot(data[['time','wage']].groupby('time').mean(),'k--',label='mean')
ax.legend(fontsize='xx-small')