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
- Simulation (generating sample paths)
- Computing stationary distributions
Definitions
A Markov matrix (or stochastic kernel) is an N by N square matrix
p
such that- all elements are nonnegative, and
- each row sums to one (for any
x
we havesum(p[x,:]) = 1
)
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
- Each row
p[x,:]
of the matrixp
can be regarded as a distribution onS
- The distribution
p[x,:]
gives the probabilities forX[t+1]
whenX[t] = x
More formally, the Markov chain
X
is constructed as follows:- First,
X[0]
is drawn from some initial distribution (probability mass function)q
X[0] = x
with probabilityq[x]
for eachx
inS
- At each subsequent time
t
, the new stateX[t+1]
is drawn fromp[X[t],:]
- Moves to state
y
with probabilityp[X[t],y]
- Moves to state
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
- a Markov kernel (matrix)
p
, - an initial condition
init
which representsX[0]
, and - a positive integer
sample_size
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.- Outer loops don't need to be optimized: savings minimal
- Eliminate inner loops, that would be repeated many times
As a general rule, don't over-optimize your code
- Unnecessary
- Less clarity
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
- Here
q
is a row vector, postmultiplied by the Markov matrixp
- At least one such distribution exists
- See EDTC, theorem 4.3.5
- There may be many
- If
p
is the identity then allq
are stationary - A condition for uniqueness: see EDTC, theorem 4.3.18
- One sufficient condition: all elements of
p
are strictly positive
- One sufficient condition: all elements of
- If
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- See EDTC, section 4.3.1 for details
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
- Slow or infeasible when N is large
- Must use other methods (e.g., iterative or simulation)
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).
- That is, fraction of time that
X
spends aty
converges toq[y]
- In NumPy terms,
np.mean(X == y)
is approximately equal toq[y]
- Here
X
is an array storing the time series
- Here
Note that for this LLN, the initial condition does not matter
Summary: To solve for
q
we can- Sample a time series and store it in
X
- compute
np.mean(X == y)
for eachy
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- Providing a keyword argument
lae
such thatlae=True
returns the look-ahead estimator insteadlae=False
returns the standard Monte Carlo estimator- The default is
False
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:
- Download the data file
- Load the matrix using
loadtxt()
- Solve for the exact stationary distribution q using the first (i.e. linear algebra) technique
- Generate 100 observations of the Monte Carlo estimator (
n = 1000
) and calculate average d1 distance from q - Generate 100 observations of the look-ahead estimator (
n = 1000
) and calculate average d1 distance from q
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:
- The function
test_estimator()
is written to avoid duplicating code- Duplicating code is naughty, train yourself to avoid it
0 comments:
Post a Comment