Skip to content

Commit 8a2f29c

Browse files
committed
MARKOV: Put the Markov functions into a class wrapper and created a way to work around issues with multiple eigenvalues near unity to prevent nonstationary distirbutions from being considered stationary.
1 parent 36a45ed commit 8a2f29c

File tree

2 files changed

+208
-16
lines changed

2 files changed

+208
-16
lines changed

quantecon/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from .linproc import LinearProcess
1414
from .lqcontrol import LQ
1515
from .lss import LSS
16-
from .mc_tools import mc_compute_stationary, mc_sample_path
16+
from .mc_tools import DMarkov, mc_sample_path, mc_compute_stationary
1717
from .quadsums import var_quadratic_sum, m_quadratic_sum
1818
from .rank_nullspace import rank_est, nullspace
1919
from .riccati import dare

quantecon/mc_tools.py

Lines changed: 207 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,231 @@
11
"""
2+
Authors: Chase Coleman, Spencer Lyon, Daisuke Oyama, Tom Sargent,
3+
John Stachurski
4+
25
Filename: mc_tools.py
36
4-
Authors: Thomas J. Sargent, John Stachurski
7+
This file contains some useful objects for handling a discrete markov
8+
transition matrix. It contains code written by several people and
9+
was ultimately compiled into a single file to take advantage of the
10+
pros of each.
511
612
"""
13+
from __future__ import division
714
import numpy as np
15+
import scipy.linalg as la
16+
import sympy.mpmath as mp
817
from .discrete_rv import DiscreteRV
918

1019

11-
def mc_compute_stationary(P):
20+
class DMarkov(object):
21+
"""
22+
This class is used as a container for a discrete Markov transition
23+
matrix or a discrete Markov chain. It stores useful information
24+
such as the stationary distributions and allows simulation using a
25+
specified initial distribution.
26+
27+
28+
Parameters
29+
----------
30+
P : array_like(float, ndim=2)
31+
The transition matrix. Must be of shape n x n.
32+
pi_0 : array_like(float, ndim=1), optional(default=None)
33+
The initial probability distribution. If no intial distribution
34+
is specified, then it will be a distribution with equally
35+
probability on each state
36+
37+
38+
Attributes
39+
----------
40+
P : array_like(float, ndim=2)
41+
The transition matrix. Must be of shape n x n.
42+
pi_0 : array_like(float, ndim=1)
43+
The initial probability distribution.
44+
stationary_dists : array_like(float, ndim=2)
45+
An array with invariant distributions as columns
46+
ergodic_sets : list(lists(int))
47+
A list of lists where each list in the main list
48+
has one of the ergodic sets.
49+
50+
51+
Methods
52+
-------
53+
invariant_distributions : This method finds invariant
54+
distributions
55+
56+
simulate_chain : Simulates the markov chain for a given
57+
initial distribution
58+
"""
59+
60+
def __init__(self, P, pi0=None):
61+
self.P = P
62+
n, m = P.shape
63+
self.n = n
64+
65+
if pi0 is None:
66+
self.pi0 = np.ones(n)/n
67+
68+
else:
69+
self.pi0 = pi0
70+
71+
# Check Properties
72+
# double check that P is a square matrix
73+
if n != m:
74+
raise ValueError('The transition matrix must be square!')
75+
76+
# Double check that the rows of P sum to one
77+
if np.all(np.sum(P, axis=1) != np.ones(P.shape[0])):
78+
raise ValueError('The rows must sum to 1. P is a trans matrix')
79+
80+
def __repr__(self):
81+
msg = "Markov process with transition matrix \n P = \n {0}"
82+
return msg.format(self.P)
83+
84+
def __str__(self):
85+
return str(self.__repr__)
86+
87+
def find_invariant_distributions(self, precision=None, tol=None):
88+
"""
89+
This method computes the stationary distributions of P.
90+
These are the eigenvectors that correspond to the unit eigen-
91+
values of the matrix P' (They satisfy pi_{t+1}' = pi_{t}' P). It
92+
simply calls the outer function mc_compute_stationary
93+
94+
Parameters
95+
----------
96+
precision : scalar(int), optional(default=None)
97+
Specifies the precision(number of digits of precision) with
98+
which to calculate the eigenvalues. Unless your matrix has
99+
multiple eigenvalues that are near unity then no need to
100+
worry about this.
101+
tol : scalar(float), optional(default=None)
102+
Specifies the bandwith of eigenvalues to consider equivalent
103+
to unity. It will consider all eigenvalues in [1-tol,
104+
1+tol] to be 1. If tol is None then will use 2*1e-
105+
precision. Only used if precision is defined
106+
107+
Returns
108+
-------
109+
stat_dists : np.ndarray : float
110+
This is an array that has the stationary distributions as
111+
its columns.
112+
113+
absorb_states : np.ndarray : ints
114+
This is a vector that says which of the states are
115+
absorbing states
116+
117+
"""
118+
P = self.P
119+
120+
invar_dists = mc_compute_stationary(P, precision=precision, tol=tol)
121+
122+
# Check to make sure all of the elements of invar_dist are positive
123+
if np.any(invar_dists<-1e-16):
124+
# print("Elements of your invariant distribution were negative; " +
125+
# "trying with additional precision")
126+
127+
if precision is None:
128+
invar_dists = mc_compute_stationary(P, precision=18, tol=tol)
129+
130+
elif precision is not None:
131+
raise ValueError("Elements of your invariant distribution were" +
132+
"negative. Try computing with higher precision")
133+
134+
self.invar_dists = invar_dists
135+
136+
return invar_dists.squeeze()
137+
138+
def simulate_markov(self, init=0, sample_size=1000):
139+
"""
140+
This method takes an initial distribution (or state) and
141+
simulates the markov chain with transition matrix P (defined by
142+
class) and initial distrubution init. See mc_sample_path.
143+
144+
Parameters
145+
----------
146+
P : array_like(float, ndim=2)
147+
A discrete Markov transition matrix
148+
149+
init : array_like(float ndim=1) or scalar(int)
150+
If init is an array_like then it is treated as the initial
151+
distribution across states. If init is a scalar then it
152+
treated as the deterministic initial state.
153+
154+
sample_size : scalar(int), optional(default=1000)
155+
The length of the sample path.
156+
157+
Returns
158+
-------
159+
sim : array_like(int, ndim=1)
160+
The simulation of states
161+
"""
162+
sim = mc_sample_path(self.P, init, sample_size)
163+
164+
return sim
165+
166+
167+
def mc_compute_stationary(P, precision=None, tol=None):
12168
"""
13169
Computes the stationary distribution of Markov matrix P.
14170
15171
Parameters
16172
----------
17173
P : array_like(float, ndim=2)
18174
A discrete Markov transition matrix
175+
precision : scalar(int), optional(default=None)
176+
Specifies the precision(number of digits of precision) with
177+
which to calculate the eigenvalues. Unless your matrix has
178+
multiple eigenvalues that are near unity then no need to worry
179+
about this.
180+
tol : scalar(float), optional(default=None)
181+
Specifies the bandwith of eigenvalues to consider equivalent to
182+
unity. It will consider all eigenvalues in [1-tol, 1+tol] to be
183+
1. If tol is None then will use 2*1e-precision. Only used if
184+
precision is defined
19185
20186
Returns
21187
-------
22-
solution : array_like(float, ndim=1)
23-
The stationary distribution for P
24-
25-
Note: Currently only supports transition matrices with a unique
26-
invariant distribution. See issue 19.
188+
solution : array_like(float, ndim=2)
189+
The stationary distributions for P
27190
28191
"""
29-
n = len(P) # P is n x n
30-
I = np.identity(n) # Identity matrix
31-
B, b = np.ones((n, n)), np.ones((n, 1)) # Matrix and vector of ones
32-
A = np.transpose(I - P + B)
33-
solution = np.linalg.solve(A, b).flatten()
192+
n = P.shape[0]
193+
194+
if precision is None:
195+
# Compute eigenvalues and eigenvectors
196+
eigvals, eigvecs = la.eig(P, left=True, right=False)
197+
198+
# Find the index for unit eigenvalues
199+
index = np.where(abs(eigvals - 1.) < 1e-12)[0]
34200

