Latent Class Analysis of Meritocratic Beliefs

Data analysis

Authors
Affiliation

Andreas Laffert

Department of Sociology, University of Chile

Juan Carlos Castillo

Department of Sociology, University of Chile

Tomás Urzúa

Department of Sociology, University of Chile

René Canales

Department of Sociology, University of Chile

Published

April 1, 2026

1 Aim

This study applies latent class analysis (LCA) to a multidimensional scale of meritocratic beliefs developed by Castillo et al. (2023) to identify distinct latent profiles of how individuals combine meritocratic and non-meritocratic beliefs.

Rather than focusing on validating the measurement model, the analysis seeks to uncover patterns of response configurations across items, thereby identifying underlying typologies of meritocratic orientations.

The dataset used is db_proc.RData generated in the prod_prep.R script.

2 Data

  • Dataset: EDUMERCO (2025)
  • Mode: Online survey (CAWI)
  • Population: Adults in the Metropolitan Region of Chile
  • Sampling: Non-probability quota-based design
    • Quotas based on age, gender, education, and socioeconomic status
  • Sample size: N = 3,470
  • Analytical sample: N = 2,740

3 Measures

3.1 Meritocracy scale

We use a multidimensional scale that distinguishes between (Table 1):

  1. Perceptions and preferences
  2. Meritocratic and non-meritocratic principles

This results in four dimensions:

  • Perceived meritocracy: beliefs about whether effort and ability are rewarded
  • Perceived non-meritocracy: beliefs about the role of connections and family wealth
  • Preference for meritocracy: normative support for rewarding effort and talent
  • Preference for non-meritocracy: normative acceptance of advantages linked to privilege

Each dimension is measured with two items, for a total of eight items. Responses are recorded on a four-point Likert scale ranging from strongly disagree (1) to strongly agree (4).

Table 1: Items for perceptions and preferences for meritocracy scale
Components Dimensions Item (English) Item original (Spanish)
Perception Meritocratic In Chile people are rewarded for their efforts En Chile las personas son recompensadas por sus esfuerzos
In Chile people are rewarded for their intelligence and ability En Chile las personas son recompensadas por su inteligencia y habilidad
Non meritocratic In Chile those with wealthy parents do much better in life En Chile a quienes tienen padres ricos les va mucho mejor en la vida
In Chile those with good contacts do much better in life En Chile a quienes tienen buenos contactos les va mejor en la vida
Preference Meritocratic Those who work harder should reap greater rewards than those who work less hard Quienes más se esfuerzan deberían obtener mayores recompensas que quienes se esfuerzan menos
Those with more talent should reap greater rewards than those with less talent Quienes poseen más talento deberían obtener mayores recompensas que quienes poseen menos talento
Non meritocratic It is good that those who have rich parents do better in life Está bien que quienes tengan padres ricos les vaya mejor en la vida
It is good that those who have good contacts do better in life Está bien que quienes tengan buenos contactos les vaya mejor en la vida

4 Method

4.1 Latent class analysis

Latent class analysis (LCA) is a statistical method that models associations among observed variables by assuming that an unobserved categorical latent variable explains their underlying structure. This latent variable represents discrete subgroups within the population, where individuals in the same class share similar response patterns.

LCA is especially appropriate for categorical and ordinal indicators, such as Likert-scale items.

4.1.1 Analytical purpose

LCA can serve both exploratory and confirmatory purposes.

  • As an exploratory technique, it identifies latent typologies of belief configurations.
  • As a confirmatory technique, it allows researchers to test hypotheses about heterogeneity in the population.

Substantively, LCA enables the derivation of empirically grounded ideal types of meritocratic orientations.

4.1.2 Key assumptions

  • Each individual belongs to one latent class.
  • Individuals within the same class share similar response probabilities.
  • Observed indicators are conditionally independent once class membership is taken into account (local independence).

4.1.3 Model parameters

The model estimates two main sets of parameters:

  1. Class probabilities
    These indicate the probability of belonging to each latent class and reflect the relative size of each group in the population.

  2. Conditional response probabilities
    These indicate the probability of endorsing a particular response category, given membership in a latent class. These parameters are used to interpret and label the classes.

4.2 Interpretation

Latent classes are defined by distinct response profiles across the eight items. These profiles capture different combinations of:

  • meritocratic and non-meritocratic beliefs
  • perceptions and normative preferences

This allows us to identify complex belief systems in which individuals may, for example, endorse meritocratic ideals while simultaneously recognizing, or even accepting, non-meritocratic advantages.

4.3 Model estimation and fit

Models are estimated iteratively by increasing the number of classes.

Model selection is based primarily on standard fit criteria, especially:

  • Bayesian Information Criterion (BIC)
  • Akaike Information Criterion (AIC)

Lower values indicate a better fit, while also balancing model complexity and parsimony.

4.4 Contribution

This strategy shifts the analysis from a variable-centered approach to a person-centered one. In doing so, it allows us to:

  • capture heterogeneity in meritocratic belief systems
  • identify meaningful subgroups of respondents
  • examine how meritocratic and non-meritocratic beliefs coexist within individuals

5 Libraries

Show the code
if (! require("pacman")) install.packages("pacman")

pacman::p_load(tidyverse,
               sjmisc, 
               here,
               lavaan,
               psych,
               corrplot,
               ggdist,
               sjlabelled,
               patchwork,
               RColorBrewer,
               poLCA,
               reshape,
               summarytools)

options(scipen=999)
rm(list = ls())

6 Data

Show the code
load(here("input/data/proc/db_proc.RData"))

