
2 Confirmatory Latent Class Analysis
Latent profile analysis (LPA) is often used exploratorily: researchers estimate several class solutions and then decide which one best describes the data. But LPA can also be used confirmatorily, when theory makes specific predictions about the latent class structure.
To follow along and run the code yourself, open the file lca_confirmatory.qmd in the lca_course project, as explained in Appendix A.
In this first example, suppose that clinical psychological theory proposes the existence of two subpopulations:
- people without depression
- people with depression
If all we know is people’s depression score, then we can try to identify two latent classes, corresponding to these subpopulations. Suppose that the theory further specifies that a depression score of about 10 or higher is required to count as depressed. We will use this theory to formulate two increasingly specific hypotheses:
- a two-class model should fit better than a one-class model. Adding a third class should not significantly improve fit.
- the class means should be close to 0 for the non-depressed class and 10 for the depressed class.
The second hypothesis is considerably stronger because it constrains the class-specific means to exact values.
2.1 Simulate example data
We first create a simple dataset containing a single continuous depression score.
2.2 Test the number of classes
The first confirmatory hypothesis concerns class enumeration. Theory predicts two classes, so we estimate one-, two-, and three-class solutions.
| Name | Classes | LL | n | Parameters | AIC | BIC | saBIC | Entropy | prob_min | prob_max | n_min | n_max | np_ratio | np_local |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| equal var 1 | 1 | -398.3120 | 150 | 2 | 800.6240 | 806.6453 | 800.3156 | 1.0000000 | 1.0000000 | 1.0000000 | 1.0000000 | 1.0000000 | 75.0 | 75.00000 |
| equal var 2 | 2 | -346.5824 | 150 | 4 | 701.1647 | 713.2072 | 700.5480 | 0.9495445 | 0.9432520 | 0.9953478 | 0.1733333 | 0.8266667 | 37.5 | 17.33333 |
| equal var 3 | 3 | -314.9716 | 150 | 6 | 641.9432 | 660.0070 | 641.0182 | 0.9658533 | 0.9590919 | 0.9889837 | 0.1200000 | 0.7200000 | 25.0 | 13.50000 |
The fit table lets us inspect information criteria and other model-fit statistics across the competing class solutions.
If we want to test the theory, we can compare adjacent class solutions using the bootstrapped likelihood ratio test (BLRT).
| null | alt | lr | df | blrt_p | samples | |
|---|---|---|---|---|---|---|
| equal var 1 | mix1 | mix2 | 103.45929 | 2 | 0 | 67 |
| equal var 2 | mix2 | mix3 | 63.22149 | 2 | 0 | 91 |
Quiz
According to BIC, the best-fitting model is:
According to BLRT, the two-class model fits significantly better than the 1- and 3-class model.
2.3 Translate theoretical assumptions into parameter constraints
The models above still allow all parameters to be estimated freely from the data. That tests only the broad hypothesis that two groups exist. The groups themselves could diverge substantially from what was theorized.
Our theory posits that the non-depressed class should have a mean near 0, whereas the depressed class should have a mean near 10 (technically, the minimum value in that class should be 10, but that’s a bit more difficult to enforce).
We can represent this stronger hypothesis by fixing the two class means to these values, or by e.g. setting a parameter constraint for the two classes.
First, create the two-class profile model without estimating it and examine its parameters:
equal var 2.weights[1,2] v1 m11
0.2605042 2.3038225 0.7251967
m21
8.3585995
The relevant mean parameters are m11 and m21. We can either fix these means to the exact values of 0 and 10:
Or alternatively, we could set a lower and upper bound for these parameters, thus allowing bounded free estimation:
Now, run your chosen model:
Next, compare the unconstrained two-class solution with the theory-constrained model.
| Name | Classes | LL | n | Parameters | AIC | BIC | saBIC | Entropy | prob_min | prob_max | n_min | n_max | np_ratio | np_local |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 2 | -346.5824 | 150 | 4 | 701.1647 | 713.2072 | 700.5480 | 0.9495445 | 0.9432520 | 0.9953478 | 0.1733333 | 0.8266667 | 37.5 | 17.33333 |
| 2 | 2 | -368.6431 | 150 | 2 | 741.2862 | 747.3075 | 740.9778 | 0.9488272 | 0.9595115 | 0.9918444 | 0.1733333 | 0.8266667 | 75.0 | 52.00000 |
| 3 | 2 | -350.6497 | 150 | 4 | 709.2994 | 721.3419 | 708.6827 | 0.9599385 | 0.9670867 | 0.9939682 | 0.1600000 | 0.8400000 | 37.5 | 16.00000 |
We can also compare the models using the BLRT:
| null | alt | lr | df | blrt_p | samples | |
|---|---|---|---|---|---|---|
| 2 | mix2 | equal var 2 | -44.12149 | -2 | 0.47 | 100 |
Note that fixing the class means to exact values is a very strong assumption. In many real applications, theory may imply that class means should be near certain values rather than exactly equal to them. You can also manually specify start values, and then test if the estimated parameter values deviate significantly from these:
equal var 2.weights[1,2] v1 m11
0.2190895 2.4874123 0.8699903
m21
8.8424735
NULL
Quiz
According to the BIC, which model is best?
According to the BLRT, we should prefer the constrained model to the free model.
According to the Wald test, the mean of class 2 is not significantly different from 10.
3 Caregiver Compass
For this next example, we will use simulated data based on work by Zegwaard and colleagues, who sought to establish a typology of caregivers who support a close other receiving outpatient psychological care. Qualitative research among experts resulted in a theory postulating the existence of four types of caregivers (translated from the original Dutch):
Balanced
The balanced caregiver experiences relative balance between the costs and benefits of caring for a close other.
Imbalanced
The imbalanced caregiver experiences a precarious balance between the costs and benefits of caring for a close other.
Lonely
The lonely caregiver experiences a strong sense of isolation.
Entrapped
The entrapped caregiver strongly feels a sense of being entangled in responsibilities which are difficult to fulfill.
The goal of this confirmatory study was to validate this hypothesized class solution in a sample of caregivers. A convenience sample was used, with no prior sample size justification. To view the data documentation, run the command ?tidySEM::zegwaard_carecompass in the R console.
3.1 Loading the Data
To load the data, simply attach the tidySEM package. For convenience, we assign the variables used for analysis to an object called df. We first only use the four scales: c("burdened", "trapped", "negaffect", "loneliness").
3.2 Descriptive statistics
We use tidySEM::descriptives() to describe the data numerically. Because all scales are continuous, we select only columns for continuous data to de-clutter the table:
| name | n | missing | unique | mean | median | sd | min | max | skew_2se | kurt_2se |
|---|---|---|---|---|---|---|---|---|---|---|
| burdened | 509 | 0.0077973 | 509 | 3.383978 | 3.385765 | 0.7468695 | 1.1958284 | 5.282460 | 0.1723420 | -0.4102692 |
| trapped | 505 | 0.0155945 | 505 | 1.725223 | 1.787354 | 0.8976391 | -0.8578806 | 3.805728 | -1.0343850 | -1.5170521 |
| negaffect | 506 | 0.0136452 | 506 | 2.502401 | 2.519129 | 0.6901424 | 0.7086115 | 4.983359 | 0.0843737 | -0.3823452 |
| loneliness | 510 | 0.0058480 | 510 | 2.658997 | 2.692829 | 0.6176745 | 0.9845240 | 4.217298 | -0.3266117 | -0.6222047 |
The table indicates two potential causes for concern: there is a small percentage of missingness, and all variables have relatively high kurtosis. Since there are some missing values, we can conduct an MCAR test using mice::mcar(df):
iter imp variable
1 1 burdened trapped negaffect loneliness
1 2 burdened trapped negaffect loneliness
1 3 burdened trapped negaffect loneliness
1 4 burdened trapped negaffect loneliness
1 5 burdened trapped negaffect loneliness
2 1 burdened trapped negaffect loneliness
2 2 burdened trapped negaffect loneliness
2 3 burdened trapped negaffect loneliness
2 4 burdened trapped negaffect loneliness
2 5 burdened trapped negaffect loneliness
3 1 burdened trapped negaffect loneliness
3 2 burdened trapped negaffect loneliness
3 3 burdened trapped negaffect loneliness
3 4 burdened trapped negaffect loneliness
3 5 burdened trapped negaffect loneliness
4 1 burdened trapped negaffect loneliness
4 2 burdened trapped negaffect loneliness
4 3 burdened trapped negaffect loneliness
4 4 burdened trapped negaffect loneliness
4 5 burdened trapped negaffect loneliness
5 1 burdened trapped negaffect loneliness
5 2 burdened trapped negaffect loneliness
5 3 burdened trapped negaffect loneliness
5 4 burdened trapped negaffect loneliness
5 5 burdened trapped negaffect loneliness
Missing data patterns: 3 used, 3 removed.
Cases used: 506
Hawkins' test: median chi^2 (6) = 4.369945, median p = 0.626746
Interpretation of results:
Hawkins' test is not significant; there is no evidence to reject the assumptions of multivariate normality and MCAR.
You should find that, according to Hawkins’ test, there is no evidence to reject the assumptions of multivariate normality and MCAR. In tidySEM, missing data is accounted for using FIML by default, meaning that available data will be used without imputation. This is appropriate if data are missing completely at random, or if the missingness is contingent on the observed variables - but not if missingness is contingent on the missing data (which is impossible to diagnose, and for which no adequate solution exists).
Additionally, we can plot the data. The ggplot2 function geom_density() is useful for continuous data. Visual inspection confirms the conclusions from the descriptives() table: the data are kurtotic (peaked).
3.3 Conducting Latent Profile Analysis
As all variables are continuous, we can use the convenience function tidySEM::mx_profiles(), which is a wrapper for the generic function mx_mixture() optimized for continuous indicators. Its default settings are appropriate for LPA, assuming fixed variances across classes and zero covariances. Its arguments are data and number of classes. All variables in data are included in the analysis, which is why we first selected the indicator variables. As this is a confirmatory LCA, we do not follow a strictly data-driven class enumeration procedure. We will set the maximum number of classes \(K\) to one more than the theoretically expected number. We set a seed to ensure replicable results.
This analysis should produce some messages about cluster initialization. These relate to the selection of starting values, which relies on the K-means algorithm and is not robust to missing data. The algorithm automatically switches to hierarchical clustering, no further action is required.
3.4 Class Enumeration
To compare the fit of the theoretical model against other models, we create a model fit table using table_fit() and retain relevant columns. We also determine whether any models can be disqualified.
In this example, all models converge without issues. If, for example, the two-class solution had not converged, we could use the function res[[2]] <- mxTryHard(res[[2]]) to aid convergence.
Next, we check for local identifiability. The sample size is consistently reported as 513, which means that partially missing cases were indeed included via FIML. The smallest class size occurs in the 5-class model, where the smallest class is assigned 7% of cases, or 38 cases. This model has 28 parameters, approximately 6 per class. We thus have at least five observations per parameter in every class, and do not disqualify the 5-class model.
There are concerns about theoretical interpretability of all solutions, as the entropies and minimum classification probabilities are all low. However, in this confirmatory use case, we address this when interpreting the results.
| Name | LL | Parameters | n | BIC | Entropy | prob_min | prob_max | n_min | n_max | np_ratio | np_local |
|---|---|---|---|---|---|---|---|---|---|---|---|
| equal var 1 | -2241.982 | 8 | 513 | 4533.886 | 1.0000000 | 1.0000000 | 1.0000000 | 1.0000000 | 1.0000000 | 64.12500 | 64.1250000 |
| equal var 2 | -2031.365 | 13 | 513 | 4143.854 | 0.7404912 | 0.9116044 | 0.9298845 | 0.4191033 | 0.5808967 | 39.46154 | 35.8333333 |
| equal var 3 | -1951.349 | 18 | 513 | 4015.023 | 0.7797238 | 0.8860997 | 0.9146913 | 0.1871345 | 0.5438596 | 28.50000 | 18.0000000 |
| equal var 4 | -1916.253 | 23 | 513 | 3976.033 | 0.7548658 | 0.8142116 | 0.9151926 | 0.1559454 | 0.3430799 | 22.30435 | 16.0000000 |
| equal var 5 | -1912.370 | 28 | 513 | 3999.469 | 0.7878139 | 0.8109998 | 0.9155951 | 0.0038986 | 0.3411306 | 18.32143 | 0.4166667 |
3.4.1 Using ICs
the 4-class solution has the lowest BIC, which means it is preferred over all other solutions including a 1-class solution and a solution with more classes. Note that a scree plot for the BIC can be plotted by calling plot(fit). Following the elbow criterion, a three-class solution would also be defensible. The function ic_weights(fit) allows us to compute IC weights; it indicates that, conditional on the set of models, the 4-class model has a posterior model probability of nearly 100%.
3.4.2 Using LMR tests
Note that the use of LMR tests is discouraged; these tests make assumptions that are known to be violated for latent class analyses (Van Lissa et al., 2024). Nevertheless - if we do conduct LMR tests, we find that the tests are significant for all pairwise model comparisons, except for the 5-class model. Note that, as this analysis takes a long time, you don’t have to run it - you can load its results from a file.
3.4.3 Using BLRT tests
We can also use the BLRT test, which is the best practice solution for class enumeration, but is very computationally expensive. You can load the test results from a file, and we used a low number of replications to make it more feasible. In practice, one might use a much higher number (1000+) for published research. Keep in mind that the p-value of the BLRT is subject to Monte Carlo error; if it fluctuates when analyses are replicated or its value is very close to the critical threshold, consider increasing the number of replications.
To accelerate computations, we can use the future package for parallel computing (see ?plan to select the appropriate back-end for your system). To track the function’s progress, we use the progressr ecosystem, which allows users to choose how they want to be informed. The example below uses a progress bar:
| null | alt | lr | df | blrt_p | samples | |
|---|---|---|---|---|---|---|
| equal var 1 | mix1 | mix2 | 421.23 | 5 | 0.00 | 100 |
| equal var 2 | mix2 | mix3 | 160.03 | 5 | 0.00 | 100 |
| equal var 3 | mix3 | mix4 | 70.19 | 5 | 0.00 | 100 |
| equal var 4 | mix4 | mix5 | 7.77 | 5 | 0.36 | 100 |
In sum, across all class enumeration criteria, there is strong support for a 4-class solution.
3.5 Comparing Competing Theoretical Model Specifications
In the case of confirmatory LCA, the theory would be refuted by strong evidence against the hypothesized model and number of classes. In the preceding, we only compared the theoretical model against models with different number of classes. Imagine, however, that a Reviewer argues that variance ought to be freely estimated across classes. We could compare our theoretical model against their competing model as follows. Note that we can put two models into a list to compare them.
| Name | Classes | LL | n | Parameters | AIC | BIC | saBIC | Entropy | prob_min | prob_max | n_min | n_max | np_ratio | np_local |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 4 | -1916.253 | 513 | 23 | 3878.507 | 3976.033 | 3903.028 | 0.7548658 | 0.8142116 | 0.9151926 | 0.1559454 | 0.3430799 | 22.30435 | 16.00 |
| 2 | 4 | -1909.232 | 513 | 35 | 3888.464 | 4036.874 | 3925.778 | 0.7816344 | 0.8445960 | 0.9210385 | 0.1598441 | 0.3157895 | 14.65714 | 10.25 |
The alternative model incurs 12 additional parameters for the free variances. Yet, it has a higher BIC, which indicates that this additional complexity does not outweigh the increase in fit.
You can obtain a significance test for this model comparison by using the BLRT:
3.6 Interpreting the Final Class Solution
To interpret the final class solution, we first reorder the 4-class model by class size. This helps prevent label switching.
The 4-class model yielded classes of reasonable size; the largest class comprised 33%, and the smallest comprised 16% of cases. However, the entropy was low, \(S = .75\), indicating poor class separability. Furthermore, the posterior classification probability ranged from \([.81, .92]\), which means that at least some classes had a high classification error. We produce a table of the results below.
| label | est | se | confint | class |
|---|---|---|---|---|
| Means.burdened.class1 | 3.27 | 0.04 | [3.18, 3.36] | class1 |
| Means.trapped.class1 | 1.28 | 0.05 | [1.18, 1.38] | class1 |
| Means.negaffect.class1 | 2.31 | 0.06 | [2.20, 2.42] | class1 |
| Means.loneliness.class1 | 2.73 | 0.04 | [2.64, 2.82] | class1 |
| Variances.burdened.class1 | 0.23 | 0.02 | [0.19, 0.27] | class1 |
| Variances.trapped.class1 | 0.17 | 0.02 | [0.14, 0.20] | class1 |
| Variances.negaffect.class1 | 0.31 | 0.02 | [0.27, 0.36] | class1 |
| Variances.loneliness.class1 | 0.24 | 0.02 | [0.20, 0.28] | class1 |
| Means.burdened.class2 | 3.40 | 0.06 | [3.28, 3.52] | class2 |
| Means.trapped.class2 | 2.27 | 0.06 | [2.15, 2.38] | class2 |
| Means.negaffect.class2 | 2.81 | 0.06 | [2.70, 2.93] | class2 |
| Means.loneliness.class2 | 2.79 | 0.06 | [2.66, 2.91] | class2 |
| Means.burdened.class3 | 4.25 | 0.07 | [4.12, 4.38] | class3 |
| Means.trapped.class3 | 2.67 | 0.05 | [2.58, 2.77] | class3 |
| Means.negaffect.class3 | 2.92 | 0.06 | [2.80, 3.03] | class3 |
| Means.loneliness.class3 | 2.01 | 0.06 | [1.89, 2.14] | class3 |
| Means.burdened.class4 | 2.38 | 0.06 | [2.26, 2.50] | class4 |
| Means.trapped.class4 | 0.38 | 0.05 | [0.28, 0.49] | class4 |
| Means.negaffect.class4 | 1.78 | 0.07 | [1.65, 1.91] | class4 |
| Means.loneliness.class4 | 3.18 | 0.06 | [3.07, 3.30] | class4 |
| mix4.weights[1,1].NA | 1.00 | NA | NA | NA |
| mix4.weights[1,2].NA | 0.86 | 0.15 | [0.56, 1.15] | NA |
| mix4.weights[1,3].NA | 0.66 | 0.11 | [0.44, 0.88] | NA |
| mix4.weights[1,4].NA | 0.47 | 0.08 | [0.32, 0.63] | NA |
The results are best interpreted by examining a plot of the model and data, however. Relevant plot functions are plot_bivariate(), plot_density(), and plot_profiles(). However, we omit the density plots, because plot_bivariate() also includes them.
On the diagonal of the bivariate plot are weighted density plots: normal approximations of the density function of observed data, weighed by class probability. On the off-diagonal are plots for each pair of indicators, with the class means indicated by a point, class standard deviations indicated by lines, and covariances indicated by circles. As this model has zero covariances, all circles are round (albeit warped by the different scales of the X and Y axes)
The marginal density plots show that trappedness distinguishes classes rather well. For all other indicators, groups are not always clearly separated in terms of marginal density: class 2 and 3 coalesce on negative affect, 1 and 2 coalesce on loneliness, and 1 and 2 coalesce on burden. Nevertheless, the off-diagonal scatterplots show reasonable bivariate separation for all classes.
We can obtain a more classic profile plot using plot_profiles(res_final). This plot conveys less information than the bivariate plot, but is readily interpretable. Below is a comparison between the most common type of visualization for LPA, and the best-practices visualization provided by tidySEM. Note that the best practices plot includes class means and error bars, standard deviations, and a ribbon plot of raw data weighted by class probability to indicate how well the classes describe the observed distribution. The overlap between the classes is clearly visible in this figure; this is why the entropy and classification probabilities are relatively low.
Based on the bivariate plot, we can label class 1 as the balanced type (33%), class 2 as the imbalanced type (29%), class 3 as the entrapped type (22%), and class 4 as the lonely type (16%). Note however that the observed classes do not match the hypothesized pattern of class parameters exactly.
We will save the final model for future reference:


