🚧 🚧 🚧 Not the final version 🚧 🚧 🚧
EGM solves the consumption-savings model in milliseconds, but there is no likelihood for consumption and wealth data. The method of simulated moments (MSM) needs only the simulator: pick a few statistics of the data, and find the parameters at which the model reproduces them. Theory today, practice on Tuesday.
Running the code for this lecture
The code for this class is in the folder session19-oct29/ of the course code
repository. The model object of Class 16 and the EGM solver of Class 17 are collected
in the module deaton_egm.py in that folder, and the notebook msm.ipynb has all the
code of this page.
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.
From solver and simulator to estimator¶
A solver and a simulator that are debugged let us study a theory: run the model for a range of parameter values and see what the decision maker does. The ultimate goal is to match the model to the observed data, in order to
quantify the effects of the parts of the theoretical setup
run counterfactual experiments: simulate behavior under a policy regime that has never been observed
support or falsify the theory by its fit to the data
The workflow of a structural project is the arc of this course: model, implementation, solver, understanding how the model works, estimation, validation, counterfactuals. Step four is where the time goes: solve, simulate, ask whether the result makes economic sense, repeat. It is also where the moments for step five are found.
Estimation versus calibration¶
The terms are often used interchangeably, and calibration exercises also search for the parameters that fit the data best. What estimation adds:
standard errors, a measure of the variability of the estimates
a study of identification, to make sure that no other parameter values explain the data equally well
Estimation often concerns only a subset of the parameters, with the rest fixed at values from the literature, like a calibration. The NFXP estimation of Class 10 fixed , for instance.
Why not maximum likelihood?¶
The MLE of Class 10 is the most efficient estimator there is, when the model gives the probability of the observed data. In the bus engine model it does: the logit choice probabilities. In the consumption-savings model it does not, or not easily:
consumption is a deterministic function of the state, . Observed consumption that is off the policy function has likelihood zero, so a likelihood needs measurement error in consumption and an assumption about its distribution
wealth is a sum of past shocks, so the likelihood of a wealth path is an integral over every unobserved income draw, and in a panel of periods that is a -dimensional integral per individual
the data are often aggregated: wealth by age from one survey, consumption from another, and no individual with both
Simulation is cheap where integration is not. The simulator of Class 16 produces a synthetic panel for any , and any statistic of the synthetic panel can be compared to the same statistic of the data.
Which objects would a likelihood for consumption and wealth data have to integrate out, and which of them does the simulator draw for free?
A calibration matches three moments with three parameters exactly. What question can it not answer?
Method of moments, GMM and simulated moments¶
Matching statistics of the model to statistics of the data has a long history:
method of moments: as many moments as parameters, a system of equations, solved exactly
generalized method of moments (GMM) of Hansen (1982): more moments than parameters, minimize a weighted distance between the data moments and the theoretical moments
method of simulated moments (MSM) of McFadden (1989): GMM with the theoretical moments replaced by averages over simulated data
Let , , be the observed data, one record per individual, and let be a vector of statistics of one record, the moment functions. Three objects share the name “moments”:
The model moments are the expectation of under the model with parameters , the object GMM works with. MSM replaces them by the average over a synthetic sample of records, simulated from the model at with random draws . The vector
of moment conditions is zero in expectation at the true and at no other point, if the moments identify the parameters.
The last clause is the whole trick. With the draws fixed, every evaluation of solves and simulates the model on the same shocks, so the simulated moments are a deterministic function of that an optimizer can minimize.
Redraw the shocks at every and the criterion is a random function: an optimizer cannot tell a change in the parameters from a change in the draws.
Three conditions make the estimator work, in the words of Corbae & Guvenen (2024):
the simulation draws are independent of the data
the model is identified: only at
the simulated moments are continuous in the mean: as the simulated sample grows
The count of moments against parameters is the order condition, necessary but not sufficient:
: overidentified, the usual case, with a test of the model for free
: just identified, the criterion is zero at the optimum and the weighting matrix does not matter
: underidentified, no estimator
The mean of a statistic over individuals is a moment. Is the median? Is a regression coefficient?
Why must the number of moments be at least the number of parameters?
The simulated criterion¶
The data are simulated too, from the “true” parameters, so that we know the answer. The
module deaton_egm.py holds the model object of Class 16,
the EGM solver of Class 17, which returns the policy as points on the
endogenous grid, and the simulator.
Source
import sys, time, pathlib, tempfile, urllib.request
import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import minimize
from scipy.stats import chi2
CODE = pathlib.Path('../gh-code/session19-oct29') # the model and EGM solver of Classes 16-17
if not CODE.exists(): # otherwise fetch it from the code repository
CODE = pathlib.Path(tempfile.mkdtemp())
urllib.request.urlretrieve('https://raw.githubusercontent.com/fediskhakov/sb-dse-code/main/session19-oct29/deaton_egm.py',
CODE / 'deaton_egm.py')
sys.path.insert(0, str(CODE))
from deaton_egm import deaton, solve_egm, simulate---------------------------------------------------------------------------
HTTPError Traceback (most recent call last)
Cell In[1], line 9
5 from scipy.stats import chi2
6 CODE = pathlib.Path('../gh-code/session19-oct29') # the model and EGM solver of Classes 16-17
7 if not CODE.exists(): # otherwise fetch it from the code repository
8 CODE = pathlib.Path(tempfile.mkdtemp())
----> 9 urllib.request.urlretrieve('https://raw.githubusercontent.com/fediskhakov/sb-dse-code/main/session19-oct29/deaton_egm.py',
10 CODE / 'deaton_egm.py')
11 sys.path.insert(0, str(CODE))
12 from deaton_egm import deaton, solve_egm, simulate
File /opt/hostedtoolcache/Python/3.12.15/x64/lib/python3.12/urllib/request.py:240, in urlretrieve(url, filename, reporthook, data)
223 """
224 Retrieve a URL into a temporary location on disk.
225
(...) 236 data file as well as the resulting HTTPMessage object.
237 """
238 url_type, path = _splittype(url)
--> 240 with contextlib.closing(urlopen(url, data)) as fp:
241 headers = fp.info()
243 # Just return the local path and the "headers" for file://
244 # URLs. No sense in performing a copy unless requested.
File /opt/hostedtoolcache/Python/3.12.15/x64/lib/python3.12/urllib/request.py:215, in urlopen(url, data, timeout, cafile, capath, cadefault, context)
213 else:
214 opener = _opener
--> 215 return opener.open(url, data, timeout)
File /opt/hostedtoolcache/Python/3.12.15/x64/lib/python3.12/urllib/request.py:521, in OpenerDirector.open(self, fullurl, data, timeout)
519 for processor in self.process_response.get(protocol, []):
520 meth = getattr(processor, meth_name)
--> 521 response = meth(req, response)
523 return response
File /opt/hostedtoolcache/Python/3.12.15/x64/lib/python3.12/urllib/request.py:630, in HTTPErrorProcessor.http_response(self, request, response)
627 # According to RFC 2616, "2xx" code indicates that the client's
628 # request was successfully received, understood, and accepted.
629 if not (200 <= code < 300):
--> 630 response = self.parent.error(
631 'http', request, response, code, msg, hdrs)
633 return response
File /opt/hostedtoolcache/Python/3.12.15/x64/lib/python3.12/urllib/request.py:559, in OpenerDirector.error(self, proto, *args)
557 if http_err:
558 args = (dict, 'default', 'http_error_default') + orig_args
--> 559 return self._call_chain(*args)
File /opt/hostedtoolcache/Python/3.12.15/x64/lib/python3.12/urllib/request.py:492, in OpenerDirector._call_chain(self, chain, kind, meth_name, *args)
490 for handler in handlers:
491 func = getattr(handler, meth_name)
--> 492 result = func(*args)
493 if result is not None:
494 return result
File /opt/hostedtoolcache/Python/3.12.15/x64/lib/python3.12/urllib/request.py:639, in HTTPDefaultErrorHandler.http_error_default(self, req, fp, code, msg, hdrs)
638 def http_error_default(self, req, fp, code, msg, hdrs):
--> 639 raise HTTPError(req.full_url, code, msg, hdrs, fp)
HTTPError: HTTP Error 404: Not Foundtheta0 = np.array([0.92, 0.5]) # true beta and sigma, to be recovered
N, T, tail = 500, 40, 10 # individuals, simulated periods, observed periods
m0 = deaton(beta=theta0[0], sigma=theta0[1]) # R = 1.05, 100 grid points on [0, 20]
rng = np.random.default_rng(2026)
M0 = np.exp(rng.standard_normal(N)) # initial wealth, log-normal
Wd, Cd = simulate(m0, solve_egm(m0), M0, T, seed=rng) # the "data": N wealth and consumption paths
def moments(W, C):
'''Moment functions h(z), one row per individual, from the last `tail` periods of the paths'''
Wt, Ct = W[:, -tail:], C[:, -tail:] # the first 30 periods are burn-in, not observed
return np.column_stack([Wt.mean(1), Wt.std(1), np.diff(np.log(Ct), axis=1).std(1)])
h = moments(Wd, Cd) # N x q individual contributions
mhat = h.mean(0) # data moments
Omega = np.cov(h.T) # covariance of the contributions, q x q
for name, mh, s in zip(('mean wealth', 'std wealth', 'std consumption growth'), mhat, np.sqrt(np.diag(Omega) / N)):
print(f'{name:<25} {mh:7.4f} (s.e. {s:.4f})')Only the last ten of the forty simulated periods are “observed”: the first thirty wash out the initial wealth, so the data come from the ergodic distribution of the model. The simulation below runs the same forty periods.
The covariance matrix of the individual contributions is the one object the asymptotics need from the data, and it comes for free once the moments are means over individuals.
The simulated moments solve the model at and simulate copies of the data with a fixed seed, so the same draws are used at every :
def simmoments(theta, S=5, seed=1):
'''Simulated moments at theta: S copies of the data, same draws at every theta'''
beta, sigma = theta
if not (0 < beta < 1 / 1.05 and sigma > 0): return None # EGM does not converge for beta R >= 1
m = deaton(beta=beta, sigma=sigma)
pol = solve_egm(m) # about 3 milliseconds
Ws, Cs = simulate(m, pol, np.tile(M0, S), T, seed=np.random.default_rng(seed)) # fixed draws
return moments(Ws, Cs).mean(0)
def criterion(theta, W, S=5, seed=1):
'''The MSM criterion g' W g'''
ms = simmoments(theta, S, seed)
if ms is None: return 1e10 # a wall for the optimizer
g = mhat - ms
return g @ W @ gThe figure plots the criterion in , with at its true value and a diagonal weighting matrix, for three simulators: with fixed draws, with fixed draws, and with fresh draws at every .
Source
Wdiag = np.diag(1 / np.diag(Omega)) # moments scaled by their variances
fig, ax = plt.subplots(1, 2, figsize=(12, 4.5))
for a, (lo, hi) in zip(ax, ((0.85, 0.95), (0.912, 0.928))):
bg = np.linspace(lo, hi, 81)
a.plot(bg, [criterion([b, theta0[1]], Wdiag, S=1) for b in bg], lw=1, label='S = 1, fixed draws')
a.plot(bg, [criterion([b, theta0[1]], Wdiag, S=10) for b in bg], lw=1, label='S = 10, fixed draws')
a.plot(bg, [criterion([b, theta0[1]], Wdiag, S=1, seed=k) for k, b in enumerate(bg)], '.', ms=3, label='S = 1, fresh draws')
a.axvline(theta0[0], color='k', lw=0.5, ls=':')
a.set(xlabel='β', title=f'MSM criterion in β on [{lo}, {hi}], σ = {theta0[1]}'); a.grid(color='0.9')
ax[0].set(yscale='log'); ax[0].legend()
plt.show()Three things to see:
with fixed draws the criterion is a smooth function of and has one minimum, close to the true value
with fresh draws it is a cloud: in the zoom the simulation noise is as large as the differences the optimizer needs to see, and the minimum is wherever the draws happened to fall
a larger lowers the noise of the simulated moments, so the fixed-draw criterion with is centered better than with — but it is the fixing, not the number of draws, that makes the function minimizable
Where it breaks: a discrete choice in the simulator. The retirement decision of Class 18 is an indicator , so a simulated retirement rate is a step function of even with fixed draws: it only changes when a draw crosses the threshold.
This is the problem of McFadden (1989), and the reason the asymptotics of Pakes & Pollard (1989) do not assume a smooth criterion. Gradient-based optimizers fail on the steps; derivative-free ones, Nelder–Mead in our case, walk over them as long as the steps are small compared to the simplex.
The criterion with fresh draws is a random variable at every . What does its expectation look like, and why is the minimum of the expectation not the expectation of the minimum?
The model is solved on a grid and the policy is interpolated. Is the simulated moment a smooth function of even with fixed draws?
Asymptotic properties¶
The MSM estimator is a GMM estimator with a noisy version of the model moments, and it inherits the GMM theory of Hansen (1982) with one correction. Let be the Jacobian of the model moments,
and the asymptotic covariance of the data moments, .
Where does the factor come from? The moment conditions at carry two independent sources of noise,
the data moments with variance and the simulated moments with variance , because they average records from the same distribution. Their sum has variance , and this is the only place where the simulation enters the theory.
What the factor costs: with the standard errors are times the GMM ones, with they are 5% larger, and simulating more than ten copies of the data buys very little.
Three more things to read off the theorem:
Consistency needs no good and no large . The estimator is consistent with the identity matrix and . What and buy is precision.
The sandwich is the robust form of Class 10, with the Jacobian of the moments in place of the Hessian of the likelihood. It collapses to only at the optimal weighting matrix.
Local identification is a rank condition: has to have linearly independent columns, so every parameter must move at least one moment, and no two parameters may move the moments in the same direction. A flat direction of the criterion is a column of that is zero or a combination of the others.
Precision is governed by . A moment that barely responds to a parameter is a small entry of and a large entry of : this is how local identification is linked to the standard errors, and a weakly identified parameter announces itself through them.
Where it breaks: every formula above needs , the derivative of a function we only know through a simulator. The theorem does not require a smooth criterion, Pakes & Pollard (1989) cover step functions, but the standard errors do need the slope.
In practice is computed by finite differences of with fixed draws, and the step size must be small enough to be a derivative but large enough to jump over the steps and the interpolation kinks of the solver. Tuesday’s class shows how much rides on that choice.
The MLE of Class 10 attains the Cramér–Rao bound. Why is the MSM estimator never as efficient, even with and the optimal ?
A parameter that moves two moments in exactly opposite directions to another parameter. Is it identified?
The weighting matrix and the two-step estimator¶
The optimal weighting matrix is the inverse of the covariance of the moment conditions,
which is unknown but estimable. For moments that are means over individuals,
is the sample covariance of the contributions , the one line
np.cov(h.T) above, computed from the data once.
For moments of other kinds, autocovariances of a time series for instance, a Newey–West estimator or a bootstrap replaces it, and when the data moments come from published tables rather than micro data, is itself simulated from the model at a first-step estimate.
Iterating steps 2 and 3 until stops changing is the iterated MSM; the continuously updated version makes a function of inside the criterion. In practice the weighting matrix is often simplified, and Altonji & Segal (1996) is the reason: in small samples the optimal is estimated with error that is correlated with the moment conditions, and the second step can be more biased than the first. The choices seen in applied work, from the simplest up:
the identity, in the first step of a multi-step estimation
a diagonal matrix of inverse variances, which brings the moments to a common scale and ignores their covariances
manually chosen weights, to bring all moments to the same scale or to emphasize the moments the study is about
the optimal , from the data or from a first-step simulation
an iteratively updated , a Newey–West estimate for serially correlated moments, and model-specific adjustments
Many ways to skin a cat, and the choice should be reported with the estimates.
The test of overidentifying restrictions¶
With more moments than parameters the criterion is not zero at the optimum, and its size says whether the extra moments are compatible with the model.
The test is valid only with the optimal weighting matrix: with any other the criterion at the optimum has a different distribution. And it tests the moments chosen, not the model: a model that fits three moments can be rejected by a fourth.
def estimate(W, S=5, x0=(0.9, 0.4), seed=1):
'''Minimize the MSM criterion by Nelder-Mead: derivative free, so steps in the simulator do not matter'''
res = minimize(criterion, x0, args=(W, S, seed), method='Nelder-Mead',
options={'xatol': 1e-5, 'fatol': 1e-9})
assert res.success, res.message
return res.x, res.fun, res.nfev
def jacobian(theta, S=5, h=1e-4):
'''Jacobian D of the model moments by central differences of the simulated moments, q x K'''
theta, D = np.asarray(theta, float), []
for e in np.eye(theta.size) * h:
D.append((simmoments(theta + e, S) - simmoments(theta - e, S)) / (2 * h)) # same draws on both sides
return np.array(D).T
def vcov(theta, W, S=5):
'''Asymptotic variance of the MSM estimator, the sandwich with the (1 + 1/S) correction'''
D = jacobian(theta, S)
assert np.linalg.matrix_rank(D) == theta.size # local identification: full column rank
bread = np.linalg.inv(D.T @ W @ D)
return (1 + 1 / S) * bread @ D.T @ W @ Omega @ W @ D @ bread / NS = 5
for name, W in (('identity', np.eye(3)), ('diagonal', Wdiag), ('optimal', np.linalg.inv(Omega))):
t0 = time.perf_counter()
th, Q, nfev = estimate(W, S)
se = np.sqrt(np.diag(vcov(th, W, S)))
line = f'{name:<9} beta {th[0]:.4f} ({se[0]:.4f}) sigma {th[1]:.4f} ({se[1]:.4f}) {nfev} evaluations, {time.perf_counter() - t0:.2f} s'
if name == 'optimal': # the J-test needs the optimal weighting matrix
J = N / (1 + 1 / S) * Q
line += f' J = {J:.2f}, p-value {chi2.sf(J, 3 - 2):.3f}'
print(line)All three weighting matrices recover and within one standard error, in under a second each, and the standard errors shrink as the weighting improves. The -test does not reject: the third moment is compatible with a model that fits the first two, as it should be when the data come from the model itself.
The formula is not the only way to a standard error. A bootstrap resamples the individuals, re-runs the whole two-step estimation with a fresh simulation seed, and reports the spread of the estimates across replications.
It costs hundreds of estimations, minutes here and days in a large model, but it needs no Jacobian and no asymptotic argument. The Monte Carlo of the next class is the same idea with the true parameters known.
The standard errors with the identity matrix are larger than with the optimal one, yet the point estimates are nearly the same. When would they differ a lot?
The -test uses . What happens to it if is forgotten in the normalization?
Choosing the moments¶
The choice of moments is the crucial part of an MSM estimation, and more art than science. The principles:
the moments must be sensitive to the parameters: understanding how the model works means understanding what variation the parameters induce in the simulated data, and the moments should represent that variation
every parameter needs its own moment, in the sense of the rank condition: the columns of are the responses of the moments to the parameters, and they must be linearly independent
moments must be measurable precisely in the data: a moment with a large variance in gets a small weight and contributes little
moments should be interpretable, so that the fit can be shown in a table of data moments against simulated moments, which every MSM paper carries
The figure maps the criterion over with the diagonal weighting matrix, with all three moments on the left and with the two wealth moments only on the right.
Source
bg, sg = np.linspace(0.88, 0.95, 36), np.linspace(0.3, 0.7, 33)
B, Sg = np.meshgrid(bg, sg)
fig, ax = plt.subplots(1, 2, figsize=(12, 4.5))
for a, keep, title in zip(ax, (slice(0, 3), slice(0, 2)), ('three moments', 'two wealth moments only')):
Wk = np.zeros((3, 3)); Wk[keep, keep] = Wdiag[keep, keep] # drop a moment by zeroing its weight
Q = np.array([[criterion([b, s], Wk, S=5) for b in bg] for s in sg])
cs = a.contourf(B, Sg, np.log10(Q), levels=20, cmap='viridis')
a.plot(*theta0, 'r+', ms=12, mew=2)
a.set(xlabel='β', ylabel='σ', title=f'log10 of the MSM criterion, {title}')
fig.colorbar(cs, ax=a)
plt.show()With three moments the criterion has one basin around the truth. With the two wealth moments only, the basin stretches into a valley: a lower with a higher produces nearly the same mean and spread of wealth, impatience traded against precautionary saving.
The rank condition still holds along the valley, has rank two, but its columns are nearly collinear: the standard errors explode and an optimizer stops anywhere along the floor. The consumption moment breaks the trade-off because moves consumption growth and does not.
Where it breaks, in applications:
a parameter that moves no chosen moment: a flat criterion in that direction, an optimizer that stops at the starting value, and standard errors from a singular matrix
two parameters with the same signature on the moments: a valley, as on the right
moments that are step functions of in the simulator, which can hide a valley from a derivative-free optimizer and ruin the finite-difference Jacobian
Andrews et al. (2017) give a measure of what each moment contributes to each estimate, , the matrix that maps a perturbation of the moments into a perturbation of the estimates. It is a by-product of the standard errors and belongs in every MSM paper.
Which moments of the retirement data of Class 18 would identify the disutility of work , and which the taste shock scale ?
Wealth is measured with error in survey data. Which of the three moments above is hurt most?
MSM in practice¶
Why is the method so widely used in applied work?
it is not data hungry: it matches aggregated moments, and can combine different sources of data, a wealth profile from one survey and a retirement hazard from another
it does not rely on distributional assumptions as much as MLE, which needs the whole density of the data
it only needs the simulator, which the structural project has anyway
The price is efficiency: the standard errors are larger than those of the MLE, the weighting matrix is often simplified because of its small-sample bias, and the choice of moments is a judgment call that has to be defended. Two relatives extend the idea: indirect inference Gourieroux et al., 1993Smith, 1993 matches the coefficients of an auxiliary model, a regression or a VAR, estimated on the data and on the simulations, and simulated moments of time series Lee & Ingram, 1991Duffie & Singleton, 1993 match autocovariances of one long simulated path.
Two life-cycle applications of the kind we estimate next class:
Gourinchas & Parker (2002) estimate the discount factor and risk aversion of a consumption-savings model from the age profile of consumption, with the age profile of wealth as a check
French (2005) estimates a labor supply and retirement model from profiles of hours, participation and assets by age, the model of Class 20 with health and wages added
Iskhakov & Keane (2021) estimate a dynamic model of labor supply, savings and human capital on Australian survey data by MSM, and use it to evaluate a reform of the pension system: the whole arc from model to counterfactual in one paper.
An MSM paper reports a table of data moments and simulated moments at the estimates. What should the reader look for in it beyond the fit?
The counterfactual changes a policy parameter and simulates the model again. Which of the sources of noise in the estimates carry over to the counterfactual?
- McFadden, D. (1989). A Method of Simulated Moments for Estimation of Discrete Response Models Without Numerical Integration. Econometrica, 57(5), 995. 10.2307/1913621
- Pakes, A., & Pollard, D. (1989). Simulation and the Asymptotics of Optimization Estimators. Econometrica, 57(5), 1027–1057. 10.2307/1913622
- Hansen, L. P. (1982). Large Sample Properties of Generalized Method of Moments Estimators. Econometrica, 50(4), 1029–1054. 10.2307/1912775
- Corbae, D., & Guvenen, F. (2024). Method of Moments Estimators. In Quantitative Economics with Heterogeneity: A Guidebook. Princeton University Press.
- Altonji, J. G., & Segal, L. M. (1996). Small-Sample Bias in GMM Estimation of Covariance Structures. Journal of Business & Economic Statistics, 14(3), 353–366. 10.1080/07350015.1996.10524661
- Andrews, I., Gentzkow, M., & Shapiro, J. M. (2017). Measuring the Sensitivity of Parameter Estimates to Estimation Moments. The Quarterly Journal of Economics, 132(4), 1553–1592. 10.1093/qje/qjx023
- Gourieroux, C., Monfort, A., & Renault, E. (1993). Indirect Inference. Journal of Applied Econometrics, 8(S1), S85–S118. 10.1002/jae.3950080507
- Smith, A. A., Jr. (1993). Estimating Nonlinear Time-Series Models Using Simulated Vector Autoregressions. Journal of Applied Econometrics, 8(S1), S63–S84. 10.1002/jae.3950080506
- Lee, B.-S., & Ingram, B. F. (1991). Simulation estimation of time-series models. Journal of Econometrics, 47(2), 197–205. 10.1016/0304-4076(91)90098-X
- Duffie, D., & Singleton, K. J. (1993). Simulated Moments Estimation of Markov Models of Asset Prices. Econometrica, 61(4), 929–952. 10.2307/2951768
- Gourinchas, P.-O., & Parker, J. A. (2002). Consumption over the Life Cycle. Econometrica, 70(1), 47–89. 10.1111/1468-0262.00269
- French, E. (2005). The Effects of Health, Wealth, and Wages on Labour Supply and Retirement Behaviour. The Review of Economic Studies, 72(2), 395–427. 10.1111/j.1467-937X.2005.00337.x
- Iskhakov, F., & Keane, M. (2021). Effects of taxes and safety net pensions on life-cycle labor supply, savings and human capital: The case of Australia. Journal of Econometrics, 223(2), 401–432. 10.1016/j.jeconom.2020.01.023
- Adda, J., & Cooper, R. W. (2023). Dynamic Economics: Quantitative Methods and Applications. MIT Press. https://mitpress.mit.edu/9780262547888/dynamic-economics