glimpse(db)
Rows: 3,470
Columns: 25
$ id                  <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,…
$ just_pension        <fct> Agree, Strongly desagree, Strongly desagree, Stron…
$ perc_effort         <fct> Desagree, Strongly desagree, Strongly desagree, De…
$ perc_talent         <fct> Desagree, Desagree, Desagree, Desagree, Agree, Des…
$ perc_rich_parents   <fct> Agree, Strongly agree, Strongly agree, Strongly ag…
$ perc_contact        <fct> Strongly agree, Agree, Strongly agree, Strongly ag…
$ perc_effort_d       <fct> Low, Low, Low, Low, High, Low, Low, NA, High, Low,…
$ perc_talent_d       <fct> Low, Low, Low, Low, High, Low, Low, NA, High, Low,…
$ perc_rich_parents_d <fct> High, High, High, High, High, High, Low, High, Hig…
$ perc_contact_d      <fct> High, High, High, High, High, High, High, High, Hi…
$ pref_effort         <fct> Strongly agree, Strongly agree, Strongly agree, St…
$ pref_talent         <fct> Strongly agree, Agree, Agree, Desagree, Strongly a…
$ pref_rich_parents   <fct> Agree, Strongly agree, Desagree, Strongly agree, A…
$ pref_contact        <fct> Desagree, Strongly agree, Desagree, Strongly agree…
$ pref_effort_d       <fct> High, High, High, High, High, High, High, NA, High…
$ pref_talent_d       <fct> High, High, High, Low, High, High, NA, NA, High, H…
$ pref_rich_parents_d <fct> High, High, Low, High, High, Low, Low, NA, Low, Lo…
$ pref_contact_d      <fct> Low, High, Low, High, Low, Low, Low, NA, Low, Low,…
$ age                 <int> 72, 67, 65, 59, 57, 69, 65, 68, 65, 65, 58, 56, 35…
$ sex                 <fct> 1, 2, 1, 2, 2, 1, 2, 2, 1, 2, 2, 2, 2, 1, 1, 1, 2,…
$ educ                <int> 6, 4, 8, 5, 6, 8, 8, 6, 6, 6, 4, 9, 4, 5, 8, 8, 6,…
$ income              <fct> De $890.001 a $1.100.000 mensuales liquidos, De $3…
$ pol                 <fct> Right, Does not identify, Left, Does not identify,…
$ just_educ           <fct> Desagree, Strongly desagree, Strongly desagree, St…
$ just_healthcare     <fct> Desagree, Strongly desagree, Strongly desagree, St…
Show the code
db <- db %>% 
  mutate(
    income_4 = case_when(
      income %in% c(
        "Menos de $280.000 mensuales liquidos",
        "De $280.001 a $380.000 mensuales liquidos",
        "De $380.001 a $470.000 mensuales liquidos"
      ) ~ "Low",
      
      income %in% c(
        "De $470.001 a $610.000 mensuales liquidos",
        "De $610.001 a $730.000 mensuales liquidos"
      ) ~ "Middle-low",
      
      income %in% c(
        "De $730.001 a $890.000 mensuales liquidos",
        "De $890.001 a $1.100.000 mensuales liquidos"
      ) ~ "Middle-high",
      
      income %in% c(
        "De $1.100.001 a $2.700.000 mensuales liquidos",
        "De $2.700.001 a $4.100.000 mensuales liquidos",
        "Mas de $4.100.001 mensuales liquidos"
      ) ~ "High",
      
      TRUE ~ NA_character_
    ),
    income_4 = factor(income_4, levels = c("Low", "Middle-low", "Middle-high", "High"))
  )


db$income_4 <- sjlabelled::set_label(db$income_4, "Income")

db$age <- sjlabelled::set_label(db$age, "Age")

db$sex <- if_else(db$sex == 1, "Male", "Female")

db$sex <- factor(db$sex, levels = c("Male", "Female"))

db$sex <- sjlabelled::set_label(db$sex, "Sex")

db$educ <- sjlabelled::set_label(db$educ, "Educational level")


db$pol <- sjlabelled::set_label(db$pol, "Political identification")

7 Analysis

7.1 Descriptives

Show the code
vars_m <- c("perc_effort",
            "perc_talent",
            "perc_rich_parents",
            "perc_contact",
            "pref_effort",
            "pref_talent",
            "pref_rich_parents",
            "pref_contact")

t1 <- db %>% 
   dplyr::select(all_of(vars_m), age, sex, educ, income_4, pol)


df<-dfSummary(t1,
               plain.ascii = FALSE,
               style = "multiline",
               tmp.img.dir = "/tmp",
               graph.magnif = 0.75,
               headings = F,  # encabezado
               varnumbers = F, # num variable
               labels.col = T, # etiquetas
               na.col = T,    # missing
               graph.col = T, # plot
               valid.col = T, # n valido
               col.widths = c(20,10,10,10,10,10))

df$Variable <- NULL # delete variable column

