Skip to content

Commit f62707f

Browse files
jstacclaude
andauthored
Fix bugs and improve McCall model lecture (#708)
- Fix syntax errors: extra parenthesis in T_e function, undefined variable w - Add first-pass solution method before efficiency improvements - Add verification test comparing both solution methods - Enhance exercise with specific guidance on c values to try - Fix jnp.max to jnp.maximum for scalar comparison 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
1 parent bcddf17 commit f62707f

File tree

1 file changed

+178
-39
lines changed

1 file changed

+178
-39
lines changed

lectures/mccall_model_with_sep_markov.md

Lines changed: 178 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -168,42 +168,24 @@ $$
168168

169169
+++
170170

171+
### Optimal Policy
171172

172-
## Computational Approach
173-
174-
To solve this problem, we use the employed worker's Bellman equation to express
175-
$v_e$ in terms of $Pv_u$
173+
Once we have the solutions $v_e$ and $v_u$ to these Bellman equations, we can compute the optimal policy: accept at current wage offer $w$ if
176174

177175
$$
178-
v_e(w) =
179-
\frac{1}{1-\beta(1-\alpha)} \cdot (u(w) + \alpha\beta(Pv_u)(w))
176+
v_e(w) ≥ u(c) + β(Pv_u)(w)
180177
$$
181178

182-
Next we substitute into the unemployed agent's Bellman equation to get
179+
The optimal policy turns out to be a reservation wage strategy: accept all wages above some threshold.
183180

184181
+++
185182

186-
$$
187-
v_u(w) =
188-
\max
189-
\left\{
190-
\frac{1}{1-\beta(1-\alpha)} \cdot (u(w) + \alpha\beta(Pv_u)(w)),
191-
u(c) + \beta(Pv_u)(w)
192-
\right\}
193-
$$
194-
195-
Then we use value function iteration to solve for $v_u$.
196-
197-
With $v_u$ in hand, we can
198-
199-
1. recover $v_e$ through the equations above and
200-
2. compute optimal policy: accept if $v_e(w) ≥ u(c) + β(Pv_u)(w)$
201183

202-
The optimal policy turns out to be a reservation wage strategy: accept all wages above some threshold.
184+
## Code
203185

204-
+++
186+
Let's now implement the model.
205187

206-
## Code
188+
### Set Up
207189

208190
The default utility function is a CRRA utility function
209191

@@ -251,7 +233,152 @@ def create_js_with_sep_model(
251233
return Model(n, w_vals, P, P_cumsum, β, c, α, γ)
252234
```
253235

254-
Here's the Bellman operator for the unemployed worker's value function:
236+
237+
### Solution: First Pass
238+
239+
Let's put together a (not very efficient) routine for calculating the
240+
reservation wage.
241+
242+
(We will think carefully about efficiency below.)
243+
244+
It works by starting with guesses for $v_e$ and $v_u$ and iterating to
245+
convergence.
246+
247+
Here's are Bellman operators that update $v_u$ and $v_e$ respectively.
248+
249+
250+
```{code-cell} ipython3
251+
def T_u(model, v_u, v_e):
252+
"""
253+
Apply the unemployment Bellman update rule and return new guess of v_u.
254+
255+
"""
256+
n, w_vals, P, P_cumsum, β, c, α, γ = model
257+
h = u(c, γ) + β * P @ v_u
258+
v_u_new = jnp.maximum(v_e, h)
259+
return v_u_new
260+
```
261+
262+
```{code-cell} ipython3
263+
def T_e(model, v_u, v_e):
264+
"""
265+
Apply the employment Bellman update rule and return new guess of v_e.
266+
267+
"""
268+
n, w_vals, P, P_cumsum, β, c, α, γ = model
269+
v_e_new = u(w_vals, γ) + β * ((1 - α) * v_e + α * P @ v_u)
270+
return v_e_new
271+
```
272+
273+
Here's a routine to iterate to convergence and then compute the reservation
274+
wage.
275+
276+
```{code-cell} ipython3
277+
def solve_model_first_pass(
278+
model: Model, # instance containing default parameters
279+
v_u_init: jnp.ndarray, # initial condition for v_u
280+
v_e_init: jnp.ndarray, # initial condition for v_e
281+
tol: float=1e-6, # error tolerance
282+
max_iter: int=1_000, # maximum number of iterations for loop
283+
):
284+
n, w_vals, P, P_cumsum, β, c, α, γ = model
285+
i = 0
286+
error = tol + 1
287+
v_u = v_u_init
288+
v_e = v_e_init
289+
290+
while i < max_iter and error > tol:
291+
v_u_next = T_u(model, v_u, v_e)
292+
v_e_next = T_e(model, v_u, v_e)
293+
error_u = jnp.max(jnp.abs(v_u_next - v_u))
294+
error_e = jnp.max(jnp.abs(v_e_next - v_e))
295+
error = jnp.maximum(error_u, error_e)
296+
v_u = v_u_next
297+
v_e = v_e_next
298+
i += 1
299+
300+
# Compute accept and reject values
301+
continuation_values = u(c, γ) + β * P @ v_u
302+
303+
# Find where acceptance becomes optimal
304+
accept_indices = v_e >= continuation_values
305+
first_accept_idx = jnp.argmax(accept_indices) # index of first True
306+
307+
# If no acceptance (all False), return infinity
308+
# Otherwise return the wage at the first acceptance index
309+
w_bar = jnp.where(
310+
jnp.any(accept_indices), w_vals[first_accept_idx], jnp.inf
311+
)
312+
return v_u, v_e, w_bar
313+
```
314+
315+
316+
### Road Test
317+
318+
Let's solve the model:
319+
320+
```{code-cell} ipython3
321+
model = create_js_with_sep_model()
322+
n, w_vals, P, P_cumsum, β, c, α, γ = model
323+
v_u_init = jnp.zeros(n)
324+
v_e_init = jnp.zeros(n)
325+
v_u, v_e, w_bar_first = solve_model_first_pass(model, v_u_init, v_e_init)
326+
```
327+
328+
Next we compute the continuation values.
329+
330+
```{code-cell} ipython3
331+
h = u(c, γ) + β * P @ v_u
332+
```
333+
334+
Let's plot our results.
335+
336+
```{code-cell} ipython3
337+
fig, ax = plt.subplots(figsize=(9, 5.2))
338+
ax.plot(w_vals, h, 'g-', linewidth=2,
339+
label="continuation value function $h$")
340+
ax.plot(w_vals, v_e, 'b-', linewidth=2,
341+
label="employment value function $v_e$")
342+
ax.legend(frameon=False)
343+
ax.set_xlabel(r"$w$")
344+
plt.show()
345+
```
346+
347+
The reservation wage is at the intersection of $v_e$, and the continuation value
348+
function, which is the value of rejecting.
349+
350+
351+
## Improving Efficiency
352+
353+
The solution method desribed above works fine but we can do much better.
354+
355+
First, we use the employed worker's Bellman equation to express
356+
$v_e$ in terms of $Pv_u$
357+
358+
$$
359+
v_e(w) =
360+
\frac{1}{1-\beta(1-\alpha)} \cdot (u(w) + \alpha\beta(Pv_u)(w))
361+
$$
362+
363+
Next we substitute into the unemployed agent's Bellman equation to get
364+
365+
+++
366+
367+
$$
368+
v_u(w) =
369+
\max
370+
\left\{
371+
\frac{1}{1-\beta(1-\alpha)} \cdot (u(w) + \alpha\beta(Pv_u)(w)),
372+
u(c) + \beta(Pv_u)(w)
373+
\right\}
374+
$$
375+
376+
Then we use value function iteration to solve for $v_u$.
377+
378+
With $v_u$ in hand, we can recover $v_e$ through the equations above and
379+
then compute the reservation wage.
380+
381+
Here's the new Bellman operator for the unemployed worker's value function:
255382

256383
```{code-cell} ipython3
257384
def T(v: jnp.ndarray, model: Model) -> jnp.ndarray:
@@ -326,9 +453,7 @@ def get_reservation_wage(v: jnp.ndarray, model: Model) -> float:
326453
```
327454

328455

329-
## Computing the Solution
330-
331-
Let's solve the model:
456+
Let's solve the model using our new method:
332457

333458
```{code-cell} ipython3
334459
model = create_js_with_sep_model()
@@ -337,6 +462,14 @@ v_u = vfi(model)
337462
w_bar = get_reservation_wage(v_u, model)
338463
```
339464

465+
Let's verify that both methods produce the same reservation wage:
466+
467+
```{code-cell} ipython3
468+
print(f"Reservation wage (first method): {w_bar_first:.6f}")
469+
print(f"Reservation wage (second method): {w_bar:.6f}")
470+
print(f"Difference: {abs(w_bar - w_bar_first):.2e}")
471+
```
472+
340473
Next we compute some related quantities for plotting.
341474

342475
```{code-cell} ipython3
@@ -358,9 +491,9 @@ ax.set_xlabel(r"$w$")
358491
plt.show()
359492
```
360493

361-
The reservation wage is at the intersection of the stopping value function, which is
362-
equal to $v_e$, and the continuation value function, which is the value of
363-
rejecting
494+
The result is the same as before but we only iterate on one array --- and also
495+
our JAX code is more efficient.
496+
364497

365498
## Sensitivity Analysis
366499

@@ -704,15 +837,14 @@ def simulate_cross_section(
704837
This function generates a histogram showing the distribution of employment status across many agents:
705838

706839
```{code-cell} ipython3
707-
def plot_cross_sectional_unemployment(model: Model, t_snapshot: int = 200,
708-
n_agents: int = 20_000):
840+
def plot_cross_sectional_unemployment(
841+
model: Model,
842+
t_snapshot: int = 200, # Time of cross-sectional snapshot
843+
n_agents: int = 20_000 # Number of agents to simulate
844+
):
709845
"""
710846
Generate histogram of cross-sectional unemployment at a specific time.
711847
712-
Parameters:
713-
- model: Model instance with parameters
714-
- t_snapshot: Time period at which to take the cross-sectional snapshot
715-
- n_agents: Number of agents to simulate
716848
"""
717849
# Get final employment state directly
718850
key = jax.random.PRNGKey(42)
@@ -743,7 +875,9 @@ def plot_cross_sectional_unemployment(model: Model, t_snapshot: int = 200,
743875
plt.show()
744876
```
745877

746-
Now let's compare the time-average unemployment rate (from a single agent's long simulation) with the cross-sectional unemployment rate (from many agents at a single point in time).
878+
Now let's compare the time-average unemployment rate (from a single agent's long
879+
simulation) with the cross-sectional unemployment rate (from many agents at a
880+
single point in time).
747881

748882
We claimed above that these numbers will be approximately equal in large
749883
samples, due to ergodicity.
@@ -790,6 +924,11 @@ plot_cross_sectional_unemployment(model_low_c)
790924
Create a plot that investigates more carefully how the steady state cross-sectional unemployment rate
791925
changes with unemployment compensation.
792926

927+
Try a range of values for unemployment compensation `c`, such as `c = 0.2, 0.4, 0.6, 0.8, 1.0`.
928+
For each value, compute the steady-state cross-sectional unemployment rate and plot it against `c`.
929+
930+
What relationship do you observe between unemployment compensation and the unemployment rate?
931+
793932
```{exercise-end}
794933
```
795934

0 commit comments

Comments
 (0)