Home Steady State Distribution of an Ergodic Markov Chain
Post
Cancel

Steady State Distribution of an Ergodic Markov Chain

Introduction

In this study, two different methods to calculate steady-state probability distribution of randomly created transition probability matrices will be investigated, along with the effect of the inclusion of absorbing states. One can reach the R Markdown file used to create this report from the GitHub repository of the exercise.

Library Imports and Parameter Definitions

The only extra-Base-R library that will be included in this work is ggplot2, which will be used to generate comparison plots.

1
2
3
4
5
6
7
8
9
# Library imports

library(ggplot2)

# Defining the parameters

seed    <- 203
M       <- 200000
E       <- 0.0005

Generating Transition Probability Matrices

To generate transition probability matrices in a practical way, a function that takes the matrix size as an argument can be defined as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
GenerateTPM <- function(n) {                # Creating a function that generates
                                            # transition probability matrices
    
    TPM <- matrix( ,                        # Creating an empty (n+1) x (n+1) matrix
                  nrow = n+1,
                  ncol = n+1)               
    
    for (i in 1:(n+1)) {                    # Filling the matrix with random numbers
                                            # between 0 and 1
        
        for (j in 1:(n+1)) {
            
            TPM[i,j] <- runif(1)            # Random numbers from uniform distribution
        
        }
    }
    
    for (i in 1:(n+1)) {                    # Adjusting the matrix such that
                                            # all the rows sum up to 1
        
        TPM[i,] <- TPM[i,]/sum(TPM[i,])
        
    }
    
    return(TPM)
}

The next step is to create 3 different transition matrices:

  • P1 with n=5,
  • P2 with n=25,
  • P3 with n=50
1
2
3
4
5
6
7
8
9
set.seed(seed)

P1 <- GenerateTPM(5)
P2 <- GenerateTPM(25)
P3 <- GenerateTPM(50)

# The appearance of the smallest matrix can be checked to keep track

print(round(P1, 2))
1
2
3
4
5
6
7
##      [,1] [,2] [,3] [,4] [,5] [,6]
## [1,] 0.09 0.24 0.24 0.09 0.21 0.13
## [2,] 0.25 0.36 0.04 0.00 0.17 0.19
## [3,] 0.25 0.25 0.06 0.17 0.23 0.05
## [4,] 0.08 0.00 0.15 0.13 0.37 0.27
## [5,] 0.06 0.09 0.10 0.30 0.16 0.29
## [6,] 0.16 0.15 0.26 0.15 0.26 0.02

Applying Monte Carlo Simulation

To apply Monte Carlo Simulation with the created matrices, a function that takes one matrix and one number for repetitions as arguments can be defined:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
MonteCarlo <- function(TPM, n) {
    
    size <- nrow(TPM)               # Creating a variable to store
                                    # the size of the matrix
    
    X0 <- sample(1:size, 1)         # Setting the initial state
    
    activeState <- X0               # Initializing the variable that
                                    # stores the current state
    
    statesVec <- c()                # Creating a vector to keep track of
                                    # the occurrence of states
    
    for (rep in 1:n) {              # Creating a loop to control
                                    # the switches between states
        
        r <- runif(1)               # Assigning the random value of "r"
        
        cdf <- c(0, cumsum(TPM[activeState, ]))
        
        for (j in 1:size) {         # Creating a loop to determine
                                    # the interval of "r",
                                    # and also to update the current state
                                    # and the occurrence sequence accordingly
            
            if ((r >    cdf[j]  ) && 
                (r <=   cdf[j+1])   ) {
                
                activeState <- j
                
                statesVec <- append(statesVec, j)
                
            }
            
                
        }
        
    }
    
    return(table(statesVec)/n)
    
}

Simulating with the previously obtained probability matrices yields the following steady-state distributions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
exeTimes <- data.frame(row.names    = c("Monte Carlo",      # Creating an empty data frame
                                        "Matrix Mult."),    # to store execution times 
                       P1           = numeric(2),
                       P2           = numeric(2),
                       P3           = numeric(2)        )

set.seed(seed)

