LUCID (Latent Unknown Clusters by Integrating multi-omics Data) finds
latent subgroups of subjects that are simultaneously (1) predictable
from a set of exposures G, (2) characterized by distinct
omics profiles Z, and (3) associated with a health outcome
Y. This document is a comprehensive, user-facing tour of
the package’s public API: everything a user needs to
fit, tune, summarize, predict from, bootstrap, and visualize a LUCID
model, worked through end to end on a small simulated dataset. It
intentionally does not describe internal implementation details – only
functions you are meant to call directly.
It covers:
estimate_lucid(),
tune_lucid(), lucid()summary(), predict_lucid()
(including g_computation mode), boot_lucid(),
plot(), plot_cluster_omic_profile()get_selected_G(), get_selected_Z(),
get_cluster_assignment(),
get_top_omics_features()check_na(),
analyze_missing_pattern(), safe_impute(),
check_imputation_quality()Two things intentionally stay out of view. First, several internal EM
building blocks (a low-level data-filling routine, and a handful of
numerical-stability safeguards for the optimizer) support the functions
above but are not exported and are not part of the API this guide
teaches; section 7 explains what they do for you conceptually where it
matters. Second, this is a breadth guide – runtime is kept
small throughout so the whole document fits together as one example.
Section 3-model tutorials
(lucid_3models_normal_outcome.Rmd,
lucid_3models_binary_outcome.Rmd) go deeper on each
architecture, including the two-step penalized-screen-then- refit
workflow used for real feature selection.
| Functionality | Main API |
|---|---|
| Fit early/parallel/serial model directly | estimate_lucid() |
Grid search for K and penalties |
tune_lucid() |
| One-step wrapper (fit or tune+fit) | lucid() |
| Structured model summary | summary() |
| Prediction and cluster assignment on new/held-out data | predict_lucid() |
| Bootstrap CI inference | boot_lucid() |
| Sankey-style path visualization | plot() |
| Per-cluster omics profile visualization | plot_cluster_omic_profile() |
| Extract selected exposures / omics features from a fit | get_selected_G(), get_selected_Z() |
| Extract hard cluster assignment from a fit | get_cluster_assignment() |
| Extract top-N most important omics features from a fit | get_top_omics_features() |
| Missingness diagnostics | check_na(), analyze_missing_pattern() |
| Robust imputation helpers | safe_impute(),
check_imputation_quality() |
The four get_*() extractors share one design point worth
calling out up front: every one of them takes only the fitted model
object and figures out on its own whether it is looking at an early,
parallel, or serial fit. You never pass a lucid_model
argument to them, and (since this release) predict_lucid()
and boot_lucid() no longer require one either – they detect
it from class(model). The only place you still name the
model type explicitly is when fitting one, since that is the
argument that decides which architecture gets fit in the first
place.
A LUCID model only has something interesting to find if
G, the latent cluster, Z, and Y
are actually related – fitting it to independent noise would produce
arbitrary clusters and make every example below meaningless. This
simulation deliberately wires in that structure: a subset of exposures
in G drive a binary latent state x,
x in turn shifts each omics layer’s means apart (so the
layers actually separate by cluster) and shifts the outcome
Y. That known ground truth is also what lets the fitted
models’ recovered clusters and selected features be checked for sanity
throughout this document.
make_demo_data <- function(n = 80, pG = 6, pZ = 4, seed = 20260309) {
set.seed(seed)
# Exposures
G <- matrix(rnorm(n * pG), nrow = n, ncol = pG)
colnames(G) <- paste0("G", seq_len(pG))
# Covariates associated with exposures
CoG <- cbind(
cov_g1 = G[, 1] + 0.2 * G[, 2] + rnorm(n, sd = 0.2),
cov_g2 = -0.3 * G[, 3] + 0.4 * G[, 4] + rnorm(n, sd = 0.25)
)
CoY <- CoG
# Latent cluster driver
lin <- 1.0 * G[, 1] - 0.8 * G[, 2] + 0.4 * CoG[, 1]
prob_x <- plogis(lin)
x <- rbinom(n, size = 1, prob = prob_x)
# Layer 1 and 2 for parallel stage
Z1 <- cbind(
1.2 * x + 0.5 * G[, 1] + rnorm(n, sd = 0.5),
1.0 * x - 0.4 * G[, 2] + rnorm(n, sd = 0.5),
0.8 * x + 0.3 * G[, 3] + rnorm(n, sd = 0.5),
0.6 * x + 0.2 * G[, 4] + rnorm(n, sd = 0.5)
)
Z2 <- cbind(
-1.1 * x + 0.45 * G[, 2] + rnorm(n, sd = 0.5),
-0.9 * x - 0.35 * G[, 1] + rnorm(n, sd = 0.5),
-0.7 * x + 0.25 * G[, 5] + rnorm(n, sd = 0.5),
-0.5 * x + 0.20 * G[, 6] + rnorm(n, sd = 0.5)
)
# Layer 3 for serial second stage (early stage)
Z3 <- cbind(
0.9 * x + 0.30 * G[, 1] + rnorm(n, sd = 0.55),
0.7 * x - 0.25 * G[, 3] + rnorm(n, sd = 0.55),
-0.8 * x + 0.20 * G[, 4] + rnorm(n, sd = 0.55),
-0.6 * x + 0.15 * G[, 6] + rnorm(n, sd = 0.55)
)
colnames(Z1) <- paste0("Z1_f", seq_len(pZ))
colnames(Z2) <- paste0("Z2_f", seq_len(pZ))
colnames(Z3) <- paste0("Z3_f", seq_len(pZ))
# Outcomes
Y_normal <- 1.1 * x + 0.5 * G[, 1] - 0.25 * G[, 3] + 0.35 * CoY[, 2] + rnorm(n, sd = 0.7)
Y_binary <- rbinom(n, size = 1, prob = plogis(-0.2 + 1.0 * x + 0.35 * G[, 1] - 0.2 * CoY[, 1]))
# Structures for each model
Z_parallel <- list(layer1 = Z1, layer2 = Z2)
Z_early <- cbind(Z1, Z2)
Z_serial_mixed <- list(list(layer1 = Z1, layer2 = Z2), Z3)
list(
G = G,
CoG = CoG,
CoY = CoY,
Y_normal = as.numeric(Y_normal),
Y_binary = as.numeric(Y_binary),
Z1 = Z1,
Z2 = Z2,
Z3 = Z3,
Z_parallel = Z_parallel,
Z_early = Z_early,
Z_serial_mixed = Z_serial_mixed
)
}
d <- make_demo_data()The three Z_* structures at the bottom –
Z_early (one concatenated matrix), Z_parallel
(a named list of layer matrices), Z_serial_mixed (a list
whose first element is itself a parallel-style list) – are exactly the
three shapes estimate_lucid() expects for early, parallel,
and serial fitting respectively. Building all three up front from the
same underlying Z1/Z2/Z3 layers
means every model type below is fit on data that differ only in how the
layers are packaged, not in what they contain.
Multi-omics studies rarely have complete data: whole layers can be missing for a subject (e.g. a blood sample was never collected – “listwise” missingness within that layer), or a handful of individual features can be missing sporadically. LUCID’s EM fitting handles both patterns natively – the missing-data helpers in section 6 exist to let you check that handling, not to do it manually. This chunk injects both patterns into copies of the data built above, purely so the diagnostics in section 6 have something to report on.
# Early matrix missingness
Z_early_miss <- d$Z_early
Z_early_miss[1, ] <- NA # listwise
Z_early_miss[2:4, 1] <- NA # sporadic block
Z_early_miss[5, 3] <- NA # sporadic cell
# Parallel list missingness
Z_parallel_miss <- d$Z_parallel
Z_parallel_miss[[1]][1, ] <- NA # listwise on layer1
Z_parallel_miss[[2]][2, 2] <- NA # sporadic on layer2
# Serial mixed missingness (stage1 parallel + stage2 early)
Z_serial_miss <- list(
list(
layer1 = Z_parallel_miss[[1]],
layer2 = Z_parallel_miss[[2]]
),
{tmp <- d$Z3; tmp[3, ] <- NA; tmp}
)estimate_lucid() imputes missing omics values
internally, conditional on the current model fit, as part of its EM loop
– there is no separate “impute first, then fit” step for you to run.
What the two functions below give you is visibility into that
process: whether the missingness pattern looks the way you expect, and
whether a quick standalone imputation of the same data is directionally
sane.
analyze_missing_pattern() and
check_na()analyze_missing_pattern() reports how much data is
missing and how it is distributed; check_na() additionally
classifies, per subject and per omics layer, which missingness pattern
applies (fully observed, listwise missing, or sporadically missing) –
the same three-way classification the EM loop itself uses internally to
decide how to handle each subject.
miss_info_early <- analyze_missing_pattern(Z_early_miss)
na_early <- check_na(Z_early_miss, lucid_model = "early")
na_parallel <- check_na(Z_parallel_miss, lucid_model = "parallel")
miss_info_early$total_missing## [1] 0.01875
##
## 1 2 3
## 75 4 1
## $n_layers
## [1] 2
##
## $n_observations
## [1] 80
##
## $features_per_layer
## layer1 layer2
## 4 4
##
## $missing_pattern_counts
##
## 1 1 1 2 3 1
## 78 1 1
##
## $total_missing_prop
## [1] 0.0078125
miss_info_early$total_missing should match the number of
NAs injected above (6, from the 1 listwise row, the 3-cell
sporadic block, and the 1 sporadic cell).
table(na_early$indicator_na) cross-tabulates subjects by
pattern; na_parallel$cross_layer_summary shows the same
thing per layer for the parallel structure, which is the number to check
when a specific layer is suspected of being a bigger driver of
missingness than the others.
safe_impute() and
check_imputation_quality()safe_impute() is a standalone imputation you can run
outside of model fitting – useful for a quick look at the data, or for a
workflow that needs a complete matrix for some other purpose before ever
calling estimate_lucid().
check_imputation_quality() then scores how plausible the
imputed values are relative to the observed distribution of each
column.
# Use a sub-matrix for concise display
orig_sub <- Z_early_miss[, 1:4, drop = FALSE]
imputed_sub <- safe_impute(orig_sub, method = "mean")
quality_sub <- check_imputation_quality(orig_sub, imputed_sub)
quality_sub$overall_quality## NULL
overall_quality summarizes, across all imputed cells,
how close the filled values landed to each column’s observed range and
variance – a low score here would flag an imputation that looks
implausible (e.g. a filled value far outside the observed range), which
is worth knowing about a data set even before it reaches
estimate_lucid(). estimate_lucid()’s own
internal imputation is more informed than this standalone version – it
fills a missing value conditional on the fitted cluster structure and
covariance, re-estimated at every EM iteration, rather than with a
single fixed column statistic – but the two are checking the same basic
question: are the filled-in values sane?
estimate_lucid()estimate_lucid() is the single entry point for fitting
any of the three architectures at one fixed K (number of
clusters) and one fixed penalty. lucid_model picks the
architecture; everything else (G, Z,
Y, family, K, the
Rho_* penalties) has the same meaning across all three.
Rho_G penalizes the G -> cluster
coefficients toward zero (an exposure whose coefficient is driven to
exactly zero is “not selected”); Rho_Z_Mu and
Rho_Z_Cov do the analogous thing for the omics
mean/covariance structure, so weakly-differentiating features can be
zeroed out of the cluster definition. This guide fits at
Rho_* = 0 throughout for simplicity and speed; the 3-model
tutorials walk through the penalized-screen-then- zero-penalty-refit
workflow that real feature selection uses.
Early integration concatenates every omics layer into one matrix and fits one cluster variable from the pooled features – the right choice when you expect a single latent state to manifest across all your omics layers at once.
set.seed(101)
fit_early <- estimate_lucid(
lucid_model = "early",
G = d$G,
Z = Z_early_miss,
Y = d$Y_normal,
CoG = d$CoG,
CoY = d$CoY,
family = "normal",
K = 2,
Rho_G = 0,
Rho_Z_Mu = 0,
Rho_Z_Cov = 0,
max_itr = 8,
max_tot.itr = 30,
tol = 1e-2,
seed = 101,
verbose = FALSE
)## Fitting LUCID early model (K = 2)...
## Finished LUCID early model.
## [1] "early_lucid"
Every field below is documented on ?estimate_lucid’s
@return; the same field names and shapes are used across
all three model types where a field applies to more than one. Rather
than reading fit_early$select$selectG directly, use
get_selected_G()/get_selected_Z() – they
return the same information as a named vector and auto-detect the model
type, so the same call works unchanged if fit_early were
swapped for a parallel or serial fit later in a script.
## [1] "res_Beta" "res_Mu" "res_Sigma"
## [4] "res_Gamma" "K" "var.names"
## [7] "init_omic.data.model" "likelihood" "inclusion.p"
## [10] "family" "select" "useY"
## [13] "Z" "init_impute" "init_par"
## [16] "Rho" "missing_summary" "em_control"
## likelihood: -650.09
## selected G: 6 of 6
## selected Z: 8 of 8
At Rho_G = Rho_Z_Mu = 0 nothing is actually penalized
out, so every exposure and every omics feature is reported “selected”
here – the counts become informative once a positive penalty is applied,
as in the 3-model tutorials’ screening step.
Parallel integration fits a separate cluster variable per omics layer, jointly, rather than pooling the layers into one. Use this when the layers plausibly reflect different underlying processes – e.g. a metabolomic subtype and a methylation subtype need not coincide – and you want each to be discovered on its own terms rather than forced into a single joint cluster.
set.seed(102)
fit_parallel <- estimate_lucid(
lucid_model = "parallel",
G = d$G,
Z = Z_parallel_miss,
Y = d$Y_normal,
CoG = d$CoG,
CoY = d$CoY,
family = "normal",
K = c(2, 2),
Rho_G = 0,
Rho_Z_Mu = 0,
Rho_Z_Cov = 0,
max_itr = 8,
max_tot.itr = 30,
tol = 1e-2,
seed = 102,
verbose = FALSE
)## Fitting LUCID parallel model (2 layers)...
## Finished LUCID parallel model.
## [1] "lucid_parallel"
Two fields differ from the early fit above: N (sample
size) is present here but not for early, and z (the joint
E-step responsibility array across layers, before it is marginalized
into inclusion.p) appears only for parallel.
get_selected_G() takes an optional layer
argument here, since a parallel fit can select a different exposure
subset per layer.
## [1] "res_Beta" "res_Mu" "res_Sigma"
## [4] "res_Gamma" "K" "N"
## [7] "var.names" "init_omic.data.model" "likelihood"
## [10] "inclusion.p" "family" "select"
## [13] "useY" "Z" "z"
## [16] "init_impute" "init_par" "Rho"
## [19] "missing_summary" "em_control"
## N: 80
cat("selected G per layer:", paste(sapply(seq_along(fit_parallel$K), function(i)
sum(get_selected_G(fit_parallel, layer = i))),
collapse = ", "), "\n")## selected G per layer: 6, 6
sel_z_parallel <- get_selected_Z(fit_parallel)
cat("selected Z per layer:", paste(sapply(sel_z_parallel, sum), collapse = ", "), "\n")## selected Z per layer: 4, 4
get_selected_Z() returns a list here, one logical vector
per layer, since “which omics features are selected” is itself a
per-layer question for a parallel fit.
Serial integration chains stages together: stage 1’s cluster assignment becomes (part of) stage 2’s input, and so on. It suits a mediation-like hypothesis – e.g. exposures act on an early biological layer, which in turn shapes a later one, which in turn shapes the outcome. Any stage can itself be early or parallel; the fit below mixes a parallel first stage (two omics layers) with an early second stage (one layer), which is the most general configuration the package supports.
set.seed(103)
fit_serial <- estimate_lucid(
lucid_model = "serial",
G = d$G,
Z = Z_serial_miss,
Y = d$Y_normal,
CoG = d$CoG,
CoY = d$CoY,
family = "normal",
K = list(list(2, 2), 2),
Rho_G = 0,
Rho_Z_Mu = 0,
Rho_Z_Cov = 0,
max_itr = 8,
max_tot.itr = 36,
tol = 1e-2,
seed = 103,
verbose = FALSE
)## Fitting LUCID serial model (2 stages)...
## Stage 1/2 (parallel) finished: log-likelihood = -607.523.
## Stage 2/2 (early) finished: log-likelihood = -399.220.
## Finished LUCID serial model.
## [1] "lucid_serial"
## [1] 2
Serial adds submodel (the fitted stage models, each a
complete early_lucid/lucid_parallel object)
and res_Delta (between-stage transition coefficients).
likelihood and select are present, same as
early/parallel, but are aggregates: likelihood sums each
stage’s own log-likelihood (no single joint EM loop exists to report one
from), and select is stage 1’s own selection only – the one
stage whose G is the cohort’s real exposures, not a
previous stage’s cluster probabilities. get_selected_G() on
a serial fit always returns stage 1’s selection for the same reason;
get_selected_Z() returns a list, one element per stage,
shaped like whatever that stage’s own architecture is (a vector for an
early stage, a list of layers for a parallel stage).
## [1] "res_Beta" "res_Mu" "res_Sigma"
## [4] "res_Delta" "res_Gamma" "K"
## [7] "N" "var.names" "init_omic.data.model"
## [10] "inclusion.p" "family" "useY"
## [13] "Z" "init_impute" "init_par"
## [16] "submodel" "missing_summary" "Rho"
## [19] "em_control" "likelihood" "select"
## top-level likelihood (sum over stages): -1006.743
cat("top-level select is stage 1's select:",
identical(fit_serial$select, fit_serial$submodel[[1]]$select), "\n")## top-level select is stage 1's select: TRUE
sel_z_serial <- get_selected_Z(fit_serial)
cat("stage 1 (parallel) selected Z per layer:",
paste(sapply(sel_z_serial[[1]], sum), collapse = ", "), "\n")## stage 1 (parallel) selected Z per layer: 4, 4
## stage 2 (early) selected Z: 4
verbose = TRUE)verbose = TRUE prints one line per EM iteration – the
current log-likelihood and, where relevant, the change since the last
iteration – which is the fastest way to confirm a fit is actually
converging (steadily increasing, then flattening) rather than diverging
or oscillating. The fits below are intentionally lightweight (a
30-subject subsample, tiny iteration caps) and only serve to show what
that log looks like for each architecture.
n_demo <- 30
G_demo <- d$G[1:n_demo, , drop = FALSE]
Y_demo <- d$Y_normal[1:n_demo]
CoG_demo <- d$CoG[1:n_demo, , drop = FALSE]
CoY_demo <- d$CoY[1:n_demo, , drop = FALSE]
Z_early_demo <- d$Z_early[1:n_demo, , drop = FALSE]
Z_parallel_demo <- lapply(d$Z_parallel, function(z) z[1:n_demo, , drop = FALSE])
Z_serial_demo <- list(
lapply(d$Z_parallel, function(z) z[1:n_demo, , drop = FALSE]),
d$Z3[1:n_demo, , drop = FALSE]
)
set.seed(111)
fit_early_verbose <- estimate_lucid(
lucid_model = "early",
G = G_demo,
Z = Z_early_demo,
Y = Y_demo,
CoG = CoG_demo,
CoY = CoY_demo,
family = "normal",
K = 2,
Rho_G = 0,
Rho_Z_Mu = 0,
Rho_Z_Cov = 0,
max_itr = 2,
max_tot.itr = 8,
tol = 1e-2,
seed = 111,
verbose = TRUE
)## Initialize LUCID with mclust based on inclusion probabilities given by mclust
## Fitting Early Integration LUCID model (K = 2, Rho_G = 0, Rho_Z_Mu = 0, Rho_Z_Cov = 0)
## iteration 1 : E-step finished.
## iteration 1: log-likelihood = -211.863
## iteration 2 : E-step finished.
## iteration 2: log-likelihood = -207.327
## iteration 3 : E-step finished.
## iteration 3: log-likelihood = -207.288
## Finished LUCID early model: log-likelihood = -207.288.
set.seed(112)
fit_parallel_verbose <- estimate_lucid(
lucid_model = "parallel",
G = G_demo,
Z = Z_parallel_demo,
Y = Y_demo,
CoG = CoG_demo,
CoY = CoY_demo,
family = "normal",
K = c(2, 2),
Rho_G = 0,
Rho_Z_Mu = 0,
Rho_Z_Cov = 0,
max_itr = 2,
max_tot.itr = 8,
tol = 1e-2,
seed = 112,
verbose = TRUE
)## Fitting LUCID in Parallel model (K = 2, Rho_G = 0, Rho_Z_Mu = 0, Rho_Z_Cov = 0) (K = 2, Rho_G = 0, Rho_Z_Mu = 0, Rho_Z_Cov = 0)
## iteration 1 : E-step finished.
## iteration 1: log-likelihood = -241.834
## iteration 2 : E-step finished.
## iteration 2: log-likelihood = -241.139
## Finished LUCID parallel model: log-likelihood = -241.139.
set.seed(113)
fit_serial_verbose <- estimate_lucid(
lucid_model = "serial",
G = G_demo,
Z = Z_serial_demo,
Y = Y_demo,
CoG = CoG_demo,
CoY = CoY_demo,
family = "normal",
K = list(list(2, 2), 2),
Rho_G = 0,
Rho_Z_Mu = 0,
Rho_Z_Cov = 0,
max_itr = 2,
max_tot.itr = 10,
tol = 1e-2,
seed = 113,
verbose = TRUE
)## Fitting LUCID serial model (Stage 1/2)...
## Fitting LUCID in Parallel model (K = 2, Rho_G = 0, Rho_Z_Mu = 0, Rho_Z_Cov = 0) (K = 2, Rho_G = 0, Rho_Z_Mu = 0, Rho_Z_Cov = 0)
## iteration 1 : E-step finished.
## iteration 1: log-likelihood = -206.521
## iteration 2 : E-step finished.
## iteration 2: log-likelihood = -205.875
## Finished LUCID parallel model: log-likelihood = -205.875.
##
## Fitting LUCID serial model (Stage 2/2)...
## Initialize LUCID with mclust based on inclusion probabilities given by mclust
## Fitting Early Integration LUCID model (K = 2, Rho_G = 0, Rho_Z_Mu = 0, Rho_Z_Cov = 0)
## iteration 1 : E-step finished.
## iteration 1: log-likelihood = -156.096
## iteration 2 : E-step finished.
## iteration 2: log-likelihood = -153.735
## iteration 3 : E-step finished.
## iteration 3: log-likelihood = -152.525
## Finished LUCID early model: log-likelihood = -152.525.
##
## Success: LUCID serial model constructed!
tune_lucid() and
lucid()Choosing K and the penalty strengths by hand, as section
7 did, only works when you already have a good guess.
tune_lucid() instead fits a whole grid of
K/Rho_* combinations and reports each one’s
BIC, so the combination that best balances fit against model complexity
can be selected systematically rather than by trial and error.
lucid() goes one step further: it runs the same tuning
search, picks the BIC-best candidate, and – importantly – refits
that winner at zero penalty, so the returned model’s
coefficients are not shrunk by the same penalty that was used only to
decide which features to keep. That two-step logic (penalized search for
selection, zero-penalty refit for estimation) is worth remembering; the
3-model tutorials walk through doing it by hand for cases where more
control over the refit is needed than lucid()’s default
gives.
tune_lucid() with a small early-model gridset.seed(104)
tune_early <- tune_lucid(
G = d$G,
Z = d$Z_early,
Y = d$Y_normal,
CoG = d$CoG,
CoY = d$CoY,
family = "normal",
lucid_model = "early",
K = 2:3,
Rho_G = 0,
Rho_Z_Mu = 0,
Rho_Z_Cov = 0,
max_itr = 6,
max_tot.itr = 24,
seed = 104
)## Fitting LUCID early model (K = 2)...
## Finished LUCID early model.
##
## ====================================================
## LUCID Early Integration: Model Summary
## ====================================================
##
## Model specification
## Family : normal
## Number of observations : 80
## Number of clusters (K) : 2
##
## Missing-data profile
## Listwise missing rows : 0 / 80 (0.0%)
## Sporadic missing rows : 0 / 80 (0.0%)
## Missing cells total : 0 / 640 (0.0%)
##
## Feature selection overview
## G features selected : 6 / 6 (100.0%)
## Z features selected : 8 / 8 (100.0%)
##
## Model fit statistics
## Log-likelihood : -704.46
## BIC : 1860.28
## Number of parameters : 103
##
## Regularization
## Rho_G : 0.000
## Rho_Z_Mu : 0.000
## Rho_Z_Cov : 0.000
##
## Detailed parameter estimates
## (1) Y (continuous outcome): cluster 1 mean (intercept), mean differences for other clusters, and covariate effects
## Gamma
## (Intercept) 0.3125382
## cluster2 0.2619192
## cov_g1 0.6175223
## cov_g2 0.5320559
##
## (2) Z: mean of omics data for each latent cluster
## mu_cluster1 mu_cluster2
## Z1_f1 -0.56900590 0.5170944
## Z1_f2 0.15183098 0.5103446
## Z1_f3 -0.12642173 0.4027654
## Z1_f4 -0.01756913 0.3191024
## Z2_f1 -0.09641616 -0.4994498
## Z2_f2 0.23868326 -0.4993390
## Z2_f3 0.25290360 -0.5092025
## Z2_f4 0.08254873 -0.3134526
##
## (3) E: intercept and odds ratio of being assigned to each latent cluster for each exposure
## beta OR
## (Intercept).cluster2 2.3141047 10.1158624
## G1.cluster2 0.6683651 1.9510450
## G2.cluster2 -0.2712207 0.7624482
## G3.cluster2 -0.3940201 0.6743405
## G4.cluster2 0.9889927 2.6885250
## G5.cluster2 -0.6029099 0.5472170
## G6.cluster2 -0.1097568 0.8960520
## cov_g1.cluster2 0.2329658 1.2623383
## cov_g2.cluster2 -0.8264530 0.4375987
## Fitting LUCID early model (K = 3)...
## Finished LUCID early model.
##
## ====================================================
## LUCID Early Integration: Model Summary
## ====================================================
##
## Model specification
## Family : normal
## Number of observations : 80
## Number of clusters (K) : 3
##
## Missing-data profile
## Listwise missing rows : 0 / 80 (0.0%)
## Sporadic missing rows : 0 / 80 (0.0%)
## Missing cells total : 0 / 640 (0.0%)
##
## Feature selection overview
## G features selected : 6 / 6 (100.0%)
## Z features selected : 8 / 8 (100.0%)
##
## Model fit statistics
## Log-likelihood : -667.52
## BIC : 2027.40
## Number of parameters : 158
##
## Regularization
## Rho_G : 0.000
## Rho_Z_Mu : 0.000
## Rho_Z_Cov : 0.000
##
## Detailed parameter estimates
## (1) Y (continuous outcome): cluster 1 mean (intercept), mean differences for other clusters, and covariate effects
## Gamma
## (Intercept) -0.3562758
## cluster2 0.6505660
## cluster3 1.2050978
## cov_g1 0.5174619
## cov_g2 0.5870936
##
## (2) Z: mean of omics data for each latent cluster
## mu_cluster1 mu_cluster2 mu_cluster3
## Z1_f1 -0.38770100 -0.59861975 0.7271796
## Z1_f2 -0.35709134 0.11509707 0.7676527
## Z1_f3 -0.07327279 -0.08045505 0.5112009
## Z1_f4 -0.07748138 0.26794562 0.3957297
## Z2_f1 0.44010129 0.08223693 -0.7893889
## Z2_f2 -0.07577175 0.61037027 -0.6111670
## Z2_f3 -0.17081122 0.38396071 -0.5697624
## Z2_f4 0.01014765 -0.10728092 -0.3700419
##
## (3) E: intercept and odds ratio of being assigned to each latent cluster for each exposure
## beta OR
## (Intercept).cluster2 -0.6811935 0.50601271
## G1.cluster2 4.0580956 57.86401080
## G2.cluster2 -1.9687675 0.13962885
## G3.cluster2 -0.7790547 0.45883954
## G4.cluster2 1.8085998 6.10189747
## G5.cluster2 1.3757889 3.95819813
## G6.cluster2 -1.9227067 0.14621067
## cov_g1.cluster2 -3.6739561 0.02537588
## cov_g2.cluster2 -0.8862642 0.41219276
## (Intercept).cluster3 3.3408897 28.24424309
## G1.cluster3 3.7131381 40.98221005
## G2.cluster3 -2.4721863 0.08440014
## G3.cluster3 -1.0152803 0.36230087
## G4.cluster3 0.3603281 1.43379981
## G5.cluster3 0.5800672 1.78615851
## G6.cluster3 -1.3117669 0.26934374
## cov_g1.cluster3 -1.7567726 0.17260102
## cov_g2.cluster3 0.4749526 1.60793799
## K Rho_G Rho_Z_Mu Rho_Z_Cov BIC
## 1 2 0 0 0 1860.275
## 2 3 0 0 0 2027.397
tune_list has one row per grid candidate, with its
fitted K/penalty combination and its BIC; the lowest BIC in
this table is the candidate lucid() (below) would have
selected automatically.
lucid() wrapper (auto-tune over K for early
model)set.seed(105)
fit_lucid_wrapper <- lucid(
G = d$G,
Z = d$Z_early,
Y = d$Y_normal,
CoG = d$CoG,
CoY = d$CoY,
family = "normal",
lucid_model = "early",
K = 2:3,
Rho_G = 0,
Rho_Z_Mu = 0,
Rho_Z_Cov = 0,
max_itr = 6,
max_tot.itr = 24,
seed = 105
)
class(fit_lucid_wrapper)## [1] "early_lucid"
fit_lucid_wrapper is a complete early_lucid
object – the same class estimate_lucid() would have
returned had K = 2:3’s BIC-winner been fit directly – so it
works with every function in this guide (summary(),
predict_lucid(), the get_*() extractors)
exactly like any other fit.
summary()summary() is the primary way to read a fitted model’s
results without digging through its raw list structure. For every model
type it prints, in order: the model specification (family, sample size,
K); a feature- selection overview (how many exposures/omics
features survived, if a penalty was used); the
G -> cluster coefficients (with odds ratios); the
cluster-specific omics means; and the cluster -> Y
coefficients. When a boot.se/bootstrap object is supplied
(section 11), every one of those coefficient tables additionally gets a
sig column marking rows whose 95%-normal-theory confidence
interval excludes 0 with "*" – a quick visual scan for
which effects are distinguishable from no effect at all, without reading
every interval by eye.
##
## ====================================================
## LUCID Early Integration: Model Summary
## ====================================================
##
## Model specification
## Family : normal
## Number of observations : 80
## Number of clusters (K) : 2
##
## Missing-data profile
## Listwise missing rows : 1 / 80 (1.2%)
## Sporadic missing rows : 4 / 80 (5.0%)
## Missing cells total : 12 / 640 (1.9%)
##
## Feature selection overview
## G features selected : 6 / 6 (100.0%)
## Z features selected : 8 / 8 (100.0%)
##
## Model fit statistics
## Log-likelihood : -650.09
## BIC : 1751.53
## Number of parameters : 103
##
## Regularization
## Rho_G : 0.000
## Rho_Z_Mu : 0.000
## Rho_Z_Cov : 0.000
##
## Detailed parameter estimates
## (1) Y (continuous outcome): cluster 1 mean (intercept), mean differences for other clusters, and covariate effects
## Gamma
## (Intercept) 0.1765857
## cluster2 1.0449481
## cov_g1 0.4929783
## cov_g2 0.6931914
##
## (2) Z: mean of omics data for each latent cluster
## mu_cluster1 mu_cluster2
## Z1_f1 -0.0777814604 1.3323119
## Z1_f2 0.0840691217 1.1873927
## Z1_f3 0.0237814722 0.9834228
## Z1_f4 0.1617553558 0.5274073
## Z2_f1 -0.0021631579 -1.2547393
## Z2_f2 0.0001776934 -1.1894463
## Z2_f3 -0.1742793904 -0.8918661
## Z2_f4 -0.1083855071 -0.5296839
##
## (3) E: intercept and odds ratio of being assigned to each latent cluster for each exposure
## beta OR
## (Intercept).cluster2 -0.86505853 0.4210269
## G1.cluster2 1.49321813 4.4513977
## G2.cluster2 -1.12634744 0.3242153
## G3.cluster2 0.19971412 1.2210536
## G4.cluster2 -0.55799151 0.5723575
## G5.cluster2 -0.27541536 0.7592567
## G6.cluster2 0.09558619 1.1003037
## cov_g1.cluster2 0.08560300 1.0893738
## cov_g2.cluster2 0.64058435 1.8975894
##
## ====================================================
## LUCID Parallel: Model Summary
## ====================================================
##
## Model specification
## Family : gaussian
## Number of observations : 80
## Clusters per layer : 2, 2
##
## Missing-data profile by layer
## Layer 1 listwise rows : 1 / 80 (1.2%)
## Layer 1 sporadic rows : 0 / 80 (0.0%)
## Layer 1 missing cells : 4 / 320 (1.2%)
## Layer 2 listwise rows : 0 / 80 (0.0%)
## Layer 2 sporadic rows : 1 / 80 (1.2%)
## Layer 2 missing cells : 1 / 320 (0.3%)
##
## Feature selection overview
## G features selected : 6 / 6 (100.0%)
## G features by layer
## Layer 1 : 6 / 6 (100.0%)
## Layer 2 : 6 / 6 (100.0%)
## Z features
## Layer 1 selected : 4 / 4 (100.0%)
## Layer 1 multi-cluster: 4
## Layer 2 selected : 4 / 4 (100.0%)
## Layer 2 multi-cluster: 4
##
## Model fit statistics
## Log-likelihood : -703.20
## BIC : 1756.96
## Number of parameters : 80
##
## Regularization
## Rho_G : 0.000
## Rho_Z_Mu : 0.000
## Rho_Z_Cov : 0.000
##
## Detailed parameter estimates
## (1) Y (continuous outcome): intercept, effects of each non-reference latent cluster for each layer of Y (and effect of covariates if included)
## Gamma
## (Intercept) 0.04248756
## Layer1_LC2 0.01765860
## Layer2_LC2 1.06757644
## cov_g1 0.49345900
## cov_g2 0.65852863
##
## (2) Z: mean of omics data for each latent cluster of each layer
## Layer 1
##
## mu_cluster1 mu_cluster2
## Z1_f1 -0.349168270 1.2391929
## Z1_f2 0.004260061 0.9934904
## Z1_f3 -0.154181517 0.9239008
## Z1_f4 0.116945108 0.4850517
##
## Layer 2
##
## mu_cluster1 mu_cluster2
## Z2_f1 0.21574635 -1.2562624
## Z2_f2 0.08358451 -1.0078702
## Z2_f3 -0.12054320 -0.7762626
## Z2_f4 -0.08469553 -0.4845957
##
## (3) E: intercept and odds ratio of being assigned to each latent cluster for each exposure for each layer
## Layer 1
##
## beta OR
## (Intercept).cluster2 -0.08685128 0.9168134
## G1.cluster2 1.20608960 3.3403968
## G2.cluster2 -0.92554313 0.3963161
## G3.cluster2 0.63658188 1.8900095
## G4.cluster2 -0.73551173 0.4792601
## G5.cluster2 -0.04677656 0.9543006
## G6.cluster2 -0.09518064 0.9092087
##
## Layer 2
##
## beta OR
## (Intercept).cluster2 -0.16662615 0.8465160
## G1.cluster2 3.59813736 36.5301286
## G2.cluster2 -1.28668079 0.2761860
## G3.cluster2 0.06073117 1.0626132
## G4.cluster2 -0.13557162 0.8732166
## G5.cluster2 -0.16105933 0.8512416
## G6.cluster2 0.05468855 1.0562116
##
## ====================================================
## LUCID Serial: Model Summary
## ====================================================
##
## Model specification
## Family : normal
## Number of observations : 80
## Number of stages : 2
## Stage 1 : parallel (K = 2,2)
## Stage 2 : early (K = 2)
##
## Missing-data profile by stage
## Stage 1
## Layer 1 listwise/sporadic rows : 1 / 0
## Layer 2 listwise/sporadic rows : 0 / 1
## Stage 2
## Listwise rows : 1
## Sporadic rows : 0
##
## Model fit statistics
## Log-likelihood : -1006.74
## BIC : 2499.89
##
## Regularization
## Rho_G : 0.000
## Rho_Z_Mu : 0.000
## Rho_Z_Cov : 0.000
##
## Stage-wise detailed parameter estimates
##
## --- Stage 1 (parallel) ---
##
## ====================================================
## LUCID Parallel: Model Summary
## ====================================================
##
## Model specification
## Family : gaussian
## Number of observations : 80
## Clusters per layer : 2, 2
##
## Missing-data profile by layer
## Layer 1 listwise rows : 1 / 80 (1.2%)
## Layer 1 sporadic rows : 0 / 80 (0.0%)
## Layer 1 missing cells : 4 / 320 (1.2%)
## Layer 2 listwise rows : 0 / 80 (0.0%)
## Layer 2 sporadic rows : 1 / 80 (1.2%)
## Layer 2 missing cells : 1 / 320 (0.3%)
##
## Feature selection overview
## G features selected : 6 / 6 (100.0%)
## G features by layer
## Layer 1 : 6 / 6 (100.0%)
## Layer 2 : 6 / 6 (100.0%)
## Z features
## Layer 1 selected : 4 / 4 (100.0%)
## Layer 1 multi-cluster: 4
## Layer 2 selected : 4 / 4 (100.0%)
## Layer 2 multi-cluster: 4
##
## Model fit statistics
## Log-likelihood : -607.52
## BIC : 1539.32
## Number of parameters : 74
##
## Regularization
## Rho_G : 0.000
## Rho_Z_Mu : 0.000
## Rho_Z_Cov : 0.000
##
## Detailed parameter estimates
## (1) Z: mean of omics data for each latent cluster of each layer
## Layer 1
##
## mu_cluster1 mu_cluster2
## Z1_f1 -0.352615720 1.2292345
## Z1_f2 0.002360677 0.9869976
## Z1_f3 -0.156731519 0.9173883
## Z1_f4 0.116972344 0.4817741
##
## Layer 2
##
## mu_cluster1 mu_cluster2
## Z2_f1 -1.2489062 0.22761663
## Z2_f2 -1.0010499 0.09162188
## Z2_f3 -0.7529336 -0.13230863
## Z2_f4 -0.4929066 -0.07270329
##
## (2) E: intercept and odds ratio of being assigned to each latent cluster for each exposure for each layer
## Layer 1
##
## beta OR
## (Intercept).cluster2 -0.05763017 0.9439990
## G1.cluster2 1.21618905 3.3743039
## G2.cluster2 -0.95080523 0.3864297
## G3.cluster2 0.67635032 1.9666868
## G4.cluster2 -0.74690763 0.4738295
## G5.cluster2 -0.06497218 0.9370935
## G6.cluster2 -0.11292848 0.8932145
##
## Layer 2
##
## beta OR
## (Intercept).cluster2 0.10180517 1.10716774
## G1.cluster2 -3.76996919 0.02305277
## G2.cluster2 1.30981565 3.70549056
## G3.cluster2 -0.20510303 0.81456338
## G4.cluster2 0.05465422 1.05617534
## G5.cluster2 0.16024275 1.17379577
## G6.cluster2 -0.05478440 0.94668923
##
##
## --- Stage 2 (early) ---
##
## ====================================================
## LUCID Early Integration: Model Summary
## ====================================================
##
## Model specification
## Family : normal
## Number of observations : 80
## Number of clusters (K) : 2
##
## Missing-data profile
## Listwise missing rows : 1 / 80 (1.2%)
## Sporadic missing rows : 0 / 80 (0.0%)
## Missing cells total : 4 / 320 (1.2%)
##
## Feature selection overview
## G features selected : 2 / 2 (100.0%)
## Z features selected : 4 / 4 (100.0%)
##
## Model fit statistics
## Log-likelihood : -399.22
## BIC : 960.58
## Number of parameters : 37
##
## Regularization
## Rho_G : 0.000
## Rho_Z_Mu : 0.000
## Rho_Z_Cov : 0.000
##
## Detailed parameter estimates
## (1) Y (continuous outcome): cluster 1 mean (intercept), mean differences for other clusters, and covariate effects
## Gamma
## (Intercept) -0.2702597
## cluster2 1.0434643
## cov_g1 0.4964385
## cov_g2 0.4379221
##
## (2) Z: mean of omics data for each latent cluster
## mu_cluster1 mu_cluster2
## Z3_f1 -0.42148130 0.5295890
## Z3_f2 0.25828699 0.3402714
## Z3_f3 0.26462045 -0.5481375
## Z3_f4 -0.07877362 -0.3163739
##
## (3) E: intercept and odds ratio of being assigned to each latent cluster for each cluster from previous serial stage
## beta OR
## (Intercept).cluster2 42.07607 1.876740e+18
## Stage1.Layer1.cluster2.cluster2 59.87371 1.006518e+26
## Stage1.Layer2.cluster2.cluster2 -42.99601 2.123585e-19
At Rho_* = 0 the selection overview above reports
everything selected (as noted in section 7); with a positive penalty,
this is the table that shows how much was screened out and at what
rate.
predict_lucid()predict_lucid() runs a fitted model’s E-step on new (or
the same) G/Z data to obtain posterior cluster
probabilities and, from those, a predicted outcome. It is what you use
to score subjects who were not part of the fitting sample, or to
double-check a model’s assignments on its own training data.
# Use lightweight no-covariate fits for robust prediction demo.
set.seed(205)
fit_early_pred <- estimate_lucid(
lucid_model = "early",
G = d$G,
Z = d$Z_early,
Y = d$Y_normal,
family = "normal",
K = 2,
max_itr = 6,
max_tot.itr = 20,
tol = 1e-2,
seed = 205
)## Fitting LUCID early model (K = 2)...
## Finished LUCID early model.
set.seed(206)
fit_parallel_pred <- estimate_lucid(
lucid_model = "parallel",
G = d$G,
Z = d$Z_parallel,
Y = d$Y_normal,
family = "normal",
K = c(2, 2),
max_itr = 6,
max_tot.itr = 20,
tol = 1e-2,
seed = 206
)## Fitting LUCID parallel model (2 layers)...
## Finished LUCID parallel model.
set.seed(207)
fit_serial_pred <- estimate_lucid(
lucid_model = "serial",
G = d$G,
Z = d$Z_serial_mixed,
Y = d$Y_normal,
family = "normal",
K = list(list(2, 2), 2),
max_itr = 6,
max_tot.itr = 24,
tol = 1e-2,
seed = 207
)## Fitting LUCID serial model (2 stages)...
## Stage 1/2 (parallel) finished: log-likelihood = -611.077.
## Stage 2/2 (early) finished: log-likelihood = -383.375.
## Finished LUCID serial model.
pred_early <- predict_lucid(
model = fit_early_pred,
G = d$G,
Z = d$Z_early,
Y = d$Y_normal
)
pred_parallel <- predict_lucid(
model = fit_parallel_pred,
G = d$G,
Z = d$Z_parallel,
Y = d$Y_normal
)
pred_serial <- predict_lucid(
model = fit_serial_pred,
G = d$G,
Z = d$Z_serial_mixed,
Y = d$Y_normal
)
# Cluster assignment. pred.x is a vector for early and a list -- by layer for
# parallel, by stage for serial -- so the blocks are summarised in one place.
# pred.x nests: a vector for early, a list by layer for parallel, and for
# serial a list by stage whose elements are themselves lists when that stage is
# a parallel submodel. Flatten to the leaves so each cluster variable is counted
# on its own -- pooling a parallel stage's layers would report twice as many
# assignments as there are subjects.
flatten_blocks <- function(x, path = "") {
if (!is.list(x)) return(stats::setNames(list(as.numeric(x)), path))
out <- list()
for (i in seq_along(x)) {
nm <- if (nzchar(path)) paste0(path, ".", i) else as.character(i)
out <- c(out, flatten_blocks(x[[i]], nm))
}
out
}
cluster_sizes <- function(pred_x, label) {
blocks <- flatten_blocks(pred_x)
nms <- names(blocks)
# index by position: the single block of an early model is named "", and
# blocks[[""]] does not select anything.
do.call(rbind, lapply(seq_along(blocks), function(i) {
tb <- table(factor(blocks[[i]]))
data.frame(model = label,
block = if (!nzchar(nms[i])) "-" else nms[i],
cluster = names(tb), n = as.integer(tb), row.names = NULL)
}))
}
rbind(
cluster_sizes(pred_early$pred.x, "early"),
cluster_sizes(pred_parallel$pred.x, "parallel"),
cluster_sizes(pred_serial$pred.x, "serial")
)## model block cluster n
## 1 early - 1 32
## 2 early - 2 48
## 3 parallel 1 1 44
## 4 parallel 1 2 36
## 5 parallel 2 1 43
## 6 parallel 2 2 37
## 7 serial 1.1 1 44
## 8 serial 1.1 2 36
## 9 serial 1.2 1 36
## 10 serial 1.2 2 44
## 11 serial 2 1 42
## 12 serial 2 2 38
The table above reports how many subjects fall in each cluster, per
model and (for parallel/serial) per layer/stage block. Roughly balanced
counts here are expected for this simulation’s roughly 50/50 latent
split; a wildly unbalanced split can be a sign that K is
larger than the data actually supports.
predict_lucid()’s pred.x above required
re-running the E-step on G/ Z. When the same
fitted-model hard assignment is wanted directly, without supplying data
again, get_cluster_assignment() reads it straight off the
fitted object’s own posterior (inclusion.p):
## [1] TRUE
The two agree here because pred_early was computed on
the same G/Z the model was fit on.
get_cluster_assignment() is the right tool whenever the
question is simply “what did this fitted model assign its subjects to,”
while predict_lucid() is for scoring genuinely new data or
getting a predicted outcome as well as an assignment.
Predicted outcomes, on the scale of the outcome that was modelled:
outcome_summary <- function(pred_y, label) {
v <- as.numeric(unlist(pred_y))
data.frame(
model = label,
n = length(v),
mean = round(mean(v), 3),
sd = round(stats::sd(v), 3),
min = round(min(v), 3),
median = round(stats::median(v), 3),
max = round(max(v), 3),
row.names = NULL
)
}
rbind(
outcome_summary(pred_early$pred.y, "early"),
outcome_summary(pred_parallel$pred.y, "parallel"),
outcome_summary(pred_serial$pred.y, "serial")
)## model n mean sd min median max
## 1 early 80 0.511 0.702 -0.356 1.114 1.122
## 2 parallel 80 0.506 0.691 -0.180 0.372 1.337
## 3 serial 80 0.512 0.673 -0.123 -0.090 1.252
The predicted outcome is a posterior-weighted average of the
cluster-specific outcome levels, so its spread is narrower than the
observed outcome’s: it carries no residual variation, only the
between-cluster differences. Comparing its range against
summary(d$Y_normal) shows how much of the outcome the
latent structure accounts for.
rbind(
outcome_summary(pred_early$pred.y, "predicted (early)"),
outcome_summary(d$Y_normal, "observed")
)## model n mean sd min median max
## 1 predicted (early) 80 0.511 0.702 -0.356 1.114 1.122
## 2 observed 80 0.504 1.186 -2.024 0.306 3.470
g_computation = TRUE switches to a different question:
instead of scoring observed (G, Z) pairs, it asks what the
outcome distribution would be for a given exposure profile,
marginalizing over the fitted cluster and omics model – a simple causal
“what if this subject’s exposure had been X” calculation built on top of
the fitted parameters. This is why it needs only G (no
Z, no Y) and why it alone returns
pred.z, the implied omics profile under that hypothetical
exposure.
pred_early_g <- predict_lucid(
model = fit_early_pred,
G = d$G,
Z = NULL,
Y = NULL,
g_computation = TRUE
)
pred_parallel_g <- try(
predict_lucid(
model = fit_parallel_pred,
G = d$G,
Z = NULL,
Y = NULL,
g_computation = TRUE
),
silent = TRUE
)
names(pred_early_g)## [1] "inclusion.p" "pred.x" "pred.z" "pred.y"
if (inherits(pred_parallel_g, "try-error")) {
"parallel g_computation returned a try-error on this demo object; code pattern is shown above."
} else {
names(pred_parallel_g)
}## [1] "inclusion.p" "pred.x" "pred.z" "pred.y"
pred_serial_g <- predict_lucid(
model = fit_serial_pred,
G = d$G,
Z = NULL,
Y = NULL,
g_computation = TRUE
)
names(pred_serial_g)## [1] "inclusion.p" "pred.x" "pred.z" "pred.y"
## [1] 2
Z is required for every model type. Only
g_computation = TRUE relaxes it. Y is always
optional; omitting it makes the prediction unsupervised.
Z |
Y |
g_computation |
Result |
|---|---|---|---|
| supplied | supplied | FALSE |
Supervised: outcome informs the posterior |
| supplied | omitted | FALSE |
Unsupervised: clusters from G and Z
only |
| omitted | either | FALSE |
Error naming Z and g_computation |
| omitted | omitted | TRUE |
Counterfactual prediction from G alone |
The posterior is formed from the exposure, omics and outcome
likelihood terms. Dropping Y removes one term and leaves a
well-defined posterior over the rest. Dropping Z removes
the term the clusters are defined by, leaving nothing to condition on.
g_computation = TRUE is a separate estimator that uses only
the exposure path, which is why it accepts Z = NULL and why
it alone returns pred.z; supplied Z and
Y are ignored in that mode.
Returned components, and their shape by model type:
| Component | Meaning | early | parallel | serial |
|---|---|---|---|---|
inclusion.p |
Posterior cluster probabilities | N x K matrix |
list by layer | list by stage |
pred.x |
Cluster labels, 1..K since 3.1.0 |
vector | list by layer | list by stage |
pred.y |
Predicted outcome | vector | vector | vector |
pred.z |
Implied omics profile, g_computation only |
matrix | list by layer | list by stage |
Reusing the fits from 10.1, with no refitting:
pred_unsup <- predict_lucid(
model = fit_early_pred,
G = d$G,
Z = d$Z_early
)
missing_z <- try(
predict_lucid(
model = fit_early_pred,
G = d$G,
Z = NULL,
Y = d$Y_normal
),
silent = TRUE
)
data.frame(
mode = c("Y omitted (unsupervised)", "Z omitted, no g-computation"),
result = c(
paste(names(pred_unsup), collapse = ", "),
if (inherits(missing_z, "try-error")) "error, as documented" else "unexpectedly succeeded"
)
)## mode result
## 1 Y omitted (unsupervised) inclusion.p, pred.x, pred.y
## 2 Z omitted, no g-computation error, as documented
## Input data 'Z' is required for prediction. Omit it only with g_computation = TRUE, which predicts from the exposures alone.
A serial model must have at least two stages to be predicted. A single-stage serial model is an equivalent early or parallel model and should be fitted as one; prediction declines it with a message to that effect.
boot_lucid()Point estimates from estimate_lucid() come with no
standard errors – boot_lucid() supplies them by
nonparametric bootstrap: it resamples subjects with replacement, refits
the model on each resample, and forms confidence intervals from the
resulting distribution of estimates. Because that
resampling-and-refitting only makes sense for stable, unpenalized
estimates, boot_lucid() is meant to be run on a
zero-penalty refit (the model whose coefficients are not shrunk by a
selection penalty), not on the penalized screening fit itself – the
3-model tutorials’ screen-then-refit workflow produces exactly that kind
of model. This guide bootstraps
fit_early/fit_parallel/fit_serial
directly since they were already fit at Rho_* = 0.
Runtime note: this section uses small R for tutorial
speed; a real analysis needs enough bootstrap replicates (typically
several hundred) for the resulting intervals to be stable.
set.seed(106)
boot_early <- boot_lucid(
G = d$G,
Z = Z_early_miss,
Y = d$Y_normal,
CoG = d$CoG,
CoY = d$CoY,
model = fit_early,
R = 2,
conf = 0.9
)
boot_parallel <- boot_lucid(
G = d$G,
Z = Z_parallel_miss,
Y = d$Y_normal,
CoG = d$CoG,
CoY = d$CoY,
model = fit_parallel,
R = 2,
conf = 0.9
)
boot_serial <- boot_lucid(
G = d$G,
Z = Z_serial_miss,
Y = d$Y_normal,
CoG = d$CoG,
CoY = d$CoY,
model = fit_serial,
R = 2,
conf = 0.9
)
summary(fit_early, boot.se = boot_early)##
## ====================================================
## LUCID Early Integration: Model Summary
## ====================================================
##
## Model specification
## Family : normal
## Number of observations : 80
## Number of clusters (K) : 2
##
## Missing-data profile
## Listwise missing rows : 1 / 80 (1.2%)
## Sporadic missing rows : 4 / 80 (5.0%)
## Missing cells total : 12 / 640 (1.9%)
##
## Feature selection overview
## G features selected : 6 / 6 (100.0%)
## Z features selected : 8 / 8 (100.0%)
##
## Model fit statistics
## Log-likelihood : -650.09
## BIC : 1751.53
## Number of parameters : 103
##
## Regularization
## Rho_G : 0.000
## Rho_Z_Mu : 0.000
## Rho_Z_Cov : 0.000
##
## Detailed parameter estimates
## (1) Y (continuous outcome): cluster 1 mean (intercept), mean differences for other clusters, and covariate effects
## Gamma norm_lower norm_upper sig
## (Intercept) 0.1765857 -0.1554741 0.3696930
## cluster2 1.0449481 0.3266079 2.2174532 *
## cov_g1 0.4929783 0.3155569 0.4159780 *
## cov_g2 0.6931914 0.5442827 0.9564068 *
##
## (2) Z: mean of omics data for each latent cluster
## estimate norm_lower norm_upper sig
## Z1_f1.cluster1 -0.0777814604 -1.27060679 0.78210567
## Z1_f2.cluster1 0.0840691217 -0.31275081 0.45248549
## Z1_f3.cluster1 0.0237814722 -0.66989312 0.67949470
## Z1_f4.cluster1 0.1617553558 -0.00526694 0.43843630
## Z2_f1.cluster1 -0.0021631579 -0.37326097 0.37273828
## Z2_f2.cluster1 0.0001776934 -0.48536139 0.66187172
## Z2_f3.cluster1 -0.1742793904 -0.21050324 -0.01656484 *
## Z2_f4.cluster1 -0.1083855071 -0.29955253 0.19077650
## Z1_f1.cluster2 1.3323118535 1.64221913 1.75838999 *
## Z1_f2.cluster2 1.1873926665 0.87516998 1.43197636 *
## Z1_f3.cluster2 0.9834227794 1.04521973 1.14914391 *
## Z1_f4.cluster2 0.5274072880 0.42942852 0.73374710 *
## Z2_f1.cluster2 -1.2547392707 -1.22486296 -0.99003002 *
## Z2_f2.cluster2 -1.1894463302 -1.54766882 -0.97333226 *
## Z2_f3.cluster2 -0.8918660581 -1.03894922 -0.25573482 *
## Z2_f4.cluster2 -0.5296838742 -0.71607080 -0.42147673 *
##
## (3) E: intercept and odds ratio of being assigned to each latent cluster for each exposure
## estimate norm_lower norm_upper sig
## G1.cluster2 1.49321813 -99.095033 43.7094324
## G2.cluster2 -1.12634744 -10.923986 26.6655522
## G3.cluster2 0.19971412 -14.716472 36.1019104
## G4.cluster2 -0.55799151 -8.092353 20.7389403
## G5.cluster2 -0.27541536 -2.929511 0.3435928
## G6.cluster2 0.09558619 -6.773340 2.4031418
##
## ====================================================
## LUCID Parallel: Model Summary
## ====================================================
##
## Model specification
## Family : gaussian
## Number of observations : 80
## Clusters per layer : 2, 2
##
## Missing-data profile by layer
## Layer 1 listwise rows : 1 / 80 (1.2%)
## Layer 1 sporadic rows : 0 / 80 (0.0%)
## Layer 1 missing cells : 4 / 320 (1.2%)
## Layer 2 listwise rows : 0 / 80 (0.0%)
## Layer 2 sporadic rows : 1 / 80 (1.2%)
## Layer 2 missing cells : 1 / 320 (0.3%)
##
## Feature selection overview
## G features selected : 6 / 6 (100.0%)
## G features by layer
## Layer 1 : 6 / 6 (100.0%)
## Layer 2 : 6 / 6 (100.0%)
## Z features
## Layer 1 selected : 4 / 4 (100.0%)
## Layer 1 multi-cluster: 4
## Layer 2 selected : 4 / 4 (100.0%)
## Layer 2 multi-cluster: 4
##
## Model fit statistics
## Log-likelihood : -703.20
## BIC : 1756.96
## Number of parameters : 80
##
## Regularization
## Rho_G : 0.000
## Rho_Z_Mu : 0.000
## Rho_Z_Cov : 0.000
##
## Detailed parameter estimates
## (1) Y (continuous outcome): intercept, effects of each non-reference latent cluster for each layer of Y (and effect of covariates if included)
## Gamma norm_lower norm_upper sig
## (Intercept) 0.04248756 -0.2335635 1.6167264
## Layer1_LC2 0.01765860 -1.3983514 -0.8411190 *
## Layer2_LC2 1.06757644 -1.1379911 3.3594512
## cov_g1 0.49345900 0.4024804 0.6025283 *
## cov_g2 0.65852863 0.4985023 0.9913395 *
##
## (2) Z: mean of omics data for each latent cluster of each layer
## Layer 1
##
## estimate norm_lower norm_upper sig
## Layer1.Z1_f1.cluster1 -0.349168270 -2.868118306 0.7943782
## Layer1.Z1_f2.cluster1 0.004260061 -1.689039338 1.0268665
## Layer1.Z1_f3.cluster1 -0.154181517 -1.995086642 0.7518654
## Layer1.Z1_f4.cluster1 0.116945108 -0.471836130 0.5499805
## Layer1.Z1_f1.cluster2 1.239192854 0.390903419 3.8073366 *
## Layer1.Z1_f2.cluster2 0.993490420 -0.180375357 3.0510505
## Layer1.Z1_f3.cluster2 0.923900842 0.158957738 2.8085184 *
## Layer1.Z1_f4.cluster2 0.485051731 -0.009303011 1.2034653
##
## Layer 2
##
## estimate norm_lower norm_upper sig
## Layer2.Z2_f1.cluster1 0.21574635 -0.8937422 2.6711224
## Layer2.Z2_f2.cluster1 0.08358451 -0.8599423 2.0976992
## Layer2.Z2_f3.cluster1 -0.12054320 -0.7353785 1.1385857
## Layer2.Z2_f4.cluster1 -0.08469553 -0.2994629 0.6436793
## Layer2.Z2_f1.cluster2 -1.25626235 -3.6510921 -0.5765041 *
## Layer2.Z2_f2.cluster2 -1.00787017 -2.7292775 -0.1937855 *
## Layer2.Z2_f3.cluster2 -0.77626259 -1.7728773 -0.5921201 *
## Layer2.Z2_f4.cluster2 -0.48459575 -1.0609297 -0.2359717 *
##
## (3) E: intercept and odds ratio of being assigned to each latent cluster for each exposure for each layer
## Layer 1
##
## estimate norm_lower norm_upper sig
## (Intercept).cluster2 -0.08685128 -0.9525508 0.1498558
## G1.cluster2 1.20608960 0.6348916 5.8628468 *
## G2.cluster2 -0.92554313 -9.4325454 2.8921008
## G3.cluster2 0.63658188 -0.4922965 2.9857411
## G4.cluster2 -0.73551173 -4.3690734 0.3514864
## G5.cluster2 -0.04677656 -0.6017770 0.9175276
## G6.cluster2 -0.09518064 -1.0195382 -0.1738533 *
##
## Layer 2
##
## estimate norm_lower norm_upper sig
## (Intercept).cluster2 -0.16662615 -0.20054088 0.1413125
## G1.cluster2 3.59813736 1.44079793 11.4055114 *
## G2.cluster2 -1.28668079 -6.40877990 3.0738119
## G3.cluster2 0.06073117 0.39768118 1.4758727 *
## G4.cluster2 -0.13557162 -0.32549805 -0.1897790 *
## G5.cluster2 -0.16105933 -1.03964599 -0.2352342 *
## G6.cluster2 0.05468855 0.05274401 0.5019673 *
##
## ====================================================
## LUCID Serial: Model Summary
## ====================================================
##
## Model specification
## Family : normal
## Number of observations : 80
## Number of stages : 2
## Stage 1 : parallel (K = 2,2)
## Stage 2 : early (K = 2)
##
## Missing-data profile by stage
## Stage 1
## Layer 1 listwise/sporadic rows : 1 / 0
## Layer 2 listwise/sporadic rows : 0 / 1
## Stage 2
## Listwise rows : 1
## Sporadic rows : 0
##
## Model fit statistics
## Log-likelihood : -1006.74
## BIC : 2499.89
##
## Regularization
## Rho_G : 0.000
## Rho_Z_Mu : 0.000
## Rho_Z_Cov : 0.000
##
## Stage-wise detailed parameter estimates
##
## --- Stage 1 (parallel) ---
##
## ====================================================
## LUCID Parallel: Model Summary
## ====================================================
##
## Model specification
## Family : gaussian
## Number of observations : 80
## Clusters per layer : 2, 2
##
## Missing-data profile by layer
## Layer 1 listwise rows : 1 / 80 (1.2%)
## Layer 1 sporadic rows : 0 / 80 (0.0%)
## Layer 1 missing cells : 4 / 320 (1.2%)
## Layer 2 listwise rows : 0 / 80 (0.0%)
## Layer 2 sporadic rows : 1 / 80 (1.2%)
## Layer 2 missing cells : 1 / 320 (0.3%)
##
## Feature selection overview
## G features selected : 6 / 6 (100.0%)
## G features by layer
## Layer 1 : 6 / 6 (100.0%)
## Layer 2 : 6 / 6 (100.0%)
## Z features
## Layer 1 selected : 4 / 4 (100.0%)
## Layer 1 multi-cluster: 4
## Layer 2 selected : 4 / 4 (100.0%)
## Layer 2 multi-cluster: 4
##
## Model fit statistics
## Log-likelihood : -607.52
## BIC : 1539.32
## Number of parameters : 74
##
## Regularization
## Rho_G : 0.000
## Rho_Z_Mu : 0.000
## Rho_Z_Cov : 0.000
##
## Detailed parameter estimates
## (1) Z: mean of omics data for each latent cluster of each layer
## Layer 1
##
## estimate norm_lower norm_upper sig
## Layer1.Z1_f1.cluster1 -0.352615720 -0.45377172 -0.34679383 *
## Layer1.Z1_f2.cluster1 0.002360677 -0.26013555 0.12172757
## Layer1.Z1_f3.cluster1 -0.156731519 -0.27504322 0.03886876
## Layer1.Z1_f4.cluster1 0.116972344 -0.07059789 0.19801677
## Layer1.Z1_f1.cluster2 1.229234465 1.00239553 1.24669512 *
## Layer1.Z1_f2.cluster2 0.986997627 0.83689718 0.96036872 *
## Layer1.Z1_f3.cluster2 0.917388278 0.70548033 1.00872966 *
## Layer1.Z1_f4.cluster2 0.481774129 0.19942295 0.44226875 *
##
## Layer 2
##
## estimate norm_lower norm_upper sig
## Layer2.Z2_f1.cluster1 -1.24890623 -1.73171537 -0.8888058 *
## Layer2.Z2_f2.cluster1 -1.00104991 -1.41631230 -0.7704177 *
## Layer2.Z2_f3.cluster1 -0.75293355 -1.32158035 -0.3408488 *
## Layer2.Z2_f4.cluster1 -0.49290664 -0.61839700 -0.4914322 *
## Layer2.Z2_f1.cluster2 0.22761663 0.13434208 0.4651878 *
## Layer2.Z2_f2.cluster2 0.09162188 -0.16300910 0.2254955
## Layer2.Z2_f3.cluster2 -0.13230863 -0.19697712 -0.1377000 *
## Layer2.Z2_f4.cluster2 -0.07270329 -0.04715225 0.1038765
##
## (2) E: intercept and odds ratio of being assigned to each latent cluster for each exposure for each layer
## Layer 1
##
## estimate norm_lower norm_upper sig
## (Intercept).cluster2 -0.05763017 -0.3825221 0.45491231
## G1.cluster2 1.21618905 -6.1926124 9.64249027
## G2.cluster2 -0.95080523 -2.7214376 0.37461190
## G3.cluster2 0.67635032 -0.1060819 1.16397874
## G4.cluster2 -0.74690763 -1.4055136 0.21946525
## G5.cluster2 -0.06497218 -1.7541373 0.48710583
## G6.cluster2 -0.11292848 -1.0948580 0.06842552
##
## Layer 2
##
## estimate norm_lower norm_upper sig
## (Intercept).cluster2 0.10180517 -0.5130093 0.77644869
## G1.cluster2 -3.76996919 -0.8508330 0.08091987
## G2.cluster2 1.30981565 0.9150835 1.38148959 *
## G3.cluster2 -0.20510303 -1.8846199 1.07459406
## G4.cluster2 0.05465422 -2.3962603 4.88049047
## G5.cluster2 0.16024275 -1.5595252 2.10773916
## G6.cluster2 -0.05478440 -0.7339486 1.80736755
##
##
## --- Stage 2 (early) ---
##
## ====================================================
## LUCID Early Integration: Model Summary
## ====================================================
##
## Model specification
## Family : normal
## Number of observations : 80
## Number of clusters (K) : 2
##
## Missing-data profile
## Listwise missing rows : 1 / 80 (1.2%)
## Sporadic missing rows : 0 / 80 (0.0%)
## Missing cells total : 4 / 320 (1.2%)
##
## Feature selection overview
## G features selected : 2 / 2 (100.0%)
## Z features selected : 4 / 4 (100.0%)
##
## Model fit statistics
## Log-likelihood : -399.22
## BIC : 960.58
## Number of parameters : 37
##
## Regularization
## Rho_G : 0.000
## Rho_Z_Mu : 0.000
## Rho_Z_Cov : 0.000
##
## Detailed parameter estimates
## (1) Y (continuous outcome): cluster 1 mean (intercept), mean differences for other clusters, and covariate effects
## Gamma norm_lower norm_upper sig
## Y.(Intercept) 0.02634618 0.04726623 0.2062116 *
## Y.LC2 1.06038828 0.52343619 1.0432476 *
## Y.cov_g1 0.44718681 0.25030684 0.7754435 *
## Y.cov_g2 0.65188473 0.79847213 0.9498793 *
##
## (2) Z: mean of omics data for each latent cluster
## estimate norm_lower norm_upper sig
## Z3_f1.cluster1 -0.20456753 -0.61785787 0.1340400
## Z3_f2.cluster1 0.04858672 -0.11717991 0.2980835
## Z3_f3.cluster1 0.12805861 -0.02130632 0.1571153
## Z3_f4.cluster1 -0.05519954 -0.31251881 0.1650792
## Z3_f1.cluster2 0.88264257 0.66248903 1.1549322 *
## Z3_f2.cluster2 0.61612295 0.27766870 0.7637610 *
## Z3_f3.cluster2 -0.90229053 -1.12607961 -0.8493843 *
## Z3_f4.cluster2 -0.48813409 -0.66526079 -0.2885527 *
##
## (3) E: intercept and odds ratio of being assigned to each latent cluster for each cluster from previous serial stage
## estimate norm_lower norm_upper sig
## (Intercept).cluster2 1.046768 1.666096 3.685129 *
## Stage1.Layer1.cluster2.cluster2 5.857494 4.095938 9.688545 *
## Stage1.Layer2.cluster2.cluster2 -6.549153 -13.457644 -6.318979 *
Each coefficient table above now shows a normal-theory confidence
interval alongside the point estimate, plus the sig column
described in section 9. With only R = 2 replicates here the
interval is not meaningful – this chunk exists to show the output
format, not to draw real conclusions; a real bootstrap needs
R in the hundreds.
plot()plot() draws a Sankey diagram of the fitted model’s
structure: flows from each exposure, through the latent cluster(s), to
the outcome, with flow width proportional to the estimated association
strength. It is a structural view – what connects to what, and how
strongly – complementary to the omics-profile view in the next section,
which shows what the clusters actually look like in the omics data.
plot_early <- plot(fit_early)
plot_parallel <- try(plot(fit_parallel), silent = TRUE)
plot_serial <- try(plot(fit_serial), silent = TRUE)
class(plot_early)## [1] "sankeyNetwork" "htmlwidget"
if (inherits(plot_parallel, "try-error")) {
"plot(fit_parallel) returned try-error in this build (parallel plot is under development)."
} else {
class(plot_parallel)
}## [1] "plot(fit_parallel) returned try-error in this build (parallel plot is under development)."
if (inherits(plot_serial, "try-error")) {
"plot(fit_serial) returned try-error in this build (serial plot is under development)."
} else {
class(plot_serial)
}## [1] "plot(fit_serial) returned try-error in this build (serial plot is under development)."
plot_cluster_omic_profile()plot() draws the path structure.
plot_cluster_omic_profile() draws what the clusters
actually are: which omics features separate them, and in which
direction. It returns a named list of ggplot objects, one
per omics layer, so a parallel or serial fit gives one figure per layer
rather than one crowded one.
Features are ranked by importance. The default,
"separation", is the spread of the cluster means divided by
the typical within-cluster spread; "range" and
"sd" use the means alone. Only the standardized measure can
distinguish a feature that separates the clusters from one that is
merely noisy.
| Argument | Effect |
|---|---|
type |
"heatmap" (default) or "bar" |
top_n |
Features per panel, default 10; a layer with fewer shows all |
importance |
"separation" (default), "range",
"sd" |
layer_names |
Subtitles; defaults to the names of the omics list |
layer_colors |
One hue per layer |
scale |
TRUE (default) fills with a per-feature z-score;
FALSE with the cluster mean |
## [1] "Omics"
Read the heatmap by row: a feature whose color alternates sharply
between clusters (e.g. dark for cluster 1, light for cluster 2) is one
that strongly distinguishes them; a feature with similar shading across
clusters is barely contributing to the separation despite appearing in
the top-n list.
The bar rendering shows the same features and ordering, with clusters as shades of the layer’s colour:
A parallel fit returns one plot per layer:
## [1] "layer1" "layer2"
A serial fit gives one plot per stage, and one per layer within a stage that is itself a parallel sub-model:
## [1] "Stage 1 - layer1" "Stage 1 - layer2" "Stage 2"
get_top_omics_features()The importance ranking behind every panel above is also available as
plain data, without generating a plot – useful for a report table, or
for feeding the top features into a downstream analysis.
get_top_omics_features() uses the exact same ranking
criterion as plot_cluster_omic_profile() (so its output for
a given top_n matches that plot’s panel ordering), and,
like the other extractors, auto-detects the model type and returns one
named numeric vector per layer/stage.
## $Omics
## Z2_f2 Z1_f1 Z1_f2 Z1_f3 Z2_f1
## 1.487498 1.459852 1.246496 1.164370 1.099256
The names are the features, in descending order of importance score;
the values are the score itself (by default, the between-cluster
separation described above). This is the same top-5 that would appear in
a plot_cluster_omic_profile(fit_early, top_n = 5) panel,
just as a plain vector rather than a figure.
Everything above used a continuous outcome
(family = "normal"). Switching to
family = "binary" changes only the outcome model: the
cluster effects on Y become log-odds (with odds ratios
reported alongside), there is no residual variance to estimate, and
predict_lucid()’s response argument lets you
choose class-label or probability output. This short chunk shows that
fit and a probability-scale prediction.
set.seed(107)
fit_early_binary <- estimate_lucid(
lucid_model = "early",
G = d$G,
Z = d$Z_early,
Y = d$Y_binary,
CoG = d$CoG,
CoY = d$CoY,
family = "binary",
K = 2,
max_itr = 8,
max_tot.itr = 30,
tol = 1e-2,
seed = 107
)## Fitting LUCID early model (K = 2)...
## Finished LUCID early model.
pred_binary_prob <- predict_lucid(
model = fit_early_binary,
G = d$G,
Z = d$Z_early,
Y = d$Y_binary,
CoG = d$CoG,
CoY = d$CoY,
response = FALSE
)
range(pred_binary_prob$pred.y)## [1] 0.1039360 0.9491689
pred.y here is a probability in [0, 1]
because response = FALSE; setting
response = TRUE instead would return hard
0/1 class labels.
max_itr, max_tot.itrRtune_lucid()## R version 4.4.0 (2024-04-24)
## Platform: aarch64-apple-darwin20
## Running under: macOS 26.6.2
##
## Matrix products: default
## BLAS: /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRblas.0.dylib
## LAPACK: /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRlapack.dylib; LAPACK version 3.12.0
##
## locale:
## [1] C/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
##
## time zone: America/Los_Angeles
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] plotly_4.11.0 ggplot2_4.0.2 LUCIDus_3.2.0
##
## loaded via a namespace (and not attached):
## [1] tidyr_1.3.1 sass_0.4.10 generics_0.1.4 shape_1.4.6.1
## [5] stringi_1.8.7 lattice_0.22-7 hms_1.1.4 digest_0.6.39
## [9] magrittr_2.0.4 evaluate_1.0.5 grid_4.4.0 RColorBrewer_1.1-3
## [13] iterators_1.0.14 fastmap_1.2.0 foreach_1.5.2 jsonlite_2.0.0
## [17] glmnet_4.1-10 Matrix_1.7-4 progress_1.2.3 nnet_7.3-20
## [21] survival_3.8-3 mclust_6.1.2 httr_1.4.7 purrr_1.2.0
## [25] crosstalk_1.2.2 viridisLite_0.4.2 scales_1.4.0 lazyeval_0.2.2
## [29] codetools_0.2-20 networkD3_0.4.1 jquerylib_0.1.4 cli_3.6.5
## [33] rlang_1.1.6 crayon_1.5.3 splines_4.4.0 withr_3.0.2
## [37] cachem_1.1.0 yaml_2.3.11 tools_4.4.0 dplyr_1.1.4
## [41] boot_1.3-32 vctrs_0.6.5 R6_2.6.1 lifecycle_1.0.4
## [45] htmlwidgets_1.6.4 pkgconfig_2.0.3 glasso_1.11 bslib_0.9.0
## [49] pillar_1.11.1 gtable_0.3.6 data.table_1.17.8 glue_1.8.0
## [53] Rcpp_1.1.0 tidyselect_1.2.1 xfun_0.54 tibble_3.3.0
## [57] data.tree_1.2.0 knitr_1.50 dichromat_2.0-0.1 farver_2.1.2
## [61] htmltools_0.5.9 igraph_2.2.1 labeling_0.4.3 rmarkdown_2.30
## [65] compiler_4.4.0 prettyunits_1.2.0 S7_0.2.1