print(df, method="render")
Table 2: Descriptive statistics
Label Stats / Values Freqs (% of Valid) Graph Valid Missing
In Chile people are rewarded for their efforts
1. Strongly desagree
2. Desagree
3. Agree
4. Strongly agree
663 ( 20.0% )
1712 ( 51.7% )
814 ( 24.6% )
123 ( 3.7% )
3312 (95.4%) 158 (4.6%)
In Chile people are rewarded for their intelligence and ability
1. Strongly desagree
2. Desagree
3. Agree
4. Strongly agree
509 ( 15.4% )
1557 ( 47.0% )
1101 ( 33.3% )
144 ( 4.3% )
3311 (95.4%) 159 (4.6%)
In Chile those with wealthy parents do much better in life
1. Strongly desagree
2. Desagree
3. Agree
4. Strongly agree
125 ( 3.7% )
299 ( 8.9% )
1135 ( 33.8% )
1797 ( 53.5% )
3356 (96.7%) 114 (3.3%)
In Chile those with good contacts do much better in life
1. Strongly desagree
2. Desagree
3. Agree
4. Strongly agree
87 ( 2.6% )
113 ( 3.3% )
1216 ( 35.9% )
1973 ( 58.2% )
3389 (97.7%) 81 (2.3%)
Those who work harder should reap greater rewards than those who work less hard
1. Strongly desagree
2. Desagree
3. Agree
4. Strongly agree
85 ( 2.5% )
181 ( 5.4% )
1236 ( 37.0% )
1839 ( 55.0% )
3341 (96.3%) 129 (3.7%)
Those with more talent should reap greater rewards than those with less talent
1. Strongly desagree
2. Desagree
3. Agree
4. Strongly agree
118 ( 3.7% )
739 ( 23.0% )
1408 ( 43.9% )
942 ( 29.4% )
3207 (92.4%) 263 (7.6%)
It is good that those who have rich parents do better in life
1. Strongly desagree
2. Desagree
3. Agree
4. Strongly agree
470 ( 15.2% )
1072 ( 34.7% )
1322 ( 42.8% )
228 ( 7.4% )
3092 (89.1%) 378 (10.9%)
It is good that those who have good contacts do better in life
1. Strongly desagree
2. Desagree
3. Agree
4. Strongly agree
492 ( 15.5% )
1280 ( 40.3% )
1168 ( 36.7% )
239 ( 7.5% )
3179 (91.6%) 291 (8.4%)
Age
Mean (sd) : 44.2 (16.4)
min ≤ med ≤ max:
18 ≤ 43 ≤ 89
IQR (CV) : 26 (0.4)
70 distinct values 3470 (100.0%) 0 (0.0%)
Sex
1. Male
2. Female
1655 ( 48.0% )
1791 ( 52.0% )
3446 (99.3%) 24 (0.7%)
Educational level
Mean (sd) : 5.8 (1.8)
min ≤ med ≤ max:
1 ≤ 6 ≤ 9
IQR (CV) : 4 (0.3)
1 : 12 ( 0.3% )
2 : 52 ( 1.5% )
3 : 125 ( 3.6% )
4 : 992 ( 28.6% )
5 : 298 ( 8.6% )
6 : 726 ( 20.9% )
7 : 395 ( 11.4% )
8 : 693 ( 20.0% )
9 : 177 ( 5.1% )
3470 (100.0%) 0 (0.0%)
Income
1. Low
2. Middle-low
3. Middle-high
4. High
838 ( 24.1% )
860 ( 24.8% )
840 ( 24.2% )
932 ( 26.9% )
3470 (100.0%) 0 (0.0%)
Political identification
1. Left
2. Center
3. Right
4. Does not identify
805 ( 24.2% )
234 ( 7.0% )
846 ( 25.5% )
1436 ( 43.2% )
3321 (95.7%) 149 (4.3%)

Generated by summarytools 1.1.5 (R version 4.5.2)
2026-04-01

Show the code
db <- db %>% 
  mutate(
    across(
      .cols = all_of(vars_m),
      .fns = ~as.numeric(.)
    ))
 
labels1 <- c("Strongly desagree" = 1, 
             "Desagree" = 2, 
             "Agree" = 3, 
             "Strongly agree" = 4)
db <- db %>% 
  mutate(
    across(
      .cols = all_of(vars_m),
      .fns = ~  sjlabelled::set_labels(., labels = labels1)
    )
  )


df <- db %>% 
  dplyr::select(all_of(vars_m)) %>% 
  drop_na()

theme_set(theme_ggdist())
colors <- RColorBrewer::brewer.pal(n = 4, name = "RdBu")


a <- df %>% 
  dplyr::select(perc_effort, 
                perc_talent,
                perc_rich_parents,
                perc_contact) %>% 
  sjPlot::plot_likert(geom.colors = colors,
                      title = c("a. Perceptions"),
                      geom.size = 0.8,
                      axis.labels = c("Effort", "Talent", "Rich parents", "Contacts"),
                      catcount = 4,
                      values  =  "sum.outside",
                      reverse.colors = F,
                      reverse.scale = T,
                      show.n = FALSE,
                      show.prc.sign = T
  ) +
  ggplot2::theme(legend.position = "none")

b <- df %>% 
  dplyr::select(pref_effort, 
                pref_talent,
                pref_rich_parents,
                pref_contact) %>% 
  sjPlot::plot_likert(geom.colors = colors,
                      title = c("b. Preferences"),
                      geom.size = 0.8,
                      axis.labels = c("Effort", "Talent", "Rich parents", "Contacts"),
                      catcount = 4,
                      values  =  "sum.outside",
                      reverse.colors = F,
                      reverse.scale = T,
                      show.n = FALSE,
                      show.prc.sign = T
  ) +
  ggplot2::theme(legend.position = "bottom")

likerplot <- a / b + plot_annotation(caption = paste0("Source: own elaboration based on Survey EDUMERCO"," (n = ",dim(df)[1],")"
))

likerplot
Figure 1: Distribution of responses on the scale of perceptions and preferences regarding meritocracy
Show the code
M <- df %>% 
  psych::polychoric()

diag(M$rho) <- NA

rownames(M$rho) <- c("A. Perception Effort",
                     "B. Perception Talent",
                     "C. Perception Rich parents",
                     "D. Perception Contacts",
                     "E. Preference Effort",
                     "F. Preference Talent",
                     "G. Preference Rich parents",
                     "H. Preference Contacts")

#set Column names of the matrix
colnames(M$rho) <-c("(A)", "(B)","(C)","(D)","(E)","(F)","(G)",
                    "(H)")

testp <- cor.mtest(M$rho, conf.level = 0.95)

#Plot the matrix using corrplot
corrplot::corrplot(M$rho,
                   method = "color",
                   addCoef.col = "black",
                   type = "upper",
                   tl.col = "black",
                   col = colorRampPalette(c("#E16462", "white", "#0D0887"))(12),
                   bg = "white",
                   na.label = "-") 
Figure 2: Correlation matrix

7.2 Latent class analysis with dichotomized items

We estimated latent class models with one to five classes. Model fit improved monotonically as the number of classes increased, as indicated by decreasing AIC and BIC values and statistically significant likelihood-ratio tests at each step (Table 3).

Show the code
# ---- build table (no list-cols) -----------------------------------------

N_used <- nrow(db_lca)

m_patterns <- n_patterns_from_data(db_lca)  # scalar