exeTimes[c("Monte Carlo"), c("P1")] <- Sys.time()

P1MC <- MonteCarlo(P1, M)

exeTimes[c("Monte Carlo"), c("P1")] <- Sys.time() - exeTimes[c("Monte Carlo"), c("P1")]

P1MC
1
2
3
## statesVec
##        1        2        3        4        5        6 
## 0.143100 0.177635 0.136880 0.148900 0.225485 0.168000
1
2
3
4
5
6
7
exeTimes[c("Monte Carlo"), c("P2")] <- Sys.time()

P2MC <- MonteCarlo(P2, M)

exeTimes[c("Monte Carlo"), c("P2")] <- Sys.time() - exeTimes[c("Monte Carlo"), c("P2")]

P2MC
1
2
3
4
5
6
7
8
9
## statesVec
##        1        2        3        4        5        6        7        8 
## 0.043520 0.032165 0.043510 0.042040 0.043160 0.036385 0.037285 0.041145 
##        9       10       11       12       13       14       15       16 
## 0.042865 0.039145 0.040090 0.035130 0.037885 0.033085 0.035305 0.035915 
##       17       18       19       20       21       22       23       24 
## 0.032975 0.031045 0.037090 0.043020 0.043805 0.033115 0.039085 0.039055 
##       25       26 
## 0.043275 0.038905
1
2
3
4
5
6
7
exeTimes[c("Monte Carlo"), c("P3")] <- Sys.time()

P3MC <- MonteCarlo(P3, M)

exeTimes[c("Monte Carlo"), c("P3")] <- Sys.time() - exeTimes[c("Monte Carlo"), c("P3")]

P3MC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
## statesVec
##        1        2        3        4        5        6        7        8 
## 0.021075 0.018225 0.019560 0.019240 0.018770 0.020910 0.017380 0.017940 
##        9       10       11       12       13       14       15       16 
## 0.019640 0.019665 0.022670 0.020470 0.020085 0.019405 0.017140 0.018220 
##       17       18       19       20       21       22       23       24 
## 0.019765 0.020750 0.015515 0.018245 0.018785 0.017710 0.022470 0.017800 
##       25       26       27       28       29       30       31       32 
## 0.021980 0.019665 0.018545 0.021540 0.021550 0.020760 0.017310 0.018465 
##       33       34       35       36       37       38       39       40 
## 0.020820 0.020365 0.018765 0.020685 0.022740 0.023395 0.018875 0.016125 
##       41       42       43       44       45       46       47       48 
## 0.018185 0.019380 0.020755 0.018655 0.020260 0.020795 0.019210 0.021725 
##       49       50       51 
## 0.018700 0.017200 0.022115

Applying Martix Multiplication Method

A function that takes P and a stopping condition parameter ε and applies matrix multiplication until the length of the difference between a randomly selected row and row averages is less than ε can be defined as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
matrixMult <- function(TPM, E) {
    
    activeMatrix <- TPM                                                 
    
    piBar <- colMeans(activeMatrix)                     # Average of rows 
                                                        # stored in a vector
    
    randomRow <- sample(1:nrow(activeMatrix), 1)        # Picking a random row
    
    convergence <- sqrt(sum(                            # A logical variable that
        (activeMatrix[randomRow, ] - piBar )^2)  ) < E  # indicates whether
                                                        # the current convergence 
                                                        # is sufficient
    
    while (!convergence) {                              # Updating the local variables
                                                        # until the convergence condition
                                                        # is satisfied
        
        activeMatrix <- activeMatrix %*% activeMatrix   # Matrix multiplication
        
        piBar <- colMeans(activeMatrix)
        
        randomRow <- sample(1:nrow(activeMatrix), 1)
        
        convergence <- sqrt(sum((activeMatrix[randomRow, ] - piBar)^2)) < E
        
    }
    
    return(piBar)
    
}
1
2
3
4
5
6
7
8
9
set.seed(seed)

exeTimes[c("Matrix Mult."), c("P1")] <- Sys.time()

P1MM <- matrixMult(P1, E)

