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.