--- title: "Latent transition analysis" author: "Written by [Mohammed Saqr](https://saqr.me) and [Sonsoles López-Pernas](https://sonsoles.me)" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Latent transition analysis} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r options, include = FALSE} 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. - **Group:** `student` identifies whose occasions belong together. - **Occasion order:** `sequence` is the position of each course in the student's history. Students can have different numbers of occasions. - **Indicators:** five activity measures (`browse`, `lectures`, `forum_read`, `forum_post`, `attendance`), each standardized within course, so 0 is the course average. ```{r setup} 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. ```{r fit} 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. ```{r starts} 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. ```{r profiles} 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. ```{r heatmap} 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. ```{r initial} 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. ```{r transitions} 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. ```{r transition-plot} 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. ```{r sequences, fig.height = 6} 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. ```{r entropy} 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. ```{r entropy-plot} plot(moves, what = "entropy") ``` ```{r posteriors-plot} 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](https://cran.r-project.org/package=tna) models built from the estimated transitions, which `plot()` draws. ```{r networks, message = FALSE, eval = requireNamespace("tna", quietly = TRUE)} 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: - `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: ```{r grade} 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: ```{r second-order} second <- lta(course_engagement, activity, "student", n_profiles = 2, time = "sequence", order = 2, seed = 1) BIC(homogeneous, grade, second) ``` ```{r check-extensions, include = FALSE} 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 - By default the next profile depends only on the current profile and the group class, and transition probabilities are the same at every occasion; the arguments above relax each of these. - Profiles have the same definition at every occasion unless `measurement = "occasion"`. - Indicators are independent within a profile (the default diagonal covariance; `covariance_model = "full"` or `model =` relaxes it). ```{r restore-options, include = FALSE} options(old_options) ```