Skip to content

Commit 3ddc351

Browse files
jstacclaude
andcommitted
Rename and refactor McCall lecture to emphasize persistent and transitory wage shocks
Renamed mccall_correlated.md to mccall_persist_trans.md to better reflect the lecture's focus on the decomposition of wages into persistent and transitory components, distinguishing it from the earlier mccall_model_with_sep_markov.md lecture. Key changes: - Updated title to "Job Search V: Persistent and Transitory Wage Shocks" - Rewrote Overview section to: - Emphasize the persistent-transitory decomposition as the key innovation - Add references to baseline model (mccall_model) and Job Search III (mccall_model_with_sep_markov) - Explain why we return to permanent jobs (to isolate wage dynamics effects) - Note use of fitted value function iteration from Job Search IV - Replaced 'jr' abbreviation with explicit 'jax.random' throughout for clarity - Refactored draw_τ function: - Renamed to draw_duration for clarity - Extracted as standalone JIT-compiled function with explicit parameters - Prevents unnecessary recompilation when model parameters change - compute_unemployment_duration now serves as a clean wrapper - Simplified JobSearchModel class to Model - Changed Model instantiation to use positional arguments instead of keyword arguments - Fixed grammatical errors throughout the text (added missing commas, articles, etc.) - Updated _toc.yml to reflect filename change 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent f3e6c7f commit 3ddc351

File tree

2 files changed

+106
-92
lines changed

2 files changed

+106
-92
lines changed

lectures/_toc.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ parts:
6767
- file: mccall_model_with_separation
6868
- file: mccall_model_with_sep_markov
6969
- file: mccall_fitted_vfi
70-
- file: mccall_correlated
70+
- file: mccall_persist_trans
7171
- file: career
7272
- file: jv
7373
- file: odu
Lines changed: 105 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
---
2-
jupytext:
3-
text_representation:
4-
extension: .md
5-
format_name: myst
6-
kernelspec:
7-
display_name: Python 3
8-
language: python
9-
name: python3
2+
jupyter:
3+
jupytext:
4+
default_lexer: ipython
5+
text_representation:
6+
extension: .md
7+
format_name: markdown
8+
format_version: '1.3'
9+
jupytext_version: 1.17.2
10+
kernelspec:
11+
display_name: Python 3
12+
language: python
13+
name: python3
1014
---
1115

1216
```{raw} jupyter
@@ -17,39 +21,44 @@ kernelspec:
1721
</div>
1822
```
1923

20-
# Job Search V: Correlated Wage Offers
24+
# Job Search V: Persistent and Transitory Wage Shocks
2125

2226
```{contents} Contents
2327
:depth: 2
2428
```
2529

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

28-
```{code-cell} ipython
29-
:tags: [hide-output]
30-
32+
```python tags=["hide-output"]
3133
!pip install quantecon jax
3234
```
3335

34-
3536
## Overview
3637

37-
In this lecture we solve a {doc}`McCall style job search model <mccall_model>` with persistent and
38-
transitory components to wages.
38+
In this lecture we extend the {doc}`McCall job search model <mccall_model>` by decomposing wage offers into **persistent** and **transitory** components.
39+
40+
In the {doc}`baseline model <mccall_model>`, wage offers are IID over time, which is unrealistic.
41+
42+
In {doc}`Job Search III <mccall_model_with_sep_markov>`, we introduced correlated wage draws using a Markov chain, but we also added job separation.
43+
44+
Here we take a different approach: we model wage dynamics through an AR(1) process for the persistent component plus a transitory shock, while returning to the assumption that jobs are permanent (as in the {doc}`baseline model <mccall_model>`).
3945

40-
In other words, we relax the unrealistic assumption that randomness in wages is independent over time.
46+
This persistent-transitory decomposition is:
47+
- More realistic for modeling actual wage processes
48+
- Commonly used in labor economics (see, e.g., {cite}`MaCurdy1982`, {cite}`Meghir2004`)
49+
- Simple enough to analyze while capturing key features of wage dynamics
4150

42-
At the same time, we will go back to assuming that jobs are permanent and no separation occurs.
51+
By keeping jobs permanent, we can focus on understanding how persistent and transitory wage shocks affect search behavior and reservation wages.
4352

44-
This is to keep the model relatively simple as we study the impact of correlation.
53+
We will solve the model using fitted value function iteration with linear interpolation, as introduced in {doc}`Job Search IV <mccall_fitted_vfi>`.
4554

4655
We will use the following imports:
4756

48-
```{code-cell} ipython3
57+
```python
4958
import matplotlib.pyplot as plt
5059
import jax
5160
import jax.numpy as jnp
52-
import jax.random as jr
61+
import jax.random
5362
import quantecon as qe
5463
from typing import NamedTuple
5564
```
@@ -89,7 +98,7 @@ v^*(w, z) =
8998
\right\}
9099
$$
91100

