Latent transition analysis (LTA) estimates latent profiles at each occasion and the probability of moving from one profile to another between consecutive occasions. With group classes, each class has its own starting probabilities and its own transition matrix, so classes separate groups that change differently over time.
course_engagement has one row per course enrolment:
1,422 enrolments from 106 students. lta() needs three kinds
of variables.
student identifies whose
occasions belong together.sequence is the
position of each course in the student’s history. Students can have
different numbers of occasions.browse, lectures, forum_read,
forum_post, attendance), each standardized
within course, so 0 is the course average.To fit the model, we call lta() with vars
for the indicators, id for the group, time for
the occasion order, n_profiles for the number of profiles,
n_group_classes for the number of group classes,
n_starts for the number of random EM starts, and
seed for reproducible starts.
moves <- lta(course_engagement,
vars = c("browse", "lectures", "forum_read", "forum_post",
"attendance"),
id = "student", time = "sequence",
n_profiles = 3, n_group_classes = 2, n_starts = 3, seed = 1)
moves
#> Latent transition model: 3 profiles, 2 group classes
#> 1422 observations in 106 groups, up to 15 occasions (unbalanced, observed grid)
#> Log likelihood: -8242.966914; 47 parameters; BIC (groups): 16705.1155
#> Converged: TRUE after 132 iterations; best of 3 starts
#>
#> profile browse lectures forum_read forum_post attendance count proportion
#> 1 -0.776 -0.612 -0.888 -0.761 -0.929 579 0.407
#> 2 0.706 0.712 0.807 0.747 1.124 372 0.262
#> 3 0.396 0.189 0.454 0.345 0.254 471 0.331
#>
#> Variances and standard errors: get_results(x, "profiles").
#> Every other table: get_results(x, what = ), or get_results(x, "all").To check whether the starts agree, we call get_results()
with "starts". Starts that reach the same log likelihood
support the solution.
To see the profiles, we call plot() with
what = "profiles" (on a transition fit, plot()
alone draws the transition matrices). Each line is a profile’s mean on
each indicator, and its label gives the profile’s share of the
cases.
To compare profiles on a common scale, we call plot()
with what = "heatmap", which shows each mean in standard
deviations from the indicator’s overall mean.
To obtain the starting probabilities, we call
get_results() with "initial".
probability is the chance of starting in each profile,
prevalence the expected share of all occasions in it, and
group_class_probability the share of groups in the
class.
To obtain the transition probabilities, we call
get_results() with "transitions".
probability is the chance of moving from profile
from to profile to at the next occasion, and
the probabilities out of each profile sum to one. stable
marks staying in the same profile, expected_count is the
expected number of such moves in the data, and
estimated = FALSE marks a row the data cannot inform.
get_results(moves, "transitions")
#> group_class from to probability expected_count stable estimated group_class_probability
#> 1 1 1 1 0.6146 127.94 TRUE TRUE 0.7
#> 2 1 1 2 0.1756 36.56 FALSE TRUE 0.7
#> 3 1 1 3 0.2098 43.68 FALSE TRUE 0.7
#> 4 1 2 1 0.1277 38.86 FALSE TRUE 0.7
#> 5 1 2 2 0.3773 114.82 TRUE TRUE 0.7
#> 6 1 2 3 0.4950 150.61 FALSE TRUE 0.7
#> 7 1 3 1 0.1141 46.48 FALSE TRUE 0.7
#> 8 1 3 2 0.3605 146.89 FALSE TRUE 0.7
#> 9 1 3 3 0.5254 214.12 TRUE TRUE 0.7
#> 10 2 1 1 0.9386 303.23 TRUE TRUE 0.3
#> 11 2 1 2 0.0313 10.10 FALSE TRUE 0.3
#> 12 2 1 3 0.0302 9.75 FALSE TRUE 0.3
#> 13 2 2 1 0.2961 11.62 FALSE TRUE 0.3
#> 14 2 2 2 0.4819 18.91 TRUE TRUE 0.3
#> 15 2 2 3 0.2220 8.71 FALSE TRUE 0.3
#> 16 2 3 1 0.3392 11.44 FALSE TRUE 0.3
#> 17 2 3 2 0.1406 4.74 FALSE TRUE 0.3
#> 18 2 3 3 0.5202 17.54 TRUE TRUE 0.3To draw the transition matrices, we call plot() with
what = "transitions": one heatmap per group class, current
profile in rows and next profile in columns.
To see each group’s path, we call plot() with
what = "sequences": one row per group, coloured by its most
probable profile at each occasion. These assignments ignore
classification uncertainty.
To obtain classification certainty, we call
get_results() with "entropy". Relative entropy
is reported for occasions and for groups: 1 means every case is assigned
with certainty, 0 means no separation.
get_results(moves, "entropy")
#> level n_classes n_units entropy_sum relative_entropy
#> 1 individuals 3 1422 309.5 0.802
#> 2 groups 2 106 16.3 0.778To see where the uncertainty lies, we call plot() with
what = "entropy" for the per-case uncertainty within each
profile, and with what = "posteriors" for the posterior
probability of each case’s assigned profile.
To analyse the transitions as networks, we call
get_tna() for one network pooled over group classes and
get_group_tna() for one network per class. Both return tna models built from
the estimated transitions, which plot() draws.
Each default assumption of the model can be tested by relaxing it in one argument:
transition_covariates lets covariates shift the
transition probabilities: the log odds of moving to each other profile
rather than staying. A covariate may change over occasions.initial_covariates lets covariates shift the starting
profile.transitions = "occasion" estimates a separate
transition matrix for each move.measurement = "occasion" lets the profiles’ means and
variances differ by occasion.order = 2 makes the next profile depend on the two
previous ones.mover_stayer = TRUE adds a class of groups that never
change profile.enumerate_classes(..., time = "sequence") compares
numbers of profiles and group classes, and bootstrap_lrt()
tests a model against a nested one.
Does a student’s grade in the previous course change how likely they
are to move between engagement profiles? previous_grade
changes from course to course:
activity <- c("browse", "lectures", "forum_read", "forum_post", "attendance")
homogeneous <- lta(course_engagement, activity, "student", n_profiles = 2,
time = "sequence", seed = 1)
grade <- lta(course_engagement, activity, "student", n_profiles = 2,
time = "sequence", transition_covariates = "previous_grade",
seed = 1)
get_results(grade, "transition_coefficients")
#> group_class from to term estimate standard_error statistic p_value
#> 1 group_class_1 profile_1 profile_2 (Intercept) -1.8398 0.118 -15.566 1.25e-54
#> 2 group_class_1 profile_1 profile_2 previous_grade 0.0338 0.121 0.280 7.79e-01
#> 3 group_class_1 profile_2 profile_1 (Intercept) -1.4632 0.130 -11.256 2.17e-29
#> 4 group_class_1 profile_2 profile_1 previous_grade 0.0438 0.130 0.337 7.36e-01
#> conf_low conf_high odds_ratio
#> 1 -2.071 -1.608 0.159
#> 2 -0.203 0.270 1.034
#> 3 -1.718 -1.208 0.231
#> 4 -0.211 0.298 1.045The grade’s odds ratios are close to one, and its intervals, on the
log-odds scale of estimate, include zero: in these data,
the previous grade does not change the chance of switching profile. A
second-order chain, where the next profile depends on the two previous
ones, does not earn its extra parameters either:
second <- lta(course_engagement, activity, "student", n_profiles = 2,
time = "sequence", order = 2, seed = 1)
BIC(homogeneous, grade, second)
#> df BIC
#> homogeneous 23 16778
#> grade 25 16787
#> second 27 16786These extensions return a fit with transition and initial logit coefficients, Wald standard errors and probabilities per occasion; they were checked against Mplus, depmixS4 and LMest.
measurement = "occasion".covariance_model = "full" or
model = relaxes it).