# Robust extractors (avoid list-columns)
get_K <- function(fit) as.integer(ncol(fit$posterior))  # classes = cols of posterior
get_ll  <- function(fit) as.numeric(fit$llik)
get_aic <- function(fit) as.numeric(fit$aic)
get_bic <- function(fit) as.numeric(fit$bic)
get_npar <- function(fit) {
  if (!is.null(fit$npar)) return(as.numeric(fit$npar))
  # fallback: compute from probs
  K <- get_K(fit)
  Rj <- vapply(fit$probs, function(m) ncol(m), integer(1))
  as.numeric(sum(K * (Rj - 1)) + (K - 1))
}

# Table of fit indices
fit_tbl <- tibble(
  K     = map_int(fits, get_K),
  N     = nrow(db_lca),
  logLik= map_dbl(fits, get_ll),
  npar  = map_dbl(fits, get_npar),
  AIC   = map_dbl(fits, get_aic),
  BIC   = map_dbl(fits, get_bic)
) %>%
  arrange(K) %>%
  mutate(
    # Differences relative to preceding (K-1) model
    dAIC = AIC - lag(AIC),
    dBIC = BIC - lag(BIC),
    
    # Likelihood-ratio test (K vs K-1)
    # Test statistic: 2*(LL_K - LL_{K-1}) ~ Chi-square with df = npar_K - npar_{K-1}
    # Note: in LCA, models are not strictly nested in the regular sense;
    # this LRT is widely used as a heuristic, but interpret with caution.
    LRT = 2 * (logLik - lag(logLik)),
    df_LRT = npar - lag(npar),
    p_LRT = ifelse(!is.na(LRT) & df_LRT > 0, pchisq(LRT, df = df_LRT, lower.tail = FALSE), NA_real_)
  )


fit_tbl_report <- fit_tbl %>%
  transmute(
    K, N,
    logLik = round(logLik, 1),
    npar = npar,
    AIC = round(AIC, 1),
    dAIC = round(dAIC, 1),
    BIC = round(BIC, 1),
    dBIC = round(dBIC, 1),
    LRT = round(LRT, 1),
    df_LRT = df_LRT,
    p_LRT = signif(p_LRT, 3)
  )

fit_tbl_report %>% 
 kableExtra::kable(format = "html",
                    align = "c",
                    booktabs = T,
                    escape = F,
                    caption = NULL) %>%
  kableExtra::kable_styling(full_width = T,
                            latex_options = "hold_position",
                            bootstrap_options=c("striped", "bordered", "condensed"),
                            font_size = 23) %>%
  kableExtra::column_spec(c(1,8), width = "3.5cm") %>% 
  kableExtra::column_spec(2:7, width = "4cm") %>% 
  kableExtra::column_spec(4, width = "5cm")
Table 3: Fit comparision of latent class models
K N logLik npar AIC dAIC BIC dBIC LRT df_LRT p_LRT
1 2740 -11331.7 8 22679.5 22726.8
2 2740 -10793.8 17 21621.6 -1057.8 21722.2 -1004.6 1075.8 9 0
3 2740 -10457.9 26 20967.8 -653.9 21121.6 -600.6 671.9 9 0
4 2740 -10162.8 35 20395.7 -572.1 20602.7 -518.9 590.1 9 0
5 2740 -10080.8 44 20249.6 -146.0 20509.9 -92.8 164.0 9 0
Show the code
# Visual: AIC/BIC by K (lower is better)
fit_long <- fit_tbl %>%
  dplyr::select(K, AIC, BIC) %>%
  pivot_longer(-K, names_to = "criterion", values_to = "value")

ggplot(fit_long, aes(x = K, y = value, group = criterion)) +
  geom_line() +
  geom_point() +
  facet_wrap(~criterion, scales = "free_y") +
  labs(x = "Number of classes (K)", y = "Value") 
Figure 3: Visual: AIC/BIC by K (lower is better)
Show the code
# Visual: ΔAIC and ΔBIC (negative = improvement over K-1)
delta_long <- fit_tbl %>%
  dplyr::select(K, dAIC, dBIC) %>%
  pivot_longer(-K, names_to = "delta", values_to = "value")

ggplot(delta_long, aes(x = K, y = value, group = delta)) +
  geom_hline(yintercept = 0) +
  geom_line() +
  geom_point() +
  facet_wrap(~delta, scales = "free_y") +
  labs(x = "K", y = "Change vs K-1 (negative = better)") 
Figure 4: ΔAIC and ΔBIC (negative = improvement over K-1)

However, the incremental improvement in fit became substantially smaller after the four-class solution. Whereas the transition from one to four classes yielded large reductions in AIC and BIC, the five-class model provided only a modest additional improvement (AIC = 20,249.6; BIC = 20,509.9) relative to the four-class solution (AIC = 20,395.7; BIC = 20,602.7). Thus, although the five-class model fit the data slightly better in statistical terms, the gain was comparatively limited.

We therefore retained the four-class solution as the final model. This choice was guided by both statistical and substantive criteria: the four-class model achieved a strong balance between goodness of fit, parsimony, and interpretability, whereas the five-class solution did not yield enough additional differentiation to justify the increase in complexity.

7.3 Four latent class model

The Table 4 shows the conditional probabilities of each item associated with a latent class and their respective sizes. The four-class solution suggests that respondents are not primarily differentiated along a simple continuum from “more meritocratic” to “less meritocratic.” Instead, the latent classes are organized by distinct combinations of: (1) perceptions of meritocracy, (2) perceptions of non-meritocratic advantage, and (3) normative preferences regarding whether merit or privilege should determine success. As such, the results reveal heterogeneity not only in empirical diagnoses of how society works, but also in normative evaluations of whether these mechanisms are considered acceptable. This interpretation is consistent with the item-response profiles across classes.

Two broader patterns structure the class solution. First, perceptions of non-meritocratic advantage are high across most classes, particularly regarding the role of wealthy parents and personal connections, suggesting that recognition of privilege is widespread. Second, the main line of differentiation lies in normative preferences toward non-meritocratic advantage. In other words, the classes differ less in whether they perceive privilege and more in whether they consider such privilege legitimate or acceptable.

