From 1861646760f1867d1b5cc60d62024eff3bec8267 Mon Sep 17 00:00:00 2001 From: John Stachurski Date: Mon, 3 Nov 2025 05:47:51 +0900 Subject: [PATCH 1/7] Split lake model lecture into two consecutive lectures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split the lake_model.md lecture into two parts to improve readability and organization: 1. **lake_model.md** - Basic lake model with exogenous transition rates - Covers the foundational lake model theory - Aggregate and rate dynamics - Individual worker dynamics and ergodicity - Includes exercises that use only basic lake model functions 2. **endogenous_lake.md** - Extension with endogenous job finding rate - Makes the job finding rate endogenous using McCall search model - Fiscal policy analysis with unemployment insurance - Welfare optimization **Key changes:** - Created new endogenous_lake.md file with content from original line 624 onwards - Updated lake_model.md to end before the endogenous section - Added clear introduction to endogenous_lake.md indicating it's a continuation - Moved exercises (model_ex2, model_ex3) to end of lake_model.md (they only use basic functions) - Consolidated parameter source citations in lake_model.md where defaults are defined - Removed redundant parameter value listings in exercises - Updated plot colors to cycle through matplotlib's default colors (C0 for lines, C1 for hlines) - Updated _toc.yml to include both lectures consecutively 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- lectures/_toc.yml | 1 + lectures/endogenous_lake.md | 479 ++++++++++++++++++++++++++++++++++++ lectures/lake_model.md | 383 +--------------------------- 3 files changed, 493 insertions(+), 370 deletions(-) create mode 100644 lectures/endogenous_lake.md diff --git a/lectures/_toc.yml b/lectures/_toc.yml index 1474e4679..4411f27e7 100644 --- a/lectures/_toc.yml +++ b/lectures/_toc.yml @@ -100,6 +100,7 @@ parts: numbered: true chapters: - file: lake_model + - file: endogenous_lake - file: rational_expectations - file: re_with_feedback - file: markov_perf diff --git a/lectures/endogenous_lake.md b/lectures/endogenous_lake.md new file mode 100644 index 000000000..0c8807c8d --- /dev/null +++ b/lectures/endogenous_lake.md @@ -0,0 +1,479 @@ +--- +jupytext: + text_representation: + extension: .md + format_name: myst + format_version: 0.13 + jupytext_version: 1.17.1 +kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +--- + +(endogenous_lake)= +```{raw} jupyter + +``` + +# Lake Model with an Endogenous Job Finding Rate + +```{index} single: Lake Model, Endogenous +``` + +```{contents} Contents +:depth: 2 +``` + +In addition to what's in Anaconda, this lecture will need the following libraries: + +```{code-cell} ipython3 +:tags: [hide-output] + +!pip install quantecon jax +``` + +## Overview + +This lecture is a continuation of the {doc}`lake model lecture `. + +We strongly recommend you read that lecture first before proceeding with this one. + +In the previous lecture, we studied a lake model of unemployment and employment where the transition rates between states were exogenous parameters. + +In this lecture, we extend the model by making the job finding rate endogenous. + +Specifically, the transition rate from unemployment to employment will be determined by the McCall search model {cite}`McCall1970`. + +All details relevant to the following discussion can be found in {doc}`our treatment ` of that model. + +Let's start with some imports: + +```{code-cell} ipython3 +import matplotlib.pyplot as plt +import jax +import jax.numpy as jnp +from typing import NamedTuple +from quantecon.distributions import BetaBinomial +from functools import partial +import jax.scipy.stats as stats +``` + +## Endogenous job finding rate + +We now make the hiring rate endogenous. + +The transition rate from unemployment to employment will be determined by the McCall search model {cite}`McCall1970`. + +All details relevant to the following discussion can be found in {doc}`our treatment ` of that model. + +### Reservation wage + +The most important thing to remember about the model is that optimal decisions +are characterized by a reservation wage $\bar w$ + +* If the wage offer $w$ in hand is greater than or equal to $\bar w$, then the worker accepts. +* Otherwise, the worker rejects. + +As we saw in {doc}`our discussion of the model `, the reservation wage depends on the wage offer distribution and the parameters + +* $\alpha$, the separation rate +* $\beta$, the discount factor +* $\gamma$, the offer arrival rate +* $c$, unemployment compensation + +### Linking the McCall search model to the lake model + +Suppose that all workers inside a lake model behave according to the McCall search model. + +The exogenous probability of leaving employment remains $\alpha$. + +But their optimal decision rules determine the probability $\lambda$ of leaving unemployment. + +This is now + +```{math} +:label: lake_lamda + +\lambda += \gamma \mathbb P \{ w_t \geq \bar w\} += \gamma \sum_{w' \geq \bar w} p(w') +``` + +### Fiscal policy + +We can use the McCall search version of the Lake Model to find an optimal level of unemployment insurance. + +We assume that the government sets unemployment compensation $c$. + +The government imposes a lump-sum tax $\tau$ sufficient to finance total unemployment payments. + +To attain a balanced budget at a steady state, taxes, the steady state unemployment rate $u$, and the unemployment compensation rate must satisfy + +$$ +\tau = u c +$$ + +The lump-sum tax applies to everyone, including unemployed workers. + +Thus, the post-tax income of an employed worker with wage $w$ is $w - \tau$. + +The post-tax income of an unemployed worker is $c - \tau$. + +For each specification $(c, \tau)$ of government policy, we can solve for the worker's optimal reservation wage. + +This determines $\lambda$ via {eq}`lake_lamda` evaluated at post tax wages, which in turn determines a steady state unemployment rate $u(c, \tau)$. + +For a given level of unemployment benefit $c$, we can solve for a tax that balances the budget in the steady state + +$$ +\tau = u(c, \tau) c +$$ + +To evaluate alternative government tax-unemployment compensation pairs, we require a welfare criterion. + +We use a steady state welfare criterion + +$$ +W := e \, {\mathbb E} [V \, | \, \text{employed}] + u \, U +$$ + +where the notation $V$ and $U$ is as defined in the {doc}`McCall search model lecture `. + +The wage offer distribution will be a discretized version of the lognormal distribution $LN(\log(20),1)$, as shown in the next figure + +```{code-cell} ipython3 +def create_wage_distribution(max_wage: float, + wage_grid_size: int, + log_wage_mean: float): + """Create wage distribution""" + w_vec_temp = jnp.linspace(1e-8, max_wage, + wage_grid_size + 1) + cdf = stats.norm.cdf(jnp.log(w_vec_temp), + loc=jnp.log(log_wage_mean), scale=1) + pdf = cdf[1:] - cdf[:-1] + p_vec = pdf / pdf.sum() + w_vec = (w_vec_temp[1:] + w_vec_temp[:-1]) / 2 + return w_vec, p_vec + +w_vec, p_vec = create_wage_distribution(170, 200, 20) + +# Plot the wage distribution +fig, ax = plt.subplots() + +ax.plot(w_vec, p_vec) +ax.set_xlabel('wages') +ax.set_ylabel('probability') + +plt.tight_layout() +plt.show() +``` + +### Fiscal policy code + +We will make use of techniques from the {doc}`McCall model lecture ` + +The first piece of code implements value function iteration + +```{code-cell} ipython3 +:tags: [output_scroll] + +@jax.jit +def u(c, σ=2.0): + return jnp.where(c > 0, (c**(1 - σ) - 1) / (1 - σ), -10e6) + + +class McCallModel(NamedTuple): + """ + Stores the parameters for the McCall search model + """ + α: float # Job separation rate + β: float # Discount rate + γ: float # Job offer rate + c: float # Unemployment compensation + σ: float # Utility parameter + w_vec: jnp.ndarray # Possible wage values + p_vec: jnp.ndarray # Probabilities over w_vec + + +def create_mccall_model(α=0.2, β=0.98, γ=0.7, c=6.0, σ=2.0, + w_vec=None, p_vec=None) -> McCallModel: + """ + Create a McCallModel. + """ + if w_vec is None: + n = 60 # Number of possible outcomes for wage + + # Wages between 10 and 20 + w_vec = jnp.linspace(10, 20, n) + a, b = 600, 400 # Shape parameters + dist = BetaBinomial(n-1, a, b) + p_vec = jnp.array(dist.pdf()) + return McCallModel(α=α, β=β, γ=γ, c=c, σ=σ, w_vec=w_vec, p_vec=p_vec) + + +@jax.jit +def bellman(mcm: McCallModel, V, U): + """ + Update the Bellman equations. + """ + α, β, γ, c, σ = mcm.α, mcm.β, mcm.γ, mcm.c, mcm.σ + w_vec, p_vec = mcm.w_vec, mcm.p_vec + + V_new = u(w_vec, σ) + β * ((1 - α) * V + α * U) + U_new = u(c, σ) + β * (1 - γ) * U + β * γ * (jnp.maximum(U, V) @ p_vec) + + return V_new, U_new + + +@jax.jit +def solve_mccall_model(mcm: McCallModel, tol=1e-5, max_iter=2000): + """ + Iterates to convergence on the Bellman equations. + """ + def cond_fun(state): + V, U, i, error = state + return jnp.logical_and(error > tol, i < max_iter) + + def body_fun(state): + V, U, i, error = state + V_new, U_new = bellman(mcm, V, U) + error_1 = jnp.max(jnp.abs(V_new - V)) + error_2 = jnp.abs(U_new - U) + error_new = jnp.maximum(error_1, error_2) + return V_new, U_new, i + 1, error_new + + # Initial state + V_init = jnp.ones(len(mcm.w_vec)) + U_init = 1.0 + i_init = 0 + error_init = tol + 1 + + init_state = (V_init, U_init, i_init, error_init) + V_final, U_final, _, _ = jax.lax.while_loop( + cond_fun, body_fun, init_state) + + return V_final, U_final +``` + +We also need to import the lake model functions from the previous lecture. + +```{code-cell} ipython3 +class LakeModel(NamedTuple): + """ + Parameters for the lake model + """ + λ: float + α: float + b: float + d: float + A: jnp.ndarray + R: jnp.ndarray + g: float + + +def create_lake_model( + λ: float = 0.283, # job finding rate + α: float = 0.013, # separation rate + b: float = 0.0124, # birth rate + d: float = 0.00822 # death rate + ) -> LakeModel: + """ + Create a LakeModel instance with default parameters. + + Computes and stores the transition matrices A and R, + and the labor force growth rate g. + + """ + # Compute growth rate + g = b - d + + # Compute transition matrix A + A = jnp.array([ + [(1-d) * (1-λ) + b, (1-d) * α + b], + [(1-d) * λ, (1-d) * (1-α)] + ]) + + # Compute normalized transition matrix R + R = A / (1 + g) + + return LakeModel(λ=λ, α=α, b=b, d=d, A=A, R=R, g=g) + + +@jax.jit +def rate_steady_state(model: LakeModel) -> jnp.ndarray: + r""" + Finds the steady state of the system :math:`x_{t+1} = R x_{t}` + by computing the eigenvector corresponding to the largest eigenvalue. + + By the Perron-Frobenius theorem, since :math:`R` is a non-negative + matrix with columns summing to 1 (a stochastic matrix), the largest + eigenvalue equals 1 and the corresponding eigenvector gives the steady state. + """ + λ, α, b, d, A, R, g = model + eigenvals, eigenvec = jnp.linalg.eig(R) + + # Find the eigenvector corresponding to the largest eigenvalue + # (which is 1 for a stochastic matrix by Perron-Frobenius theorem) + max_idx = jnp.argmax(jnp.abs(eigenvals)) + + # Get the corresponding eigenvector + steady_state = jnp.real(eigenvec[:, max_idx]) + + # Normalize to ensure positive values and sum to 1 + steady_state = jnp.abs(steady_state) + steady_state = steady_state / jnp.sum(steady_state) + + return steady_state +``` + +Now let's compute and plot welfare, employment, unemployment, and tax revenue as a +function of the unemployment compensation rate + +```{code-cell} ipython3 +class EconomyParameters(NamedTuple): + """Parameters for the economy""" + α: float + α_q: float # Quarterly (α is monthly) + b: float + d: float + β: float + γ: float + σ: float + log_wage_mean: float + wage_grid_size: int + max_wage: float + +def create_economy_params(α=0.013, b=0.0124, d=0.00822, + β=0.98, γ=1.0, σ=2.0, + log_wage_mean=20, + wage_grid_size=200, + max_wage=170) -> EconomyParameters: + """Create economy parameters with default values""" + α_q = (1-(1-α)**3) # Convert monthly to quarterly + return EconomyParameters(α=α, α_q=α_q, b=b, d=d, β=β, γ=γ, σ=σ, + log_wage_mean=log_wage_mean, + wage_grid_size=wage_grid_size, + max_wage=max_wage) + + +@jax.jit +def compute_optimal_quantities(c, τ, + params: EconomyParameters, w_vec, p_vec): + """ + Compute the reservation wage, job finding rate and value functions + of the workers given c and τ. + """ + mcm = create_mccall_model( + α=params.α_q, + β=params.β, + γ=params.γ, + c=c-τ, # Post tax compensation + σ=params.σ, + w_vec=w_vec-τ, # Post tax wages + p_vec=p_vec + ) + + V, U = solve_mccall_model(mcm) + w_idx = jnp.searchsorted(V - U, 0) + w_bar = jnp.where(w_idx == len(V), jnp.inf, mcm.w_vec[w_idx]) + + λ = params.γ * jnp.sum(p_vec * (w_vec - τ > w_bar)) + return w_bar, λ, V, U + + +@jax.jit +def compute_steady_state_quantities(c, τ, + params: EconomyParameters, w_vec, p_vec): + """ + Compute the steady state unemployment rate given c and τ using optimal + quantities from the McCall model and computing corresponding steady + state quantities + """ + w_bar, λ, V, U = compute_optimal_quantities(c, τ, + params, w_vec, p_vec) + + # Compute steady state employment and unemployment rates + model = create_lake_model(λ=λ, α=params.α_q, b=params.b, d=params.d) + u, e = rate_steady_state(model) + + # Compute steady state welfare + mask = (w_vec - τ > w_bar) + w = jnp.sum(V * p_vec * mask) / jnp.sum(p_vec * mask) + welfare = e * w + u * U + + return e, u, welfare + + +def find_balanced_budget_tax(c, params: EconomyParameters, + w_vec, p_vec): + """ + Find the tax level that will induce a balanced budget + """ + def steady_state_budget(t): + e, u, w = compute_steady_state_quantities(c, t, + params, w_vec, p_vec) + return t - u * c + + # Use a simple bisection method + t_low, t_high = 0.0, 0.9 * c + tol = 1e-6 + max_iter = 100 + + for i in range(max_iter): + t_mid = (t_low + t_high) / 2 + budget = steady_state_budget(t_mid) + + if abs(budget) < tol: + return t_mid + elif budget < 0: + t_low = t_mid + else: + t_high = t_mid + + return t_mid + + +# Create economy parameters and wage distribution +params = create_economy_params() +w_vec, p_vec = create_wage_distribution(params.max_wage, + params.wage_grid_size, + params.log_wage_mean) + +# Levels of unemployment insurance we wish to study +c_vec = jnp.linspace(5, 140, 60) + +tax_vec = [] +unempl_vec = [] +empl_vec = [] +welfare_vec = [] + +for c in c_vec: + t = find_balanced_budget_tax(c, params, w_vec, p_vec) + e_rate, u_rate, welfare = compute_steady_state_quantities(c, t, params, + w_vec, p_vec) + tax_vec.append(t) + unempl_vec.append(u_rate) + empl_vec.append(e_rate) + welfare_vec.append(welfare) + +fig, axes = plt.subplots(2, 2, figsize=(12, 10)) + +plots = [unempl_vec, empl_vec, tax_vec, welfare_vec] +titles = ['unemployment', 'employment', 'tax', 'welfare'] + +for ax, plot, title in zip(axes.flatten(), plots, titles): + ax.plot(c_vec, plot, lw=2, alpha=0.7) + ax.set_title(title) + +plt.tight_layout() +plt.show() +``` + +Welfare first increases and then decreases as unemployment benefits rise. + +The level that maximizes steady state welfare is approximately 62. diff --git a/lectures/lake_model.md b/lectures/lake_model.md index 7cc6420a2..f76ec5e03 100644 --- a/lectures/lake_model.md +++ b/lectures/lake_model.md @@ -256,6 +256,11 @@ def create_lake_model( return LakeModel(λ=λ, α=α, b=b, d=d, A=A, R=R, g=g) ``` +The default parameter values are: + +* $\alpha = 0.013$ and $\lambda = 0.283$ are based on {cite}`davis2006flow` +* $b = 0.0124$ and $d = 0.00822$ are set to match monthly [birth](https://www.cdc.gov/nchs/fastats/births.htm) and [death rates](https://www.cdc.gov/nchs/fastats/deaths.htm), respectively, in the U.S. population + As an experiment, let's create two instances, one with $α=0.013$ and another with $α=0.03$ ```{code-cell} ipython3 @@ -417,7 +422,7 @@ e, f = jnp.linalg.eigvals(model.R) print(f"Eigenvalue magnitudes: {abs(e):.2f}, {abs(f):.2f}") ``` -Let's look at the convergence of the unemployment and employment rates to steady state levels (dashed black line) +Let's look at the convergence of the unemployment and employment rates to steady state levels (dashed line) ```{code-cell} ipython3 xbar = rate_steady_state(model) @@ -430,7 +435,7 @@ titles = ['unemployment rate', 'employment rate'] for i, title in enumerate(titles): axes[i].plot(x_path[i, :], lw=2, alpha=0.5) - axes[i].hlines(xbar[i], 0, T, 'black', '--') + axes[i].hlines(xbar[i], 0, T, color='C1', linestyle='--') axes[i].set_title(title) plt.tight_layout() @@ -608,7 +613,7 @@ titles = ['percent of time unemployed', 'percent of time employed'] for i, plot in enumerate(to_plot): axes[i].plot(plot, lw=2, alpha=0.5) - axes[i].hlines(xbar[i], 0, T, linestyles='--') + axes[i].hlines(xbar[i], 0, T, color='C1', linestyle='--') axes[i].set_title(titles[i]) plt.tight_layout() @@ -621,361 +626,6 @@ In this case it takes much of the sample for these two objects to converge. This is largely due to the high persistence in the Markov chain. -## Endogenous job finding rate - -We now make the hiring rate endogenous. - -The transition rate from unemployment to employment will be determined by the McCall search model {cite}`McCall1970`. - -All details relevant to the following discussion can be found in {doc}`our treatment ` of that model. - -### Reservation wage - -The most important thing to remember about the model is that optimal decisions -are characterized by a reservation wage $\bar w$ - -* If the wage offer $w$ in hand is greater than or equal to $\bar w$, then the worker accepts. -* Otherwise, the worker rejects. - -As we saw in {doc}`our discussion of the model `, the reservation wage depends on the wage offer distribution and the parameters - -* $\alpha$, the separation rate -* $\beta$, the discount factor -* $\gamma$, the offer arrival rate -* $c$, unemployment compensation - -### Linking the McCall search model to the lake model - -Suppose that all workers inside a lake model behave according to the McCall search model. - -The exogenous probability of leaving employment remains $\alpha$. - -But their optimal decision rules determine the probability $\lambda$ of leaving unemployment. - -This is now - -```{math} -:label: lake_lamda - -\lambda -= \gamma \mathbb P \{ w_t \geq \bar w\} -= \gamma \sum_{w' \geq \bar w} p(w') -``` - -### Fiscal policy - -We can use the McCall search version of the Lake Model to find an optimal level of unemployment insurance. - -We assume that the government sets unemployment compensation $c$. - -The government imposes a lump-sum tax $\tau$ sufficient to finance total unemployment payments. - -To attain a balanced budget at a steady state, taxes, the steady state unemployment rate $u$, and the unemployment compensation rate must satisfy - -$$ -\tau = u c -$$ - -The lump-sum tax applies to everyone, including unemployed workers. - -Thus, the post-tax income of an employed worker with wage $w$ is $w - \tau$. - -The post-tax income of an unemployed worker is $c - \tau$. - -For each specification $(c, \tau)$ of government policy, we can solve for the worker's optimal reservation wage. - -This determines $\lambda$ via {eq}`lake_lamda` evaluated at post tax wages, which in turn determines a steady state unemployment rate $u(c, \tau)$. - -For a given level of unemployment benefit $c$, we can solve for a tax that balances the budget in the steady state - -$$ -\tau = u(c, \tau) c -$$ - -To evaluate alternative government tax-unemployment compensation pairs, we require a welfare criterion. - -We use a steady state welfare criterion - -$$ -W := e \, {\mathbb E} [V \, | \, \text{employed}] + u \, U -$$ - -where the notation $V$ and $U$ is as defined in the {doc}`McCall search model lecture `. - -The wage offer distribution will be a discretized version of the lognormal distribution $LN(\log(20),1)$, as shown in the next figure - -```{code-cell} ipython3 -def create_wage_distribution(max_wage: float, - wage_grid_size: int, - log_wage_mean: float): - """Create wage distribution""" - w_vec_temp = jnp.linspace(1e-8, max_wage, - wage_grid_size + 1) - cdf = stats.norm.cdf(jnp.log(w_vec_temp), - loc=jnp.log(log_wage_mean), scale=1) - pdf = cdf[1:] - cdf[:-1] - p_vec = pdf / pdf.sum() - w_vec = (w_vec_temp[1:] + w_vec_temp[:-1]) / 2 - return w_vec, p_vec - -w_vec, p_vec = create_wage_distribution(170, 200, 20) - -# Plot the wage distribution -fig, ax = plt.subplots() - -ax.plot(w_vec, p_vec) -ax.set_xlabel('wages') -ax.set_ylabel('probability') - -plt.tight_layout() -plt.show() -``` - -We take a period to be a month. - -We set $b$ and $d$ to match monthly [birth](https://www.cdc.gov/nchs/fastats/births.htm) and [death rates](https://www.cdc.gov/nchs/fastats/deaths.htm), respectively, in the U.S. population - -* $b = 0.0124$ -* $d = 0.00822$ - -Following {cite}`davis2006flow`, we set $\alpha$, the hazard rate of leaving employment, to - -* $\alpha = 0.013$ - -### Fiscal policy code - -We will make use of techniques from the {doc}`McCall model lecture ` - -The first piece of code implements value function iteration - -```{code-cell} ipython3 -:tags: [output_scroll] - -@jax.jit -def u(c, σ=2.0): - return jnp.where(c > 0, (c**(1 - σ) - 1) / (1 - σ), -10e6) - - -class McCallModel(NamedTuple): - """ - Stores the parameters for the McCall search model - """ - α: float # Job separation rate - β: float # Discount rate - γ: float # Job offer rate - c: float # Unemployment compensation - σ: float # Utility parameter - w_vec: jnp.ndarray # Possible wage values - p_vec: jnp.ndarray # Probabilities over w_vec - - -def create_mccall_model(α=0.2, β=0.98, γ=0.7, c=6.0, σ=2.0, - w_vec=None, p_vec=None) -> McCallModel: - """ - Create a McCallModel. - """ - if w_vec is None: - n = 60 # Number of possible outcomes for wage - - # Wages between 10 and 20 - w_vec = jnp.linspace(10, 20, n) - a, b = 600, 400 # Shape parameters - dist = BetaBinomial(n-1, a, b) - p_vec = jnp.array(dist.pdf()) - return McCallModel(α=α, β=β, γ=γ, c=c, σ=σ, w_vec=w_vec, p_vec=p_vec) - - -@jax.jit -def bellman(mcm: McCallModel, V, U): - """ - Update the Bellman equations. - """ - α, β, γ, c, σ = mcm.α, mcm.β, mcm.γ, mcm.c, mcm.σ - w_vec, p_vec = mcm.w_vec, mcm.p_vec - - V_new = u(w_vec, σ) + β * ((1 - α) * V + α * U) - U_new = u(c, σ) + β * (1 - γ) * U + β * γ * (jnp.maximum(U, V) @ p_vec) - - return V_new, U_new - - -@jax.jit -def solve_mccall_model(mcm: McCallModel, tol=1e-5, max_iter=2000): - """ - Iterates to convergence on the Bellman equations. - """ - def cond_fun(state): - V, U, i, error = state - return jnp.logical_and(error > tol, i < max_iter) - - def body_fun(state): - V, U, i, error = state - V_new, U_new = bellman(mcm, V, U) - error_1 = jnp.max(jnp.abs(V_new - V)) - error_2 = jnp.abs(U_new - U) - error_new = jnp.maximum(error_1, error_2) - return V_new, U_new, i + 1, error_new - - # Initial state - V_init = jnp.ones(len(mcm.w_vec)) - U_init = 1.0 - i_init = 0 - error_init = tol + 1 - - init_state = (V_init, U_init, i_init, error_init) - V_final, U_final, _, _ = jax.lax.while_loop( - cond_fun, body_fun, init_state) - - return V_final, U_final -``` - -Now let's compute and plot welfare, employment, unemployment, and tax revenue as a -function of the unemployment compensation rate - -```{code-cell} ipython3 -class EconomyParameters(NamedTuple): - """Parameters for the economy""" - α: float - α_q: float # Quarterly (α is monthly) - b: float - d: float - β: float - γ: float - σ: float - log_wage_mean: float - wage_grid_size: int - max_wage: float - -def create_economy_params(α=0.013, b=0.0124, d=0.00822, - β=0.98, γ=1.0, σ=2.0, - log_wage_mean=20, - wage_grid_size=200, - max_wage=170) -> EconomyParameters: - """Create economy parameters with default values""" - α_q = (1-(1-α)**3) # Convert monthly to quarterly - return EconomyParameters(α=α, α_q=α_q, b=b, d=d, β=β, γ=γ, σ=σ, - log_wage_mean=log_wage_mean, - wage_grid_size=wage_grid_size, - max_wage=max_wage) - - -@jax.jit -def compute_optimal_quantities(c, τ, - params: EconomyParameters, w_vec, p_vec): - """ - Compute the reservation wage, job finding rate and value functions - of the workers given c and τ. - """ - mcm = create_mccall_model( - α=params.α_q, - β=params.β, - γ=params.γ, - c=c-τ, # Post tax compensation - σ=params.σ, - w_vec=w_vec-τ, # Post tax wages - p_vec=p_vec - ) - - V, U = solve_mccall_model(mcm) - w_idx = jnp.searchsorted(V - U, 0) - w_bar = jnp.where(w_idx == len(V), jnp.inf, mcm.w_vec[w_idx]) - - λ = params.γ * jnp.sum(p_vec * (w_vec - τ > w_bar)) - return w_bar, λ, V, U - - -@jax.jit -def compute_steady_state_quantities(c, τ, - params: EconomyParameters, w_vec, p_vec): - """ - Compute the steady state unemployment rate given c and τ using optimal - quantities from the McCall model and computing corresponding steady - state quantities - """ - w_bar, λ, V, U = compute_optimal_quantities(c, τ, - params, w_vec, p_vec) - - # Compute steady state employment and unemployment rates - model = create_lake_model(λ=λ, α=params.α_q, b=params.b, d=params.d) - u, e = rate_steady_state(model) - - # Compute steady state welfare - mask = (w_vec - τ > w_bar) - w = jnp.sum(V * p_vec * mask) / jnp.sum(p_vec * mask) - welfare = e * w + u * U - - return e, u, welfare - - -def find_balanced_budget_tax(c, params: EconomyParameters, - w_vec, p_vec): - """ - Find the tax level that will induce a balanced budget - """ - def steady_state_budget(t): - e, u, w = compute_steady_state_quantities(c, t, - params, w_vec, p_vec) - return t - u * c - - # Use a simple bisection method - t_low, t_high = 0.0, 0.9 * c - tol = 1e-6 - max_iter = 100 - - for i in range(max_iter): - t_mid = (t_low + t_high) / 2 - budget = steady_state_budget(t_mid) - - if abs(budget) < tol: - return t_mid - elif budget < 0: - t_low = t_mid - else: - t_high = t_mid - - return t_mid - - -# Create economy parameters and wage distribution -params = create_economy_params() -w_vec, p_vec = create_wage_distribution(params.max_wage, - params.wage_grid_size, - params.log_wage_mean) - -# Levels of unemployment insurance we wish to study -c_vec = jnp.linspace(5, 140, 60) - -tax_vec = [] -unempl_vec = [] -empl_vec = [] -welfare_vec = [] - -for c in c_vec: - t = find_balanced_budget_tax(c, params, w_vec, p_vec) - e_rate, u_rate, welfare = compute_steady_state_quantities(c, t, params, - w_vec, p_vec) - tax_vec.append(t) - unempl_vec.append(u_rate) - empl_vec.append(e_rate) - welfare_vec.append(welfare) - -fig, axes = plt.subplots(2, 2, figsize=(12, 10)) - -plots = [unempl_vec, empl_vec, tax_vec, welfare_vec] -titles = ['unemployment', 'employment', 'tax', 'welfare'] - -for ax, plot, title in zip(axes.flatten(), plots, titles): - ax.plot(c_vec, plot, lw=2, alpha=0.7) - ax.set_title(title) - -plt.tight_layout() -plt.show() -``` - -Welfare first increases and then decreases as unemployment benefits rise. - -The level that maximizes steady state welfare is approximately 62. - ## Exercises ```{exercise-start} @@ -983,14 +633,7 @@ The level that maximizes steady state welfare is approximately 62. ``` Consider an economy with an initial stock of workers $N_0 = 100$ at the -steady state level of employment in the baseline parameterization - -* $\alpha = 0.013$ -* $\lambda = 0.283$ -* $b = 0.0124$ -* $d = 0.00822$ - -(The values for $\alpha$ and $\lambda$ follow {cite}`davis2006flow`) +steady state level of employment in the baseline parameterization. Suppose that in response to new legislation the hiring rate reduces to $\lambda = 0.2$. @@ -1065,7 +708,7 @@ titles = ['unemployment rate', 'employment rate'] for i, title in enumerate(titles): axes[i].plot(x_path[i, :]) - axes[i].hlines(xbar[i], 0, T, linestyles='--') + axes[i].hlines(xbar[i], 0, T, color='C1', linestyle='--') axes[i].set_title(title) plt.tight_layout() @@ -1136,9 +779,9 @@ additional 30 periods ```{code-cell} ipython3 # Use final state from period 20 as initial condition -X_path2 = generate_path(stock_update, X_path1[:, -1], T-T_hat, +X_path2 = generate_path(stock_update, X_path1[:, -1], T-T_hat, model=model_baseline) -x_path2 = generate_path(rate_update, x_path1[:, -1], T-T_hat, +x_path2 = generate_path(rate_update, x_path1[:, -1], T-T_hat, model=model_baseline) ``` @@ -1173,7 +816,7 @@ titles = ['unemployment rate', 'employment rate'] for i, title in enumerate(titles): axes[i].plot(x_path[i, :]) - axes[i].hlines(x0[i], 0, T, linestyles='--') + axes[i].hlines(x0[i], 0, T, color='C1', linestyle='--') axes[i].set_title(title) plt.tight_layout() From 16aca76ee1906824d22237db2f5d018363271412 Mon Sep 17 00:00:00 2001 From: John Stachurski Date: Mon, 3 Nov 2025 08:16:22 +0900 Subject: [PATCH 2/7] Improve endogenous_lake.md: Split code blocks and add descriptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reorganized the endogenous lake model lecture for better readability and comprehension. **Changes:** - Split large monolithic code blocks into 12 smaller, focused blocks - Added descriptive sentences before each code block explaining its purpose - Separated function definitions from their usage and plotting code - Added new subsection "Computing optimal unemployment insurance" **Code block breakdown:** 1. Wage distribution function definition 2. Wage distribution creation and visualization 3. Utility function and McCall model data structure 4. Bellman equation operator 5. Value function iteration solver 6. Lake model functions (from previous lecture) 7. Economy parameters container 8. Function to compute optimal worker quantities 9. Function to compute steady state outcomes 10. Function to find balanced budget tax rate 11. Computation loop across policy range 12. Results visualization Each block now has a clear explanation of what it does and how it fits into the overall analysis, making the lecture more pedagogically effective. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- lectures/endogenous_lake.md | 48 ++++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/lectures/endogenous_lake.md b/lectures/endogenous_lake.md index 0c8807c8d..efb7e14c5 100644 --- a/lectures/endogenous_lake.md +++ b/lectures/endogenous_lake.md @@ -144,7 +144,9 @@ $$ where the notation $V$ and $U$ is as defined in the {doc}`McCall search model lecture `. -The wage offer distribution will be a discretized version of the lognormal distribution $LN(\log(20),1)$, as shown in the next figure +The wage offer distribution will be a discretized version of the lognormal distribution $LN(\log(20),1)$. + +We first define a function to create a discretized wage distribution: ```{code-cell} ipython3 def create_wage_distribution(max_wage: float, @@ -159,29 +161,28 @@ def create_wage_distribution(max_wage: float, p_vec = pdf / pdf.sum() w_vec = (w_vec_temp[1:] + w_vec_temp[:-1]) / 2 return w_vec, p_vec +``` + +Let's create a wage distribution and visualize it: +```{code-cell} ipython3 w_vec, p_vec = create_wage_distribution(170, 200, 20) -# Plot the wage distribution fig, ax = plt.subplots() - ax.plot(w_vec, p_vec) ax.set_xlabel('wages') ax.set_ylabel('probability') - plt.tight_layout() plt.show() ``` ### Fiscal policy code -We will make use of techniques from the {doc}`McCall model lecture ` +We will make use of techniques from the {doc}`McCall model lecture `. -The first piece of code implements value function iteration +First, we define the utility function and the McCall model data structure: ```{code-cell} ipython3 -:tags: [output_scroll] - @jax.jit def u(c, σ=2.0): return jnp.where(c > 0, (c**(1 - σ) - 1) / (1 - σ), -10e6) @@ -214,8 +215,11 @@ def create_mccall_model(α=0.2, β=0.98, γ=0.7, c=6.0, σ=2.0, dist = BetaBinomial(n-1, a, b) p_vec = jnp.array(dist.pdf()) return McCallModel(α=α, β=β, γ=γ, c=c, σ=σ, w_vec=w_vec, p_vec=p_vec) +``` +Next, we implement the Bellman equation operator: +```{code-cell} ipython3 @jax.jit def bellman(mcm: McCallModel, V, U): """ @@ -228,8 +232,11 @@ def bellman(mcm: McCallModel, V, U): U_new = u(c, σ) + β * (1 - γ) * U + β * γ * (jnp.maximum(U, V) @ p_vec) return V_new, U_new +``` +Now we define the value function iteration solver: +```{code-cell} ipython3 @jax.jit def solve_mccall_model(mcm: McCallModel, tol=1e-5, max_iter=2000): """ @@ -260,7 +267,7 @@ def solve_mccall_model(mcm: McCallModel, tol=1e-5, max_iter=2000): return V_final, U_final ``` -We also need to import the lake model functions from the previous lecture. +We also need the lake model functions from the previous lecture to compute steady state unemployment rates: ```{code-cell} ipython3 class LakeModel(NamedTuple): @@ -331,8 +338,11 @@ def rate_steady_state(model: LakeModel) -> jnp.ndarray: return steady_state ``` -Now let's compute and plot welfare, employment, unemployment, and tax revenue as a -function of the unemployment compensation rate +### Computing optimal unemployment insurance + +Now we set up the infrastructure to compute optimal unemployment insurance levels. + +First, we define a container for the economy's parameters: ```{code-cell} ipython3 class EconomyParameters(NamedTuple): @@ -359,8 +369,11 @@ def create_economy_params(α=0.013, b=0.0124, d=0.00822, log_wage_mean=log_wage_mean, wage_grid_size=wage_grid_size, max_wage=max_wage) +``` +Next, we define a function that computes optimal worker behavior given policy parameters: +```{code-cell} ipython3 @jax.jit def compute_optimal_quantities(c, τ, params: EconomyParameters, w_vec, p_vec): @@ -384,8 +397,11 @@ def compute_optimal_quantities(c, τ, λ = params.γ * jnp.sum(p_vec * (w_vec - τ > w_bar)) return w_bar, λ, V, U +``` +This function computes the steady state outcomes given unemployment insurance and tax levels: +```{code-cell} ipython3 @jax.jit def compute_steady_state_quantities(c, τ, params: EconomyParameters, w_vec, p_vec): @@ -407,8 +423,11 @@ def compute_steady_state_quantities(c, τ, welfare = e * w + u * U return e, u, welfare +``` +We need a function to find the tax rate that balances the government budget: +```{code-cell} ipython3 def find_balanced_budget_tax(c, params: EconomyParameters, w_vec, p_vec): """ @@ -436,8 +455,11 @@ def find_balanced_budget_tax(c, params: EconomyParameters, t_high = t_mid return t_mid +``` +Now we compute how employment, unemployment, taxes, and welfare vary with the unemployment compensation rate: +```{code-cell} ipython3 # Create economy parameters and wage distribution params = create_economy_params() w_vec, p_vec = create_wage_distribution(params.max_wage, @@ -460,7 +482,11 @@ for c in c_vec: unempl_vec.append(u_rate) empl_vec.append(e_rate) welfare_vec.append(welfare) +``` + +Let's visualize the results: +```{code-cell} ipython3 fig, axes = plt.subplots(2, 2, figsize=(12, 10)) plots = [unempl_vec, empl_vec, tax_vec, welfare_vec] From 848e7f17159e665b2e7c7a9f3ccdf2183d7d25c8 Mon Sep 17 00:00:00 2001 From: John Stachurski Date: Mon, 3 Nov 2025 08:30:52 +0900 Subject: [PATCH 3/7] Add exercise to endogenous_lake.md lecture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added an exercise exploring how the welfare-maximizing level of unemployment compensation varies with the job separation rate. **Exercise:** - Computes optimal unemployment compensation for different separation rates - Uses brute force search over c_vec to find welfare-maximizing c - Plots the relationship between α and optimal c - Includes economic interpretation of results **Solution shows:** - As separation rate increases, optimal unemployment insurance increases - Makes intuitive sense: more frequent job loss → higher value of insurance - Demonstrates practical application of the welfare optimization framework 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- lectures/endogenous_lake.md | 63 +++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/lectures/endogenous_lake.md b/lectures/endogenous_lake.md index efb7e14c5..dbce4f639 100644 --- a/lectures/endogenous_lake.md +++ b/lectures/endogenous_lake.md @@ -503,3 +503,66 @@ plt.show() Welfare first increases and then decreases as unemployment benefits rise. The level that maximizes steady state welfare is approximately 62. + +## Exercises + +```{exercise} +:label: endogenous_lake_ex1 + +How does the welfare-maximizing level of unemployment compensation $c$ change with the job separation rate $\alpha$? + +Compute and plot the optimal $c$ (the value that maximizes welfare) for a range of separation rates $\alpha$ from 0.01 to 0.025. + +For each $\alpha$ value, find the optimal $c$ by computing welfare across the range of $c$ values and selecting the maximum. +``` + +```{solution-start} endogenous_lake_ex1 +:class: dropdown +``` + +Here is one solution: + +```{code-cell} ipython3 +# Range of separation rates to explore +α_values = jnp.linspace(0.01, 0.025, 15) + +# We'll store the optimal c for each α +optimal_c_values = [] + +for α_val in α_values: + # Create economy parameters with this α + params_α = create_economy_params(α=α_val) + + # Create wage distribution + w_vec_α, p_vec_α = create_wage_distribution(params_α.max_wage, + params_α.wage_grid_size, + params_α.log_wage_mean) + + # Compute welfare for each c value + welfare_values = [] + for c in c_vec: + t = find_balanced_budget_tax(c, params_α, w_vec_α, p_vec_α) + e_rate, u_rate, welfare = compute_steady_state_quantities(c, t, params_α, + w_vec_α, p_vec_α) + welfare_values.append(welfare) + + # Find the c that maximizes welfare + max_idx = jnp.argmax(jnp.array(welfare_values)) + optimal_c = c_vec[max_idx] + optimal_c_values.append(optimal_c) + +# Plot the relationship +fig, ax = plt.subplots(figsize=(10, 6)) +ax.plot(α_values, optimal_c_values, lw=2, marker='o') +ax.set_xlabel(r'Separation rate $\alpha$') +ax.set_ylabel('Optimal unemployment compensation $c$') +ax.set_title('How optimal unemployment insurance varies with job separation rate') +ax.grid(True, alpha=0.3) +plt.tight_layout() +plt.show() +``` + +We see that as the separation rate increases (workers lose their jobs more frequently), the welfare-maximizing level of unemployment compensation also increases. This makes intuitive sense: when job loss is more common, more generous unemployment insurance becomes more valuable for smoothing consumption and maintaining worker welfare. + +```{solution-end} +``` From 0503f6d7451474319a17912b0371bae1583a3894 Mon Sep 17 00:00:00 2001 From: John Stachurski Date: Mon, 3 Nov 2025 19:30:43 +0900 Subject: [PATCH 4/7] misc --- lectures/endogenous_lake.md | 218 ++++++++++++++++++++---------------- 1 file changed, 122 insertions(+), 96 deletions(-) diff --git a/lectures/endogenous_lake.md b/lectures/endogenous_lake.md index dbce4f639..219bfabcb 100644 --- a/lectures/endogenous_lake.md +++ b/lectures/endogenous_lake.md @@ -41,16 +41,15 @@ In addition to what's in Anaconda, this lecture will need the following librarie This lecture is a continuation of the {doc}`lake model lecture `. -We strongly recommend you read that lecture first before proceeding with this one. +We recommend you read that lecture first before proceeding with this one. -In the previous lecture, we studied a lake model of unemployment and employment where the transition rates between states were exogenous parameters. +In the previous lecture, we studied a lake model of unemployment and employment +where the transition rates between states were exogenous parameters. In this lecture, we extend the model by making the job finding rate endogenous. Specifically, the transition rate from unemployment to employment will be determined by the McCall search model {cite}`McCall1970`. -All details relevant to the following discussion can be found in {doc}`our treatment ` of that model. - Let's start with some imports: ```{code-cell} ipython3 @@ -63,107 +62,56 @@ from functools import partial import jax.scipy.stats as stats ``` -## Endogenous job finding rate -We now make the hiring rate endogenous. -The transition rate from unemployment to employment will be determined by the McCall search model {cite}`McCall1970`. -All details relevant to the following discussion can be found in {doc}`our treatment ` of that model. +## Set Up + +The basic structure of the model will be as discussed in the {doc}`lake model lecture `. + +The only difference is that the hiring rate is endogenous, determined by the +decisions of optimizing agents inhabiting a McCall search model {cite}`McCall1970` with +IID wage offers and job separation at rate $\alpha$. + ### Reservation wage -The most important thing to remember about the model is that optimal decisions -are characterized by a reservation wage $\bar w$ +In the model, the optimal policy is characterized by a reservation wage $\bar w$ * If the wage offer $w$ in hand is greater than or equal to $\bar w$, then the worker accepts. * Otherwise, the worker rejects. -As we saw in {doc}`our discussion of the model `, the reservation wage depends on the wage offer distribution and the parameters +The reservation wage depends on the wage offer distribution and the parameters * $\alpha$, the separation rate * $\beta$, the discount factor * $\gamma$, the offer arrival rate * $c$, unemployment compensation -### Linking the McCall search model to the lake model - -Suppose that all workers inside a lake model behave according to the McCall search model. - -The exogenous probability of leaving employment remains $\alpha$. - -But their optimal decision rules determine the probability $\lambda$ of leaving unemployment. - -This is now - -```{math} -:label: lake_lamda - -\lambda -= \gamma \mathbb P \{ w_t \geq \bar w\} -= \gamma \sum_{w' \geq \bar w} p(w') -``` - -### Fiscal policy - -We can use the McCall search version of the Lake Model to find an optimal level of unemployment insurance. - -We assume that the government sets unemployment compensation $c$. - -The government imposes a lump-sum tax $\tau$ sufficient to finance total unemployment payments. - -To attain a balanced budget at a steady state, taxes, the steady state unemployment rate $u$, and the unemployment compensation rate must satisfy - -$$ -\tau = u c -$$ - -The lump-sum tax applies to everyone, including unemployed workers. - -Thus, the post-tax income of an employed worker with wage $w$ is $w - \tau$. - -The post-tax income of an unemployed worker is $c - \tau$. - -For each specification $(c, \tau)$ of government policy, we can solve for the worker's optimal reservation wage. - -This determines $\lambda$ via {eq}`lake_lamda` evaluated at post tax wages, which in turn determines a steady state unemployment rate $u(c, \tau)$. - -For a given level of unemployment benefit $c$, we can solve for a tax that balances the budget in the steady state - -$$ -\tau = u(c, \tau) c -$$ - -To evaluate alternative government tax-unemployment compensation pairs, we require a welfare criterion. - -We use a steady state welfare criterion - -$$ -W := e \, {\mathbb E} [V \, | \, \text{employed}] + u \, U -$$ - -where the notation $V$ and $U$ is as defined in the {doc}`McCall search model lecture `. The wage offer distribution will be a discretized version of the lognormal distribution $LN(\log(20),1)$. We first define a function to create a discretized wage distribution: ```{code-cell} ipython3 -def create_wage_distribution(max_wage: float, - wage_grid_size: int, - log_wage_mean: float): - """Create wage distribution""" - w_vec_temp = jnp.linspace(1e-8, max_wage, - wage_grid_size + 1) - cdf = stats.norm.cdf(jnp.log(w_vec_temp), - loc=jnp.log(log_wage_mean), scale=1) +def create_wage_distribution( + max_wage: float, + wage_grid_size: int, + log_wage_mean: float + ): + w_vec_temp = jnp.linspace( + 1e-8, max_wage, wage_grid_size + 1 + ) + cdf = stats.norm.cdf( + jnp.log(w_vec_temp), loc=jnp.log(log_wage_mean), scale=1 + ) pdf = cdf[1:] - cdf[:-1] p_vec = pdf / pdf.sum() w_vec = (w_vec_temp[1:] + w_vec_temp[:-1]) / 2 return w_vec, p_vec ``` -Let's create a wage distribution and visualize it: +To illustrate the code, let's create a wage distribution and visualize it: ```{code-cell} ipython3 w_vec, p_vec = create_wage_distribution(170, 200, 20) @@ -176,14 +124,10 @@ plt.tight_layout() plt.show() ``` -### Fiscal policy code - -We will make use of techniques from the {doc}`McCall model lecture `. -First, we define the utility function and the McCall model data structure: +Now we define the utility function and the McCall model data structure: ```{code-cell} ipython3 -@jax.jit def u(c, σ=2.0): return jnp.where(c > 0, (c**(1 - σ) - 1) / (1 - σ), -10e6) @@ -201,27 +145,27 @@ class McCallModel(NamedTuple): p_vec: jnp.ndarray # Probabilities over w_vec -def create_mccall_model(α=0.2, β=0.98, γ=0.7, c=6.0, σ=2.0, - w_vec=None, p_vec=None) -> McCallModel: - """ - Create a McCallModel. - """ +def create_mccall_model( + α=0.2, β=0.98, γ=0.7, c=6.0, σ=2.0, + w_vec=None, + p_vec=None + ) -> McCallModel: if w_vec is None: n = 60 # Number of possible outcomes for wage - # Wages between 10 and 20 w_vec = jnp.linspace(10, 20, n) a, b = 600, 400 # Shape parameters dist = BetaBinomial(n-1, a, b) p_vec = jnp.array(dist.pdf()) - return McCallModel(α=α, β=β, γ=γ, c=c, σ=σ, w_vec=w_vec, p_vec=p_vec) + return McCallModel( + α=α, β=β, γ=γ, c=c, σ=σ, w_vec=w_vec, p_vec=p_vec + ) ``` -Next, we implement the Bellman equation operator: +Next, we implement the Bellman operator ```{code-cell} ipython3 -@jax.jit -def bellman(mcm: McCallModel, V, U): +def T(mcm: McCallModel, V, U): """ Update the Bellman equations. """ @@ -234,7 +178,9 @@ def bellman(mcm: McCallModel, V, U): return V_new, U_new ``` -Now we define the value function iteration solver: +Now we define the value function iteration solver. + +We'll use a compiled while loop for extra speed. ```{code-cell} ipython3 @jax.jit @@ -248,7 +194,7 @@ def solve_mccall_model(mcm: McCallModel, tol=1e-5, max_iter=2000): def body_fun(state): V, U, i, error = state - V_new, U_new = bellman(mcm, V, U) + V_new, U_new = T(mcm, V, U) error_1 = jnp.max(jnp.abs(V_new - V)) error_2 = jnp.abs(U_new - U) error_new = jnp.maximum(error_1, error_2) @@ -262,11 +208,15 @@ def solve_mccall_model(mcm: McCallModel, tol=1e-5, max_iter=2000): init_state = (V_init, U_init, i_init, error_init) V_final, U_final, _, _ = jax.lax.while_loop( - cond_fun, body_fun, init_state) - + cond_fun, body_fun, init_state + ) return V_final, U_final ``` + + +### Lake model code + We also need the lake model functions from the previous lecture to compute steady state unemployment rates: ```{code-cell} ipython3 @@ -338,6 +288,82 @@ def rate_steady_state(model: LakeModel) -> jnp.ndarray: return steady_state ``` + +### Linking the McCall search model to the lake model + +Suppose that all workers inside a lake model behave according to the McCall search model. + +The exogenous probability of leaving employment remains $\alpha$. + +But their optimal decision rules determine the probability $\lambda$ of leaving unemployment. + +This is now + +```{math} +:label: lake_lamda + +\lambda += \gamma \mathbb P \{ w_t \geq \bar w\} += \gamma \sum_{w' \geq \bar w} p(w') +``` + +Here + +* $\bar w$ is the reservation wage determined by the parameters and +* $p$ is the wage offer distribution. + + + +## Fiscal policy + +In this section, we will put the lake model to work, examining outcomes +associated with different levels of unemployment compensation. + +Our aim is to find an optimal level of unemployment insurance. + +We assume that the government sets unemployment compensation $c$. + +The government imposes a lump-sum tax $\tau$ sufficient to finance total +unemployment payments. + +To attain a balanced budget at a steady state, taxes, the steady state +unemployment rate $u$, and the unemployment compensation rate must satisfy + +$$ + \tau = u c +$$ + +The lump-sum tax applies to everyone, including unemployed workers. + +* The post-tax income of an employed worker with wage $w$ is $w - \tau$. +* The post-tax income of an unemployed worker is $c - \tau$. + +For each specification $(c, \tau)$ of government policy, we can solve for the +worker's optimal reservation wage. + +This determines $\lambda$ via {eq}`lake_lamda` evaluated at post tax wages, +which in turn determines a steady state unemployment rate $u(c, \tau)$. + +For a given level of unemployment benefit $c$, we can solve for a tax that balances the budget in the steady state + +$$ + \tau = u(c, \tau) c +$$ + +To evaluate alternative government tax-unemployment compensation pairs, we require a welfare criterion. + +We use a steady state welfare criterion + +$$ + W := e \, {\mathbb E} [V \, | \, \text{employed}] + u \, U +$$ + +where the notation $V$ and $U$ is as defined above and the expectation is at the +steady state. + + + + ### Computing optimal unemployment insurance Now we set up the infrastructure to compute optimal unemployment insurance levels. From ab93a1aaebc3fbb4eb75eabb72d7d2a1c3ba6104 Mon Sep 17 00:00:00 2001 From: John Stachurski Date: Tue, 4 Nov 2025 13:02:00 +0900 Subject: [PATCH 5/7] Fix unstable exercise solution in endogenous_lake.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The exercise solution was producing an unstable, noisy relationship between separation rate and optimal unemployment compensation due to extremely flat welfare function near its maximum. Changes: - Expanded α range from [0.01, 0.025] to [0.01, 0.04] with fewer points (8 vs 15) - Increased c grid resolution from 40 to 150 points - Implemented centroid method: compute weighted average of near-optimal c values instead of using argmax, which is unstable on flat functions - Updated solution text to correctly state that optimal c decreases (not increases) with separation rate, with proper economic explanation Result: Clean, monotonically decreasing relationship from c=68.88 to c=60.72 as α increases from 0.01 to 0.04. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- lectures/endogenous_lake.md | 211 ++++++++++++++++++++++++------------ 1 file changed, 144 insertions(+), 67 deletions(-) diff --git a/lectures/endogenous_lake.md b/lectures/endogenous_lake.md index 219bfabcb..c8b2e0bbc 100644 --- a/lectures/endogenous_lake.md +++ b/lectures/endogenous_lake.md @@ -88,10 +88,10 @@ The reservation wage depends on the wage offer distribution and the parameters * $\gamma$, the offer arrival rate * $c$, unemployment compensation +The wage offer distribution will be a discretized version of a lognormal distribution. -The wage offer distribution will be a discretized version of the lognormal distribution $LN(\log(20),1)$. +We first define a function to create such a discrete distribution. -We first define a function to create a discretized wage distribution: ```{code-cell} ipython3 def create_wage_distribution( @@ -99,9 +99,12 @@ def create_wage_distribution( wage_grid_size: int, log_wage_mean: float ): - w_vec_temp = jnp.linspace( - 1e-8, max_wage, wage_grid_size + 1 - ) + """ + Creates a discretized version of a lognormal density LN(log(m),1), where + m is log_wage_mean. + + """ + w_vec_temp = jnp.linspace(1e-8, max_wage, wage_grid_size + 1) cdf = stats.norm.cdf( jnp.log(w_vec_temp), loc=jnp.log(log_wage_mean), scale=1 ) @@ -111,7 +114,9 @@ def create_wage_distribution( return w_vec, p_vec ``` -To illustrate the code, let's create a wage distribution and visualize it: + +The cell below creates a discretized $LN(\log(20),1)$ wage distribution and +plots it. ```{code-cell} ipython3 w_vec, p_vec = create_wage_distribution(170, 200, 20) @@ -125,7 +130,12 @@ plt.show() ``` -Now we define the utility function and the McCall model data structure: +Now we organize the code for solving the McCall model, given a set of parameters. + +For background on the model and our solution method, see the {doc}`lecture on the McCall model with separation ` + +Our first step is to define the utility function and the McCall model data structure. + ```{code-cell} ipython3 def u(c, σ=2.0): @@ -188,11 +198,11 @@ def solve_mccall_model(mcm: McCallModel, tol=1e-5, max_iter=2000): """ Iterates to convergence on the Bellman equations. """ - def cond_fun(state): + def cond(state): V, U, i, error = state return jnp.logical_and(error > tol, i < max_iter) - def body_fun(state): + def update(state): V, U, i, error = state V_new, U_new = T(mcm, V, U) error_1 = jnp.max(jnp.abs(V_new - V)) @@ -208,7 +218,7 @@ def solve_mccall_model(mcm: McCallModel, tol=1e-5, max_iter=2000): init_state = (V_init, U_init, i_init, error_init) V_final, U_final, _, _ = jax.lax.while_loop( - cond_fun, body_fun, init_state + cond, update, init_state ) return V_final, U_final ``` @@ -312,6 +322,20 @@ Here * $\bar w$ is the reservation wage determined by the parameters and * $p$ is the wage offer distribution. +Wage offers across the population of workers are independent draws from $p$. + +Here we calculate $\lambda$ at the default parameters: + + +```{code-cell} ipython3 +mcm = create_mccall_model(w_vec=w_vec, p_vec=p_vec) +V, U = solve_mccall_model(mcm) +w_idx = jnp.searchsorted(V - U, 0) +w_bar = jnp.where(w_idx == len(V), jnp.inf, mcm.w_vec[w_idx]) +λ = γ * jnp.sum(p_vec * (w_vec > w_bar)) +print(f"Job finding rate at default paramters ={λ}.") +``` + ## Fiscal policy @@ -371,10 +395,9 @@ Now we set up the infrastructure to compute optimal unemployment insurance level First, we define a container for the economy's parameters: ```{code-cell} ipython3 -class EconomyParameters(NamedTuple): +class Economy(NamedTuple): """Parameters for the economy""" α: float - α_q: float # Quarterly (α is monthly) b: float d: float β: float @@ -384,14 +407,20 @@ class EconomyParameters(NamedTuple): wage_grid_size: int max_wage: float -def create_economy_params(α=0.013, b=0.0124, d=0.00822, - β=0.98, γ=1.0, σ=2.0, - log_wage_mean=20, - wage_grid_size=200, - max_wage=170) -> EconomyParameters: - """Create economy parameters with default values""" - α_q = (1-(1-α)**3) # Convert monthly to quarterly - return EconomyParameters(α=α, α_q=α_q, b=b, d=d, β=β, γ=γ, σ=σ, +def create_economy( + α=0.013, + b=0.0124, + d=0.00822, + β=0.98, + γ=1.0, + σ=2.0, + log_wage_mean=20, + wage_grid_size=200, + max_wage=170 + ) -> Economy: + """ + Create an economy with a set of default values""" + return Economy(α=α, b=b, d=d, β=β, γ=γ, σ=σ, log_wage_mean=log_wage_mean, wage_grid_size=wage_grid_size, max_wage=max_wage) @@ -401,51 +430,69 @@ Next, we define a function that computes optimal worker behavior given policy pa ```{code-cell} ipython3 @jax.jit -def compute_optimal_quantities(c, τ, - params: EconomyParameters, w_vec, p_vec): +def compute_optimal_quantities( + c: float, + τ: float, + economy: Economy, + w_vec: jnp.array, + p_vec: jnp.array + ): """ Compute the reservation wage, job finding rate and value functions of the workers given c and τ. + """ mcm = create_mccall_model( - α=params.α_q, - β=params.β, - γ=params.γ, + α=economy.α, + β=economy.β, + γ=economy.γ, c=c-τ, # Post tax compensation - σ=params.σ, + σ=economy.σ, w_vec=w_vec-τ, # Post tax wages p_vec=p_vec ) + # Compute reservation wage under given parameters V, U = solve_mccall_model(mcm) w_idx = jnp.searchsorted(V - U, 0) w_bar = jnp.where(w_idx == len(V), jnp.inf, mcm.w_vec[w_idx]) - λ = params.γ * jnp.sum(p_vec * (w_vec - τ > w_bar)) + # Compute job finding rate + λ = economy.γ * jnp.sum(p_vec * (w_vec - τ > w_bar)) + return w_bar, λ, V, U ``` + This function computes the steady state outcomes given unemployment insurance and tax levels: ```{code-cell} ipython3 @jax.jit -def compute_steady_state_quantities(c, τ, - params: EconomyParameters, w_vec, p_vec): +def compute_steady_state_quantities( + c, τ, economy: Economy, w_vec, p_vec + ): """ Compute the steady state unemployment rate given c and τ using optimal quantities from the McCall model and computing corresponding steady state quantities + """ - w_bar, λ, V, U = compute_optimal_quantities(c, τ, - params, w_vec, p_vec) - # Compute steady state employment and unemployment rates - model = create_lake_model(λ=λ, α=params.α_q, b=params.b, d=params.d) + # Find optimal values and policies by solving the McCall model, as well + # as the corresponding job finding rate. + w_bar, λ, V, U = compute_optimal_quantities(c, τ, economy, w_vec, p_vec) + + # Set up a lake model using the given parameters and the job finding rate. + model = create_lake_model(λ=λ, α=economy.α, b=economy.b, d=economy.d) + + # Compute steady state employment and unemployment rates from this lake + model. u, e = rate_steady_state(model) - # Compute steady state welfare + # Compute expected lifetime value conditional on being employed. mask = (w_vec - τ > w_bar) w = jnp.sum(V * p_vec * mask) / jnp.sum(p_vec * mask) + # Compute steady state welfare. welfare = e * w + u * U return e, u, welfare @@ -454,25 +501,30 @@ def compute_steady_state_quantities(c, τ, We need a function to find the tax rate that balances the government budget: ```{code-cell} ipython3 -def find_balanced_budget_tax(c, params: EconomyParameters, - w_vec, p_vec): +def find_balanced_budget_tax(c, economy: Economy, w_vec, p_vec): """ - Find the tax level that will induce a balanced budget + Find the tax rate that will induce a balanced budget given unemployment + compensation c. + """ + def steady_state_budget(t): - e, u, w = compute_steady_state_quantities(c, t, - params, w_vec, p_vec) + """ + For given tax rate t, compute the budget surplus. + + """ + e, u, w = compute_steady_state_quantities(c, t, economy, w_vec, p_vec) return t - u * c - # Use a simple bisection method + # Use a simple bisection method to find the tax rate that balances the + # budget (but setting the surplus to zero + t_low, t_high = 0.0, 0.9 * c tol = 1e-6 max_iter = 100 - for i in range(max_iter): t_mid = (t_low + t_high) / 2 budget = steady_state_budget(t_mid) - if abs(budget) < tol: return t_mid elif budget < 0: @@ -486,14 +538,14 @@ def find_balanced_budget_tax(c, params: EconomyParameters, Now we compute how employment, unemployment, taxes, and welfare vary with the unemployment compensation rate: ```{code-cell} ipython3 -# Create economy parameters and wage distribution -params = create_economy_params() -w_vec, p_vec = create_wage_distribution(params.max_wage, - params.wage_grid_size, - params.log_wage_mean) +# Create economy and wage distribution +economy = create_economy() +w_vec, p_vec = create_wage_distribution( + economy.max_wage, economy.wage_grid_size, economy.log_wage_mean +) # Levels of unemployment insurance we wish to study -c_vec = jnp.linspace(5, 140, 60) +c_vec = jnp.linspace(5, 140, 40) tax_vec = [] unempl_vec = [] @@ -501,9 +553,10 @@ empl_vec = [] welfare_vec = [] for c in c_vec: - t = find_balanced_budget_tax(c, params, w_vec, p_vec) - e_rate, u_rate, welfare = compute_steady_state_quantities(c, t, params, - w_vec, p_vec) + t = find_balanced_budget_tax(c, economy, w_vec, p_vec) + e_rate, u_rate, welfare = compute_steady_state_quantities( + c, t, economy, w_vec, p_vec + ) tax_vec.append(t) unempl_vec.append(u_rate) empl_vec.append(e_rate) @@ -535,11 +588,14 @@ The level that maximizes steady state welfare is approximately 62. ```{exercise} :label: endogenous_lake_ex1 -How does the welfare-maximizing level of unemployment compensation $c$ change with the job separation rate $\alpha$? +How does the welfare-maximizing level of unemployment compensation $c$ change +with the job separation rate $\alpha$? -Compute and plot the optimal $c$ (the value that maximizes welfare) for a range of separation rates $\alpha$ from 0.01 to 0.025. +Compute and plot the optimal $c$ (the value that maximizes welfare) for a range +of separation rates $\alpha$ from 0.01 to 0.04. -For each $\alpha$ value, find the optimal $c$ by computing welfare across the range of $c$ values and selecting the maximum. +For each $\alpha$ value, find the optimal $c$ by computing welfare across the +range of $c$ values and selecting the maximum. ``` ```{solution-start} endogenous_lake_ex1 @@ -549,32 +605,46 @@ For each $\alpha$ value, find the optimal $c$ by computing welfare across the ra Here is one solution: ```{code-cell} ipython3 -# Range of separation rates to explore -α_values = jnp.linspace(0.01, 0.025, 15) +# Range of separation rates to explore (wider range, fewer points) +α_values = jnp.linspace(0.01, 0.04, 8) # We'll store the optimal c for each α optimal_c_values = [] +# Use a finer grid for c values to get better resolution +c_vec_fine = jnp.linspace(5, 140, 150) + for α_val in α_values: # Create economy parameters with this α - params_α = create_economy_params(α=α_val) + params_α = create_economy(α=α_val) # Create wage distribution - w_vec_α, p_vec_α = create_wage_distribution(params_α.max_wage, - params_α.wage_grid_size, - params_α.log_wage_mean) + w_vec_α, p_vec_α = create_wage_distribution( + params_α.max_wage, params_α.wage_grid_size, params_α.log_wage_mean + ) # Compute welfare for each c value welfare_values = [] - for c in c_vec: + for c in c_vec_fine: t = find_balanced_budget_tax(c, params_α, w_vec_α, p_vec_α) - e_rate, u_rate, welfare = compute_steady_state_quantities(c, t, params_α, - w_vec_α, p_vec_α) + e_rate, u_rate, welfare = compute_steady_state_quantities( + c, t, params_α, w_vec_α, p_vec_α + ) welfare_values.append(welfare) - # Find the c that maximizes welfare - max_idx = jnp.argmax(jnp.array(welfare_values)) - optimal_c = c_vec[max_idx] + # The welfare function is very flat near its maximum. + # Using argmax on a single point can be unstable due to numerical noise. + # Instead, we find all c values within 99.9% of maximum welfare and + # compute their weighted average (centroid). This gives a more stable + # estimate of the optimal unemployment compensation level. + welfare_array = jnp.array(welfare_values) + max_welfare = jnp.max(welfare_array) + threshold = 0.999 * max_welfare + near_optimal_mask = welfare_array >= threshold + + # Compute weighted average of c values in the near-optimal region + optimal_c = jnp.sum(c_vec_fine * near_optimal_mask * welfare_array) / \ + jnp.sum(near_optimal_mask * welfare_array) optimal_c_values.append(optimal_c) # Plot the relationship @@ -588,7 +658,14 @@ plt.tight_layout() plt.show() ``` -We see that as the separation rate increases (workers lose their jobs more frequently), the welfare-maximizing level of unemployment compensation also increases. This makes intuitive sense: when job loss is more common, more generous unemployment insurance becomes more valuable for smoothing consumption and maintaining worker welfare. +We see that as the separation rate increases (workers lose their jobs more +frequently), the welfare-maximizing level of unemployment compensation +decreases. + +This occurs because higher separation rates increase steady-state unemployment, +which raises the tax burden needed to finance unemployment benefits. The +optimal policy balances insurance against distortionary taxation. + ```{solution-end} ``` From c50e976eeb3006a252d380a31ccafa20033722ea Mon Sep 17 00:00:00 2001 From: Matt McKay Date: Tue, 4 Nov 2025 21:14:57 +1100 Subject: [PATCH 6/7] Update lectures/endogenous_lake.md --- lectures/endogenous_lake.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lectures/endogenous_lake.md b/lectures/endogenous_lake.md index c8b2e0bbc..42e0bca98 100644 --- a/lectures/endogenous_lake.md +++ b/lectures/endogenous_lake.md @@ -332,7 +332,7 @@ mcm = create_mccall_model(w_vec=w_vec, p_vec=p_vec) V, U = solve_mccall_model(mcm) w_idx = jnp.searchsorted(V - U, 0) w_bar = jnp.where(w_idx == len(V), jnp.inf, mcm.w_vec[w_idx]) -λ = γ * jnp.sum(p_vec * (w_vec > w_bar)) +λ = mcm.γ * jnp.sum(p_vec * (w_vec > w_bar)) print(f"Job finding rate at default paramters ={λ}.") ``` From dcea44f6846357c6477f6d8bd12f319adb395b3b Mon Sep 17 00:00:00 2001 From: John Stachurski Date: Wed, 5 Nov 2025 03:49:32 +0900 Subject: [PATCH 7/7] Fix syntax error in endogenous_lake.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed incomplete statement `model.` on line 489 that was causing a syntax error when converting to Python with jupytext. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- lectures/endogenous_lake.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lectures/endogenous_lake.md b/lectures/endogenous_lake.md index 42e0bca98..fed4191a5 100644 --- a/lectures/endogenous_lake.md +++ b/lectures/endogenous_lake.md @@ -486,7 +486,7 @@ def compute_steady_state_quantities( model = create_lake_model(λ=λ, α=economy.α, b=economy.b, d=economy.d) # Compute steady state employment and unemployment rates from this lake - model. + # model. u, e = rate_steady_state(model) # Compute expected lifetime value conditional on being employed.