92-
In this expression, $u$ is a utility function and $\mathbb E_z$ is expectation of next period variables given current $z$.
101+
In this expression, $u$ is a utility function and $\mathbb E_z$ is the expectation of next period variables given current $z$.
93102

94103
The variable $z$ enters as a state in the Bellman equation because its current value helps predict future wages.
95104

@@ -137,7 +146,7 @@ $$
137146
\frac{u(w)}{1-\beta} \geq f^*(z)
138147
$$
139148

140-
For utility we take $u(c) = \ln(c)$.
149+
For utility, we take $u(c) = \ln(c)$.
141150

142151
The reservation wage is the wage where equality holds in the last expression.
143152

@@ -167,8 +176,8 @@ Here's a `NamedTuple` that stores the model parameters and data.
167176

168177
Default parameter values are embedded in the model.
169178

170-
```{code-cell} ipython3
171-
class JobSearchModel(NamedTuple):
179+
```python
180+
class Model(NamedTuple):
172181
μ: float # transient shock log mean
173182
s: float # transient shock log variance
174183
d: float # shift coefficient of persistent state
@@ -180,9 +189,9 @@ class JobSearchModel(NamedTuple):
180189
e_draws: jnp.ndarray
181190

182191
def create_job_search_model=0.0, s=1.0, d=0.0, ρ=0.9, σ=0.1, β=0.98, c=5.0,
183-
mc_size=1000, grid_size=100, key=jr.PRNGKey(1234)):
192+
mc_size=1000, grid_size=100, key=jax.random.PRNGKey(1234)):
184193
"""
185-
Create a JobSearchModel with computed grid and draws.
194+
Create a Model with computed grid and draws.
186195
"""
187196
# Set up grid
188197
z_mean = d / (1 - ρ)
@@ -192,20 +201,19 @@ def create_job_search_model(μ=0.0, s=1.0, d=0.0, ρ=0.9, σ=0.1, β=0.98, c=5.0
192201
z_grid = jnp.linspace(a, b, grid_size)
193202

194203
# Draw and store shocks
195-
e_draws = jr.normal(key, (2, mc_size))
204+
e_draws = jax.random.normal(key, (2, mc_size))
196205

197-
return JobSearchModel(μ=μ, s=s, d=d, ρ=ρ, σ=σ, β=β, c=c,
198-
z_grid=z_grid, e_draws=e_draws)
206+
return Model(μ, s, d, ρ, σ, β, c, z_grid, e_draws)
199207
```
200208

201-
Next we implement the $Q$ operator.
209+
Next, we implement the $Q$ operator.
202210

203-
```{code-cell} ipython3
211+
```python
204212
def Q(model, f_in):
205213
"""
206214
Apply the operator Q.
207215
208-
* model is an instance of JobSearchModel
216+
* model is an instance of Model
209217
* f_in is an array that represents f
210218
* returns Qf
211219
@@ -235,7 +243,7 @@ def Q(model, f_in):
235243

236244
Here's a function to compute an approximation to the fixed point of $Q$.
237245

238-
```{code-cell} ipython3
246+
```python
239247
@jax.jit
240248
def compute_fixed_point(model, tol=1e-4, max_iter=1000):
241249
"""
@@ -266,16 +274,16 @@ def compute_fixed_point(model, tol=1e-4, max_iter=1000):
266274

267275
Let's try generating an instance and solving the model.
268276

269-
```{code-cell} ipython3
277+
```python
270278
model = create_job_search_model()
271279

272280
with qe.Timer():
273281
f_star = compute_fixed_point(model).block_until_ready()
274282
```
275283

276-
Next we will compute and plot the reservation wage function defined in {eq}`corr_mcm_barw`.
284+
Next, we will compute and plot the reservation wage function defined in {eq}`corr_mcm_barw`.
277285

278-
```{code-cell} ipython3
286+
```python
279287
res_wage_function = jnp.exp(f_star * (1 - model.β))
280288

281289
fig, ax = plt.subplots()
@@ -292,10 +300,10 @@ Notice that the reservation wage is increasing in the current state $z$.
292300
This is because a higher state leads the agent to predict higher future wages,
293301
increasing the option value of waiting.
294302

295-
Let's try changing unemployment compensation and look at its impact on the
303+
Let's try changing unemployment compensation and looking at its impact on the
296304
reservation wage:
297305

298-
```{code-cell} ipython3
306+
```python
299307
c_vals = 1, 2, 3
300308

301309
fig, ax = plt.subplots()
@@ -317,74 +325,80 @@ at all state values.
317325

318326
## Unemployment duration
319327

320-
Next we study how mean unemployment duration varies with unemployment compensation.
328+
Next, we study how mean unemployment duration varies with unemployment compensation.
329+
330+
For simplicity, we'll fix the initial state at $z_t = 0$.
331+
332+
```python
333+
@jax.jit
334+
def draw_duration(key, μ, s, d, ρ, σ, β, z_grid, f_star, t_max=10_000):
335+
"""
336+
Draw unemployment duration for a single simulation.
337+
338+
"""
339+
def f_star_function(z):
340+
return jnp.interp(z, z_grid, f_star)
341+
342+
def cond_fun(loop_state):
343+
z, t, unemployed, key = loop_state
344+
return jnp.logical_and(unemployed, t < t_max)
345+
346+
def body_fun(loop_state):
347+
z, t, unemployed, key = loop_state
348+
key1, key2, key = jax.random.split(key, 3)
349+
350+
# Draw current wage
351+
y = jnp.exp(μ + s * jax.random.normal(key1))
352+
w = jnp.exp(z) + y
353+
res_wage = jnp.exp(f_star_function(z) * (1 - β))
354+
355+
# Check if optimal to stop
356+
accept = w >= res_wage
357+
τ = jnp.where(accept, t, t_max)
358+
359+
# Update state if not accepting
360+
z_new = jnp.where(accept, z,
361+
ρ * z + d + σ * jax.random.normal(key2))
362+
t_new = t + 1
363+
unemployed_new = jnp.logical_not(accept)
364+
365+
return z_new, t_new, unemployed_new, key
366+
367+
# Initial loop_state: (z, t, unemployed, key)
368+
init_state = (0.0, 0, True, key)
369+
z_final, t_final, unemployed_final, _ = jax.lax.while_loop(
370+
cond_fun, body_fun, init_state)
371+
372+
# Return final time if job found, otherwise t_max
373+
return jnp.where(unemployed_final, t_max, t_final)
321374

322-
For simplicity we’ll fix the initial state at $z_t = 0$.
323375

324-
```{code-cell} ipython3
325376
def compute_unemployment_duration(
326-
model, key=jr.PRNGKey(1234), num_reps=100_000
377+
model, key=jax.random.PRNGKey(1234), num_reps=100_000
327378
):
328379
"""
329380
Compute expected unemployment duration.
330381
331382
"""
332383
f_star = compute_fixed_point(model)
333384
μ, s, d = model.μ, model.s, model.d
334-
ρ, σ, β, c = model.ρ, model.σ, model.β, model.c
385+
ρ, σ, β = model.ρ, model.σ, model.β
335386
z_grid = model.z_grid
336387

337-
@jax.jit
338-
def f_star_function(z):
339-
return jnp.interp(z, z_grid, f_star)
340-
341-
@jax.jit
342-
def draw_τ(key, t_max=10_000):
343-
def cond_fun(loop_state):
344-
z, t, unemployed, key = loop_state
345-
return jnp.logical_and(unemployed, t < t_max)
346-
347-
def body_fun(loop_state):
348-
z, t, unemployed, key = loop_state
349-
key1, key2, key = jr.split(key, 3)
350-
351-
# Draw current wage
352-
y = jnp.exp(μ + s * jr.normal(key1))
353-
w = jnp.exp(z) + y
354-
res_wage = jnp.exp(f_star_function(z) * (1 - β))
355-
356-
# Check if optimal to stop
357-
accept = w >= res_wage
358-
τ = jnp.where(accept, t, t_max)
359-
360-
# Update state if not accepting
361-
z_new = jnp.where(accept, z,
362-
ρ * z + d + σ * jr.normal(key2))
363-
t_new = t + 1
364-
unemployed_new = jnp.logical_not(accept)
365-
366-
return z_new, t_new, unemployed_new, key
367-
368-
# Initial loop_state: (z, t, unemployed, key)
369-
init_state = (0.0, 0, True, key)
370-
z_final, t_final, unemployed_final, _ = jax.lax.while_loop(
371-
cond_fun, body_fun, init_state)
372-
373-
# Return final time if job found, otherwise t_max
374-
return jnp.where(unemployed_final, t_max, t_final)
375-
376388
# Generate keys for all simulations
377-
keys = jr.split(key, num_reps)
378-
389+
keys = jax.random.split(key, num_reps)
390+
379391
# Vectorize over simulations
380-
τ_vals = jax.vmap(draw_τ)(keys)
381-
392+
τ_vals = jax.vmap(
393+
lambda k: draw_duration(k, μ, s, d, ρ, σ, β, z_grid, f_star)
394+
)(keys)
395+
382396
return jnp.mean(τ_vals)
383397
```
384398

385399
Let's test this out with some possible values for unemployment compensation.
386400

387-
```{code-cell} ipython3
401+
```python
388402
c_vals = jnp.linspace(1.0, 10.0, 8)
389403
durations = []
390404
for i, c in enumerate(c_vals):
@@ -396,7 +410,7 @@ durations = jnp.array(durations)
396410

397411
Here is a plot of the results.
398412

399-
```{code-cell} ipython3
413+
```python
400414
fig, ax = plt.subplots()
401415
ax.plot(c_vals, durations)
402416
ax.set_xlabel("unemployment compensation")
@@ -423,9 +437,9 @@ Investigate how mean unemployment duration varies with the discount factor $\bet
423437
:class: dropdown
424438
```
425439

426-
Here is one solution
440+
Here is one solution:
427441

428-
```{code-cell} ipython3
442+
```python
429443
beta_vals = jnp.linspace(0.94, 0.99, 8)
430444
durations = []
431445
for i, β in enumerate(beta_vals):
@@ -435,7 +449,7 @@ for i, β in enumerate(beta_vals):
435449
durations = jnp.array(durations)
436450
```
437451

438-
```{code-cell} ipython3
452+
```python
439453
fig, ax = plt.subplots()
440454
ax.plot(beta_vals, durations)
441455
ax.set_xlabel(r"$\beta$")

0 commit comments

Comments
 (0)