exeTimes[c("Matrix Mult."), c("P1")] <- Sys.time() - exeTimes[c("Matrix Mult."), c("P1")]

P1MM
1
## [1] 0.1418896 0.1752635 0.1367005 0.1508137 0.2276087 0.1677241
1
2
3
4
5
6
7
exeTimes[c("Matrix Mult."), c("P2")] <- Sys.time()

P2MM <- matrixMult(P2, E)

exeTimes[c("Matrix Mult."), c("P2")] <- Sys.time() - exeTimes[c("Matrix Mult."), c("P2")]

P2MM
1
2
3
4
5
##  [1] 0.04261998 0.03275381 0.04323659 0.04276693 0.04295757 0.03671693
##  [7] 0.03694794 0.04117294 0.04242615 0.03842398 0.04019447 0.03560988
## [13] 0.03771862 0.03295448 0.03598425 0.03575689 0.03313544 0.03162181
## [19] 0.03708412 0.04285083 0.04333381 0.03306816 0.03928567 0.03890044
## [25] 0.04354170 0.03893662
1
2
3
4
5
6
7
exeTimes[c("Matrix Mult."), c("P3")] <- Sys.time()

P3MM <- matrixMult(P3, E)

exeTimes[c("Matrix Mult."), c("P3")] <- Sys.time() - exeTimes[c("Matrix Mult."), c("P3")]

P3MM
1
2
3
4
5
6
7
8
9
##  [1] 0.02036234 0.01811745 0.01958708 0.01933626 0.01850812 0.02098865
##  [7] 0.01734776 0.01838142 0.01958771 0.01913874 0.02268059 0.02094601
## [13] 0.01978689 0.01907543 0.01730207 0.01811857 0.01923823 0.02069250
## [19] 0.01549397 0.01832879 0.01855905 0.01765549 0.02279090 0.01745068
## [25] 0.02238944 0.01951077 0.01893984 0.02127761 0.02170173 0.02076694
## [31] 0.01758487 0.01852527 0.02110391 0.01969839 0.01914967 0.01991998
## [37] 0.02259353 0.02353297 0.01934586 0.01617337 0.01900646 0.01907645
## [43] 0.02077114 0.01867158 0.02087822 0.02070635 0.01963375 0.02154106
## [49] 0.01849888 0.01731532 0.02221192

Comparison of the Results Obtained by Two Different Methods

First off, the similarity between the obtained steady-state distributions by the two methods can be checked. This can be done by plotting the distributions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ggplot(data.frame(P1MC, P1MM),
       aes(x = statesVec)       )               +
    
    geom_line(aes(y     = Freq,
                  color = 'Monte Carlo',
                  group = 1)            )       +
    
    geom_line(aes(y     = P1MM,
                  color = 'Matrix Mult.',
                  group = 1)                )   +
    
    labs(x = "State",
         y = "Probability")                     +
    
    scale_color_manual(name     = "Method",
                       values   = c("Monte Carlo"     = "darkblue",
                                    "Matrix Mult."    = "firebrick3")   )

Figure 1

Figure 1. Steady State Distributions Obtained Using P1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ggplot(data.frame(P2MC, P2MM),
       aes(x = statesVec)       )               +
    
    geom_line(aes(y     = Freq,
                  color = 'Monte Carlo',
                  group = 1)            )       +
    
    geom_line(aes(y     = P2MM,
                  color = 'Matrix Mult.',
                  group = 1)                )   +
    
    labs(x = "State",
         y = "Probability")                     +
    
    scale_color_manual(name     = "Method",
                       values   = c("Monte Carlo"     = "darkblue",
                                    "Matrix Mult."    = "firebrick3")   )

Figure 2

Figure 2. Steady State Distributions Obtained Using P2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ggplot(data.frame(P3MC, P3MM),
       aes(x = statesVec)       )               +
    
    geom_line(aes(y     = Freq,
                  color = 'Monte Carlo',
                  group = 1)            )       +
    
    geom_line(aes(y     = P3MM,
                  color = 'Matrix Mult.',
                  group = 1)                )   +
    
    labs(x = "State",
         y = "Probability")                     +
    
    theme(axis.text.x  = element_blank())       +
    
    scale_color_manual(name     = "Method",
                       values   = c("Monte Carlo"     = "darkblue",
                                    "Matrix Mult."    = "firebrick3")   )

