11"""
2+ Authors: Chase Coleman, Spencer Lyon, Daisuke Oyama, Tom Sargent,
3+ John Stachurski
4+
25Filename: 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
714import numpy as np
15+ import scipy .linalg as la
16+ import sympy .mpmath as mp
817from .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
38231def 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