Show the code
fit4 <- fits[[4]]

# 2) Extract conditional probabilities per item and class
# fit4$probs is a list: each element is K x Rj matrix.
# For dichotomous items: K x 2 (categories = colnames or "1","2")
probs_long <- bind_rows(lapply(names(fit4$probs), function(item) {
  mat <- fit4$probs[[item]]
  df <- as.data.frame(mat)
  df$class <- 1:nrow(mat)
  df$item <- item
  df
})) %>%
  pivot_longer(cols = -c(class, item), names_to = "category", values_to = "prob") %>%
  mutate(
    class = as.integer(class),
    prob = as.numeric(prob)
  )

# If levels are c("0","1") or c("1","2") you'll want the "higher"/"yes" one.
# Here I will assume endorsement is the LAST level (most common after dichotomization).
endorsement_level <- tail(levels(db_lca$a1), 1)

# 4) Build a clean table: P(endorsement | class) for each item
p_endorse <- probs_long %>%
  filter(category == "Pr(2)") %>%
  dplyr::select(item, class, p = prob) %>%
  mutate(item = factor(item, levels = names(fit4$probs)))

# 5) Plot: profile lines (one line per class)
p_endorse <- p_endorse %>% 
  mutate(
    variable = case_when(item == "a1" ~ "perc_effort",
                         item == "a2" ~ "perc_talent",
                         item == "a3" ~ "perc_rich_parents",
                         item == "a4" ~ "perc_contacts",
                         item == "a5" ~ "pref_effort",
                         item == "a6" ~ "pref_talent",
                         item == "a7" ~ "pref_rich_parents",
                         item == "a8" ~ "pref_contacts"
                         ),
    variable = factor(variable,
                      levels = c("perc_effort",
                                 "perc_talent",
                                 "perc_rich_parents",
                                 "perc_contacts",
                                 "pref_effort",
                                 "pref_talent",
                                 "pref_rich_parents",
                                 "pref_contacts")))

p_endorse <- p_endorse %>% 
  mutate(class = factor(class),
         p = round(p,2))

g_class <- ggplot(p_endorse, aes(x = variable, y = p, group = class, colour = class)) +
  geom_line(linewidth = 0.7) +
  geom_point(size = 2) +  
  MetBrewer::scale_color_met_d("VanGogh2") +
  coord_cartesian(ylim = c(0, 1)) +
  labs(x = "Item", y = paste0("P(Y = ", "High", " | class)"), group = "Class", colour = "Class") +
  theme(legend.position = "top",
        text = element_text(size = 14),
        axis.text.x = element_text(angle = 70, vjust = 1, hjust = 1))


library(plotly)

ggplotly(g_class)
Figure 5: P(endorsement | class) for each item
Show the code
# 7) Report table (wide): items x classes with probabilities
class_prev <- tibble(
  class = 1:length(fit4$P),
  pi = as.numeric(fit4$P)
) %>%
  mutate(pi_pct = 100 * pi)

report_probs <- p_endorse %>%
  mutate(class = paste0("Class_", class)) %>%
  pivot_wider(names_from = class, values_from = p) %>%
  mutate(across(starts_with("Class_"), ~ round(.x, 3)))

prev_lab <- class_prev %>%
  transmute(class = paste0("Class_", class),
            lab = paste0(class, " (", round(pi_pct, 1), "%)"))

report_probs2 <- report_probs
for (i in seq_len(nrow(prev_lab))) {
  old <- prev_lab$class[i]
  new <- prev_lab$lab[i]
  names(report_probs2)[names(report_probs2) == old] <- new
}

report_probs2[,-c(1)] %>% 
   kableExtra::kable(format = "html",
                    align = "c",
                    booktabs = T,
                    escape = F,
                    caption = NULL) %>%
  kableExtra::kable_styling(full_width = T,
                            latex_options = "hold_position",
                            bootstrap_options=c("striped", "bordered", "condensed"),
                            font_size = 23)
Table 4: Items x classes with probabilities
variable Class_1 (36.3%) Class_2 (20.7%) Class_3 (6.8%) Class_4 (36.2%)
perc_effort 0.07 1.00 0.40 0.06
perc_talent 0.24 0.96 0.42 0.18
perc_rich_parents 0.91 0.88 0.08 0.96
perc_contacts 0.98 0.99 0.25 0.99
pref_effort 0.94 0.95 0.57 0.94
pref_talent 0.77 0.82 0.46 0.70
pref_rich_parents 0.82 0.63 0.25 0.15
pref_contacts 0.82 0.62 0.26 0.00

Class 1: Legitimation of Privilege under Structural Realism (36.3%)

The first class is characterized by low perceived meritocracy and very high perceived non-meritocracy. Respondents in this group do not believe that effort or talent are strongly rewarded, but they do strongly believe that family wealth and social contacts shape life chances. At the same time, they express high support for rewarding effort and talent, yet also show high normative acceptance of advantages linked to wealthy parents and personal networks.

Substantively, this class combines a realistic diagnosis of a socially stratified system with a normative legitimation of privilege. These respondents do not merely recognize that success is shaped by inherited or relational advantages; they also appear to accept that this is appropriate. Importantly, their endorsement of meritocratic rewards does not operate as a moral alternative to privilege, but rather coexists with it. This suggests a dual normative orientation in which merit and privilege are not seen as contradictory principles, but as compatible bases of social inequality.

Class 2: Structurally Aware Meritocrats (20.7%)

The second class displays very high levels of both perceived meritocracy and perceived non-meritocracy. Respondents in this group strongly believe that effort and talent are rewarded, while also recognizing the importance of wealthy parents and social contacts. Normatively, they express very strong support for meritocratic rewards, but only moderate acceptance of non-meritocratic advantages.

This profile suggests a belief system in which merit and privilege are understood as operating simultaneously. These respondents do not deny the role of structural advantage, yet neither do they reject the view that effort and talent matter. Normatively, they strongly endorse meritocracy, while showing a more ambivalent stance toward privilege. Compared to Class 1, they are less willing to legitimize inherited or network-based advantage, but they do not reject it entirely. This class can therefore be interpreted as one that sees the system as at least partially meritocratic and broadly considers this arrangement acceptable.

