43. Job Search II: Search and Separation#

In addition to what’s in Anaconda, this lecture will need the following libraries:

!pip install quantecon jax myst-nb

Hide code cell output

Requirement already satisfied: quantecon in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (0.10.1)
Requirement already satisfied: jax in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (0.8.0)
Requirement already satisfied: myst-nb in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (1.3.0)
Requirement already satisfied: numba>=0.49.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from quantecon) (0.61.0)
Requirement already satisfied: numpy>=1.17.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from quantecon) (2.1.3)
Requirement already satisfied: requests in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from quantecon) (2.32.3)
Requirement already satisfied: scipy>=1.5.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from quantecon) (1.15.3)
Requirement already satisfied: sympy in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from quantecon) (1.13.3)
Requirement already satisfied: jaxlib<=0.8.0,>=0.8.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from jax) (0.8.0)
Requirement already satisfied: ml_dtypes>=0.5.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from jax) (0.5.3)
Requirement already satisfied: opt_einsum in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from jax) (3.4.0)
Requirement already satisfied: importlib_metadata in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from myst-nb) (8.5.0)
Requirement already satisfied: ipython in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from myst-nb) (8.30.0)
Requirement already satisfied: jupyter-cache>=0.5 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from myst-nb) (1.0.1)
Requirement already satisfied: nbclient in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from myst-nb) (0.10.2)
Requirement already satisfied: myst-parser>=1.0.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from myst-nb) (3.0.1)
Requirement already satisfied: nbformat>=5.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from myst-nb) (5.10.4)
Requirement already satisfied: pyyaml in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from myst-nb) (6.0.2)
Requirement already satisfied: sphinx>=5 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from myst-nb) (7.4.7)
Requirement already satisfied: typing-extensions in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from myst-nb) (4.12.2)
Requirement already satisfied: ipykernel in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from myst-nb) (6.29.5)
Requirement already satisfied: attrs in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from jupyter-cache>=0.5->myst-nb) (24.3.0)
Requirement already satisfied: click in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from jupyter-cache>=0.5->myst-nb) (8.1.8)
Requirement already satisfied: sqlalchemy<3,>=1.3.12 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from jupyter-cache>=0.5->myst-nb) (2.0.39)
Requirement already satisfied: tabulate in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from jupyter-cache>=0.5->myst-nb) (0.9.0)
Requirement already satisfied: greenlet!=0.4.17 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from sqlalchemy<3,>=1.3.12->jupyter-cache>=0.5->myst-nb) (3.1.1)
Requirement already satisfied: docutils<0.22,>=0.18 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from myst-parser>=1.0.0->myst-nb) (0.21.2)
Requirement already satisfied: jinja2 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from myst-parser>=1.0.0->myst-nb) (3.1.6)
Requirement already satisfied: markdown-it-py~=3.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from myst-parser>=1.0.0->myst-nb) (3.0.0)
Requirement already satisfied: mdit-py-plugins~=0.4 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from myst-parser>=1.0.0->myst-nb) (0.5.0)
Requirement already satisfied: mdurl~=0.1 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from markdown-it-py~=3.0->myst-parser>=1.0.0->myst-nb) (0.1.0)
Requirement already satisfied: sphinxcontrib-applehelp in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from sphinx>=5->myst-nb) (2.0.0)
Requirement already satisfied: sphinxcontrib-devhelp in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from sphinx>=5->myst-nb) (2.0.0)
Requirement already satisfied: sphinxcontrib-jsmath in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from sphinx>=5->myst-nb) (1.0.1)
Requirement already satisfied: sphinxcontrib-htmlhelp>=2.0.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from sphinx>=5->myst-nb) (2.1.0)
Requirement already satisfied: sphinxcontrib-serializinghtml>=1.1.9 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from sphinx>=5->myst-nb) (2.0.0)
Requirement already satisfied: sphinxcontrib-qthelp in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from sphinx>=5->myst-nb) (2.0.0)
Requirement already satisfied: Pygments>=2.17 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from sphinx>=5->myst-nb) (2.19.1)
Requirement already satisfied: snowballstemmer>=2.2 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from sphinx>=5->myst-nb) (2.2.0)
Requirement already satisfied: babel>=2.13 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from sphinx>=5->myst-nb) (2.16.0)
Requirement already satisfied: alabaster~=0.7.14 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from sphinx>=5->myst-nb) (0.7.16)
Requirement already satisfied: imagesize>=1.3 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from sphinx>=5->myst-nb) (1.4.1)
Requirement already satisfied: packaging>=23.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from sphinx>=5->myst-nb) (24.2)
Requirement already satisfied: MarkupSafe>=2.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from jinja2->myst-parser>=1.0.0->myst-nb) (3.0.2)
Requirement already satisfied: jupyter-client>=6.1.12 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from nbclient->myst-nb) (8.6.3)
Requirement already satisfied: jupyter-core!=5.0.*,>=4.12 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from nbclient->myst-nb) (5.7.2)
Requirement already satisfied: traitlets>=5.4 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from nbclient->myst-nb) (5.14.3)
Requirement already satisfied: python-dateutil>=2.8.2 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from jupyter-client>=6.1.12->nbclient->myst-nb) (2.9.0.post0)
Requirement already satisfied: pyzmq>=23.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from jupyter-client>=6.1.12->nbclient->myst-nb) (26.2.0)
Requirement already satisfied: tornado>=6.2 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from jupyter-client>=6.1.12->nbclient->myst-nb) (6.5.1)
Requirement already satisfied: platformdirs>=2.5 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from jupyter-core!=5.0.*,>=4.12->nbclient->myst-nb) (4.3.7)
Requirement already satisfied: fastjsonschema>=2.15 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from nbformat>=5.0->myst-nb) (2.20.0)
Requirement already satisfied: jsonschema>=2.6 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from nbformat>=5.0->myst-nb) (4.23.0)
Requirement already satisfied: jsonschema-specifications>=2023.03.6 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from jsonschema>=2.6->nbformat>=5.0->myst-nb) (2023.7.1)
Requirement already satisfied: referencing>=0.28.4 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from jsonschema>=2.6->nbformat>=5.0->myst-nb) (0.30.2)
Requirement already satisfied: rpds-py>=0.7.1 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from jsonschema>=2.6->nbformat>=5.0->myst-nb) (0.22.3)
Requirement already satisfied: llvmlite<0.45,>=0.44.0dev0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from numba>=0.49.0->quantecon) (0.44.0)
Requirement already satisfied: six>=1.5 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from python-dateutil>=2.8.2->jupyter-client>=6.1.12->nbclient->myst-nb) (1.17.0)
Requirement already satisfied: charset-normalizer<4,>=2 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from requests->quantecon) (3.3.2)
Requirement already satisfied: idna<4,>=2.5 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from requests->quantecon) (3.7)
Requirement already satisfied: urllib3<3,>=1.21.1 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from requests->quantecon) (2.3.0)
Requirement already satisfied: certifi>=2017.4.17 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from requests->quantecon) (2025.4.26)
Requirement already satisfied: zipp>=3.20 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from importlib_metadata->myst-nb) (3.21.0)
Requirement already satisfied: comm>=0.1.1 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from ipykernel->myst-nb) (0.2.1)
Requirement already satisfied: debugpy>=1.6.5 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from ipykernel->myst-nb) (1.8.11)
Requirement already satisfied: matplotlib-inline>=0.1 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from ipykernel->myst-nb) (0.1.6)
Requirement already satisfied: nest-asyncio in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from ipykernel->myst-nb) (1.6.0)
Requirement already satisfied: psutil in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from ipykernel->myst-nb) (5.9.0)
Requirement already satisfied: decorator in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from ipython->myst-nb) (5.1.1)
Requirement already satisfied: jedi>=0.16 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from ipython->myst-nb) (0.19.2)
Requirement already satisfied: prompt-toolkit<3.1.0,>=3.0.41 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from ipython->myst-nb) (3.0.43)
Requirement already satisfied: stack-data in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from ipython->myst-nb) (0.2.0)
Requirement already satisfied: pexpect>4.3 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from ipython->myst-nb) (4.8.0)
Requirement already satisfied: wcwidth in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from prompt-toolkit<3.1.0,>=3.0.41->ipython->myst-nb) (0.2.5)
Requirement already satisfied: parso<0.9.0,>=0.8.4 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from jedi>=0.16->ipython->myst-nb) (0.8.4)
Requirement already satisfied: ptyprocess>=0.5 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from pexpect>4.3->ipython->myst-nb) (0.7.0)
Requirement already satisfied: executing in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from stack-data->ipython->myst-nb) (0.8.3)
Requirement already satisfied: asttokens in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from stack-data->ipython->myst-nb) (3.0.0)
Requirement already satisfied: pure-eval in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from stack-data->ipython->myst-nb) (0.2.2)
Requirement already satisfied: mpmath<1.4,>=1.1.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from sympy->quantecon) (1.3.0)

