33. Job Search V: On-the-Job Search#
Contents
33.1. Overview#
33.1.1. Model features#
job-specific human capital accumulation combined with on-the-job search
infinite horizon dynamic programming with one state variable and two controls
using LinearAlgebra, Statistics
using Distributions, Interpolations
using FastGaussQuadrature, SpecialFunctions
using LaTeXStrings, Plots, NLsolve, Random
33.2. Model#
Let
denote the time- job-specific human capital of a worker employed at a given firm denote current wages
Let
is investment in job-specific human capital for the current role is search effort, devoted to obtaining new offers from other firms
For as long as the worker remains in the current job, evolution of
When search effort at
Value of offer is
Worker has the right to reject the current offer and continue with existing job.
In particular,
Letting
Agent’s objective: maximize expected discounted sum of wages via controls
Taking the expectation of
Here nonnegativity of
33.2.1. Parameterization#
In the implementation below, we will focus on the parameterization.
with default parameter values
The Beta(2,2) distribution is supported on
33.2.2. Quadrature#
In order to calculate expectations over the continuously valued
Gaussian Quadrature methods use orthogonal polynomials to generate
Here we will use Gauss-Jacobi Quadrature which is ideal for expectations over beta.
See quadrature and interpolation for details on the derivation in this particular case.
function gauss_jacobi(F::Beta, N)
s, wj = FastGaussQuadrature.gaussjacobi(N, F.β - 1, F.α - 1)
x = (s .+ 1) ./ 2
C = 2.0^(-(F.α + F.β - 1.0)) / SpecialFunctions.beta(F.α, F.β)
w = C .* wj
return x, w
end
f(x) = x^2
F = Beta(2, 2)
x, w = gauss_jacobi(F, 20)
# compare to monte-carlo integration
@show dot(w, f.(x)), mean(f.(rand(F, 1000)));
(dot(w, f.(x)), mean(f.(rand(F, 1000)))) = (0.3, 0.30202367170400946)
33.2.3. Back-of-the-Envelope Calculations#
Before we solve the model, let’s make some quick calculations that provide intuition on what the solution should look like.
To begin, observe that the worker has two instruments to build capital and hence wages:
invest in capital specific to the current job via
search for a new job with better job-specific capital match via
Since wages are
Our risk neutral worker should focus on whatever instrument has the highest expected return.
The relative expected return will depend on
For example, suppose first that
If
and , then since , taking expectations of (33.1) gives expected next period capital equal to .If
and , then next period capital is .
Both rates of return are good, but the return from search is better.
Next suppose that
If
and , then expected next period capital is againIf
and , then
Return from investment via
Combining these observations gives us two informal predictions:
At any given state
, the two controls and will function primarily as substitutes — worker will focus on whichever instrument has the higher expected return.For sufficiently small
, search will be preferable to investment in job-specific human capital. For larger , the reverse will be true.
Now let’s turn to implementation, and see if we can match our predictions.
33.3. Implementation#
The following code solves the DP problem described above
function jv_worker(; A = 1.4,
alpha = 0.6,
beta = 0.96,
grid_size = 50,
quad_size = 30,
epsilon = 1e-4)
G(x, phi) = A .* (x .* phi) .^ alpha
pi_func = sqrt
F = Beta(2, 2)
# Discretize the grid using Gauss-Jacobi quadrature
# u are nodes, w are weights.
u, w = gauss_jacobi(F, quad_size)
# Set up grid over the state space for DP
# Max of grid is the max of a large quantile value for F and the
# fixed point y = G(y, 1).
grid_max = max(A^(1.0 / (1.0 - alpha)), quantile(F, 1 - epsilon))
# range for range(epsilon, grid_max, grid_size). Needed for
# CoordInterpGrid below
x_grid = range(epsilon, grid_max, length = grid_size)
return (; A, alpha, beta, x_grid, G,
pi_func, F, u, w, epsilon)
end
function T!(jv, V, new_V)
# simplify notation
(; G, pi_func, beta, u, w, epsilon) = jv
# prepare interpoland of value function
Vf = LinearInterpolation(jv.x_grid, V, extrapolation_bc = Line())
# instantiate the linesearch variables
max_val = -1.0
cur_val = 0.0
max_s = 1.0
max_phi = 1.0
search_grid = range(epsilon, 1.0, length = 15)
# objective function
function w_x(x, s, phi)
h(u_val) = Vf(max(G(x, phi), u_val))
integral = dot(h.(u), w) # using quadrature weights/values
q = pi_func(s) * integral + (1.0 - pi_func(s)) * Vf(G(x, phi))
return -x * (1.0 - phi - s) - beta * q
end
for (i, x) in enumerate(jv.x_grid)
for s in search_grid
for phi in search_grid
cur_val = ifelse(s + phi <= 1.0, -w_x(x, s, phi), -1.0)
if cur_val > max_val
max_val, max_s, max_phi = cur_val, s, phi
end
end
end
new_V[i] = max_val
end
end
function T!(jv, V, out::Tuple)
# simplify notation
(; G, pi_func, beta, u, w, epsilon) = jv
# prepare interpoland of value function
Vf = LinearInterpolation(jv.x_grid, V, extrapolation_bc = Line())
# instantiate variables
s_policy, phi_policy = out[1], out[2]
# instantiate the linesearch variables
max_val = -1.0
cur_val = 0.0
max_s = 1.0
max_phi = 1.0
search_grid = range(epsilon, 1.0, length = 15)
# objective function
function w_x(x, s, phi)
h(u) = Vf(max(G(x, phi), u))
integral = dot(h.(u), w)
q = pi_func(s) * integral + (1.0 - pi_func(s)) * Vf(G(x, phi))
return -x * (1.0 - phi - s) - beta * q
end
for (i, x) in enumerate(jv.x_grid)
for s in search_grid
for phi in search_grid
cur_val = ifelse(s + phi <= 1.0, -w_x(x, s, phi), -1.0)
if cur_val > max_val
max_val, max_s, max_phi = cur_val, s, phi
end
end
end
s_policy[i], phi_policy[i] = max_s, max_phi
end
end
function T(jv, V; ret_policies = false)
out = ifelse(ret_policies, (similar(V), similar(V)), similar(V))
T!(jv, V, out)
return out
end
T (generic function with 1 method)
The code is written to be relatively generic—and hence reusable.
For example, we use generic
instead of specific .
Regarding the imports
fixed_quadis a simple non-adaptive integration routinefmin_slsqpis a minimization routine that permits inequality constraints
Next we write a constructor called jv_worker that
packages all the parameters and other basic attributes of a given model
implements the method
Tfor value function iteration
The T method
takes a candidate value function
where
Here we are minimizing instead of maximizing to fit with optimization routines.
When we represent V giving values on grid x_grid.
But to evaluate the right-hand side of (33.3), we need a function, so
we replace the arrays V and x_grid with a function Vf that gives linear
interpolation of V on x_grid.
Hence in the preliminaries of T
from the array
Vwe define a linear interpolationVfof its valuesc1is used to implement the constraintc2is used to implement , a numerically stablealternative to the true constraint
c3does the same for
Inside the for loop, for each x in the grid over the state space, we
set up the function
The function is minimized over all feasible
The latter is much faster, but convergence to the global optimum is not guaranteed. Grid search is a simple way to check results.
33.4. Solving for Policies#
Let’s plot the optimal policies and see what they look like.
The code is as follows
wp = jv_worker(; grid_size = 25)
v_init = collect(wp.x_grid) .* 0.5
f(x) = T(wp, x)
V = fixedpoint(f, v_init)
sol_V = V.zero
s_policy, phi_policy = T(wp, sol_V, ret_policies = true)
# plot solution
p = plot(wp.x_grid, [phi_policy s_policy sol_V],
title = [L"$\phi$ policy" L"$s$ policy" "value function"],
color = [:orange :blue :green],
xaxis = (L"x", (0.0, maximum(wp.x_grid))),
yaxis = ((-0.1, 1.1)), size = (800, 800),
legend = false, layout = (3, 1),
bottom_margin = Plots.PlotMeasures.Length(:mm, 20))
The horizontal axis is the state
Overall, the policies match well with our predictions from section.
Worker switches from one investment strategy to the other depending on relative return.
For low values of
, the best option is to search for a new job.Once
is larger, worker does better by investing in human capital specific to the current position.
33.5. Exercises#
33.5.1. Exercise 1#
Let’s look at the dynamics for the state process
The dynamics are given by (33.1) when
Since the dynamics are random, analysis is a bit subtle.
One way to do it is to plot, for each plot_grid, a
large number
K = 50
plot_grid_max, plot_grid_size = 1.2, 100
plot_grid = range(0, plot_grid_max, length = plot_grid_size)
plot(plot_grid, plot_grid, color = :black, linestyle = :dash,
lims = (0, plot_grid_max), legend = :none)
By examining the plot, argue that under the optimal policies, the state
Argue that at the steady state,
33.5.2. Exercise 2#
In the preceding exercise we found that
Since these results were calculated at a value of
Intuitively, an infinitely patient worker would like to maximize steady state wages, which are a function of steady state capital.
You can take it as given—it’s certainly true—that the infinitely patient worker does not
search in the long run (i.e.,
Thus, given
Steady state wages can be written as
Graph
Can you give a rough interpretation for the value that you see?
33.6. Solutions#
33.6.1. Exercise 1#
Here’s code to produce the 45 degree diagram
wp = jv_worker(grid_size = 25)
# simplify notation
(; G, pi_func, F) = wp
v_init = collect(wp.x_grid) * 0.5
f2(x) = T(wp, x)
V2 = fixedpoint(f2, v_init)
sol_V2 = V2.zero
s_policy, phi_policy = T(wp, sol_V2, ret_policies = true)
# Turn the policy function arrays into CoordInterpGrid objects for interpolation
s = LinearInterpolation(wp.x_grid, s_policy, extrapolation_bc = Line())
phi = LinearInterpolation(wp.x_grid, phi_policy, extrapolation_bc = Line())
h_func(x, b, U) = (1 - b) * G(x, phi(x)) + b * max(G(x, phi(x)), U)
h_func (generic function with 1 method)
using Random
Random.seed!(42)
K = 50
plot_grid_max, plot_grid_size = 1.2, 100
plot_grid = range(0, plot_grid_max, length = plot_grid_size)
ticks = [0.25, 0.5, 0.75, 1.0]
xs = []
ys = []
for x in plot_grid
for i in 1:K
b = rand() < pi_func(s(x)) ? 1 : 0
U = rand(wp.F)
y = h_func(x, b, U)
push!(xs, x)
push!(ys, y)
end
end
plot(plot_grid, plot_grid, color = :black, linestyle = :dash, legend = :none)
scatter!(xs, ys, alpha = 0.25, color = :green, lims = (0, plot_grid_max),
ticks = ticks)
plot!(xlabel = L"x_t", ylabel = L"x_{t+1}", guidefont = font(16))
Looking at the dynamics, we can see that
If
is below about 0.2 the dynamics are random, but is very likelyAs
increases the dynamics become deterministic, and converges to a steady state value close to 1
Referring back to the figure here.
[ref]section <jv_solve>
we see that
33.6.2. Exercise 2#
wp = jv_worker(grid_size = 25)
xbar(phi) = (wp.A * phi^wp.alpha)^(1.0 / (1.0 - wp.alpha))
phi_grid = range(0, 1, length = 100)
plot(phi_grid, [xbar(phi) * (1 - phi) for phi in phi_grid], color = :blue,
label = L"w^\phi", legendfont = font(12), xlabel = L"\phi",
guidefont = font(16), grid = false, legend = :topleft)
Observe that the maximizer is around 0.6.
This this is similar to the long run value for
Hence the behaviour of the infinitely patent worker is similar to that
of the worker with
This seems reasonable, and helps us confirm that our dynamic programming solutions are probably correct.