Class 3: Weakly Structured or Ambivalent Orientations (6.8%)

The third class is the smallest and least clearly structured. It is characterized by moderate levels of perceived meritocracy, low levels of perceived non-meritocracy, moderate support for meritocratic rewards, and relatively low support for non-meritocratic advantages. Unlike the other classes, respondents in this group do not strongly perceive either merit or privilege as dominant mechanisms of success.

Substantively, this class appears to represent a more ambivalent or weakly articulated orientation. Its members neither strongly endorse meritocratic ideals nor strongly perceive the system as dominated by privilege. Likewise, they do not express a strong normative defense of non-meritocratic advantage. Given its small size, this class should be interpreted with caution and evaluated for stability across specifications. Still, it may capture respondents whose views are less ideologically crystallized or whose understanding of success is not organized around the dominant opposition between merit and privilege.

Class 4: Critical Meritocrats (36.2%)

The fourth class combines low perceived meritocracy with very high perceived non-meritocracy. Respondents in this group do not believe that effort and talent are what society actually rewards, but they strongly believe that wealthy parents and social contacts shape success. Normatively, however, they strongly support meritocratic rewards and sharply reject the legitimacy of non-meritocratic advantages, especially those associated with personal networks.

This class is particularly important substantively because it captures a configuration in which meritocracy is upheld as a moral ideal while privilege is recognized as an empirical reality. In this sense, respondents in this group clearly distinguish between how society works and how it should work. They do not perceive the current system as meritocratic, but they do express strong support for a society in which effort and talent, rather than inherited or relational advantages, determine success. This class illustrates the analytical value of distinguishing perceptions from preferences, since older one-dimensional approaches to meritocratic beliefs would likely obscure this tension between normative commitment to merit and critical awareness of privilege.

Overall Interpretation

Taken together, the latent class solution suggests that meritocratic beliefs are best understood as multidimensional and internally heterogeneous. The main contrast is not between those who believe in meritocracy and those who do not, but between different ways of combining empirical perceptions of inequality with normative judgments about its legitimacy. Put differently, the classes vary across two central axes: first, whether respondents perceive success as driven by merit or by privilege; and second, whether they consider these mechanisms normatively acceptable.

This framework helps clarify the broader structure of the results. Class 2 combines recognition of both merit and privilege with strong support for meritocratic rewards and moderate tolerance of privilege. Class 4 combines recognition of privilege with a clear normative rejection of it, while maintaining strong support for merit as an ideal. Class 1 shares with Class 4 a critical diagnosis of how society works, but differs sharply in its normative legitimization of privilege. Finally, Class 3 stands apart as a smaller and less clearly structured group with comparatively weak commitments on both the perceptual and normative dimensions. Overall, the results show that support for meritocracy does not necessarily imply denial of privilege, and that recognition of privilege does not necessarily entail its rejection. This is precisely the kind of complexity that a person-centered latent class approach is able to recover.

8 Additional analysis

This is a section for pending tasks, where we include additional analyses that are currently in the exploratory phase.

8.1 Regression anlaysis

Here, we will use the probability of belonging to each class as an independent variable that predicts the outcome of market fairness in pensions, in addition to including covariates such as age, gender, education, income, and political affiliation. Finally, we include interaction terms between income and market fairness in pensions.

Show the code
coef_df <-
  broom::tidy(m1, conf.int = TRUE) %>%
    mutate(model = "MAP (dummies; ref=Class 4)") %>%
    filter(term != "(Intercept)") %>%
    mutate(
      contrast = case_when(
        term == "class_hat1" ~ "Class 1 vs 4",
        term == "class_hat2" ~ "Class 2 vs 4",
        term == "class_hat3" ~ "Class 3 vs 4",
        TRUE ~ term
      )) %>%
  mutate(
    contrast = factor(contrast, levels = c("Class 1 vs 4", "Class 2 vs 4", "Class 3 vs 4"))
  )

coef_df <- coef_df %>%
  mutate(sign = if_else(estimate < 0, "negative", "positive"))

ggplot(coef_df, aes(x = estimate, y = contrast, color = sign)) +
  geom_vline(xintercept = 0, linewidth = 0.7, color = "grey60", linetype = "dashed") +
  geom_pointrange(
    aes(xmin = conf.low, xmax = conf.high),
    fatten = 2,
    size = 1
  ) +
  scale_color_manual(
    values = c("negative" = "black", "positive" = "#ca1137"),
    guide = "none"
  ) +
  labs(x = "Coefficient (OLS) with 95% CI", y = NULL) +
  theme(text = element_text(size = 14))
Figure 6: Market justice preferences in pensions by latent class of meritocracy (MAP model)
Show the code
coef_df <-
  broom::tidy(m5, conf.int = TRUE) %>%
    mutate(model = "MAP (dummies; ref=Class 4)") %>%
    filter(term != "(Intercept)") %>%
    mutate(
      contrast = case_when(
        term == "class_hat1" ~ "Class 1 vs 4",
        term == "class_hat2" ~ "Class 2 vs 4",
        term == "class_hat3" ~ "Class 3 vs 4",
        
        term == "sexFemale" ~ "Female (Ref.= Male)",
        term == "educ" ~ "Education (in years)",
        term == "age" ~ "Age",
        term == "income_4Middle-low" ~ "Income Middle-Low",
        term == "income_4Middle-high" ~ "Income Middle-High",
        term == "income_4High" ~ "Income High",           
        TRUE ~ term
      )) 

coef_df <- coef_df %>%
  mutate(sign = if_else(estimate < 0, "negative", "positive"))

