46. Job Search V: Risk-Sensitive Preferences#

GPU

This lecture was built using a machine with access to a GPU.

Google Colab has a free tier with GPUs that you can access as follows:

  1. Click on the “play” icon top right

  2. Select Colab

  3. Set the runtime environment to include a GPU

46.1. Overview#

Risk-sensitive preferences are a common addition to various types of dynamic programming problems.

This lecture gives an introduction to risk-sensitive recursive preferences via job search.

Some motivation is given below.

46.2. Outline#

In real-world job-related decisions, individuals and households care about risk.

For example, some individuals might prefer to take a moderate offer already in hand over the risky possibility of a higher offer in the next period, even without discounting future payoffs.

(A bird in the hand is worth two in the bush, etc.)

In previous job search lectures in this series, we inserted some degree of risk aversion by adding a concave flow utility function \(u\).

Unfortunately, this strategy does not isolate preferences over the kind of risk we described above.

This is because adding a concave utility function changes the agent’s preferences in other ways, such as in their desire for consumption smoothing.

Hence, if we want to study the pure effects of risk, we need a different solution.

One possibility is to add risk-sensitive preferences.

Here we show how this can be done and study what effects it has on agent choices.

We’ll use JAX and the QuantEcon library:

!pip install quantecon jax

Hide code cell output

Collecting quantecon
  Downloading quantecon-0.10.1-py3-none-any.whl.metadata (5.3 kB)
Requirement already satisfied: jax in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (0.8.1)
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.1,>=0.8.1 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from jax) (0.8.1)
Requirement already satisfied: ml_dtypes>=0.5.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from jax) (0.5.4)
Requirement already satisfied: opt_einsum in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from jax) (3.4.0)
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: 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: mpmath<1.4,>=1.1.0 in /home/runner/miniconda3/envs/quantecon/lib/python3.13/site-packages (from sympy->quantecon) (1.3.0)
Downloading quantecon-0.10.1-py3-none-any.whl (325 kB)
Installing collected packages: quantecon
Successfully installed quantecon-0.10.1

We use the following imports.

import jax
import jax.numpy as jnp
from jax import lax
import quantecon as qe
from typing import NamedTuple
import numpy as np
import matplotlib.pyplot as plt
Matplotlib is building the font cache; this may take a moment.

46.3. Introduction to risk-sensitivity#

Let’s start our discussion in a static environment.

If \(Y\) is a random payoff and an agent’s evaluation of the payoff is \(e := \mathbb{E} Y\), then we say that the agent is risk neutral.

Sometimes we want to model agents as risk averse.

One way to do this is to change their evaluation of the payoff \(Y\) to

\[ e_{\theta} = \frac{1}{\theta} \ln\left( \mathbb{E} [ \exp(\theta Y) ] \right) \]

where \(\theta\) is a number satisfying \(\theta < 0\).

The value \(e_{\theta}\) is sometimes called the entropic risk-adjusted expectation of \(Y\).

46.3.1. A Gaussian example#

One way to see the impact is to suppose that \(Y\) has the normal distribution \(N(\mu, \sigma^2)\), so that its mean is \(\mu\) and its variance is \(\sigma^2\).

For this \(Y\) we aim to compute the risk-adjusted expectation.

This becomes straightforward if we recognize that \(\mathbb{E}[\exp(\theta Y)]\) is the moment generating function (MGF) of the normal distribution.

Using the well-known expression for the MGF of the normal distribution, we get

\[ \mathbb{E}[\exp(\theta Y)] = \exp\left(\theta\mu + \frac{\theta^2\sigma^2}{2}\right) \]

Therefore,

\[ e_\theta = \frac{1}{\theta} \ln\left( \exp\left(\theta\mu + \frac{\theta^2\sigma^2}{2}\right) \right) = \frac{1}{\theta} \left(\theta\mu + \frac{\theta^2\sigma^2}{2}\right) \]

Simplifying yields

\[ e_\theta = \mu + \frac{\theta\sigma^2}{2} \]

We see immediately that the agent prefers a higher average payoff \(\mu\).

At the same time, given that \(\theta < 0\), the risk-adjusted expectation decreases in \(\sigma\).

In particular, \(e_\theta\) decreases as risk increases.

Here is a visualization of \(e_\theta\) as a function of \(\mu\) and \(\sigma\) using a contour plot, with \(\theta=-1\).

theta = -1

mu_vals = np.linspace(-2, 5, 200)
sigma_vals = np.linspace(0.1, 3, 200)
mu_grid, sigma_grid = np.meshgrid(mu_vals, sigma_vals)

e_theta = mu_grid + (theta * sigma_grid**2) / 2

