Nested Latent Class Model
Posted: 03 Aug 2025, 22:31
by kiki
Hi,
I am currently working with latent class models in Apollo and would like to inquire whether it is possible to implement a hierarchical latent class model. That is, a model structure allows for subclasses nested within higher-level latent classes.
If this is supported, could you kindly provide a code example and guidance on how to structure such a model in Apollo?
Thank you very much for your time and assistance!
Best regards,
kiki
Re: Nested Latent Class Model
Posted: 13 Aug 2025, 14:53
by kiki
Hi Stephone,
Thank you! The mathematical example can be referred in the paper named "A model for the simultaneous inference of attribute nonattendance and taste heterogeneity", Eq(1)-(9).
The outer latent classes are specified as a conventional latent class model, where each class has its own set of parameters. The inner IANA latent classes have only one set of parameters, which are shared across all inner classes. This parameter structure seems inconsistent with Apollo’s requirements. In the Apollo, all existing specifications of the latent class model have the same parameter structure (same number and naming of parameters) to construct the joint likelihood. When I attempt to include two indices in the utility function to reflect this structure, it triggers errors
Best regards,
kiki
Re: Nested Latent Class Model
Posted: 08 Oct 2025, 20:13
by kiki
Hi Stephane,
I have attached the code I tried to implement for a two-layer latent class model. In the code, “k” corresponds to the inner classes (following the IANA structure) and “s” corresponds to the outer classes (following the conventional latent class structure).
When I run the code, I encounter the following error:
The attached code has an error "Error in apollo_estimate(apollo_beta, apollo_fixed, apollo_probabilities, :
SPECIFICATION ISSUE - Parameter asc3_A does not influence the log-likelihood of your model!".
I have removed the random specification for asc3, but the error still remains.
First of all, I would greatly appreciate your help in addressing this error. Additionally, could you review the code to check its overall logic? The mathematical formulation we are using follows the paper “A Model for the Simultaneous Inference of Attribute Nonattendance and Taste Heterogeneity.”
If you need any further information or data sample to test the code, I would be happy to provide it via email. Thank you very much!
#################################################################
# ================================================================= #
# Clean, fixed and runnable Apollo script: 3-class LC + ANA (IANA)
# ================================================================= #
rm(list=ls())
library(apollo)
apollo_initialise()
apollo_control = list(
modelName = "TypeII_ANA_fixed",
modelDescr = "3-class LC with inner ANA (fixed runnable example)",
indivID = "ID",
nCores = 3,
debug = TRUE,
analyticGrad = FALSE,
outputDirectory = "~/TypeII_ANA_output" # 修改为你想要的目录
)
# ---------------------------
# Load data - replace path if needed
# ---------------------------
database = read.csv("xxxx.csv", header=TRUE)
# ---------------------------
# Parameters (starting values)
# ---------------------------
apollo_beta = c(
# RANDOM ASC (for alt3 across classes)
asc3_A=0, asc3_B=0, asc3_C=0,
#mu_asc3_A = 0, sigma_asc3_A = 0,
# mu_asc3_B = 0, sigma_asc3_B = 0,
#mu_asc3_C = 0, sigma_asc3_C = 0,
# class-specific coefficients (A, B, C)
b_passport_A = 0, b_tele_A = 0, b_home_A = 0,
b_size1_A = 0, b_size2_A = 0, b_size3_A = 0,
b_weight1_A = 0, b_weight2_A = 0, b_weight3_A = 0,
b_incentive1_A = 0, b_incentive2_A = 0, b_incentive3_A = 0,
b_extracost_A = 0,
b_passport_B = 0, b_tele_B = 0, b_home_B = 0,
b_size1_B = 0, b_size2_B = 0, b_size3_B = 0,
b_weight1_B = 0, b_weight2_B = 0, b_weight3_B = 0,
b_incentive1_B = 0, b_incentive2_B = 0, b_incentive3_B = 0,
b_extracost_B = 0,
b_passport_C = 0, b_tele_C = 0, b_home_C = 0,
b_size1_C = 0, b_size2_C = 0, b_size3_C = 0,
b_weight1_C = 0, b_weight2_C = 0, b_weight3_C = 0,
b_incentive1_C = 0, b_incentive2_C = 0, b_incentive3_C = 0,
b_extracost_C = 0,
# example socio-demo effects in class allocation (you had many; kept a subset)
mu_agecombine1_A = 0, mu_agecombine6_A = 0,
mu_male_A = 0, mu_hhincombine1_A = 0, mu_hhincombine9_A = 0,
mu_agecombine1_B = 0, mu_agecombine6_B = 0,
mu_male_B = 0, mu_hhincombine1_B = 0, mu_hhincombine9_B = 0,
# class allocation constants
delta_A = 0, delta_B = 0
)
apollo_fixed = c() # none fixed
# Random draws settings (if used)
#apollo_draws = list(interDrawsType="MLHS", interNDraws=475, interNormDraws=c("draw_asc3_A","draw_asc3_B","draw_asc3_C"))
#apollo_randCoeff = function(apollo_beta, apollo_inputs){
# randcoeff = list()
# randcoeff[["asc3_A"]] = mu_asc3_A + sigma_asc3_A * draw_asc3_A
# randcoeff[["asc3_B"]] = mu_asc3_B + sigma_asc3_B * draw_asc3_B
# randcoeff[["asc3_C"]] = mu_asc3_C + sigma_asc3_C * draw_asc3_C
# return(randcoeff)
#}
# ---------------------------
# Build attend_matrix (ANA patterns)
# attribute_count = 3
# Here: columns correspond to [incentive, passport, size] (same order as your code)
# ---------------------------
attribute_count = 3
attend_vector = c(0,1)
attend_matrix = expand.grid(replicate(attribute_count, attend_vector, simplify=FALSE))
colnames(attend_matrix) <- c("attend_incentive","attend_passport","attend_size")
# ---------------------------
# Create apollo_inputs properly (must include database)
# ---------------------------
apollo_inputs = apollo_validateInputs()
apollo_inputs$database = database
apollo_inputs$attend_matrix = attend_matrix
# ---------------------------
# Probability function (we compute per-class-per-attendance, combine inner then outer)
# ---------------------------
apollo_probabilities = function(apollo_beta, apollo_inputs, functionality="estimate"){
### Attach parameters and inputs
apollo_attach(apollo_beta, apollo_inputs)
on.exit(apollo_detach(apollo_beta, apollo_inputs))
# Generate random coefficients
#randcoeff = apollo_randCoeff(apollo_beta, apollo_inputs)
# Get attend patterns and dims
attend = apollo_inputs$attend_matrix
nK = nrow(attend) # number of inner attendance patterns
nS = 3 # number of outer classes (A,B,C)
P = list()
# generic MNL settings
mnl_settings = list(
alternatives = c(alt1=1, alt2=2, alt3=3),
avail = list(alt1=1, alt2=1, alt3=1),
choiceVar = choice
)
# We'll compute P_{s,k} for s in 1..3 and k in 1..nK
# store as P[["s1k1"]], etc.
for(k in 1:nK){
# read attendance indicators for this pattern
att_inc = attend[k,"attend_incentive"]
att_pass = attend[k,"attend_passport"]
att_size = attend[k,"attend_size"]
for(s in 1:nS){
# select class-specific base coefficients and apply attendance modifiers where you intended
# Following the pattern in your original code: for class B some coefficients were multiplied by attendance; others left as is.
# Here I follow your original pattern: passport_B and size_C and incentive_A were multiplied by attend matrix when originally coded.
# Implemented as:
if(s==1){ # class A
asc3_s = asc3_A
# asc3_s = randcoeff[["asc3_A"]]
b_passport_s = b_passport_A
b_tele_s = b_tele_A
b_home_s = b_home_A
b_size1_s = b_size1_A
b_size2_s = b_size2_A
b_size3_s = b_size3_A
b_weight1_s = b_weight1_A
b_weight2_s = b_weight2_A
b_weight3_s = b_weight3_A
b_incent1_s = att_inc * b_incentive1_A # in your earlier code incentive in A was multiplied by attend_matrix[k,1]
b_incent2_s = att_inc * b_incentive2_A
b_incent3_s = att_inc * b_incentive3_A
b_extracost_s= b_extracost_A
} else if(s==2){ # class B
asc3_s = asc3_B
# asc3_s = randcoeff[["asc3_B"]]
b_passport_s = att_pass * b_passport_B # earlier you multiplied passport_B by attendance
b_tele_s = b_tele_B
b_home_s = b_home_B
b_size1_s = b_size1_B
b_size2_s = b_size2_B
b_size3_s = b_size3_B
b_weight1_s = b_weight1_B
b_weight2_s = b_weight2_B
b_weight3_s = b_weight3_B
b_incent1_s = b_incentive1_B
b_incent2_s = b_incentive2_B
b_incent3_s = b_incentive3_B
b_extracost_s= b_extracost_B
} else { # class C
asc3_s = asc3_C
# asc3_s = randcoeff[["asc3_C"]]
b_passport_s = b_passport_C
b_tele_s = b_tele_C
b_home_s = b_home_C
# earlier you multiplied size_C by attendance; reflect that:
b_size1_s = att_size * b_size1_C
b_size2_s = att_size * b_size2_C
b_size3_s = att_size * b_size3_C
b_weight1_s = b_weight1_C
b_weight2_s = b_weight2_C
b_weight3_s = b_weight3_C
b_incent1_s = b_incentive1_C
b_incent2_s = b_incentive2_C
b_incent3_s = b_incentive3_C
b_extracost_s= b_extracost_C
}
# Build utilities for alt1, alt2, alt3 following your original formula (replace variable names to match your data)
V = list()
V[["alt1"]] =
b_tele_s * tele_1 +
b_passport_s * passport_1 +
b_home_s * home_1 +
b_size1_s * dummysize1_1 + b_size2_s * dummysize1_2 + b_size3_s * dummysize1_3 +
b_weight1_s * dummyweight1_1 + b_weight2_s * dummyweight1_2 + b_weight3_s * dummyweight1_3 +
b_incent1_s * dummyincentive1_1 + b_incent2_s * dummyincentive1_2 + b_incent3_s * dummyincentive1_3 +
b_extracost_s * extracost1
V[["alt2"]] =
b_tele_s * tele_1 +
b_passport_s * passport_1 +
b_home_s * home_1 +
b_size1_s * dummysize2_1 + b_size2_s * dummysize2_2 + b_size3_s * dummysize2_3 +
b_weight1_s * dummyweight2_1 + b_weight2_s * dummyweight2_2 + b_weight3_s * dummyweight2_3 +
b_incent1_s * dummyincentive2_1 + b_incent2_s * dummyincentive2_2 + b_incent3_s * dummyincentive2_3 +
b_extracost_s * extracost2
V[["alt3"]] = asc3_s # alt3 only has ASC (as in your code)
mnl_settings$utilities = V
# Unique componentName per (s,k) to avoid duplicate warnings
mnl_settings$componentName = paste0("Class_", s, "_k", k)
# within-class probabilities
P[[paste0("Class_", s, "_k", k)]] = apollo_mnl(mnl_settings, functionality)
P[[paste0("Class_", s, "_k", k)]] = apollo_panelProd(P[[paste0("Class_", s, "_k", k)]], apollo_inputs, functionality)
} # end for s
} # end for k
# ------------------------------------------------------------
# Combine over inner ANA (k) to get class-specific probabilities
# ------------------------------------------------------------
# If you want inner class allocation to be estimated with parameters (delta_attend...), you should:
# - define delta_attend_incentive etc in apollo_beta
# - build a utilities list V_inner and call apollo_classAlloc(V_inner)
# For now we use uniform weights across attendance patterns (1/nK).
pi_k = rep(1/nK, nK)
innerP = list()
for(s in 1:3){
# sum over k: pi_k[k] * P_s_k
tmp = 0
for(k in 1:nK){
tmp = tmp + pi_k[k] * P[[paste0("Class_", s, "_k", k)]]
}
innerP[[paste0("Class_", s)]] = tmp
}
# ------------------------------------------------------------
# Outer class allocation (use soc-demo variables as utilities)
# ------------------------------------------------------------
# Build utilities named to match innerP list names
V_outer = list()
V_outer[["Class_1"]] = delta_A + mu_agecombine1_A * age_combine_1 + mu_agecombine6_A*age_combine_6 + mu_male_A*gender_1 + mu_hhincombine1_A*hh_income_combine_1 + mu_hhincombine9_A*hh_income_combine_9
V_outer[["Class_2"]] = delta_B + mu_agecombine1_B * age_combine_1 + mu_agecombine6_B*age_combine_6 + mu_male_B*gender_1 + mu_hhincombine1_B*hh_income_combine_1 + mu_hhincombine9_B*hh_income_combine_9
V_outer[["Class_3"]] = 0
# classAlloc must have classes names matching innerP names
classAlloc_settings = list(classes = c(Class_1=1, Class_2=2, Class_3=3), utilities = V_outer)
pi_values_s = apollo_classAlloc(classAlloc_settings)
# ------------------------------------------------------------
# Combine innerP (inClassProb) with outer classProb
# ------------------------------------------------------------
lc_settings = list(inClassProb = innerP, classProb = pi_values_s)
P[["choice"]] = apollo_lc(lc_settings, apollo_inputs, functionality)
# combine & prepare
P = apollo_combineModels(P, apollo_inputs, functionality, components = c("choice"))
# P = apollo_avgInterDraws(P, apollo_inputs, functionality)
P = apollo_prepareProb(P, apollo_inputs, functionality)
return(P)
}
# ---------------------------
# Quick check: compute LL at starting values
# ---------------------------
apollo_llCalc(apollo_beta, apollo_probabilities, apollo_inputs)
# ---------------------------
# Estimate model
# ---------------------------
model = apollo_estimate(apollo_beta, apollo_fixed, apollo_probabilities, apollo_inputs,
estimate_settings = list(estimationRoutine = "BFGS", maxIterations = 1000))
# Output
apollo_modelOutput(model)
apollo_saveOutput(model)
##############################################################################
Best regards,
kiki