Go to top, next, previous, or johnstachurski.net

Application: Finite Markov Chains

Finite Markov chains are found in many of the workhorse models of macroeconomics, operations research, etc.

Here we study efficient numerical methods for Markov chains

Definitions

A Markov matrix (or stochastic kernel) is an N by N square matrix p such that

We study a random process X that evolves on S = range(N) = [0,...,N-1]

S is called the state space

The basic idea is that

More formally, the Markov chain X is constructed as follows:

In psuedo-Python,

X[0] = a draw from q (a nonnegative array with len(q) = N and sum(q) = 1)
while 1:
    X[t+1] = a draw from p[X[t],:]

Simulation

Let's try to simulate this process.

Problem:

Write a function which takes

and returns a sample path of length sample_size

Hint: Use the DiscreteRV class from this lecture

To test your solution you can use the small matrix

p = np.array([[.4, .6], [.2, .8]])

For a long series, the fraction of the sample that takes value 0 will be about 0.25

Solution:

## Filename: mc_sample.py
## Author: John Stachurski

import numpy as np
from discreterv import DiscreteRV

def sample_path(p, init=0, sample_size=1000): 
    """
    A function that generates sample paths of a finite Markov chain with 
    kernel p on state space S = [0,...,N-1], starting from state init.

    Parameters: 

        * p is a 2D NumPy array, nonnegative, rows sum to 1
        * init is an integer in S
        * sample_size is an integer

    Returns: A flat NumPy array containing the sample
    """
    N = len(p)
    # Let P[x] be the distribution corresponding to p[x,:]
    P = [DiscreteRV(p[x,:]) for x in range(N)]
    X = np.empty(sample_size, dtype=int)
    X[0] = init
    for t in range(sample_size - 1):
        X[t+1] = P[X[t]].draw()
    return X

Notice here that we don't eliminate all for loops.

As a general rule, don't over-optimize your code

We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. -- Donald Knuth

Stationary Distributions

A distribution q is called stationary if q = q p

Stationary Distribution by Inversion

Let's suppose that p has a unique stationary distribution

To solve for it we can solve the linear system



for q, where I is the identity, B is the matrix of ones, b is the column vector of ones

Problem:

Write a function which takes p as a parameter and returns the stationary distribution, assuming that it is unique

You can test it using the matrix

p = np.array([[.4, .6], [.2, .8]])

The (unique) stationary distribution is (0.25, 0.75)

Solution:

## Filename: mc_stationary1.py
## Author: John Stachurski

import numpy as np

def stationary1(p):
    """
    Parameters: 
    
        * p is a 2D NumPy array, assumed to be stationary

    Returns: A flat array giving the stationary distribution
    """
    N = len(p)                               # p is N x N
    I = np.identity(N)                       # Identity matrix
    B, b = np.ones((N, N)), np.ones((N, 1))  # Matrix and vector of ones
    A = np.transpose(I - p + B) 
    solution = np.linalg.solve(A, b)
    return solution.flatten()                # Return a flat array

Stationary Distribution by Monte Carlo

Problem with this method: when solving a linear system of equations like this one, the number of operations is O(N3), where N is the size of the state space

Let's discuss a simulation based method

If p is stable with stationary distribution q, then the LLN for Markov chains tells us that



with probability one (see EDTC, section 4.3.4 for details).

Note that for this LLN, the initial condition does not matter

Summary: To solve for q we can

Problem:

Write a function to compute q this way, assuming stability of p

Use sample_path() (see above) to generate the sample path

Again, you can test it using the matrix

p = np.array([[.4, .6], [.2, .8]])

Results should agree with previous solution

Solution:

## Filename: mc_stationary2.py
## Author: John Stachurski

import numpy as np

# Import the sample_path function defined above
from mc_sample import sample_path

def stationary2(p, n=1000): 
    """ 
    Computes the stationary distribution via the LLN for stable
    Markov chains.

    Parameters: 

        * p is a 2D NumPy array
        * n is a positive integer giving the sample size

    Returns: An array containing the estimated stationary distribution
    """
    N = len(p)
    X = sample_path(p, sample_size=n)    # Sample path starting from X = 0
    q = [np.mean(X == y) for y in range(N)]
    return np.array(q)

A More Efficient Method

Let's suppose that p is stable as before, with unique stationary distribution q

The LLN for stable Markov chains can be used to show that



with probability one (see EDTC, section 6.1.4)

Moreover, this look-ahead estimator



is more efficient than the standard Monte Carlo estimator



introduced above because it uses p, which contains information about the dynamics

Problem:

Modify the function stationary2() above

Again, you can test it using the matrix

p = np.array([[.4, .6], [.2, .8]])

Results should agree with previous solutions

Solution:

## Filename: mc_stationary3.py
## Author: John Stachurski

import numpy as np

# Import the sample_path function defined above
from mc_sample import sample_path

def stationary3(p, n=1000, lae=False): 
    """ 
    Computes the stationary distribution via the LLN for stable
    Markov chains.

    Parameters: 

        * p is a 2D NumPy array
        * n is a positive integer giving the sample size
        * lae is a flag indicating whether to use the look-ahead method

    Returns: An array containing the estimated stationary distribution
    """
    N = len(p)
    X = sample_path(p, sample_size=n)    # Sample path starting from X = 0
    if lae:
        solution = [np.mean(p[X,y]) for y in range(N)]
    else:
        solution = [np.mean(X == y) for y in range(N)]
    return np.array(solution)

Testing

How accurate is the look-ahead estimator compared to standard Monte Carlo?

Let's generate some observations and compare average accuracy

The measure of accuracy will be the d1 distance



The matrix we will use is 2000 x 2000, stored in this file

Problem:

Solution:

## Filename: mc_test.py
## Author: John Stachurski


import numpy as np
from mc_stationary1 import stationary1 
from mc_stationary3 import stationary3

p = np.loadtxt('matrix_dat.txt')


def test_estimator(q, f, replications=100):
    """
    The estimator f returns an estimate of q.  Draw n=replications 
    observations and return average d1 distance

    Parameters:

        * q is a NumPy array representing the exact distribution
        * f is a function that, when called, returns an estimate of q
            as a NumPy array
        * replications is a positive integer giving the sample size

    Returns: A float
    """
    results = np.empty(replications)
    for i in range(replications):
        results[i] = np.sum(np.abs(f() - q))
    return results.mean()


def main():

    q = stationary1(p)  # Exact stationary distribution

    print "Standard MC, average distance:"
    print test_estimator(q, lambda: stationary3(p))

    print "Look-ahead MC, average distance:"
    print test_estimator(q, lambda: stationary3(p, lae=True))

Comment: