Skip to content

Commit 78561ef

Browse files
jstacclaude
andauthored
Enhance McCall separation model lecture: Add full model solution before simplification (#697)
This commit improves the pedagogical flow of the job search lecture by: - Introducing a direct solution using both Bellman equations (v_u and v_e) - Computing the reservation wage from the full model solution - Presenting the simplifying transformation as a more efficient approach - Verifying both methods produce identical results (difference < 1e-6) - Fixing several bugs in the new code sections The enhanced structure helps students understand the model fundamentals before learning the computational optimization. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
1 parent 516568e commit 78561ef

File tree

1 file changed

+210
-106
lines changed

1 file changed

+210
-106
lines changed

lectures/mccall_model_with_separation.md

Lines changed: 210 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ Let
134134
Here **maximum lifetime value** means the value of {eq}`objective` when
135135
the worker makes optimal decisions at all future points in time.
136136

137-
As we now show, these obtaining these functions is key to solving the new model.
137+
As we now show, obtaining these functions is key to solving the model.
138138

139139
### The Bellman Equations
140140

@@ -199,27 +199,211 @@ enough information to solve for both $v_e$ and $v_u$.
199199

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

202-
(ast_mcm)=
203-
### A Simplifying Transformation
204202

205-
Rather than jumping straight into solving these equations, let's see if we can
206-
simplify them somewhat.
203+
### The Reservation Wage
207204

208-
(This process will be analogous to our {ref}`second pass <mm_op2>` at the plain vanilla
209-
McCall model, where we reduced the Bellman equation to an equation in an unknown
210-
scalar value, rather than an unknown vector.)
211205

212-
First, let
206+
Let
213207

214208
```{math}
215209
:label: defh_mm
216210
217-
h := u(c) + \beta \sum_{w' \in \mathbb W} v_u(w') q(w')
211+
h := u(c) + \beta \sum_{w' \in \mathbb W} v_u(w') q(w')
218212
```
219213

220-
be the continuation value associated with unemployment (the value of rejecting the current offer).
214+
This is the continuation value for an unemployed agent (the value of rejecting the current offer).
215+
216+
If we know $v_u$ then we can easily compute $h$.
221217

222-
We can now write {eq}`bell2_mccall` as
218+
From {eq}`bell2_mccall`, we see that an unemployed agent accepts current offer $w$ if $v_e(w) \geq h$.
219+
220+
This means precisely that the value of accepting is higher than the value of rejecting.
221+
222+
It is clear that $v_e$ is (at least weakly) increasing in $w$, since the agent is never made worse off by a higher wage offer.
223+
224+
Hence, we can express the optimal choice as accepting wage offer $w$ if and only if $w \geq \bar w$,
225+
where the **reservation wage** $\bar w$ is the first wage level $w$ such that
226+
227+
$$
228+
v_e(w) \geq h
229+
$$
230+
231+
232+
233+
## Code
234+
235+
Let's now implement a solution method based on the two Bellman equations.
236+
237+
### Set Up
238+
239+
In the code, you'll see that we use a class to store the various parameters and other
240+
objects associated with a given model.
241+
242+
This helps to tidy up the code and provides an object that's easy to pass to functions.
243+
244+
The default utility function is a CRRA utility function
245+
246+
```{code-cell} ipython3
247+
def u(c, γ):
248+
return (c**(1 - γ) - 1) / (1 - γ)
249+
```
250+
251+
Also, here's a default wage distribution, based around the BetaBinomial
252+
distribution:
253+
254+
```{code-cell} ipython3
255+
n = 60 # n possible outcomes for w
256+
w_default = jnp.linspace(10, 20, n) # wages between 10 and 20
257+
a, b = 600, 400 # shape parameters
258+
dist = BetaBinomial(n-1, a, b) # distribution
259+
q_default = jnp.array(dist.pdf()) # probabilities as a JAX array
260+
```
261+
262+
Here's our model class for the McCall model with separation.
263+
264+
```{code-cell} ipython3
265+
class Model(NamedTuple):
266+
α: float = 0.2 # job separation rate
267+
β: float = 0.98 # discount factor
268+
γ: float = 2.0 # utility parameter (CRRA)
269+
c: float = 6.0 # unemployment compensation
270+
w: jnp.ndarray = w_default # wage outcome space
271+
q: jnp.ndarray = q_default # probabilities over wage offers
272+
```
273+
274+
275+
### Operators
276+
277+
To iterate on the Bellman equations, we define two operators, one for each value function.
278+
279+
These operators take the current value functions as inputs and return updated versions.
280+
281+
```{code-cell} ipython3
282+
def T_u(model, v_u, v_e):
283+
"""
284+
Apply the unemployment Bellman update rule.
285+
286+
"""
287+
α, β, γ, c, w, q = model
288+
h = u(c, γ) + β * (v_u @ q)
289+
return jnp.maximum(v_e, h)
290+
```
291+
292+
```{code-cell} ipython3
293+
def T_e(model, v_u, v_e):
294+
"""
295+
Apply the employment Bellman update rule.
296+
297+
"""
298+
α, β, γ, c, w, q = model
299+
v_e_new = u(w, γ) + β * ((1 - α) * v_e + α * (v_u @ q))
300+
return v_e_new
301+
```
302+
303+
304+
305+
### Iteration
306+
307+
Here's our iteration routine, which alternates between updating $v_u$ and $v_e$ until convergence.
308+
309+
We iterate until successive realizations are closer together than some small tolerance level.
310+
311+
```{code-cell} ipython3
312+
def solve_full_model(
313+
model,
314+
tol: float=1e-6,
315+
max_iter: int=1_000,
316+
):
317+
"""
318+
Solves for both value functions v_u and v_e iteratively.
319+
320+
"""
321+
α, β, γ, c, w, q = model
322+
i = 0
323+
error = tol + 1
324+
v_e = v_u = w / (1 - β)
325+
326+
while i < max_iter and error > tol:
327+
v_u_next = T_u(model, v_u, v_e)
328+
v_e_next = T_e(model, v_u, v_e)
329+
error_u = jnp.max(jnp.abs(v_u_next - v_u))
330+
error_e = jnp.max(jnp.abs(v_e_next - v_e))
331+
error = jnp.max(jnp.array([error_u, error_e]))
332+
v_u = v_u_next
333+
v_e = v_e_next
334+
i += 1
335+
336+
return v_u, v_e
337+
```
338+
339+
340+
341+
### Computing the Reservation Wage
342+
343+
Now that we can solve for both value functions, let's compute the reservation wage.
344+
345+
Recall from above that the reservation wage $\bar w$ solves
346+
$v_e(\bar w) = h$, where $h$ is the continuation value defined in {eq}`defh_mm`.
347+
348+
Let's compare $v_e$ and $h$ to see what they look like.
349+
350+
We'll use the default parameterizations found in the code above.
351+
352+
```{code-cell} ipython3
353+
model = Model()
354+
α, β, γ, c, w, q = model
355+
v_u, v_e = solve_full_model(model)
356+
h = u(c, γ) + β * (v_u @ q)
357+
358+
fig, ax = plt.subplots()
359+
ax.plot(w, v_e, 'b-', lw=2, alpha=0.7, label='$v_e$')
360+
ax.plot(w, [h] * len(w), 'g-', lw=2, alpha=0.7, label='$h$')
361+
ax.set_xlim(min(w), max(w))
362+
ax.legend()
363+
plt.show()
364+
```
365+
366+
The value $v_e$ is increasing because higher $w$ generates a higher wage flow conditional on staying employed.
367+
368+
The reservation wage is the $w$ where these lines meet.
369+
370+
Let's compute this reservation wage explicitly:
371+
372+
```{code-cell} ipython3
373+
def compute_reservation_wage_full(model):
374+
"""
375+
Computes the reservation wage using the full model solution.
376+
"""
377+
α, β, γ, c, w, q = model
378+
v_u, v_e = solve_full_model(model)
379+
h = u(c, γ) + β * (v_u @ q)
380+
# Find the first w such that v_e(w) >= h
381+
accept = v_e >= h
382+
i = jnp.argmax(accept)
383+
w_bar = jnp.where(jnp.any(accept), w[i], jnp.inf)
384+
return w_bar
385+
386+
w_bar_full = compute_reservation_wage_full(model)
387+
print(f"Reservation wage (full model): {w_bar_full:.4f}")
388+
```
389+
390+
391+
392+
393+
(ast_mcm)=
394+
## A Simplifying Transformation
395+
396+
The approach above works, but iterating over two vector-valued functions is computationally expensive.
397+
398+
Let's return to the key equations and see if we can simplify them to reduce the problem to a single scalar equation.
399+
400+
(This process will be analogous to our {ref}`second pass <mm_op2>` at the plain vanilla
401+
McCall model, where we reduced the Bellman equation to an equation in an unknown
402+
scalar value, rather than an unknown vector.)
403+
404+
First, recall $h$ as defined in {eq}`defh_mm`.
405+
406+
Using $h$, we can now write {eq}`bell2_mccall` as
223407

224408
$$
225409
v_u(w) = \max \left\{ v_e(w), \, h \right\}
@@ -291,28 +475,7 @@ h = u(c) + \beta \sum_{w' \in \mathbb W} \max \left\{ \frac{u(w') + \alpha(h - u
291475

292476
This is a single scalar equation in $h$.
293477

294-
### The Reservation Wage
295-
296-
Suppose we can use {eq}`bell_scalar` to solve for $h$.
297-
298-
Once we have $h$, we can obtain $v_e$ from {eq}`v_e_closed`.
299478

300-
We can then determine optimal behavior for the worker.
301-
302-
From {eq}`bell2_mccall`, we see that an unemployed agent accepts current offer
303-
$w$ if $v_e(w) \geq h$.
304-
305-
This means precisely that the value of accepting is higher than the value of rejecting.
306-
307-
It is clear that $v_e$ is (at least weakly) increasing in $w$, since the agent is never made worse off by a higher wage offer.
308-
309-
Hence, we can express the optimal choice as accepting wage offer $w$ if and only if
310-
311-
$$
312-
w \geq \bar w
313-
\quad \text{where} \quad
314-
\bar w \text{ solves } v_e(\bar w) = h
315-
$$
316479

317480
### Solving the Bellman Equations
318481

@@ -336,50 +499,11 @@ Once convergence is achieved, we can compute $v_e$ from {eq}`v_e_closed`.
336499

337500
(It is possible to prove that {eq}`bell_iter` converges via the Banach contraction mapping theorem.)
338501

339-
## Implementation
340-
341-
Let's implement this iterative process.
342-
343-
In the code, you'll see that we use a class to store the various parameters and other
344-
objects associated with a given model.
345-
346-
This helps to tidy up the code and provides an object that's easy to pass to functions.
347-
348-
The default utility function is a CRRA utility function
349-
350-
```{code-cell} ipython3
351-
def u(c, γ):
352-
return (c**(1 - γ) - 1) / (1 - γ)
353-
```
354-
355-
Also, here's a default wage distribution, based around the BetaBinomial
356-
distribution:
357-
358-
```{code-cell} ipython3
359-
n = 60 # n possible outcomes for w
360-
w_default = jnp.linspace(10, 20, n) # wages between 10 and 20
361-
a, b = 600, 400 # shape parameters
362-
dist = BetaBinomial(n-1, a, b) # distribution
363-
q_default = jnp.array(dist.pdf()) # probabilities as a JAX array
364-
```
365-
366-
Here's our model class for the McCall model with separation.
367502

368-
```{code-cell} ipython3
369-
class Model(NamedTuple):
370-
α: float = 0.2 # job separation rate
371-
β: float = 0.98 # discount factor
372-
γ: float = 2.0 # utility parameter (CRRA)
373-
c: float = 6.0 # unemployment compensation
374-
w: jnp.ndarray = w_default # wage outcome space
375-
q: jnp.ndarray = q_default # probabilities over wage offers
376-
```
377503

378-
Now we iterate until successive realizations are closer together than some small tolerance level.
379-
380-
We then return the current iterate as an approximate solution.
504+
## Implementation
381505

382-
First, we define a function to compute $v_e$ from $h$:
506+
First, we define a function to compute $v_e$ from $h$ using {eq}`v_e_closed`.
383507

384508
```{code-cell} ipython3
385509
def compute_v_e(model, h):
@@ -431,37 +555,6 @@ def solve_model(model, tol=1e-5, max_iter=2000):
431555
return v_e_final, h_final
432556
```
433557

434-
### The Reservation Wage: First Pass
435-
436-
The optimal choice of the agent is summarized by the reservation wage.
437-
438-
As discussed above, the reservation wage is the $\bar w$ that solves
439-
$v_e(\bar w) = h$ where $h$ is the continuation value.
440-
441-
Let's compare $v_e$ and $h$ to see what they look like.
442-
443-
We'll use the default parameterizations found in the code above.
444-
445-
```{code-cell} ipython3
446-
model = Model()
447-
v_e, h = solve_model(model)
448-
449-
fig, ax = plt.subplots()
450-
ax.plot(model.w, v_e, 'b-', lw=2, alpha=0.7, label='$v_e$')
451-
ax.plot(model.w, [h] * len(model.w),
452-
'g-', lw=2, alpha=0.7, label='$h$')
453-
ax.set_xlim(min(model.w), max(model.w))
454-
ax.legend()
455-
plt.show()
456-
```
457-
458-
The value $v_e$ is increasing because higher $w$ generates a higher wage flow conditional on staying employed.
459-
460-
461-
The reservation wage is the $w$ where these lines meet.
462-
463-
464-
### Computing the Reservation Wage
465558

466559
Here's a function `compute_reservation_wage` that takes an instance of `Model`
467560
and returns the associated reservation wage.
@@ -470,7 +563,7 @@ and returns the associated reservation wage.
470563
def compute_reservation_wage(model):
471564
"""
472565
Computes the reservation wage of an instance of the McCall model
473-
by finding the smallest w such that v_e(w) >= h.
566+
by finding the smallest w such that v_e(w) >= h.
474567
475568
"""
476569
# Find the first i such that v_e(w_i) >= h and return w[i]
@@ -482,6 +575,17 @@ def compute_reservation_wage(model):
482575
return w_bar
483576
```
484577

578+
Let's verify that this simplified approach gives the same answer as the full model:
579+
580+
```{code-cell} ipython3
581+
w_bar_simplified = compute_reservation_wage(model)
582+
print(f"Reservation wage (simplified): {w_bar_simplified:.4f}")
583+
print(f"Reservation wage (full model): {w_bar_full:.4f}")
584+
print(f"Difference: {abs(w_bar_simplified - w_bar_full):.6f}")
585+
```
586+
587+
As we can see, both methods produce essentially the same reservation wage, but the simplified method is much more efficient.
588+
485589
Next we will investigate how the reservation wage varies with parameters.
486590

487591

0 commit comments

Comments
 (0)