So far every dynamic choice in the course was discrete, and the maximum in the Bellman equation was taken over a short list. With a continuous choice the maximum is an optimization problem in every state! We will learn a few solution techniques for the models with continuous choice in the next few classes before addressing the estimation of these models. Along the way we need to remind ourselves of function approximation.
Running the code for this lecture
The code for this class is in the folder session15-oct15/ of the course code
repository.
You should have cloned that repository already β if not, the instructions are in the algorithms and complexity lecture.
Update your copy before the class. Editing a file in place makes git pull refuse to
update it, so discard whatever you changed while experimenting:
cd sb-dse-code
git reset --hard HEAD
git pullNothing in this repository is submitted, so experiment freely β but copy anything you want to keep out of it first, or commit that to a branch of your own.
Setting up the Python environment is covered in Python.
Homework iota_deaton: the Deaton model with discretized choice
Graded homework: task iota_deaton in the class repository. Collect it and work on a
copy in your own repository:
git pull upstream main # collect the task
cp -r tasks/iota_deaton solutions/iota_deaton # work on the copyImplement the consumption-savings model with the formulas of this class and solve it with the two solvers with discretized choice, on the grid and with a separate choice grid. Check both solutions with the two accuracy checks, constant simulated consumption and the Euler equation residuals, explain what they show, and find settings of each solver for which it passes both. Then compare the two methods.
The notebook deaton.ipynb in the task folder carries the details of the task, step by
step.
Submit it as a pull request, following the git workflow. One of you presents a solution at the start of the next Tuesday class.
Cake eatingΒΆ
The cake eating problem has the structure of the all so important consumption-savings model and an analytic solution, so every numerical method can be checked against the truth.
Choice: how much cake to eat in period
State: the size of the cake , with given
Transition: what is not eaten is left for the future,
Preferences: flow utility and discount factor
The consumer maximizes the discounted utility over the whole horizon,
Look how easy it is to get the familiar Bellman equation in this case. Let
The last equation is due to the fact that infinitely many time periods look the same from the point of view of today and tomorrow. Realizing further that in this case nothing can depend on index of time, we can drop the time index to arrive at
The right-hand side defines the Bellman operator from space of value functions to itself
operator is a contraction by Blackwellβs sufficient conditions, as in Class 8
therefore the value function iterations (VFI) converge to the unique solution from any starting point
The question of this class is how to compute when and are continuous.
Classify this dynamic optimization problem by type (see the classification in Class 7)
What is different from the models you saw in macro economics?
Can we solve this analytically?
The analytic solutionΒΆ
Let , what is the solution?
The consumer eats a constant fraction of what is left, every period.
Derivation by guess and verify
A guess and verify!
Because when (Inada condition), the lower bound never binds. But so the upper bound never binds either as long as . (why?)
Substitute the guess into the Bellman equation:
The first order condition gives and . Substituting back,
Matching the coefficients on gives , so and . Matching the constants gives .
The analytic solution exists only because the model is so simple
only few models admit an analytical solution
more complicated models require backward induction by hand + spotting the regularities before actual guessing
Its value, at least here, as well as any analytic property is the ability to verify numerical methods
Would the same approach be possible with ?
The Bellman operator is a contraction. Given this, how should we compute the maximum on its right-hand side?
Solving on a gridΒΆ
The simplest numerical approach replaces both the state and the choice with a finite grid, on . To avoid evaluating between grid points, make the next period cake the decision variable so we have:
here denotes the iteration of the VFI algorithm
Every term is known on the grid, so one iteration is a maximum over an matrix of choices, exactly as in the discrete models of the earlier classes:
import numpy as np
import matplotlib.pyplot as plt
class cake_ongrid():
'''Cake eating on a grid: the next period cake is chosen from the state grid'''
def __init__(self, beta=.9, Wbar=10, ngrid=50):
self.beta = beta # discount factor
self.Wbar = Wbar # upper bound on cake size
self.ngrid = ngrid # number of grid points
self.epsilon = np.finfo(float).eps # smallest positive float
self.grid = np.linspace(self.epsilon, Wbar, ngrid) # grid for both state and choice
def bellman(self, V0):
'''Bellman operator, V0 is a vector of values on the grid'''
c = self.grid - self.grid[:, np.newaxis] # states in columns, next cake in rows
c[c == 0] = self.epsilon # avoid log(0) when nothing is eaten
mask = c > 0 # feasible choices: eat a positive amount
matV1 = np.full((self.ngrid, self.ngrid), -np.inf) # -inf marks infeasible choices
matV0 = np.repeat(V0.reshape(self.ngrid, 1), self.ngrid, 1) # V0 of the next cake, in rows
matV1[mask] = np.log(c[mask]) + self.beta * matV0[mask] # maximand of the Bellman equation
V1 = np.amax(matV1, axis=0) # maximum in every column
c1 = self.grid - self.grid[np.argmax(matV1, axis=0)] # optimal consumption
return V1, c1
def solve(self, maxiter=1000, tol=1e-4, callback=None):
'''Value function iterations'''
V0 = np.log(self.grid) # start from eating everything
for i in range(maxiter):
V1, c1 = self.bellman(V0)
if callback: callback(iter=i, grid=self.grid, value=V1, policy=c1)
if np.all(abs(V1 - V0) < tol):
break
V0 = V1
else:
raise RuntimeError('No convergence: maximum number of iterations achieved')
return V1, c1Source
def check_analytic(model, title=''):
'''Plot the numerical solution of a cake eating model against the analytic one'''
b = model.beta
aV = lambda w: np.log(w)/(1-b) + np.log(1-b)/(1-b) + b*np.log(b)/(1-b)**2
aP = lambda w: (1-b)*w
xg = np.linspace(model.Wbar/1000, model.Wbar, 1000) # dense grid for the analytic solution
V, policy = model.solve()
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4.5))
ax1.plot(model.grid[1:], V[1:], label='numerical')
ax1.plot(xg, aV(xg), label='analytic')
ax2.plot(model.grid, policy, label='numerical')
ax2.plot(xg, aP(xg), label='analytic')
for ax, t in ((ax1, 'Value function'), (ax2, 'Policy function')):
ax.set(xlabel='Cake size, W', title=t)
ax.grid(color='0.9')
ax.legend()
fig.suptitle(title)
plt.show()
return np.max(np.abs(policy - aP(model.grid)))
err = check_analytic(cake_ongrid(beta=0.9, Wbar=10, ngrid=100), 'Same grid for state and choice, 100 points')
print(f'Largest error in consumption: {err:.3f}')
Largest error in consumption: 0.293
The value function is close, but the policy is a staircase. On this grid the consumer can only eat the difference of two grid points, so with 100 points on every consumption level is a multiple of about 0.1, whatever the true optimum.
Where it breaks: the choice set is tied to the state grid. Refining the state grid is the only way to refine the choices, the cost of one iteration grows with , and in a model where next periodβs state is not a grid point at all, the trick is not available. That is the case as soon as savings earn interest or income is random.
Why is the error largest at small cakes, relative to the size of the cake?
With interest, for , do we have a solver in which the problem can be converted in the same way?
Function approximationΒΆ
Any method that separates the choice from the state grid needs the value function between grid points. The interpolation problem: given nodes and the values of an unknown function, find a function from a chosen class with for all , and use in place of at any .
Interpolation matches the data exactly: the approximation passes through every . This is what a solver needs, because the data are computed exactly.
Curve fitting finds the central tendency of noisy data, as in regression, with more data points than parameters. Better fitted for the situations with (potential) measurement errors.
Extrapolation, evaluating the approximation/fitted curve outside the initial domain , should be avoided unless the theoretical properties of are known there. A wider grid is almost always the better fix.
The quality of an approximation is measured in the uniform norm aka infinity norm, , the largest error anywhere on the interval, which is what matters when stands in for inside a solver.
There are two families of methods:
connect the dots piece by piece, or
replace by one simpler function on the whole interval (which could still connect all dots to interpolate)
SplinesΒΆ
linear splines: order 2, degree 1, continuous
quadratic splines: order 3, degree 2, continuous first derivative
cubic splines: order 4, degree 3, continuous first and second derivatives
To compute spline interpolation one would need to solve the following system:
passing through both ends on intervals = equations
continuous derivatives on interior knots = equations
number of coefficients for one interval =
total number of unknowns =
equations
additional boundary conditions on the derivatives at and
Linear interpolation is the spline of order 2: continuous, with kinks at the knots.
on it is , and
for a twice continuously differentiable and the largest spacing ,
Halving the spacing divides the error by four.
The cubic spline, need two boundary conditions
for the natural spline, or
the slopes , for the clamped spline.
The coefficients solve a tridiagonal linear system, at a cost proportional to .
For a clamped spline and with four continuous derivatives,
so halving the spacing divides the error by sixteen.
In Python both are available:
np.interpfor the linear splinescipy.interpolate.CubicSplinefor the cubic spline
Polynomial approximationΒΆ
A polynomial of degree through distinct nodes exists and is unique.
How good it is (in terms of approximation error) depends on where the nodes are.
The error has two factors.
first depends only on .
second, the node polynomial , depends only on the nodes, and it is the one we control.
With equally spaced nodes the node polynomial is small in the middle of the interval and grows rapidly towards its ends.
For functions whose derivatives grow fast, such as on , the error near the ends grows without bound as increases: the Runge phenomenon. More nodes make the approximation worse.
The nodes that make smallest are the zeros of the Chebyshev polynomial , where the maximum equals , its smallest possible value.
The error bound becomes .
The Chebyshev polynomials are
or equivalently
The zeros of , Chebyshev nodes, are
They cluster towards the ends of .
The polynomials are orthogonal with respect to the weight , and also discretely orthogonal on the Chebyshev nodes, so the interpolating polynomial has explicit coefficients, no linear system needed:
A general interval is mapped to by , as in the flexible logit of Class 13.
For with continuous derivatives, Chebyshev interpolation of degree satisfies Judd, 1998
which falls faster than any power of when is smooth: Chebyshev interpolation is within a logarithmic factor of the best polynomial approximation in the uniform norm.
The four methods side by side, first with 12 nodes on a function with some curvature, then with a growing number of nodes on the Runge function:
Source
from scipy import interpolate
from numpy.polynomial import polynomial, chebyshev
import warnings
def approximations(f, a, b, n):
'''Linear spline, cubic spline, polynomial on equal nodes and Chebyshev interpolation with n nodes'''
xe = np.linspace(a, b, n) # equally spaced nodes
with warnings.catch_warnings():
warnings.simplefilter('ignore') # polyfit warns about poor conditioning
p = polynomial.polyfit(xe, f(xe), n-1)
return {'linear spline': lambda x: np.interp(x, xe, f(xe)),
'cubic spline': interpolate.CubicSpline(xe, f(xe)),
'polynomial, equal nodes': lambda x: polynomial.polyval(x, p),
'Chebyshev, own nodes': chebyshev.Chebyshev.interpolate(f, n-1, domain=[a, b])}
f = lambda x: np.exp(-x/4)*np.sin(x) + 1/(1 + x**2) # function to approximate
a, b, n = -5, 10, 12 # interval and number of nodes
xd = np.linspace(a, b, 1000) # dense grid to measure errors
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 16))
ax1.plot(xd, f(xd), 'k', lw=2, label='f(x)')
ax1.scatter(np.linspace(a, b, n), f(np.linspace(a, b, n)), c='k', zorder=3)
for name, g in approximations(f, a, b, n).items():
ax1.plot(xd, g(xd), label=name)
ax2.semilogy(xd, np.abs(g(xd) - f(xd)) + 1e-16, label=name)
ax1.set(ylim=(-1.5, 2), title=f'{n} nodes')
ax2.set(ylim=(1e-6, 15), title='Absolute error')
for ax in (ax1, ax2):
ax.legend(fontsize=8); ax.grid(color='0.9')
plt.show()
In economic models we control the three things that determine the accuracy:
the number of nodes, more is better, at a cost in every iteration of the solver
the location of the nodes, denser where the function curves more: the error bounds scale with the derivatives of , which for the log-utility value function are largest near zero wealth
the type of the approximation, matched to the function: smooth functions favor Chebyshev polynomials, functions with kinks favor splines, because the bounds above need the derivatives to exist
Where it breaks:
polynomials and cubic splines do not preserve monotonicity or concavity, so an approximated value function can have a local maximum that the true one does not
linear interpolation preserves both, which is why it is the default in the solvers below, despite its lower accuracy.
in several dimensions all of these methods generalize as tensor products, but the number of nodes grows exponentially with the dimension.
Curse of dimentionalitytaking derivatives of the approximations usually make little sense, much better to compute and interpolate the derivative function β οΈ
Discretized choice with interpolationΒΆ
With an interpolated value function the choice no longer has to live on the state grid.
Keep the grid for the state, and give the choice its own grid for every state point.
Scale to the feasible interval so that the constraint is respected by construction:
where is the interpolated value function and denotes the VFI iteration index
Where in the code below are the grids?
Why not use common grid over choices?
Where is interpolation in the code and which algorithm is used?
class cake_discretized():
'''Cake eating with a separate choice grid and interpolated value function'''
def __init__(self, beta=.9, Wbar=10, ngrid=50, nchgrid=100):
self.beta = beta # discount factor
self.Wbar = Wbar # upper bound on cake size
self.ngrid = ngrid # number of points on the state grid
self.nchgrid = nchgrid # number of points on the choice grid
self.epsilon = np.finfo(float).eps # smallest positive float
self.grid = np.linspace(self.epsilon, Wbar, ngrid) # grid for the state
self.share = np.linspace(self.epsilon, 1, nchgrid) # choices as shares of the cake
def bellman(self, V0):
'''Bellman operator, V0 is a vector of values on the state grid'''
c = self.share[:, np.newaxis] * self.grid # choices in rows, scaled to [0,W] in each column
W1 = self.grid - c # next period cake, off the grid in general
matV1 = np.log(c) + self.beta * np.interp(W1, self.grid, V0) # linear interpolation of V0
i = np.argmax(matV1, axis=0) # optimal choice in every column
return matV1[i, np.arange(self.ngrid)], c[i, np.arange(self.ngrid)]
solve = cake_ongrid.solve # the same value function iterationsSource
err = check_analytic(cake_discretized(beta=0.9, Wbar=10, ngrid=100, nchgrid=100),
'Separate choice grid, 100 + 100 points')
print(f'Largest error in consumption: {err:.3f}')
print(f'{"state grid":>10} {"choice grid":>12} {"max error in c":>15}')
for ng, nch in ((50, 50), (100, 100), (100, 500), (500, 100), (500, 500)):
m = cake_discretized(beta=0.9, Wbar=10, ngrid=ng, nchgrid=nch)
V, c = m.solve()
print(f'{ng:>10} {nch:>12} {np.max(np.abs(c - (1-m.beta)*m.grid)):>15.4f}')
Largest error in consumption: 0.060
state grid choice grid max error in c
50 50 0.1162
100 100 0.0597
100 500 0.0365
500 100 0.0106
500 500 0.0121
The policy is now five times closer to the truth with the same number of points. The table shows that both grids matter. A choice grid of shares pins consumption down to within , but here the grid of shares happens to contain a point within 0.001 of the optimal share , so most of the remaining error comes from the interpolation of on the state grid. The error is not even monotone in the grid sizes, a warning against reading accuracy off a single run.
How to increase the accuracy further:
more points on both grids, at a cost of evaluations per iteration
choice grids that respect the constraints of the model, as above
state grid points relocated to where the value function curves most
a better approximation of , or no discretization of the choice at all: the next class maximizes over a continuous in every state, and solves the first order condition instead
Linear interpolation of under-states it between the grid points, because is concave. Which consumption choices does that bias favor?
Why does the error not disappear when the state grid is refined with the choice grid held fixed?
The consumption-savings modelΒΆ
The cake eating model becomes the workhorse consumption-savings model of Deaton (1991) once savings earn interest and the consumer has an income:
is wealth at the beginning of the period, cash on hand
: consumption cannot exceed wealth, the consumer cannot borrow
is the gross return on savings and next periodβs income, both of which can be random
and the discount factor as before
Cake eating is the special case and .
The timing of the model separates the choice from the uncertainty:
Savings , the post-decision state, carry all the information about the current period that next period needs, and the constraint is the same as .
Computing expectationsΒΆ
Let income be log-normal, , so and . The expectation in the Bellman equation is an integral, and it has to be computed at every state and every choice on every iteration. We approximate it by quadrature, a weighted sum over fixed nodes with weights :
How to choose the nodes and the weights, and how quadrature compares with Monte Carlo integration, is covered in Class 16.
Both discretized solvers carry over to this model, with one difference. On the grid, next period wealth is chosen from the state grid and consumption is backed out as , which works only when income is deterministic: with random income is not a choice, so the on-grid trick fails. With a choice grid, consumption is the choice and is interpolated at for every quadrature node.
The Euler equationΒΆ
The first order condition for an interior choice and the envelope theorem,
give , and together the Euler equation
The marginal utility of consuming today equals the discounted expected marginal utility of saving it and consuming tomorrow. When the borrowing constraint binds, and the left side is larger. The Euler equation holds for the exact solution, whichever method computed it, so it is the basis of the solvers of the next two classes and of the accuracy checks for all of them.
Checking the accuracy of a solutionΒΆ
The cake eating model has an analytic solution; the consumption-savings model does not. Two checks still measure the accuracy of any numerical solution: a special case of the model with a sharp prediction, and the Euler equation, which holds at every point.
Constant consumption: Keaneβs testΒΆ
Take the deterministic model, with certainty, and let . Then the Euler equation becomes
so as long as the borrowing constraint does not bind, consumption is the same in every period. Along such a path wealth must also stay constant, otherwise it diverges. From the consumer eats the annuity value of the wealth,
and saves , so the constraint never binds.
Keaneβs test uses this prediction: solve the deterministic model with numerically, simulate the consumer from some initial wealth, and check that the simulated consumption profile is flat, at the level . An accurate solver passes. An inaccurate one produces consumption that drifts up or down over time. The error of the policy function accumulates through wealth, so the test magnifies small errors that a plot of the policy function hides.
The test is cheap and has a known answer, but it checks the solution only along the simulated paths, and only for one parameter combination. It is a necessary condition for an accurate solver, not a sufficient one.
Euler equation residualsΒΆ
The Euler equation holds for the exact solution at every point where the borrowing constraint does not bind, at any parameter values. For a numerical policy , interpolated between the grid points, the relative Euler equation residual
measures how far the policy is from satisfying it, as a fraction of marginal utility. It is computed on a grid much denser than the solverβs, to catch the errors between the grid points, and only where : at a corner solution the Euler equation holds as an inequality, and the residual is not an error.
The two checks complement each other. The Euler residuals apply at any parameter values and at every point of the state space, but only to one step of the dynamics. The constant consumption check applies to one special case, but follows the errors as they accumulate over a whole simulated path. The homework runs both on the two solvers with discretized choice of this class.
Why must wealth stay constant when consumption is constant, and income is deterministic? What happens to wealth if consumption is a little too high?
Can a solver pass Keaneβs test and still be inaccurate? Can it have small Euler residuals and still fail Keaneβs test?
- Deaton, A. (1991). Saving and Liquidity Constraints. Econometrica, 59(5), 1221β1248. 10.2307/2938366
- Judd, K. L. (1998). Numerical Methods in Economics (p. 633). MIT Press. https://mitpress.mit.edu/9780262100717/numerical-methods-in-economics/