Markov Stability Analysis

library(Nestimate)

Nestimate provides two functions for Markov-chain stability analysis of transition networks:

Both functions accept a netobject, cograph_network, tna object, row-stochastic matrix, or a raw wide sequence data frame.


Data

trajectories contains 138 learners recorded at 15 time-steps each with three engagement states: Active, Average, and Disengaged.

dim(trajectories)
#> [1] 138  15
table(as.vector(trajectories), useNA = "always")
#> 
#>     Active    Average Disengaged       <NA> 
#>        703        813        354        200

Selecting mostly-active learners

We keep learners who were Active more than half the time (> 7 of 15 steps).

df <- as.data.frame(trajectories)
active_count  <- rowSums(df == "Active", na.rm = TRUE)
mostly_active <- active_count > ncol(df) / 2
cat(sum(mostly_active), "of", nrow(df), "learners qualify\n")
#> 42 of 138 learners qualify
sub <- df[mostly_active, ]

Sequence plots

state_pal <- c(Active = "#1a7a1a", Average = "#E69F00", Disengaged = "#CC79A7")

sequence_plot(
  df,
  type         = "heatmap",
  sort         = "lcs",
  k            = 3,
  k_color      = "white",
  k_line_width = 2,
  state_colors = state_pal,
  na_color     = "grey88",
  main         = "Full sample - all 138 learners",
  time_label   = "Time-step",
  y_label      = "Learner",
  legend       = "bottom"
)

sequence_plot(
  sub,
  type         = "heatmap",
  sort         = "lcs",
  k            = 2,
  k_color      = "white",
  k_line_width = 2,
  state_colors = state_pal,
  na_color     = "grey88",
  main         = "Mostly-active learners - Active > 7 of 15 steps (n = 42)",
  time_label   = "Time-step",
  y_label      = "Learner",
  legend       = "bottom"
)


Transition networks

net_all <- build_network(df,  method = "relative")
net_sub <- build_network(sub, method = "relative")

round(net_all$weights, 3)
#>            Active Average Disengaged
#> Active      0.698   0.267      0.035
#> Average     0.204   0.610      0.186
#> Disengaged  0.120   0.397      0.483
round(net_sub$weights, 3)
#>            Active Average Disengaged
#> Active      0.819   0.164      0.017
#> Average     0.683   0.288      0.029
#> Disengaged  0.643   0.143      0.214

In the mostly-active group nearly every state transitions predominantly back to Active, and the probability of entering Disengaged is almost zero.


Mean First Passage Times

pt_all <- passage_time(net_all)
pt_sub <- passage_time(net_sub)
print(pt_all, digits = 2)
#> Mean First Passage Times (3 states)
#> 
#>            Active Average Disengaged
#> Active       2.69    3.63      10.36
#> Average      5.51    2.26       7.97
#> Disengaged   6.16    2.78       5.41
#> 
#> Stationary distribution:
#>     Active    Average Disengaged 
#>     0.3719     0.4431     0.1850
print(pt_sub, digits = 2)
#> Mean First Passage Times (3 states)
#> 
#>            Active Average Disengaged
#> Active       1.27    6.12      51.99
#> Average      1.47    5.36      51.29
#> Disengaged   1.54    6.28      41.75
#> 
#> Stationary distribution:
#>     Active    Average Disengaged 
#>     0.7895     0.1866     0.0240

Active to Disengaged rises from 10.4 to ~40 steps – mostly-active learners are nearly four times harder to disengage. The diagonal equals the mean recurrence time (how many steps between consecutive visits to the same state).

plot(pt_all, title = "Full sample (n = 138)")

plot(pt_sub, title = "Mostly-active learners (n = 42)")

In the mostly-active heatmap the Active column is uniformly dark (quickly reachable from any state) and the Disengaged column is uniformly pale (nearly unreachable).


Stationary distribution

#>        State Full sample Mostly active
#> 1     Active       37.2%         78.9%
#> 2    Average       44.3%         18.7%
#> 3 Disengaged       18.5%          2.4%

In the mostly-active group ~79% of long-run time is spent Active (vs 37% in the full sample).


Markov Stability

ms_all <- markov_stability(net_all)
ms_sub <- markov_stability(net_sub)

print(ms_all)
#> Markov Stability Analysis
#> 
#>       state persistence stationary_prob return_time sojourn_time
#>      Active      0.6976          0.3719        2.69         3.31
#>     Average      0.6099          0.4431        2.26         2.56
#>  Disengaged      0.4831          0.1850        5.41         1.93
#>  avg_time_to_others avg_time_from_others
#>                6.99                 5.84
#>                6.74                 3.20
#>                4.47                 9.16
print(ms_sub)
#> Markov Stability Analysis
#> 
#>       state persistence stationary_prob return_time sojourn_time
#>      Active      0.8191          0.7895        1.27         5.53
#>     Average      0.2885          0.1866        5.36         1.41
#>  Disengaged      0.2143          0.0240       41.75         1.27
#>  avg_time_to_others avg_time_from_others
#>               29.06                 1.50
#>               26.38                 6.20
#>                3.91                51.64

The stability data frame contains:

Column Meaning
persistence P[i,i] – probability of staying at the next step
stationary_prob Long-run proportion of time in this state
return_time Expected steps between consecutive visits (1/pi)
sojourn_time Expected consecutive steps before leaving (1/(1-P[i,i]))
avg_time_to_others Mean MFPT leaving this state to all others
avg_time_from_others Mean MFPT from all other states arriving here
plot(ms_all, title = "Full sample")

plot(ms_sub, title = "Mostly-active learners")

Active leads on persistence (0.81) and sojourn time (5.3 steps) – once a learner is active they stay active. Average has the lowest avg_time_from_others (1.6 steps) – the most accessible hub state. Disengaged has avg_time_from_others of ~39 steps – effectively unreachable.