Latent transition analysis

knitr::opts_chunk$set(collapse = TRUE, comment = "#>", fig.width = 7,
                      fig.height = 4.5, fig.align = "center", dpi = 96)
old_options <- options(digits = 3, width = 100)

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.

Data

course_engagement has one row per course enrolment: 1,422 enrolments from 106 students. lta() needs three kinds of variables.

library(latents)

Fit

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

To check whether the starts agree, we call get_results() with "starts". Starts that reach the same log likelihood support the solution.

get_results(moves, "starts")

Profiles

To see the profiles, we call plot() on the fit. Each line is a profile's mean on each indicator, and its label gives the profile's share of the cases.

plot(moves)

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.

plot(moves, what = "heatmap")

Starting probabilities

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.

get_results(moves, "initial")

Transitions

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")

To draw the transition matrices, we call plot() with what = "transitions": one heatmap per group class, current profile in rows and next profile in columns.

plot(moves, what = "transitions")

Sequences

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.

plot(moves, what = "sequences")

Classification

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")

To 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.

plot(moves, what = "entropy")
plot(moves, what = "posteriors")

Transition networks

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.

plot(get_tna(moves))
plot(get_group_tna(moves))

Beyond homogeneous transitions

Each default assumption of the model can be tested by relaxing it in one argument:

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")

The grade's odds ratios are close to one and their intervals include one: 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)
slopes <- subset(get_results(grade, "transition_coefficients"),
                 term == "previous_grade")
stopifnot(all(slopes$conf_low < 0 & slopes$conf_high > 0),
          BIC(homogeneous) < BIC(grade), BIC(homogeneous) < BIC(second))

These 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.

Assumptions

options(old_options)


Try the latents package in your browser

Any scripts or data that you put into this service are public.

latents documentation built on Oct. 16, 2026, 5:09 p.m.