Skip to content

Commit 905151e

Browse files
jstacclaude
andcommitted
Add lifetime value analysis for volatility in McCall model
- Add new section showing expected lifetime value increases with volatility - Implement parallel JAX code for simulating job search with optimal policy - Use jax.vmap to vectorize 10,000 simulation replications - Compute reservation wage and lifetime value across volatility levels - Plot demonstrates option value of search under uncertainty - Fix LaTeX escape sequence warnings in xlabel (use raw strings) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 2ea4ce0 commit 905151e

File tree

1 file changed

+156
-6
lines changed

1 file changed

+156
-6
lines changed

lectures/mccall_model.md

Lines changed: 156 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -929,21 +929,169 @@ Now let's plot the reservation wage as a function of volatility:
929929
fig, ax = plt.subplots()
930930
931931
ax.plot(σ_vals, res_wages_volatility, linewidth=2)
932-
ax.set_xlabel('volatility ($\sigma$)', fontsize=12)
932+
ax.set_xlabel(r'volatility ($\sigma$)', fontsize=12)
933933
ax.set_ylabel('reservation wage', fontsize=12)
934-
ax.set_title('Reservation wage increases with volatility')
935-
ax.grid(True, alpha=0.3)
936-
937934
plt.show()
938935
```
939936

940937
As expected, the reservation wage is increasing in $\sigma$.
941938

942-
This confirms that workers prefer more volatile wage distributions, all else equal,
943-
because they can capitalize on high offers while rejecting low ones.
939+
### Lifetime Value and Volatility
940+
941+
We've seen that the reservation wage increases with volatility. Now let's verify that
942+
the lifetime value at the optimal policy also increases with volatility.
943+
944+
The intuition is that higher volatility provides more upside potential while the
945+
worker can protect against downside risk by rejecting low offers. This option value
946+
translates into higher expected lifetime utility.
947+
948+
To demonstrate this, we'll:
949+
1. Compute the reservation wage for each volatility level
950+
2. Simulate the worker's job search process following the optimal policy
951+
3. Calculate the expected discounted lifetime income
952+
953+
The simulation works as follows: starting unemployed, the worker draws wage offers
954+
and accepts the first offer that exceeds their reservation wage. We then compute
955+
the present value of this income stream.
956+
957+
```{code-cell} ipython3
958+
@jax.jit
959+
def simulate_lifetime_value(key, model, w_bar, max_search_periods=1000):
960+
"""
961+
Simulate one realization of the job search and compute lifetime value.
962+
963+
Parameters:
964+
-----------
965+
key : jax.random.PRNGKey
966+
Random key for JAX
967+
model : McCallModelContinuous
968+
The model containing parameters
969+
w_bar : float
970+
The reservation wage
971+
max_search_periods : int
972+
Maximum number of search periods before forcing acceptance
973+
974+
Returns:
975+
--------
976+
lifetime_value : float
977+
Discounted sum of lifetime income
978+
"""
979+
c, β, σ, μ, w_draws = model
980+
981+
def search_step(state):
982+
t, key, accepted, wage = state
983+
key, subkey = jax.random.split(key)
984+
# Draw wage offer
985+
s = jax.random.normal(subkey)
986+
w = jnp.exp(μ + σ * s)
987+
# Check if we accept
988+
accept_now = w >= w_bar
989+
# Update state: if we accept now, store the wage
990+
wage = jnp.where(accept_now, w, wage)
991+
accepted = jnp.logical_or(accepted, accept_now)
992+
t = t + 1
993+
return t, key, accepted, wage
994+
995+
def search_cond(state):
996+
t, _, accepted, _ = state
997+
# Continue searching if not accepted and haven't hit max periods
998+
return jnp.logical_and(jnp.logical_not(accepted), t < max_search_periods)
999+
1000+
# Initial state: period 0, not accepted, wage 0
1001+
initial_state = (0, key, False, 0.0)
1002+
t_final, _, _, final_wage = jax.lax.while_loop(search_cond, search_step, initial_state)
1003+
1004+
# Compute lifetime value
1005+
# During unemployment (periods 0 to t_final-1): receive c each period
1006+
# After employment (period t_final onwards): receive final_wage forever
1007+
unemployment_value = c * (1 - β**t_final) / (1 - β)
1008+
employment_value = (β**t_final) * final_wage / (1 - β)
1009+
lifetime_value = unemployment_value + employment_value
1010+
1011+
return lifetime_value
1012+
1013+
1014+
@jax.jit
1015+
def compute_mean_lifetime_value(model, w_bar, num_reps=10000, seed=1234):
1016+
"""
1017+
Compute mean lifetime value across many simulations.
1018+
1019+
Parameters:
1020+
-----------
1021+
model : McCallModelContinuous
1022+
The model containing parameters
1023+
w_bar : float
1024+
The reservation wage
1025+
num_reps : int
1026+
Number of simulation replications
1027+
seed : int
1028+
Random seed
1029+
1030+
Returns:
1031+
--------
1032+
mean_value : float
1033+
Average lifetime value across all replications
1034+
"""
1035+
key = jax.random.PRNGKey(seed)
1036+
keys = jax.random.split(key, num_reps)
1037+
1038+
# Vectorize the simulation across all replications
1039+
simulate_fn = jax.vmap(simulate_lifetime_value, in_axes=(0, None, None))
1040+
lifetime_values = simulate_fn(keys, model, w_bar)
1041+
1042+
return jnp.mean(lifetime_values)
1043+
```
1044+
1045+
Now let's compute both the reservation wage and the expected lifetime value
1046+
for each volatility level:
1047+
1048+
```{code-cell} ipython3
1049+
# Use the same volatility range and mean wage
1050+
σ_vals = jnp.linspace(0.1, 1.0, 25)
1051+
mean_wage = 20.0
1052+
1053+
# Storage for results
1054+
res_wages_vol = []
1055+
lifetime_values_vol = []
1056+
1057+
for σ in σ_vals:
1058+
μ = compute_μ_for_mean(σ, mean_wage)
1059+
model = create_mccall_continuous(σ=float(σ), μ=float(μ))
1060+
1061+
# Compute reservation wage
1062+
w_bar = compute_reservation_wage_continuous(model)
1063+
res_wages_vol.append(w_bar)
1064+
1065+
# Compute expected lifetime value
1066+
lv = compute_mean_lifetime_value(model, w_bar)
1067+
lifetime_values_vol.append(lv)
1068+
1069+
res_wages_vol = jnp.array(res_wages_vol)
1070+
lifetime_values_vol = jnp.array(lifetime_values_vol)
1071+
```
1072+
1073+
Let's visualize the expected lifetime value as a function of volatility:
1074+
1075+
```{code-cell} ipython3
1076+
fig, ax = plt.subplots()
1077+
1078+
ax.plot(σ_vals, lifetime_values_vol, linewidth=2, color='green')
1079+
ax.set_xlabel(r'volatility ($\sigma$)', fontsize=12)
1080+
ax.set_ylabel('expected lifetime value', fontsize=12)
1081+
plt.show()
1082+
```
1083+
1084+
The plot confirms that despite workers setting higher reservation wages when facing
1085+
more volatile wage offers (as shown above), they achieve higher expected lifetime
1086+
values due to the option value of search.
1087+
1088+
This demonstrates a key insight from the McCall model: volatility in wage offers
1089+
benefits workers who can optimally time their job acceptance decision.
1090+
9441091

9451092
## Exercises
9461093

1094+
9471095
```{exercise}
9481096
:label: mm_ex1
9491097
@@ -958,6 +1106,8 @@ given the parameters, and then simulate to see how long it takes to accept.
9581106
Repeat a large number of times and take the average.
9591107
9601108
Plot mean unemployment duration as a function of $c$ in `c_vals`.
1109+
1110+
Try to explain what you see.
9611111
```
9621112

9631113
```{solution-start} mm_ex1

0 commit comments

Comments
 (0)