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

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)

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

(26)\[\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}\]
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

(27)\[\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\).

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.

σ = 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\).

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\).

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

S = model.solve(show=True)
S.head()
Solving infinite-horizon model collocation equation by Newton's method
iter change       time    
------------------------------
   0       2.1e+03    0.1720
   1       3.5e+01    0.4380
   2       1.9e+01    0.6730
   3       1.4e+00    0.9291
   4       2.8e-12    1.1958
Elapsed Time =    1.20 Seconds
wage i value resid j* value[idle] value[active]
wage
unemployed 0.000000 0.000000 0 1911.101186 0.000000e+00 idle 1911.101186 1906.101213
0.133422 0.133422 0 1911.101997 -1.405169e-10 idle 1911.101997 1906.102026
0.266845 0.266845 0 1911.102810 -1.452918e-10 idle 1911.102810 1906.102841
0.400267 0.400267 0 1911.103625 -5.434231e-11 idle 1911.103625 1906.103656
0.533689 0.533689 0 1911.104440 9.526957e-11 idle 1911.104440 1906.104473

Compute and Print Critical Action Wages

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}')
Critical Search Wage =  93.8
Critical Quit Wage   =  79.4

Plot Action-Contingent Value Function

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")
<matplotlib.legend.Legend at 0x1e145301a60>
../../_images/04 Job Search Model_21_1.png

Plot Residual

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)
<matplotlib.legend.Legend at 0x1e140654250>
../../_images/04 Job Search Model_23_1.png

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.

T = 40
nrep = 10000
sinit = np.full((1, nrep), wbar)
iinit = 0
data = model.simulate(T, sinit, iinit, seed=945)
data.head()
time _rep i wage j*
0 0 0 unemployed 100.0 active
1 0 1 unemployed 100.0 active
2 0 2 unemployed 100.0 active
3 0 3 unemployed 100.0 active
4 0 4 unemployed 100.0 active

Plot Expected Discrete State Path

data.head()
time _rep i wage j*
0 0 0 unemployed 100.0 active
1 0 1 unemployed 100.0 active
2 0 2 unemployed 100.0 active
3 0 3 unemployed 100.0 active
4 0 4 unemployed 100.0 active
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)
[<matplotlib.lines.Line2D at 0x1e14565f2b0>]
../../_images/04 Job Search Model_33_1.png

Plot Simulated and Expected Continuous State Path

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')
<matplotlib.legend.Legend at 0x1e1456e97f0>
../../_images/04 Job Search Model_35_1.png