43.1. Overview#

Previously we looked at the McCall job search model [McCall, 1970] as a way of understanding unemployment and worker decisions.

One unrealistic feature of that version of the model was that every job is permanent.

In this lecture, we extend the model by introducing job separation.

Once separation enters the picture, the agent comes to view

  • the loss of a job as a capital loss, and

  • a spell of unemployment as an investment in searching for an acceptable job

The other minor addition is that a utility function will be included to make worker preferences slightly more sophisticated.

We’ll need the following imports

import matplotlib.pyplot as plt
import numpy as np
import jax
import jax.numpy as jnp
from typing import NamedTuple
from quantecon.distributions import BetaBinomial
from myst_nb import glue

43.2. The model#

The model is similar to the baseline McCall job search model.

It concerns the life of an infinitely lived worker and

  • the opportunities he or she (let’s say he to save one character) has to work at different wages

  • exogenous events that destroy his current job

  • his decision making process while unemployed

The worker can be in one of two states: employed or unemployed.

He wants to maximize

(43.1)#\[{\mathbb E} \sum_{t=0}^\infty \beta^t u(y_t)\]

At this stage the only difference from the baseline model is that we’ve added some flexibility to preferences by introducing a utility function \(u\).

It satisfies \(u'> 0\) and \(u'' < 0\).

Wage offers \(\{ W_t \}\) are IID with common distribution \(q\).

The set of possible wage values is denoted by \(\mathbb W\).

43.2.1. Timing and decisions#

At the start of each period, the agent can be either

  • unemployed or

  • employed at some existing wage level \(w\).

If currently employed at wage \(w\), the worker

  1. receives utility \(u(w)\) from their current wage and

  2. is fired with some (small) probability \(\alpha\), becoming unemployed next period.

If currently unemployed, the worker receives random wage offer \(W_t\) and either accepts or rejects.

If he accepts, then he begins work immediately at wage \(W_t\).

If he rejects, then he receives unemployment compensation \(c\).

The process then repeats.

Note

We do not allow for job search while employed—this topic is taken up in a later lecture.

43.3. Solving the model#

We drop time subscripts in what follows and primes denote next period values.

Let

  • \(v_e(w)\) be maximum lifetime value for a worker who enters the current period employed with wage \(w\)

  • \(v_u(w)\) be maximum lifetime for a worker who who enters the current period unemployed and receives wage offer \(w\).

Here, maximum lifetime value means the value of (43.1) when the worker makes optimal decisions at all future points in time.

As we now show, obtaining these functions is key to solving the model.

43.3.1. The Bellman equations#

We recall that, in the original job search model, the value function (the value of being unemployed with a given wage offer) satisfied a Bellman equation.

Here this function again satisfies a Bellman equation that looks very similar.

(43.2)#\[ v_u(w) = \max \left\{ v_e(w), \, u(c) + \beta \sum_{w' \in \mathbb W} v_u(w') q(w') \right\}\]

The difference is that the value of accepting is \(v_e(w)\) rather than \(w/(1-\beta)\).

We have to make this change because jobs are not permanent.

Accepting transitions the worker to employment and hence yields reward \(v_e(w)\), which we discuss below.

Rejecting leads to unemployment compensation and unemployment tomorrow.

Equation (43.2) expresses the value of being unemployed with offer \(w\) in hand as a maximum over the value of two options: accept or reject the current offer.

The function \(v_e\) also satisfies a Bellman equation:

(43.3)#\[ v_e(w) = u(w) + \beta \left[ (1-\alpha)v_e(w) + \alpha \sum_{w' \in \mathbb W} v_u(w') q(w') \right]\]

Note

This equation differs from a traditional Bellman equation because there is no max.

There is no max because an employed agent has no choices.

Nonetheless, in keeping with most of the literature, we also refer to it as a Bellman equation.

Equation (43.3) expresses the value of being employed at wage \(w\) in terms of

  • current reward \(u(w)\) plus

  • discounted expected reward tomorrow, given the \(\alpha\) probability of being fired

As we will see, equations (43.3) and (43.2) provide enough information to solve for both \(v_e\) and \(v_u\).

Once we have them in hand, we will be able to make optimal choices.

43.3.2. The reservation wage#

Let

(43.4)#\[ h := u(c) + \beta \sum_{w' \in \mathbb W} v_u(w') q(w')\]

This is the continuation value for an unemployed agent – the value of rejecting the current offer and then making optimal choices.

From (43.2), we see that an unemployed agent accepts current offer \(w\) if \(v_e(w) \geq h\).

This means precisely that the value of accepting is higher than the value of rejecting.

The function \(v_e\) is increasing in \(w\), since an employed agent is never made worse off by a higher current wage.

Hence, we can express the optimal choice as accepting wage offer \(w\) if and only if \(w \geq \bar w\), where the reservation wage \(\bar w\) is the first wage level \(w \in \mathbb W\) such that

\[ v_e(w) \geq h \]

43.4. Code#

Let’s now implement a solution method based on the two Bellman equations (43.2) and (43.3).

43.4.1. Set up#

The default utility function is a CRRA utility function

def u(x, γ):
    return (x**(1 - γ) - 1) / (1 - γ)

Also, here’s a default wage distribution, based around the BetaBinomial distribution:

n = 60                                  # n possible outcomes for w
w_default = jnp.linspace(10, 20, n)     # wages between 10 and 20
a, b = 600, 400                         # shape parameters
dist = BetaBinomial(n-1, a, b)          # distribution
q_default = jnp.array(dist.pdf())       # probabilities as a JAX array

Here’s our model class for the McCall model with separation.

class Model(NamedTuple):
    α: float = 0.2              # job separation rate
    β: float = 0.98             # discount factor
    γ: float = 2.0              # utility parameter (CRRA)
    c: float = 6.0              # unemployment compensation
    w: jnp.ndarray = w_default  # wage outcome space
    q: jnp.ndarray = q_default  # probabilities over wage offers

43.4.2. Operators#

We’ll use a similar iterative approach to solving the Bellman equations that we adopted in the first job search lecture.

As a first step, to iterate on the Bellman equations, we define two operators, one for each value function.

These operators take the current value functions as inputs and return updated versions.

def T_u(model, v_u, v_e):
    """
    Apply the unemployment Bellman update rule and return new guess of v_u.

    """
    α, β, γ, c, w, q = model
    h = u(c, γ) + β * (v_u @ q)
    v_u_new = jnp.maximum(v_e, h)
    return v_u_new
def T_e(model, v_u, v_e):
    """
    Apply the employment Bellman update rule and return new guess of v_e.

    """
    α, β, γ, c, w, q = model
    v_e_new = u(w, γ) + β * ((1 - α) * v_e + α * (v_u @ q))
    return v_e_new

43.4.3. Iteration#

Now we write an iteration routine, which updates the pair of arrays \(v_u\), \(v_e\) until convergence.

More precisely, we iterate until successive realizations are closer together than some small tolerance level.

def solve_full_model(
        model,
        tol: float = 1e-6,
        max_iter: int = 1_000,
    ):
    """
    Solves for both value functions v_u and v_e iteratively.

    """
    α, β, γ, c, w, q = model
    i = 0
    error = tol + 1
    v_e = v_u = w / (1 - β)

    while i < max_iter and error > tol:
        v_u_next = T_u(model, v_u, v_e)
        v_e_next = T_e(model, v_u, v_e)
        error_u = jnp.max(jnp.abs(v_u_next - v_u))
        error_e = jnp.max(jnp.abs(v_e_next - v_e))
        error = jnp.max(jnp.array([error_u, error_e]))
        v_u = v_u_next
        v_e = v_e_next
        i += 1

    return v_u, v_e

43.4.4. Computing the reservation wage#

Now that we can solve for both value functions, let’s investigate the reservation wage.

Recall from above that the reservation wage \(\bar w\) is the first \(w \in \mathbb W\) satisfying \(v_e(w) \geq h\), where \(h\) is the continuation value defined in (43.4).

Let’s compare \(v_e\) and \(h\) to see what they look like.

We’ll use the default parameterizations found in the code above.

model = Model()
α, β, γ, c, w, q = model
v_u, v_e = solve_full_model(model)
h = u(c, γ) + β * (v_u @ q)

fig, ax = plt.subplots()
ax.plot(w, v_e, 'b-', lw=2, alpha=0.7, label='$v_e$')
ax.plot(w, [h] * len(w), 'g-', lw=2, alpha=0.7, label='$h$')
ax.set_xlim(min(w), max(w))
ax.legend()
plt.show()
_images/e31c9d66847c95a5a1e63e8bc3705a73c657f39173ba0cbc4817dee911849945.png

The value \(v_e\) is increasing because higher \(w\) generates a higher wage flow conditional on staying employed.

The reservation wage is the \(w\) where these lines meet.

Let’s compute this reservation wage explicitly:

def compute_reservation_wage_full(model):
    """
    Computes the reservation wage using the full model solution.
    """
    α, β, γ, c, w, q = model
    v_u, v_e = solve_full_model(model)
    h = u(c, γ) + β * (v_u @ q)
    # Find the first w such that v_e(w) >= h, or +inf if none exist
    accept = v_e >= h
    i = jnp.argmax(accept)  # returns first accept index
    w_bar = jnp.where(jnp.any(accept), w[i], jnp.inf)
    return w_bar

w_bar_full = compute_reservation_wage_full(model)
print(f"Reservation wage (full model): {w_bar_full:.4f}")
Reservation wage (full model): 11.8644

This value seems close to where the two lines meet.

43.5. A simplifying transformation#

The approach above works, but iterating over two vector-valued functions is computationally expensive.

With some mathematics and some brain power, we can form a solution method that is far more efficient.

(This process will be analogous to our second pass at the plain vanilla McCall model, where we reduced the Bellman equation to an equation in an unknown scalar value, rather than an unknown vector.)

First, we use the continuation value \(h\), as defined in (43.4), to write (43.2) as

\[ v_u(w) = \max \left\{ v_e(w), \, h \right\} \]

Taking the expectation of both sides and then discounting, this becomes

\[ \beta \sum_{w'} v_u(w') q(w') = \beta \sum_{w'} \max \left\{ v_e(w'), \, h \right\} q(w') \]

Adding \(u(c)\) to both sides and using (43.4) again gives

(43.5)#\[h = u(c) + \beta \sum_{w'} \max \left\{ v_e(w'), \, h \right\} q(w')\]

This is a nice scalar equation in the continuation value, which is already useful.

But we can go further, but eliminating \(v_e\) from the above equation.

43.5.1. Simplifying to a single equation#

As a first step, we rearrange the expression defining \(h\) (see (43.4)) to obtain

\[ \sum_{w'} v_u(w') q(w') = \frac{h - u(c)}{\beta} \]

Using this, the Bellman equation for \(v_e\), as given in (43.3), can now be rewritten as

(43.6)#\[v_e(w) = u(w) + \beta \left[ (1-\alpha)v_e(w) + \alpha \frac{h - u(c)}{\beta} \right]\]

Our next step is to solve (43.6) for \(v_e\) as a function of \(h\).

Rearranging (43.6) gives

\[ v_e(w) = u(w) + \beta(1-\alpha)v_e(w) + \alpha(h - u(c)) \]

or

\[ v_e(w) - \beta(1-\alpha)v_e(w) = u(w) + \alpha(h - u(c)) \]

Solving for \(v_e(w)\) gives

(43.7)#\[ v_e(w) = \frac{u(w) + \alpha(h - u(c))}{1 - \beta(1-\alpha)}\]

Substituting this into (43.5) yields

(43.8)#\[h = u(c) + \beta \sum_{w' \in \mathbb W} \max \left\{ \frac{u(w') + \alpha(h - u(c))}{1 - \beta(1-\alpha)}, \, h \right\} q(w')\]

Finally we have a single scalar equation in \(h\)!

If we can solve this for \(h\), we can easily recover \(v_e\) using (43.7).

Then we have enough information to compute the reservation wage.

43.5.2. Solving the Bellman equations#

To solve (43.8), we use the iteration rule

(43.9)#\[h_{n+1} = u(c) + \beta \sum_{w' \in \mathbb W} \max \left\{ \frac{u(w') + \alpha(h_n - u(c))}{1 - \beta(1-\alpha)}, \, h_n \right\} q(w')\]

starting from some initial condition \(h_0\).

(It is possible to prove that (43.9) converges via the Banach contraction mapping theorem.)

43.6. Implementation#

To implement iteration on \(h\), we provide a function that provides one update, from \(h_n\) to \(h_{n+1}\)

def update_h(model, h):
    " One update of the scalar h. "
    α, β, γ, c, w, q = model
    v_e = compute_v_e(model, h)
    h_new = u(c, γ) + β * (jnp.maximum(v_e, h) @ q)
    return h_new

Also, we provide a function to compute \(v_e\) from (43.7).

def compute_v_e(model, h):
    " Compute v_e from h using the closed-form expression. "
    α, β, γ, c, w, q = model
    return (u(w, γ) + α * (h - u(c, γ))) / (1 - β * (1 - α))

This function will be applied once convergence is achieved.

Now we can write our model solver.

@jax.jit
def solve_model(model, tol=1e-5, max_iter=2000):
    " Iterates to convergence on the Bellman equations. "

    def cond(loop_state):
        h, i, error = loop_state
        return jnp.logical_and(error > tol, i < max_iter)

    def update(loop_state):
        h, i, error = loop_state
        h_new = update_h(model, h)
        error_new = jnp.abs(h_new - h)
        return h_new, i + 1, error_new

    # Initialize
    h_init = u(model.c, model.γ) / (1 - model.β)
    i_init = 0
    error_init = tol + 1
    init_state = (h_init, i_init, error_init)

    final_state = jax.lax.while_loop(cond, update, init_state)
    h_final, _, _ = final_state

    # Compute v_e from the converged h
    v_e_final = compute_v_e(model, h_final)

    return v_e_final, h_final

Finally, here’s a function compute_reservation_wage that uses all the logic above, taking an instance of Model and returning the associated reservation wage.

def compute_reservation_wage(model):
    """
    Computes the reservation wage of an instance of the McCall model
    by finding the smallest w such that v_e(w) >= h.

    """
    # Find the first i such that v_e(w_i) >= h and return w[i]
    # If no such w exists, then w_bar is set to np.inf
    v_e, h = solve_model(model)
    accept = v_e >= h
    i = jnp.argmax(accept)   # take first accept index
    w_bar = jnp.where(jnp.any(accept), model.w[i], jnp.inf)
    return w_bar

Let’s verify that this simplified approach gives the same answer as the full model:

w_bar_simplified = compute_reservation_wage(model)
print(f"Reservation wage (simplified): {w_bar_simplified:.4f}")
print(f"Reservation wage (full model): {w_bar_full:.4f}")
print(f"Difference: {abs(w_bar_simplified - w_bar_full):.6f}")
Reservation wage (simplified): 11.8644
Reservation wage (full model): 11.8644
Difference: 0.000000

As we can see, both methods produce essentially the same reservation wage.

However, the simplified method is far more efficient.

Next we will investigate how the reservation wage varies with parameters.

43.7. Impact of parameters#

In each instance below, we’ll show you a figure and then ask you to reproduce it in the exercises.

43.7.1. The reservation wage and unemployment compensation#

First, let’s look at how \(\bar w\) varies with unemployment compensation.

In the figure below, we use the default parameters in the Model class, apart from c (which takes the values given on the horizontal axis)

_images/b6feeaaa4044da3ebd6b38c23b15594f8091d0f88ded081f2286c35ebda3e095.png

As expected, higher unemployment compensation causes the worker to hold out for higher wages.

In effect, the cost of continuing job search is reduced.

43.7.2. The reservation wage and discounting#

Next, let’s investigate how \(\bar w\) varies with the discount factor.

The next figure plots the reservation wage associated with different values of \(\beta\)

_images/1744bc6d5f1b5b87b26cf3aad5c59230cd2088274ab09e4ce78b6deb83515163.png

Again, the results are intuitive: More patient workers will hold out for higher wages.

43.7.3. The reservation wage and job destruction#

Finally, let’s look at how \(\bar w\) varies with the job separation rate \(\alpha\).

Higher \(\alpha\) translates to a greater chance that a worker will face termination in each period once employed.

_images/2d5a7ac689f7539736641700b16a5a2be3b8904834113b3b8bcaf5d225f78be6.png

Once more, the results are in line with our intuition.

If the separation rate is high, then the benefit of holding out for a higher wage falls.

Hence the reservation wage is lower.

43.8. Exercises#

Exercise 43.1

Reproduce all the reservation wage figures shown above.

Regarding the values on the horizontal axis, use

grid_size = 25
c_vals = jnp.linspace(2, 12, grid_size)         # unemployment compensation
β_vals = jnp.linspace(0.8, 0.99, grid_size)     # discount factors
α_vals = jnp.linspace(0.05, 0.5, grid_size)     # separation rate