ggplot(coef_df, aes(x = estimate, y = contrast, color = sign)) +
  geom_vline(xintercept = 0, linewidth = 0.7, color = "grey60", linetype = "dashed") +
  geom_pointrange(
    aes(xmin = conf.low, xmax = conf.high),
    fatten = 2,
    size = 1
  ) +
  scale_color_manual(
    values = c("negative" = "black", "positive" = "#ca1137"),
    guide = "none"
  ) +
  labs(x = "Coefficient (OLS) with 95% CI", y = NULL) +
  theme(text = element_text(size = 14))
Figure 7: Market justice preferences in pensions by latent class of meritocracy and covariates (MAP model)
Show the code
coef_df <-
  broom::tidy(m6, conf.int = TRUE) %>%
    slice_tail(n=9) %>%
    mutate(
      contrast = case_when(
        term == "class_hat1:income_4Middle-low" ~ "Class 1 x Income Middle-Low",
        term == "class_hat2:income_4Middle-low" ~ "Class 2 x Income Middle-Low",
        term == "class_hat3:income_4Middle-low" ~ "Class 3 x Income Middle-Low", 
        
        term == "class_hat1:income_4Middle-high" ~ "Class 1 x Income Middle-High",
        term == "class_hat2:income_4Middle-high" ~ "Class 2 x Income Middle-High",
        term == "class_hat3:income_4Middle-high" ~ "Class 3 x Income Middle-High",  
        
        term == "class_hat1:income_4High" ~ "Class 1 x Income High",
        term == "class_hat2:income_4High" ~ "Class 2 x Income High",
        term == "class_hat3:income_4High" ~ "Class 3 x Income High", 
        TRUE ~ term
      )) 

coef_df <- coef_df %>%
  mutate(sign = if_else(estimate < 0, "negative", "positive"))

ggplot(coef_df, aes(x = estimate, y = contrast, color = sign)) +
  geom_vline(xintercept = 0, linewidth = 0.7, color = "grey60", linetype = "dashed") +
  geom_pointrange(
    aes(xmin = conf.low, xmax = conf.high),
    fatten = 2,
    size = 1
  ) +
  scale_color_manual(
    values = c("negative" = "black", "positive" = "#ca1137"),
    guide = "none"
  ) +
  labs(x = "Coefficient (OLS) with 95% CI", y = NULL) +
  theme(text = element_text(size = 14))
Figure 8: Interaction between latent class of meritocracy and income on market justice preferences in pensions

8.2 SEM framework

In this analysis, we will use a structural equation modeling framework to estimate the latent structure of the meritocracy scale and how its dimensions relate to market justice preferences, which are treated as a latent factor of preferences in the areas of pensions, education, and healthcare.

lavaan 0.6-21 ended normally after 49 iterations

  Estimator                                       DWLS
  Optimization method                           NLMINB
  Number of model parameters                        61

  Number of observations                          2521

Model Test User Model:
                                              Standard      Scaled
  Test Statistic                               873.228     790.373
  Degrees of freedom                               124         124
  P-value (Unknown)                                 NA       0.000
  Scaling correction factor                                  1.164
  Shift parameter                                           39.866
    simple second-order correction                                

Model Test Baseline Model:

  Test statistic                             31744.918   21261.307
  Degrees of freedom                                55          55
  P-value                                           NA       0.000
  Scaling correction factor                                  1.494

User Model versus Baseline Model:

  Comparative Fit Index (CFI)                    0.976       0.969
  Tucker-Lewis Index (TLI)                       0.990       0.986
                                                                  
  Robust Comparative Fit Index (CFI)                            NA
  Robust Tucker-Lewis Index (TLI)                               NA

Root Mean Square Error of Approximation:

  RMSEA                                          0.049       0.046
  90 Percent confidence interval - lower         0.046       0.043
  90 Percent confidence interval - upper         0.052       0.049
  P-value H_0: RMSEA <= 0.050                    0.705       0.979
  P-value H_0: RMSEA >= 0.080                    0.000       0.000
                                                                  
  Robust RMSEA                                                  NA
  90 Percent confidence interval - lower                        NA
  90 Percent confidence interval - upper                        NA
  P-value H_0: Robust RMSEA <= 0.050                            NA
  P-value H_0: Robust RMSEA >= 0.080                            NA

Standardized Root Mean Square Residual:

  SRMR                                           0.039       0.039

Parameter Estimates:

  Parameterization                               Delta
  Standard errors                           Robust.sem
  Information                                 Expected
  Information saturated (h1) model        Unstructured

Latent Variables:
                   Estimate  Std.Err  z-value  P(>|z|)   Std.lv  Std.all
  mjp =~                                                                
    just_pension      1.000                               0.716    0.697
    just_educ         1.285    0.037   34.516    0.000    0.920    0.881
    just_healthcar    1.354    0.040   33.647    0.000    0.969    0.924
  perc_merit =~                                                         
    perc_effort       1.000                               0.932    0.932
    perc_talent       0.800    0.070   11.457    0.000    0.745    0.745
  perc_nmerit =~                                                        
    perc_rch_prnts    1.000                               0.816    0.816
    perc_contact      1.100    0.039   28.390    0.000    0.897    0.897
  pref_merit =~                                                         
    pref_effort       1.000                               0.883    0.883
    pref_talent       0.610    0.041   15.024    0.000    0.539    0.539
  pref_nmerit =~                                                        
    pref_rch_prnts    1.000                               0.836    0.836
    pref_contact      1.013    0.047   21.561    0.000    0.847    0.847

Regressions:
                   Estimate  Std.Err  z-value  P(>|z|)   Std.lv  Std.all
  mjp ~                                                                 
    perc_merit        0.047    0.018    2.691    0.007    0.062    0.062
    perc_nmerit      -0.178    0.030   -5.847    0.000   -0.203   -0.203
    pref_merit        0.022    0.029    0.763    0.446    0.027    0.027
    pref_nmerit       0.311    0.021   14.575    0.000    0.363    0.363
    age              -0.002    0.001   -1.741    0.082   -0.002   -0.040
    educ             -0.004    0.010   -0.361    0.718   -0.005   -0.009
    sex_female       -0.211    0.033   -6.423    0.000   -0.295   -0.148
    incom_4Mddl.lw   -0.045    0.045   -1.001    0.317   -0.063   -0.027
    incm_4Mddl.hgh   -0.029    0.046   -0.637    0.524   -0.041   -0.018
    income_4High      0.067    0.051    1.317    0.188    0.094    0.042
    polCenter         0.349    0.066    5.329    0.000    0.488    0.130
    polRight          0.525    0.046   11.500    0.000    0.733    0.327
    polDs.nt.dntfy    0.268    0.041    6.523    0.000    0.374    0.183

