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.
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.library(latents)
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")
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")
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")
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")
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")
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")
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))
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")
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.
measurement = "occasion".covariance_model = "full" or model = relaxes it).options(old_options)
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.