Week 8 — Economic Models — Neoclassical Growth via Value Function Iteration
Programming and Numerical Methods for Economics (ECNM10115) · The University of Edinburgh
Learning goals. Solving the deterministic neoclassical growth model by value function iteration — the workhorse method of modern macro.
How to work through this notebook: run every cell in order (Shift+Enter). When you reach a result, pause and predict it before running — that habit is what turns reading into learning. Experiment: change parameters, break things, re-run.
Neoclassical Growth Model: Deterministic
We have the recursive formulation \[\begin{equation}
\begin{aligned}
v(k) =&
\max_{c, k'} && u(c) + \beta v(k') \\
& \text{s.t. }
&& c + k' \leq A k^\alpha + (1-\delta) k
\end{aligned}
\end{equation}\]
First, let’s code up our VFI solution algorithm. We split it into two steps: 1. An update step, where we re-solve the maximization problem 2. An outer loop, where we repeatedly call step 1, and check for convergence
# First some package housekeeping -- make sure our environment is activatedusingPkgPkg.activate(@__DIR__)# Pkg.instantiate()# Load packagesusingParametersusingQuantEconusingPlotsusingFormat
Activating new project at `C:\Users\jzurita\OneDrive - University of Edinburgh\Courses\Programming Numerical Methods\Week 8`
First, we’ll show how this looks with a ‘’vectorized’’ grid search. Note that this is not the most efficient strategy to implement even a grid search in Julia. However, it does have the virtue of being easy to translate to other programming languages. The downside of doing it this way is that we are allocating memory for many new vectors inside the loop. On modern computers, the process of allocating memory is typically very slow, and can be a large performance bottleneck in your code.
# Parametersp = ( β =0.9, # Discount Rate δ =0.1, # Depreciation of capital α =0.5, # Returns to scale A =1.0# Productivity)u(c) = c >0 ? log(c) :-Inffunctionupdate_bellman!(p, V, policy, kgrid, V0)@unpack A, β, δ, α = pfor i in1:length(kgrid) k = kgrid[i] z = A * k^α + (1-δ) * k# Vectorized grid search -- we can do this because kgrid is the x-axis# of our guess V0# V0[i] = v_0(kgrid[i]) c = z .- kgrid # vector vmax, ki′ =findmax(u.(c) .+ β .* V0) k′ = kgrid[ki′] V[i] = vmax policy.ki[i] = ki′ policy.k[i] = kgrid[ki′] policy.c[i] = c[ki′]endend
update_bellman! (generic function with 1 method)
A more sensible approach for a compiled language like Julia is to just write the loop. This will be faster in general (since it’s not allocating memory – the only new variables that get constructed in the loop are scalars) but is also much easier to read and understand, since you don’t have to spend a lot of time and mental energy trying to work out how the indices fit together.
In principle, you can also add other optimizations (like stopping the grid search early if the objective function starts to go down, which is a valid strategy for strictly concave objective functions) in a much more straightforward way.
functionupdate_bellman!(p, V, policy, kgrid, V0)@unpack A, β, δ, α = pfor i in1:length(kgrid) k = kgrid[i] z = A * k^α + (1-δ) * k vmax =-Inf ki′ =0for j in1:length(kgrid) k′ = kgrid[j] c = z - k′ v =u(c) + β * V0[j] # V0[j] = V_0(k_j)if v >= vmax vmax = v ki′ = jendend V[i] = vmax policy[i] = ki′endend
update_bellman! (generic function with 1 method)
functionsolve!(p, kgrid, V0; tol =1e-12) V =similar(V0) policy =zeros(Int, size(V0)) errors =Float64[] iter =0whiletrue# Update our value function iter +=1update_bellman!(p, V, policy, kgrid, V0) # calculate and save the errors ϵ =maximum(abs.(V .- V0))push!(errors, ϵ)# stop if we've converged ϵ < tol &&break V0 .= Vendreturn (; V, policy, iter, errors)endabsolute_error(X,X0) =mapreduce(max, X, X0) do x, x0abs(x-x0)end
absolute_error (generic function with 1 method)
Let’s plot how the errors fall over the course of our iterations:
n =1000kgrid =LinRange(1e-4, 10, n)V0 =zeros(n)solution =solve!(p, kgrid, V0)@unpack errors = solution plot(1:length(errors), errors, yscale =:log10, title ="Convergence of VFI", xlabel ="Number of Iterations", ylabel =raw"$||v_s - v_{s-1}||$", yticks =10.0.^(0:-2:-12))
Let’s try to add some policy function iteration steps, to see if that improves things…
Remember, when we do a policy iteration step, we will use the policy functions we calculated in our VFI step, and then apply them repeatedly.
functionpolicy_step!(p, V, policy, kgrid, V0)@unpack α, A, β, δ = pfor i in1:length(kgrid)# Current capital and savings for tomorrow k = kgrid[i] k′ = kgrid[policy[i]]# Current cash on hands, and consumption z = A * k^α + (1-δ) * k c = z - k′# Update value function V[i] =u(c) + β * V0[policy[i]]endendfunctionsolve_pfi!(p, kgrid, V0; tol =1e-12, policy_steps =0, maxiter =1000) V0 =copy(V0) V =similar(V0) Vs =similar(V0) policy =zeros(Int, size(V0)) errors =Float64[] iter =0whiletrue# Update our value function iter +=1# V0 ~ V_{s-1}(k)# V ~ V_{s}^0(k)update_bellman!(p, Vs, policy, kgrid, V0) V .= Vs # this is redundant, but makes this code work if policy_steps = 0# Iterate on the policy rule for i in1:policy_steps# Vs ~ V_s^{j-1}# V ~ V_s^jpolicy_step!(p, V, policy, kgrid, Vs) Vs .= Vend# calculate and save the errors ϵ =absolute_error(V, V0)push!(errors, ϵ)# stop if we've converged ϵ < tol &&break iter > maxiter &&break V0 .= Vendreturn (; V, policy, iter, errors)end
solve_pfi! (generic function with 1 method)
n =1000solution_vfi =solve_pfi!(p, LinRange(1e-4, 10, n), zeros(n))@unpack errors = solution_vfiplt =plot(1:length(errors), errors, yscale =:log10, title ="Convergence of VFI vs. PFI", label ="VFI", xlabel ="Number of Iterations", ylabel =raw"$||v_s - v_{s-1}||$", yticks =10.0.^(0:-2:-12))# solution_vfi = solve_pfi!(p, LinRange(1e-4, 10, n), zeros(n); policy_steps = 100)for k in (2,5,10) solution_pfi =solve_pfi!(p, LinRange(1e-4, 10, n), zeros(n), policy_steps = k)@unpack errors = solution_pfiplot!(1:length(errors), errors, label ="PFI: $k steps" )enddisplay(plt)
@info"Value Function Iteration"@time solution_vfi =solve_pfi!(p, LinRange(1e-4, 10, n), zeros(n))@info"PFI" k =5@time solution_vfi =solve_pfi!(p, LinRange(1e-4, 10, n), zeros(n), policy_steps =5)@info"PFI" k =100@time solution_vfi =solve_pfi!(p, LinRange(1e-4, 10, n), zeros(n), policy_steps =100)
5.434693 seconds (290 allocations: 2.201 MiB)
[ Info: Value Function Iteration
┌ Info: PFI
└ k = 5
Let’s go back to the version we saw at the end of lecture, where productivity \(A\) is a log-normal AR(1) process. \[\begin{equation}
\begin{aligned}
v(k, A) =&
\max_{c, k'} \;\;
& u(c) + &\beta \mathbb E\left[
v(k', A') \middle | A
\right] \\
& \text{s.t. }
& c + k' &\leq A k^\alpha + (1-\delta) k \\
&& \log(A') &= \rho \log(A) + \epsilon \\
&& \epsilon &\sim N(0, \sigma)
\end{aligned}
\end{equation}\] Let’s code this up, using the method we discussed in class (discretizing the TFP process with Rouwenhorst’s method)
Now let’s solve it!
functionsolve_vfi2!(p, kgrid, V0; tol =1e-12, maxiter =1000, debug=false)@unpack Na, ρ, σ = p V0 =copy(V0) V =zeros(size(V0)) policy =zeros(Int, size(V0)) errors =Float64[] iter =0# Discretize the income process mc =rouwenhorst(Na, ρ, σ) P = mc.p' agrid = mc.state_values grids = (; kgrid, agrid)whiletrue iter +=1# Step 1: Calculate Expectations EV = V * P# Step 2: Update Bellman Equationupdate_bellman2!(p, V, policy, grids, EV)# Step 3: Check for convergence ϵ =maximum(abs.(V - V0))push!(errors, ϵ) ϵ < tol &&break iter >= maxiter &&break V0 .= Vendreturn (; V, policy, iter, errors, kgrid, agrid, P)endfunctionupdate_bellman2!(p, V, policy, grids, EV)@unpack kgrid, agrid = grids @unpack α, δ, β = p # Check all the dimensions are rightlength(kgrid) ==size(V,1) ||throw(error("capital grid size doesn't match V"))length(agrid) ==size(V,2) ||throw(error("TFP grid size doesn't match V"))size(EV) ==size(V) ||throw(error("EV and V aren't the same size"))# Loop over all the indices of V for sub inCartesianIndices(V)# Unpack everything ki, ai = sub[1], sub[2] k = kgrid[ki] A =exp(agrid[ai])# Cash on hands z = A * k^α + (1-δ) * k k′ = kgrid c = z .- k′# Do a vectorized grid search # Note: the macro views makes this run a bit faster but otherwise doesn't change anything vmax, pol =@viewsfindmax(u.(c) .+ β .* EV[:, ai])# Store the max values and policies V[sub] = vmax policy[sub] = polendreturnend
update_bellman2! (generic function with 1 method)
## Run the code # Setup parametersNk =100Na =30p = (; β =0.9, δ =0.1, α =0.5, ρ =0.7, σ =0.1, Na)V0 =zeros(Nk, Na)kgrid =LinRange(1e-4, 50, Nk)# Solve the model @time solution =solve_vfi2!(p, kgrid, V0)@unpack V, policy, errors = solution
6.740031 seconds (3.41 M allocations: 906.843 MiB, 9.04% gc time, 82.39% compilation time)
plot(1:length(errors), errors, yscale =:log10, title ="Convergence in Stochastic Case", xlabel ="Iterations", ylabel =raw"$||v_s - v_{s-1}||$", label ="VFI",# yticks = 10.0.^(0:-2:-10))
Policy Functions
Let’s plot the optimal policy functions. That is, we want to show \(c(k, A)\) and \(k'(k,A)\) that solve our maximization problem.
Note that since we now have a two dimensional state space, we will need to plot these policy functions holding one of the states constant. (In principle, you can do 2d surface plots, but I find they’re not very helpful). So, we’ll plot \(c(k, A_i)\) for several different fixed TFP values \(A_i\)
@unpack α, δ = p@unpack kgrid, agrid = solutionk′ = kgrid[solution.policy]z =exp.(agrid') .* kgrid.^α .+ (1-δ) .* kgridc = z .- k′# Consumption/investment share cs = c./zks = k′./z## First let's plot consumption policy for a variety of TFP values AVals = [1, 10, 20, 30]p1 =plot(legend=:outerbottom, legend_column =-1)for ai in AValsplot!(kgrid, c[:, ai], label =format("A = {:.2f}", exp(agrid[ai])))endtitle!("Consumption Policy")ylabel!(raw"$c(k)$")xlabel!(raw"$k$")## Next let's plot investment policy for the same TFP valuesp2 =plot(legend=:outerbottom, legend_column =-1)for ai in AValsplot!(kgrid, k′[:, ai], label =format("A = {:.2f}", exp(agrid[ai])))endtitle!("Investment Policy")ylabel!(raw"$k'(k)$")xlabel!(raw"$k$")plt =plot(p1, p2, size = (800, 400), layout = (1,2))
LoadError: BoundsError: attempt to access 100×30 Matrix{Float64} at index [1:100, 100]
BoundsError: attempt to access 100×30 Matrix{Float64} at index [1:100, 100]
Stacktrace:
[1] throw_boundserror(A::Matrix{Float64}, I::Tuple{Base.Slice{Base.OneTo{Int64}}, Int64})
@ Base .\abstractarray.jl:737
[2] checkbounds
@ .\abstractarray.jl:702 [inlined]
[3] _getindex
@ .\multidimensional.jl:888 [inlined]
[4] getindex(::Matrix{Float64}, ::Function, ::Int64)
@ Base .\abstractarray.jl:1291
[5] top-level scope
@ .\In[33]:15
Note: I had to drive \(N_k\) up to 600 to make these policy functions look relatively smooth (and even now, if you look closely you can see the jagged edges), and that make our solution take almost 30 seconds. Try it with 100. What does it look like? Does it matter if the policy functions are noisy?
Try running this code with an upper bound on \(k\) that is smaller. What does it look like if you use \(\overline k = 10\)? Is that a problem for us?
(100,)
—Note: this tutorial notebook is written in Julia (outputs are embedded, so you can read everything here). To run it yourself, install Julia and IJulia — or focus on the logic, which carries over to Python directly. Work through PS7 with your group.Next week: model estimation.