Figure 3

Figure 3. Steady State Distributions Obtained Using P3

As can be seen in the above plots, the distributions are quite close, and their difference increases when the size of the matrix P is increased. This can be associated with the fact that higher matrix sizes retard convergence.

Next, execution times can be checked as follows:

1
print(round(exeTimes, 3))
1
2
3
##                  P1     P2     P3
## Monte Carlo  35.479 37.226 37.697
## Matrix Mult.  0.319  0.002  0.000

There is a huge difference between the run times, probably due to the large M value used during Monte Carlo Simulations. Keeping in the mind that the two methods have given similar distributions, it can be concluded that matrix multiplication method works more efficiently. This stems from its exponential-like approach, being multiplying the matrix P by itself repeatedly, that allows the algorithm to skip unnecessary calculations, while Monte Carlo Simulation calculates each state one by one.

Checking the Case with Absorbing States

To include an absorbing state, one element in the diagonal should be set to 1, in other words, in each matrix there should be one element that satisfies the condition pjj. Simply, p11, p22, p33 can be selected for P1, P2, P3, respectively:

1
2
3
4
5
6
7
8
P1Abs       <- P1
P2Abs       <- P2
P3Abs       <- P3

P1Abs[2, ]  <- c(0, 1, rep(0, 4))           # Row number is 1+1, since R starts indexing 
                                            # from 1 instead of 0
P2Abs[3, ]  <- c(0, 0, 1, rep(0, 23))     
P3Abs[4, ]  <- c(0, 0, 0, 1, rep(0, 47))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
exeTimesAbs <- data.frame(row.names = c("Monte Carlo",      # Creating an empty data frame
                                        "Matrix Mult."),    # to store execution times 
                          P1        = numeric(2),
                          P2        = numeric(2),
                          P3        = numeric(2)        )

set.seed(seed)

exeTimesAbs[c("Monte Carlo"), c("P1")] <- Sys.time()

P1AbsMC <- MonteCarlo(P1Abs, M)

exeTimesAbs[c("Monte Carlo"), c("P1")] <-   Sys.time() - 
                                            exeTimesAbs[c("Monte Carlo"), c("P1")]

P1AbsMC
1
2
3
## statesVec
##        2        3 
## 0.999995 0.000005
1
2
3
4
5
6
7
8
exeTimesAbs[c("Monte Carlo"), c("P2")] <- Sys.time()

P2AbsMC <- MonteCarlo(P2Abs, M)

exeTimesAbs[c("Monte Carlo"), c("P2")] <-   Sys.time() - 
                                            exeTimesAbs[c("Monte Carlo"), c("P2")]

P2AbsMC
1
2
3
4
5
## statesVec
##        3        4        5        7       13       15       19       20 
## 0.999945 0.000015 0.000005 0.000005 0.000005 0.000005 0.000005 0.000005 
##       21       24 
## 0.000005 0.000005
1
2
3
4
5
6
7
8
exeTimesAbs[c("Monte Carlo"), c("P3")] <- Sys.time()

P3AbsMC <- MonteCarlo(P3Abs, M)

exeTimesAbs[c("Monte Carlo"), c("P3")] <-   Sys.time() - 
                                            exeTimesAbs[c("Monte Carlo"), c("P3")]

P3AbsMC
1
2
3
4
5
## statesVec
##        2        3        4        8       13       14       19       22 
## 0.000005 0.000005 0.999945 0.000005 0.000005 0.000005 0.000005 0.000005 
##       25       28       46 
## 0.000005 0.000010 0.000005
1
2
3
4
5
6
7
8
9
10
set.seed(seed)

exeTimesAbs[c("Matrix Mult."), c("P1")] <- Sys.time()

P1AbsMM <- matrixMult(P1Abs, E)

