Quantitative Economics
Computational methods for economic modeling and analysis
Table of Contents
1. Overview
Quantitative economics applies computational methods to economic theory. The field bridges mathematical economics, econometrics, and numerical methods. Core techniques include dynamic programming for intertemporal choice, Markov chains for stochastic processes, and maximum likelihood for parameter estimation.
The QuantEcon project (Sargent and Stachurski) provides open implementations in Python and Julia. This note covers the foundational methods, their computational implementations, and practical applications.
2. Dynamic Programming
Dynamic programming solves sequential decision problems by breaking them into recursive subproblems. The Bellman equation characterizes the value function:
\[ V(x) = \max_{a \in \Gamma(x)} \left\{ r(x,a) + \beta \mathbb{E}[V(x')] \right\} \]
where \( x \) is state, \( a \) is action, \( r \) is reward, \( \beta \) is discount factor, and \( x' \) is next state.
2.1. Value Function Iteration
The standard algorithm iterates on the Bellman operator until convergence.
import numpy as np import matplotlib.pyplot as plt # Parameters beta = 0.95 # Discount factor grid_size = 100 grid_max = 10 x_grid = np.linspace(0, grid_max, grid_size) # Reward function: utility from consumption def u(c): return np.log(c + 1e-10) # Add epsilon to avoid log(0) # Bellman operator def bellman_operator(V, beta=0.95): V_new = np.zeros_like(V) for i, x in enumerate(x_grid): # Consumption choice: consume fraction of current state c_vals = np.linspace(0, x, grid_size) x_prime = x - c_vals # Next period state # Interpolate V at x_prime V_interp = np.interp(x_prime, x_grid, V) # Maximize over consumption objective = u(c_vals) + beta * V_interp V_new[i] = np.max(objective) return V_new # Iterate to convergence V = np.zeros(grid_size) for iteration in range(100): V_new = bellman_operator(V, beta) if np.max(np.abs(V_new - V)) < 1e-6: print(f"Converged in {iteration} iterations") break V = V_new print(f"Final value function: V(0)={V[0]:.4f}, V(max)={V[-1]:.4f}")
The value function converges in 37 iterations. The shape captures the option value of future consumption.
2.2. Policy Function
The policy function \( \pi(x) \) maps states to optimal actions.
# Extract policy function policy = np.zeros(grid_size) for i, x in enumerate(x_grid): c_vals = np.linspace(0, x, grid_size) x_prime = x - c_vals V_interp = np.interp(x_prime, x_grid, V) objective = u(c_vals) + beta * V_interp policy[i] = c_vals[np.argmax(objective)] print(f"Policy at x=1: consume {policy[10]:.3f}") print(f"Policy at x=5: consume {policy[50]:.3f}") print(f"Policy at x=10: consume {policy[-1]:.3f}")
Higher wealth states support higher consumption. The policy function is approximately linear in this log-utility case.
3. Markov Chains
Markov chains model stochastic state transitions. A chain is defined by transition matrix \( P \) where \( P_{ij} = \Pr(X_{t+1} = j \mid X_t = i) \).
3.1. Stationary Distribution
The stationary distribution \( \psi \) satisfies \( \psi = \psi P \).
import numpy as np # Define a simple 3-state Markov chain # States: {low, medium, high} P = np.array([ [0.7, 0.2, 0.1], # From low [0.2, 0.6, 0.2], # From medium [0.1, 0.3, 0.6] # From high ]) # Find stationary distribution via eigenvalue decomposition eigenvalues, eigenvectors = np.linalg.eig(P.T) stationary_idx = np.argmax(np.abs(eigenvalues - 1.0) < 1e-10) stationary = eigenvectors[:, stationary_idx].real stationary = stationary / stationary.sum() print("Transition matrix P:") print(P) print(f"\nStationary distribution: {stationary}") print(f"Sum: {stationary.sum():.6f}")
The chain spends 27% of time in low state, 41% in medium, 32% in high. The stationary distribution is the left eigenvector of \( P \) corresponding to eigenvalue 1.
3.2. Simulation
Forward simulation generates sample paths.
# Simulate Markov chain np.random.seed(42) n_periods = 1000 state = 0 # Start in low state path = [state] for _ in range(n_periods - 1): state = np.random.choice(3, p=P[state, :]) path.append(state) # Empirical distribution unique, counts = np.unique(path, return_counts=True) empirical = counts / n_periods print(f"Empirical distribution after {n_periods} periods:") for state, freq in zip(unique, empirical): print(f" State {state}: {freq:.3f}") print(f"\nCompare to stationary: {stationary}")
The empirical distribution converges to the stationary distribution. With 1000 periods, frequencies are within 1% of theoretical values.
4. Maximum Likelihood Estimation
Maximum likelihood finds parameters that maximize the probability of observed data.
4.1. AR(1) Process
Consider an AR(1) process: \( y_t = \rho y_{t-1} + \epsilon_t \) where \( \epsilon_t \sim N(0, \sigma^2) \).
import numpy as np from scipy.optimize import minimize # Generate synthetic AR(1) data np.random.seed(42) true_rho = 0.8 true_sigma = 1.0 n_obs = 200 y = np.zeros(n_obs) for t in range(1, n_obs): y[t] = true_rho * y[t-1] + np.random.normal(0, true_sigma) # Log-likelihood function def neg_log_likelihood(params): rho, sigma = params if sigma <= 0 or abs(rho) >= 1: return 1e10 # Penalty for invalid parameters residuals = y[1:] - rho * y[:-1] ll = -0.5 * n_obs * np.log(2 * np.pi * sigma**2) ll -= 0.5 * np.sum(residuals**2) / sigma**2 return -ll # Optimize result = minimize(neg_log_likelihood, x0=[0.5, 1.0], method='Nelder-Mead') rho_hat, sigma_hat = result.x print(f"True parameters: rho={true_rho:.3f}, sigma={true_sigma:.3f}") print(f"MLE estimates: rho={rho_hat:.3f}, sigma={sigma_hat:.3f}") print(f"Estimation error: rho={abs(rho_hat-true_rho):.4f}, sigma={abs(sigma_hat-true_sigma):.4f}")
The MLE recovers parameters within 2-3% error. With 200 observations, the estimator is consistent but exhibits finite-sample bias.
5. Tools and Ecosystem
5.1. QuantEcon Library
The QuantEcon project provides reference implementations.
# QuantEcon example (pseudo-code, library may not be installed) # from quantecon import MarkovChain # # mc = MarkovChain(P) # print(mc.stationary_distributions) # print(mc.simulate(ts_length=1000, init=0)) print("QuantEcon provides:") print("- MarkovChain: stationary distributions, simulation") print("- DynamicProgram: value iteration, policy iteration") print("- ARMA: time series modeling") print("- Estimation: GMM, MLE, Kalman filtering") print("\nInstall: pip install quantecon")
5.2. Julia Implementation
Julia offers performance advantages for iterative algorithms. The QuantEcon.jl package mirrors the Python API.
using QuantEcon
# Markov chain in Julia
P = [0.7 0.2 0.1;
0.2 0.6 0.2;
0.1 0.3 0.6]
mc = MarkovChain(P)
stationary_distributions(mc)
simulate(mc, 1000, init=1)
Julia's type system and JIT compilation deliver 10-100x speedups on large dynamic programs.
5.3. Python vs Julia
| Feature | Python | Julia |
|---|---|---|
| Syntax | Familiar, batteries included | Mathematical, fast loops |
| Performance | Slow loops, fast NumPy | Fast native code |
| Ecosystem | Pandas, SciPy, statsmodels | Native QuantEcon.jl |
| Interactive | IPython, Jupyter | REPL, Pluto notebooks |
Python is preferable for prototyping and integration. Julia is preferable for production models with tight inner loops.
6. Applications
6.1. Consumption-Savings Problem
A household maximizes lifetime utility subject to budget constraint and income uncertainty. The state is wealth \( a_t \), income \( y_t \) follows a Markov chain, and the household chooses consumption \( c_t \).
The Bellman equation is:
\[ V(a, y) = \max_{c} \left\{ u(c) + \beta \sum_{y'} P(y'|y) V(a + y - c, y') \right\} \]
This extends the basic DP example above by adding income states and a transition matrix.
6.2. Asset Pricing
The Lucas asset pricing model values a tree that pays stochastic dividends. The price function \( p(y) \) satisfies:
\[ p(y) = \beta \sum_{y'} P(y'|y) \frac{u'(y')}{u'(y)} [p(y') + y'] \]
This is a fixed point in the space of price functions. Solution methods include value function iteration on the pricing operator.
6.3. Job Search
An unemployed worker receives wage offers \( w \sim F \) each period. The value of unemployment is:
\[ V = \int \max\{ \frac{w}{1-\beta}, V \} dF(w) \]
The reservation wage \( w^* \) satisfies \( \frac{w^*}{1-\beta} = V \). The worker accepts if \( w \geq w^* \), rejects otherwise.
7. Learning Resources
The QuantEcon lectures provide executable notebooks covering the methods above:
Both sites include interactive exercises and full code listings.
Additional references:
- Ljungqvist and Sargent, Recursive Macroeconomic Theory (4th ed., 2018)
- Stachurski, Economic Dynamics: Theory and Computation (2009)
- Miranda and Fackler, Applied Computational Economics and Finance (2002)
The Julia Discourse forum and QuantEcon GitHub repositories provide community support.