35-
return solution
201+
# Pull out the eigenvectors that correspond to unit eig-vals
202+
uniteigvecs = eigvecs[:, index]
203+
204+
invar_dists = uniteigvecs/np.sum(uniteigvecs, axis=0)
205+
206+
# Since we will be accessing the columns of this matrix, we
207+
# might consider adding .astype(np.float, order='F') to make it
208+
# column major at beginning
209+
return invar_dists
210+
211+
else:
212+
# Create a list to store eigvals
213+
invar_dists_list = []
214+
if tol is None:
215+
# If tolerance isn't specified then use 2*precision
216+
tol = 2 * 10**(-precision + 1)
217+
218+
with mp.workdps(precision):
219+
eigvals, eigvecs = mp.eig(mp.matrix(P), left=True, right=False)
220+
221+
for ind, el in enumerate(eigvals):
222+
if el>=(mp.mpf(1)-mp.mpf(tol)) and el<=(mp.mpf(1)+mp.mpf(tol)):
223+
invar_dists_list.append(eigvecs[ind, :])
224+
225+
invar_dists = np.asarray(invar_dists_list).T
226+
invar_dists = (invar_dists/sum(invar_dists)).astype(np.float)
227+
228+
return invar_dists.squeeze()
36229

37230

38231
def mc_sample_path(P, init=0, sample_size=1000):
@@ -76,5 +269,4 @@ def mc_sample_path(P, init=0, sample_size=1000):
76269
for t in range(sample_size - 1):
77270
X[t+1] = P_dist[X[t]].draw()
78271

79-
return X
80-
272+
return X

0 commit comments

Comments
 (0)