# Create contour plot
fig, ax = plt.subplots(figsize=(10, 8))
contour = ax.contour(
    mu_grid, sigma_grid, e_theta, levels=20, colors='black', linewidths=0.5
)
contourf = ax.contourf(
    mu_grid, sigma_grid, e_theta, levels=20, cmap='viridis'
)
ax.clabel(contour, inline=True, fontsize=8)
cbar = plt.colorbar(contourf, ax=ax)
cbar.set_label(r'$e_\theta$', rotation=0, fontsize=12)

ax.set_xlabel(r'$\mu$ (mean)', fontsize=12)
ax.set_ylabel(r'$\sigma$ (standard deviation)', fontsize=12)
plt.tight_layout()
plt.show()
_images/b16e027dab05f8e57310f5601094c5f0a8663e1e98321f34679bab0f63c4f93f.png

Again, we see that the agent prefers a higher average payoff but dislikes risk.

46.3.2. A more general case#

The preceding analysis relies on the Gaussian (normal) assumption to get an analytical solution.

We can investigate all other cases using simulation.

For example, suppose that \(Y\) has the Beta\((a, b)\) distribution.

Here we set \(a=b=2.0\) and calculate \(e_{\theta}\) using Monte Carlo

The method is:

  1. sample \(Y_1, \ldots, Y_n\) from Beta\((2,2)\)

  2. replace \(\mathbb{E}\) with an average over \(\exp(\theta Y_i)\)

We do this for \(\theta\) in a grid of 100 points between \(-2\) and \(-0.1\).

Here is a plot of \(e_{\theta}\) against \(\theta\).

import jax
import jax.numpy as jnp
import matplotlib.pyplot as plt

# Set parameters
a, b = 2.0, 2.0
mc_size = 1_000_000  # Large number of Monte Carlo samples
theta_grid = jnp.linspace(-2, -0.1, 100)

# Draw samples from Beta(2, 2) distribution using JAX
key = jax.random.PRNGKey(1234)
Y_samples = jax.random.beta(key, a, b, shape=(mc_size,))

# Define function to compute e_theta for a single theta value
def compute_e_theta(theta):
    """Compute e_theta = (1/theta) * ln(E[exp(theta * Y)])"""
    expectation = jnp.mean(jnp.exp(theta * Y_samples))
    return (1 / theta) * jnp.log(expectation)

# Vectorize over theta_grid using vmap
compute_e_theta_vec = jax.vmap(compute_e_theta)
e_theta_values = compute_e_theta_vec(theta_grid)

# Plot results
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(theta_grid, e_theta_values, linewidth=2)
ax.set_xlabel(r'$\theta$', fontsize=14)
ax.set_ylabel(r'$e_\theta$', fontsize=14)
ax.set_title(r'$e_\theta$ for $Y \sim \text{Beta}(2, 2)$', fontsize=14)
ax.axhline(y=0.5, color='black', linestyle='--',
           linewidth=1, label=r'$\mathbb{E}[Y] = 0.5$')
ax.legend(fontsize=12)
plt.tight_layout()
plt.show()
W1210 01:15:46.707857    2267 cuda_executor.cc:1802] GPU interconnect information not available: INTERNAL: NVML doesn't support extracting fabric info or NVLink is not used by the device.
W1210 01:15:46.711386    2197 cuda_executor.cc:1802] GPU interconnect information not available: INTERNAL: NVML doesn't support extracting fabric info or NVLink is not used by the device.
_images/749c72c212c735f5485cb0245cb22b9b5a9b7606a41379ec376d859aa8866b18.png

The plot shows how the risk-adjusted evaluation \(e_\theta\) changes with the risk aversion parameter \(\theta\).

As \(\theta \to 0\), the value \(e_\theta\) approaches the expected value of \(Y\), which is \(\mathbb{E}[Y] = \frac{a}{a+b} = \frac{2}{4} = 0.5\) for Beta(2,2).

This makes sense because when \(\theta \to 0\), the agent becomes risk neutral.

As \(\theta\) becomes more negative, \(e_\theta\) decreases.

This reflects that a more risk-averse agent values the uncertain payoff \(Y\) less than its expected value.

46.3.3. A mean preserving spread#

The next exercise asks you to study the impact of a mean-preserving spread on the risk-adjusted expectation.

Exercise 46.1

Keep \(Y \sim \text{Beta}(2, 2)\) and fix \(\theta = -2\).

Using Monte Carlo again, calculate

\[ e_{\theta} = \frac{1}{\theta} \ln\left( \mathbb{E} [ \exp(\theta X) ] \right) \]

where \(X = Y + \sigma Z\) and \(Z\) is standard normal.

How does \(e_\theta\) change with \(\sigma\)?

Can you provide some intuition for what is happening (given that the agent is risk averse)?

Use a plot to illustrate your results.