exeTimesAbs[c("Matrix Mult."), c("P1")] <-  Sys.time() -
                                            exeTimesAbs[c("Matrix Mult."), c("P1")]

P1AbsMM
1
2
## [1] 1.515655e-05 9.998906e-01 1.915830e-05 2.327182e-05 3.069990e-05
## [6] 2.114914e-05
1
2
3
4
5
6
7
8
exeTimesAbs[c("Matrix Mult."), c("P2")] <- Sys.time()

P2AbsMM <- matrixMult(P2Abs, E)

exeTimesAbs[c("Matrix Mult."), c("P2")] <-  Sys.time() -
                                            exeTimesAbs[c("Matrix Mult."), c("P2")]

P2AbsMM
1
2
3
4
5
6
##  [1] 1.217847e-04 8.934742e-05 9.973187e-01 1.240207e-04 1.247643e-04
##  [6] 9.977755e-05 1.020275e-04 1.137461e-04 1.237126e-04 1.051454e-04
## [11] 1.147747e-04 9.908271e-05 1.032044e-04 9.266297e-05 1.041990e-04
## [16] 9.780057e-05 9.190099e-05 9.137568e-05 1.011092e-04 1.187355e-04
## [21] 1.193959e-04 9.426228e-05 1.076331e-04 1.127492e-04 1.195337e-04
## [26] 1.085060e-04
1
2
3
4
5
6
7
8
exeTimesAbs[c("Matrix Mult."), c("P3")] <- Sys.time()

P3AbsMM <- matrixMult(P3Abs, E)

exeTimesAbs[c("Matrix Mult."), c("P3")] <-  Sys.time() -
                                            exeTimesAbs[c("Matrix Mult."), c("P3")]

P3AbsMM
1
2
3
4
5
6
7
8
9
##  [1] 0.001635044 0.001455174 0.001568544 0.920604415 0.001470199 0.001687420
##  [7] 0.001399524 0.001473365 0.001575034 0.001523901 0.001823980 0.001690725
## [13] 0.001613915 0.001560759 0.001419325 0.001487944 0.001558805 0.001663124
## [19] 0.001269764 0.001484766 0.001519511 0.001435973 0.001831733 0.001385525
## [25] 0.001814823 0.001593939 0.001548640 0.001719198 0.001767902 0.001705231
## [31] 0.001439168 0.001518595 0.001686385 0.001594235 0.001561261 0.001599391
## [37] 0.001819802 0.001920896 0.001555786 0.001276327 0.001562249 0.001563628
## [43] 0.001715398 0.001498189 0.001685407 0.001660050 0.001598088 0.001771775
## [49] 0.001478922 0.001408776 0.001797467

As can be expected, in both methods, absorbing states have the major part of the total probability, reducing the other states to negligible steady-state probabilities. In all of the cases, their steady-state probabilities are above 90%, due to the infinite loop that starts after the first encounter with the absorbing state.

An important difference between the two methods this time is that Monte Carlo Simulation has 0 probabilities for some states in all three simulations, while the matrix multiplication method yields a steady-state distribution consisting full of non-zero values. This is because Monte Carlo Simulation is a more practical method and it only assigns non-zero probabilities to the states that occur before the first occurrence of the absorbing state during the simulation process. On the other side, the matrix multiplication method works in a theoretical way and assigns a small probability to every non-absorbing state.

When it comes to execution times, the new results are as follows:

1
print(round(exeTimesAbs, 3))
1
2
3
##                  P1     P2     P3
## Monte Carlo  44.896 37.699 37.463
## Matrix Mult.  0.001  0.003  0.003

It would not be fair to draw any conclusions from the execution times since there is no significant change or any obvious behaviors in differences between the trials with and without absorbing states. However, with a more elaborate work, the above algorithm implemented for Monte Carlo Simulation can be enhanced by the addition of some extra rules that stop iterations after the first occurrence of the absorbing state since from that point on there will be no change of state, and this would potentially reduce the execution time dramatically by eliminating redundant calculations.

IE 203 - Operations Research II
Boğaziçi University - Industrial Engineering Department
GitHub Repository

This post is licensed under CC BY 4.0 by the author.