Covariances:
                   Estimate  Std.Err  z-value  P(>|z|)   Std.lv  Std.all
  perc_merit ~~                                                         
    perc_nmerit      -0.127    0.020   -6.421    0.000   -0.167   -0.167
    pref_merit       -0.036    0.023   -1.583    0.113   -0.044   -0.044
    pref_nmerit       0.157    0.018    8.688    0.000    0.202    0.202
  perc_nmerit ~~                                                        
    pref_merit        0.414    0.020   20.931    0.000    0.574    0.574
    pref_nmerit      -0.049    0.017   -2.832    0.005   -0.072   -0.072
  pref_merit ~~                                                         
    pref_nmerit       0.040    0.020    2.013    0.044    0.054    0.054

Thresholds:
                   Estimate  Std.Err  z-value  P(>|z|)   Std.lv  Std.all
    just_pensin|t1    0.958    0.144    6.655    0.000    0.958    0.933
    just_educ|t1     -0.361    0.122   -2.964    0.003   -0.361   -0.346
    just_educ|t2      0.740    0.121    6.105    0.000    0.740    0.709
    just_educ|t3      1.567    0.123   12.746    0.000    1.567    1.501
    just_hlthcr|t1   -0.067    0.124   -0.541    0.588   -0.067   -0.064
    just_hlthcr|t2    1.023    0.123    8.294    0.000    1.023    0.976
    just_hlthcr|t3    1.757    0.127   13.819    0.000    1.757    1.675
    perc_effort|t1   -0.881    0.121   -7.310    0.000   -0.881   -0.881
    perc_effort|t2    0.561    0.120    4.685    0.000    0.561    0.561
    perc_effort|t3    1.817    0.126   14.384    0.000    1.817    1.817
    perc_talent|t1   -1.236    0.118  -10.503    0.000   -1.236   -1.236
    perc_talent|t2    0.113    0.116    0.974    0.330    0.113    0.113
    perc_talent|t3    1.543    0.120   12.854    0.000    1.543    1.543
    prc_rch_prnt|1   -2.500    0.116  -21.461    0.000   -2.500   -2.500
    prc_rch_prnt|2   -1.837    0.119  -15.374    0.000   -1.837   -1.837
    prc_rch_prnt|3   -0.792    0.118   -6.708    0.000   -0.792   -0.792
    perc_contct|t1   -2.350    0.123  -19.113    0.000   -2.350   -2.350
    perc_contct|t2   -1.950    0.121  -16.062    0.000   -1.950   -1.950
    perc_contct|t3   -0.584    0.121   -4.841    0.000   -0.584   -0.584
    pref_effort|t1   -1.661    0.130  -12.777    0.000   -1.661   -1.661
    pref_effort|t2   -1.111    0.126   -8.827    0.000   -1.111   -1.111
    pref_effort|t3    0.152    0.126    1.209    0.227    0.152    0.152
    pref_talent|t1   -1.217    0.117  -10.365    0.000   -1.217   -1.217
    pref_talent|t2   -0.016    0.114   -0.139    0.889   -0.016   -0.016
    pref_talent|t3    1.166    0.116   10.086    0.000    1.166    1.166
    prf_rch_prnt|1   -1.225    0.115  -10.617    0.000   -1.225   -1.225
    prf_rch_prnt|2   -0.180    0.115   -1.563    0.118   -0.180   -0.180
    prf_rch_prnt|3    1.308    0.117   11.183    0.000    1.308    1.308
    pref_contct|t1   -0.911    0.114   -7.958    0.000   -0.911   -0.911
    pref_contct|t2    0.254    0.115    2.207    0.027    0.254    0.254
    pref_contct|t3    1.594    0.118   13.488    0.000    1.594    1.594

Variances:
                   Estimate  Std.Err  z-value  P(>|z|)   Std.lv  Std.all
   .just_pension      0.542                               0.542    0.514
   .just_educ         0.244                               0.244    0.224
   .just_healthcar    0.161                               0.161    0.147
   .perc_effort       0.131                               0.131    0.131
   .perc_talent       0.444                               0.444    0.444
   .perc_rch_prnts    0.334                               0.334    0.334
   .perc_contact      0.195                               0.195    0.195
   .pref_effort       0.220                               0.220    0.220
   .pref_talent       0.709                               0.709    0.709
   .pref_rch_prnts    0.302                               0.302    0.302
   .pref_contact      0.283                               0.283    0.283
   .mjp               0.358    0.021   17.341    0.000    0.698    0.698
    perc_merit        0.869    0.075   11.605    0.000    1.000    1.000
    perc_nmerit       0.666    0.026   25.193    0.000    1.000    1.000
    pref_merit        0.780    0.053   14.680    0.000    1.000    1.000
    pref_nmerit       0.698    0.033   20.947    0.000    1.000    1.000

R-Square:
                   Estimate
    just_pension      0.486
    just_educ         0.776
    just_healthcar    0.853
    perc_effort       0.869
    perc_talent       0.556
    perc_rch_prnts    0.666
    perc_contact      0.805
    pref_effort       0.780
    pref_talent       0.291
    pref_rch_prnts    0.698
    pref_contact      0.717
    mjp               0.302

References

Castillo, J. C., Iturra, J., Maldonado, L., Atria, J., & Meneses, F. (2023). A Multidimensional Approach for Measuring Meritocratic Beliefs: Advantages, Limitations and Alternatives to the ISSP Social Inequality Survey. International Journal of Sociology, 53(6), 448–472. https://doi.org/10.1080/00207659.2023.2274712