# LUCID’s Three Model Architectures: Early, Parallel, and Serial – Continuous Outcome (HELIX Example)

## 1) Tutorial Goal and Scope

This tutorial is designed as a hands-on, end-to-end guide for fitting LUCID models on HELIX-style multi-omics data.

What you will learn:

- How to prepare common inputs (`G`, `Z`, `Y`, `CoG`, `CoY`) from one dataset.
- How to fit and interpret:
  - LUCID early integration
  - LUCID in parallel
  - LUCID in serial (all-early stages and mixed parallel+early stages)
- How to run the practical two-step strategy:
  1. Penalized screening fit (`Rho_* > 0`) for selection.
  2. Zero-penalty refit (`Rho_* = 0`) for stable inference + bootstrap CI.

Important runtime note:

- Sections 7-10 fix `K` and do the screen-then-refit by hand, so the steps are visible. Section 13 then shows `lucid()` doing the same thing natively, with tuning over a small `K` and penalty grid.
- We keep `K` fixed and use small bootstrap `R` so the vignette remains reproducible and fast.

## 2) Data Objects and Statistical Roles

The HELIX example data (`simulated_HELIX_data.rda`) provides:

- `phenotype`: exposure/covariate/outcome-style variables.
- three omics layers: `methylome`, `transcriptome`, `miRNA`.

Model inputs used throughout:

- `G`: exposure matrix (main predictors of latent cluster assignment). Built with a known signal structure – three exposures correlated with the outcome and six pure-noise exposures – so that feature selection can be checked against the truth rather than taken on trust. The outcome itself is deliberately kept out of `G`.
- `Z`: omics input (matrix for early, list for parallel, nested list for serial mixed).
- `Y`: continuous outcome vector; `Y_binary` is its median split, used to demonstrate the binary outcome model on the same subjects.
- `CoG`: covariates for `G -> X` model.
- `CoY`: covariates for `X -> Y` model.

## 3) Hyperparameter Guide (Practical)

Hyperparameter | Meaning | Tutorial choice and rationale
--- | --- | ---
`K` | number of latent clusters | fixed to small values for speed and interpretability
`Rho_G` | penalty on `G -> X` coefficients | positive in screening fit, zero in inference refit
`Rho_Z_Mu` | penalty on cluster-specific omics means | positive in screening fit, zero in inference refit
`Rho_Z_Cov` | penalty on omics covariance matrices | positive in screening fit, zero in inference refit
`max_itr`, `max_tot.itr`, `tol` | EM controls | modest values to balance speed/stability
`family` | outcome model family | `normal` for this tutorial
`R` (in `boot_lucid()`) | number of bootstrap resamples | kept tiny (2-5) here for speed; use hundreds for a real analysis
`seed` | reproducibility | fixed before each fit/bootstrap

## 4) Setup and Source Package Code

    # Keep knitting on error, so that one failing step reports itself and the rest of
    # the tutorial still runs. The status table in section 17 records what happened.
    knitr::opts_chunk$set(error = TRUE)

    # Lightweight registry so the document can verify itself rather than relying on
    # the reader to notice a missing output.
    .reg <- new.env(parent = emptyenv()); .reg$rows <- list()
    check_obj <- function(name, expected_class = NULL, section = "") {
      ok <- exists(name, envir = globalenv())
      cls <- if (ok) class(get(name, envir = globalenv()))[1] else NA_character_
      status <- if (!ok) "MISSING"
                else if (!is.null(expected_class) && !identical(cls, expected_class)) "unexpected class"
                else "ok"
      .reg$rows[[length(.reg$rows) + 1L]] <-
        data.frame(section = section, object = name, class = cls,
                   status = status, stringsAsFactors = FALSE)
      invisible(NULL)
    }

    library(LUCIDus)

    # The HELIX simulation object bundled with the package.
    data(simulated_HELIX_data)

## 5) Build Modeling Inputs (With Missingness Injection)

This chunk creates a compact tutorial dataset and deliberately injects both:

- listwise missingness (whole row missing for a layer), and
- sporadic missingness (some cells missing),

so we can observe missing-data handling in summaries.

    # Use a smaller subset for vignette speed while preserving model behavior.
    idx <- 1:90
    ph <- simulated_HELIX_data$phenotype[idx, ]
    n <- nrow(ph)

    set.seed(2026)

    # ---------------------------------------------------------------------------
    # A tutorial dataset with a KNOWN answer.
    #
    # The HELIX omics matrices are real simulated data with their own structure, and
    # the exposures shipped with them have no relationship to it. That is fine for
    # demonstrating that code runs, but it makes feature selection impossible to
    # judge: there is no right answer to compare against. So we plant one.
    #
    # The generating story, which is the DAG LUCID assumes:
    #
    #     causal exposures  ->  latent subgroup  ->  omics profile
    #                                            ->  outcome
    #
    # Three exposures carry the subgroup signal with graded strength; six are pure
    # noise. Half the features of each omics layer are shifted by subgroup
    # membership; the rest are left as they came. Selection therefore has an
    # unambiguous target, and the tutorial can check its answer instead of asserting
    # it.
    # ---------------------------------------------------------------------------

    # The true latent subgroup. Retained so every selection claim below can be
    # checked against it.
    x_true <- rbinom(n, 1, 0.5)

    # Exposures. g_causal_* predict subgroup membership; g_noise_* do not.
    # Effect sizes are deliberately moderate. Stronger exposures make selection
    # look better but drive the G -> X model to saturation, where every subject sits
    # at posterior probability 1 and no counterfactual shift can move anything --
    # which would make the g-computation demonstration in section 12.3 vacuous.
    G <- cbind(
      g_causal_1 =  1.0 * (x_true - 0.5) + rnorm(n, sd = 0.8),   # strongest
      g_causal_2 = -0.8 * (x_true - 0.5) + rnorm(n, sd = 0.8),   # moderate, negative
      g_causal_3 =  0.6 * (x_true - 0.5) + rnorm(n, sd = 0.8),   # weakest
      g_noise_1 = rnorm(n), g_noise_2 = rnorm(n), g_noise_3 = rnorm(n),
      g_noise_4 = rnorm(n), g_noise_5 = rnorm(n), g_noise_6 = rnorm(n)
    )
    G <- as.matrix(scale(G))

    causal_exposures <- c("g_causal_1", "g_causal_2", "g_causal_3")

    # Exposure penalty used throughout. Section 7.2 shows what this value recovers;
    # section 11 sweeps it, and sweeps the omics penalty separately.
    RHO_G <- 0.05

    # Covariates for G->X (CoG) and X->Y (CoY).
    # Here we use age-related and sex covariates from phenotype.
    CoG <- cbind(
      hs_child_age_yrs_None = as.numeric(ph$hs_child_age_yrs_None),
      sex_male = as.numeric(ph$e3_sex_None == "male")
    )
    CoY <- CoG

    # Two outcomes on the SAME subjects, so the normal and binary results below are
    # directly comparable: the only thing that changes between them is the outcome
    # model, not the sample, the omics, or the injected missingness.
    #
    # Continuous outcome: the real CK-18 measurement, plus a subgroup effect so the
    # cluster -> outcome arm of the model has something to estimate.
    Y <- as.numeric(ph$ck18_scaled) + 1.2 * x_true

    # Binary outcome: median split. The median is used rather than a higher
    # threshold because it splits these 90 subjects 45/45, and a balanced outcome
    # gives the K = 2 outcome model the most to work with at this sample size.
    Y_binary <- as.integer(Y > median(Y))
    cat("binary outcome balance:\n"); print(table(Y_binary))

    ## binary outcome balance:

    ## Y_binary
    ##  0  1
    ## 45 45

    # Construct three omics layers and standardize each, then plant the subgroup
    # signal in the first three features of every layer. The remaining seven per
    # layer are left as they came and act as omics noise.
    meth <- scale(simulated_HELIX_data$methylome[idx, 1:10, drop = FALSE])
    tran <- scale(simulated_HELIX_data$transcriptome[idx, 1:10, drop = FALSE])
    mir  <- scale(simulated_HELIX_data$miRNA[idx, 1:10, drop = FALSE])

    signal_features <- 1:3
    omics_shift <- 3.0
    meth[, signal_features] <- meth[, signal_features] + omics_shift * x_true
    tran[, signal_features] <- tran[, signal_features] - omics_shift * x_true
    mir[,  signal_features] <- mir[,  signal_features] + omics_shift * x_true

    # Column positions of the signal features once the layers are stacked for the
    # early model, so selection can be scored against them later.
    signal_cols_early <- c(signal_features,
                           ncol(meth) + signal_features,
                           ncol(meth) + ncol(tran) + signal_features)

    # Early model uses one combined Z matrix.
    Z_early <- cbind(meth, tran, mir)

    # Parallel model uses list-of-layers.
    Z_parallel <- list(methylome = meth, transcriptome = tran, miRNA = mir)

    # Inject listwise + sporadic missingness for demonstration.
    Z_early_miss <- Z_early
    Z_early_miss[1, ] <- NA      # listwise row
    Z_early_miss[2:4, 1] <- NA   # sporadic block
    Z_early_miss[5, 3] <- NA     # sporadic cell

    Z_parallel_miss <- Z_parallel
    Z_parallel_miss[[1]][1, ] <- NA  # listwise in layer 1
    Z_parallel_miss[[2]][2, 2] <- NA # sporadic in layer 2
    Z_parallel_miss[[3]][3, 1] <- NA # sporadic in layer 3

    # Quick structural sanity check.
    str(list(
      G = G,
      CoG = CoG,
      CoY = CoY,
      Y = Y,
      Z_early = Z_early_miss,
      Z_parallel = Z_parallel_miss
    ), max.level = 1)

    ## List of 6
    ##  $ G         : num [1:90, 1:9] 1.587 1.051 -0.744 0.168 0.834 ...
    ##   ..- attr(*, "dimnames")=List of 2
    ##   ..- attr(*, "scaled:center")= Named num [1:9] -0.0377 0.0866 0.0539 -0.0695 0.1933 ...
    ##   .. ..- attr(*, "names")= chr [1:9] "g_causal_1" "g_causal_2" "g_causal_3" "g_noise_1" ...
    ##   ..- attr(*, "scaled:scale")= Named num [1:9] 1.053 0.864 0.732 1.038 1.211 ...
    ##   .. ..- attr(*, "names")= chr [1:9] "g_causal_1" "g_causal_2" "g_causal_3" "g_noise_1" ...
    ##  $ CoG       : num [1:90, 1:2] 7.48 7.21 8.59 8.48 6.06 ...
    ##   ..- attr(*, "dimnames")=List of 2
    ##  $ CoY       : num [1:90, 1:2] 7.48 7.21 8.59 8.48 6.06 ...
    ##   ..- attr(*, "dimnames")=List of 2
    ##  $ Y         : num [1:90] 1.817 1.67 0.479 -0.281 0.394 ...
    ##  $ Z_early   : num [1:90, 1:30] NA NA NA NA 3.17 ...
    ##   ..- attr(*, "dimnames")=List of 2
    ##  $ Z_parallel:List of 3

## 6) Helper Functions for Selected-Feature Refit

These helpers implement a robust refit pipeline:

1. Read feature-selection indicators from penalized fit.
2. Build selected-only `G`/`Z` inputs.
3. Refit with all penalties set to zero for bootstrap inference.

    # get_selected_G()/get_selected_Z() (from the package itself) already return a
    # well-shaped, aligned logical mask straight from the fitted object -- no
    # length mismatch is possible, since they derive it from the model's own
    # recorded fields. The one thing left for a tutorial to decide is what to do
    # if a penalty happened to deselect EVERY feature: refitting on zero columns
    # would fail, so this keeps everything instead in that one edge case.
    keep_or_all <- function(mask) if (any(mask, na.rm = TRUE)) mask else rep(TRUE, length(mask))

    # Build selected-only inputs for early model.
    prepare_early_selected_inputs <- function(fit_pen, G, Z) {
      list(
        G = as.matrix(G[, keep_or_all(get_selected_G(fit_pen)), drop = FALSE]),
        Z = as.matrix(Z[, keep_or_all(get_selected_Z(fit_pen)), drop = FALSE])
      )
    }

    # Build selected-only inputs for parallel model.
    prepare_parallel_selected_inputs <- function(fit_pen, G, Z) {
      keep_g <- keep_or_all(get_selected_G(fit_pen))
      Z_sel <- lapply(seq_along(Z), function(i) {
        zi <- as.matrix(Z[[i]])
        zi[, keep_or_all(get_selected_Z(fit_pen, layer = i)), drop = FALSE]
      })
      names(Z_sel) <- names(Z)
      list(
        G = as.matrix(G[, keep_g, drop = FALSE]),
        Z = Z_sel
      )
    }

    # Serial stage>1 uses latent-cluster-derived "G" internally.
    # We therefore subset stage-1 original G and each stage's Z where applicable.
    prepare_serial_selected_inputs <- function(fit_pen, G, Z) {
      G_refit <- as.matrix(G)
      keep_g1 <- get_selected_G(fit_pen)
      if (length(keep_g1) == ncol(G_refit)) {
        G_refit <- G_refit[, keep_or_all(keep_g1), drop = FALSE]
      }

      selected_z <- get_selected_Z(fit_pen)
      Z_refit <- Z
      for (i in seq_along(fit_pen$submodel)) {
        sm <- fit_pen$submodel[[i]]
        if (inherits(sm, "early_lucid")) {
          zi <- as.matrix(Z_refit[[i]])
          Z_refit[[i]] <- zi[, keep_or_all(selected_z[[i]]), drop = FALSE]
        } else if (inherits(sm, "lucid_parallel")) {
          zi_list <- Z_refit[[i]]
          for (j in seq_along(zi_list)) {
            zij <- as.matrix(zi_list[[j]])
            zi_list[[j]] <- zij[, keep_or_all(selected_z[[i]][[j]]), drop = FALSE]
          }
          Z_refit[[i]] <- zi_list
        }
      }

      list(G = G_refit, Z = Z_refit)
    }

    # Zero-penalty refit, for any model type.
    #
    # The three model types previously had three byte-identical wrappers differing
    # only in `lucid_model` and whether `useY` was forwarded; they are one function
    # here. Everything about the model -- family, K, initialization, EM controls --
    # is carried over from the screening fit, so the ONLY difference between the
    # screening fit and this one is that the penalties are zero. That is what makes
    # the refit estimates unshrunk and therefore suitable for bootstrap inference.
    refit_selected <- function(model_type, fit_pen, inputs, Y,
                               CoG = NULL, CoY = NULL, seed = 1, verbose = FALSE) {
      args <- list(
        lucid_model = model_type,
        G = inputs$G,
        Z = inputs$Z,
        Y = Y,
        CoG = CoG,
        CoY = CoY,
        family = fit_pen$family,
        K = fit_pen$K,
        init_omic.data.model = fit_pen$init_omic.data.model,
        init_impute = fit_pen$init_impute,
        init_par = fit_pen$init_par,
        Rho_G = 0,
        Rho_Z_Mu = 0,
        Rho_Z_Cov = 0,
        max_itr = fit_pen$em_control$max_itr,
        max_tot.itr = fit_pen$em_control$max_tot.itr,
        tol = fit_pen$em_control$tol,
        seed = seed,
        verbose = verbose
      )
      # Every fitted class records useY, so it is carried over for all three model
      # types. The original three wrappers omitted it on the early path, which meant
      # an unsupervised screening fit would have been silently refitted supervised.
      args$useY <- fit_pen$useY
      do.call(estimate_lucid, args)
    }

    # Dispatcher for the three input-preparation helpers above.
    prepare_selected_inputs <- function(model_type, fit_pen, G, Z) {
      switch(model_type,
        early    = prepare_early_selected_inputs(fit_pen, G, Z),
        parallel = prepare_parallel_selected_inputs(fit_pen, G, Z),
        serial   = prepare_serial_selected_inputs(fit_pen, G, Z),
        stop("unknown model_type: ", model_type)
      )
    }

    # Compact stage-wise feature-selection report for serial fits, built entirely
    # from get_selected_G()/get_selected_Z() -- no per-stage dispatch of its own.
    serial_selection_report <- function(fit_serial_pen) {
      selected_z <- get_selected_Z(fit_serial_pen)
      out <- vector("list", length(fit_serial_pen$submodel))
      for (i in seq_along(fit_serial_pen$submodel)) {
        sm <- fit_serial_pen$submodel[[i]]
        if (inherits(sm, "early_lucid")) {
          out[[i]] <- list(
            stage = i,
            model = "early",
            selected_G = if (i == 1) sum(get_selected_G(fit_serial_pen)) else NA,
            total_G = if (i == 1) length(get_selected_G(fit_serial_pen)) else NA,
            selected_Z = sum(selected_z[[i]]),
            total_Z = length(selected_z[[i]])
          )
        } else {
          out[[i]] <- list(
            stage = i,
            model = "parallel",
            selected_G = if (i == 1) sum(get_selected_G(fit_serial_pen)) else NA,
            total_G = if (i == 1) length(get_selected_G(fit_serial_pen)) else NA,
            selected_Z_by_layer = sapply(selected_z[[i]], sum),
            total_Z_by_layer = sapply(selected_z[[i]], length)
          )
        }
      }
      out
    }

## 7) Early Model Tutorial

### 7.1 Penalized screening fit (`Rho_* > 0`)

Interpretation target:

- Which exposures/omics features survive shrinkage?
- Is missing-data handling reported as expected?

    set.seed(1101)

    # Screening model: penalties help identify a parsimonious subset.
    early_fit_pen <- estimate_lucid(
      lucid_model = "early",
      G = G,
      Z = Z_early_miss,
      Y = Y,
      CoG = CoG,
      CoY = CoY,
      family = "normal",
      K = 2,
      Rho_G = RHO_G,
      Rho_Z_Mu = 0,
      Rho_Z_Cov = 0,
      max_itr = 15,
      max_tot.itr = 40,
      tol = 1e-2,
      seed = 1101,
      verbose = FALSE
    )

    ## Fitting LUCID early model (K = 2)...
    ## Finished LUCID early model. Selected G: 4/9; Selected Z: 30/30.

    # Summary shows model fit, missing profile, selection, and parameter tables.
    summary(early_fit_pen)

    ##
    ## ====================================================
    ## LUCID Early Integration: Model Summary
    ## ====================================================
    ##
    ## Model specification
    ##   Family                 : normal
    ##   Number of observations : 90
    ##   Number of clusters (K) : 2
    ##
    ## Missing-data profile
    ##   Listwise missing rows  : 1 / 90 (1.1%)
    ##   Sporadic missing rows  : 4 / 90 (4.4%)
    ##   Missing cells total    : 34 / 2700 (1.3%)
    ##
    ## Feature selection overview
    ##   G features selected    : 4 / 9 (44.4%)
    ##   Z features selected    : 30 / 30 (100.0%)
    ##
    ## Model fit statistics
    ##   Log-likelihood         : -3017.71
    ##   BIC                    : 10548.73
    ##   Number of parameters   : 1003
    ##
    ## Regularization
    ##   Rho_G                  : 0.050
    ##   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.02464967
    ## cluster2              1.49980545
    ## hs_child_age_yrs_None 0.02165410
    ## sex_male              0.07030121
    ##
    ## (2) Z: mean of omics data for each latent cluster
    ##                    mu_cluster1  mu_cluster2
    ## cg_GRHL3          1.1543296157  2.058870236
    ## cg_BTF3L4         1.1744273766  2.012566626
    ## cg_AL358472.7     1.0236239022  2.454782946
    ## cg_HDGF           0.0163608615 -0.032519086
    ## cg_TDRD5          0.0431530037 -0.148481256
    ## cg_CSRNP3        -0.0261459444  0.088311279
    ## cg_HSPD1          0.1803236589 -0.578765527
    ## cg_EPM2AIP1      -0.0042038336  0.011434519
    ## cg_AC025171.1    -0.0267854692  0.017327370
    ## cg_VTRNA1_3       0.0739126958 -0.269673022
    ## tc_TC01006069_nc -0.9713931499 -2.644045031
    ## tc_SLC9A4        -1.1102799358 -2.211705681
    ## tc_RAB6C_AS1     -1.1111594356 -2.254127299
    ## tc_LOC100129029   0.0619205718 -0.129531552
    ## tc_BRE            0.0219307188 -0.058832943
    ## tc_TC03001220_nc -0.0284856626 -0.006481685
    ## tc_TC04002114_nc  0.0883068847 -0.236455005
    ## tc_TC04002369_nc  0.0106849041 -0.023325912
    ## tc_BEND4         -0.0786234411  0.257013206
    ## tc_SLC9A3         0.1545597759 -0.400505271
    ## miR.101.3p        1.0047746226  2.525228725
    ## miR.125a.5p       1.0704611990  2.362679176
    ## miR.125b.1.3p     1.0373718165  2.451506323
    ## miR.127.3p       -0.1390736642  0.456784794
    ## miR.140.5p       -0.0009644617  0.005667020
    ## miR.142.3p       -0.0937787966  0.259762116
    ## miR.144.5p       -0.1228981798  0.348240526
    ## miR.19a.3p       -0.0871818782  0.253085903
    ## miR.19b.3p       -0.0554155122  0.163435553
    ## miR.21.5p        -0.0352845287  0.121911272
    ##
    ## (3) E: intercept and odds ratio of being assigned to each latent cluster for each exposure
    ##                                       beta         OR
    ## (Intercept).cluster2            3.21838105 24.9876337
    ## g_causal_1.cluster2             0.20289409  1.2249427
    ## g_causal_2.cluster2            -0.50164801  0.6055319
    ## g_causal_3.cluster2             0.09877953  1.1038229
    ## g_noise_5.cluster2             -0.24152982  0.7854254
    ## hs_child_age_yrs_None.cluster2 -0.64123438  0.5266419
    ## sex_male.cluster2               0.15550225  1.1682446

### 7.1b Object structure: what’s in the fitted object

`summary()` above is a printed report; `early_fit_pen` itself is a plain R list, and every field on it is documented on `?estimate_lucid`’s `@return`. This is what an early-integration fit actually holds:

    names(early_fit_pen)

    ##  [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"

The fields fall into two groups:

- Common to every model type (early, parallel, serial all have these, though the shape differs): `res_Beta`/`res_Mu`/`res_Sigma`/`res_Gamma` (the G->X, X->Z, X->Y estimates), `likelihood`, `select` (feature-selection indicators), `inclusion.p` (posterior cluster membership), `K`, `var.names`, `family`, `useY`, `Z`, `Rho` (the penalties applied), `missing_summary`, `em_control` (convergence diagnostics), `init_impute`, `init_par`.
- Early-specific: none of the remaining fields are early-only – `N` (the sample size) is present for parallel/serial but not early (use `nrow(fit$inclusion.p)` instead); `z`, `res_Delta`, and `submodel` belong to parallel and serial only, shown in sections 8 and 9.

    cat("log-likelihood:", early_fit_pen$likelihood, "\n")

    ## log-likelihood: -3017.709

    cat("exposures selected:", sum(get_selected_G(early_fit_pen)),
        "of", length(get_selected_G(early_fit_pen)), "\n")

    ## exposures selected: 4 of 9

    str(early_fit_pen$em_control, max.level = 1)

    ## List of 9
    ##  $ tol         : num 0.01
    ##  $ max_itr     : num 15
    ##  $ max_tot.itr : num 40
    ##  $ converged   : logi TRUE
    ##  $ n_iter      : num 12
    ##  $ n_restart   : int 0
    ##  $ loglik_trace: num [1:12] -3150 -3122 -3089 -3077 -3068 ...
    ##  $ n_starts    : int 1
    ##  $ n_starts_ok : int 1

### 7.2 Inspect selected features

    early_selected_G <- names(which(get_selected_G(early_fit_pen)))

    cat("exposures retained:", paste(early_selected_G, collapse = ", "), "\n\n")

    ## exposures retained: g_causal_1, g_causal_2, g_causal_3, g_noise_5

    # Score the selection against the planted truth from section 5: the three
    # g_causal_* exposures drive the latent subgroup, the six g_noise_* do not.
    data.frame(
      exposure = colnames(G),
      truth    = ifelse(colnames(G) %in% causal_exposures, "causal", "noise"),
      selected = colnames(G) %in% early_selected_G
    )

    ##     exposure  truth selected
    ## 1 g_causal_1 causal     TRUE
    ## 2 g_causal_2 causal     TRUE
    ## 3 g_causal_3 causal     TRUE
    ## 4  g_noise_1  noise    FALSE
    ## 5  g_noise_2  noise    FALSE
    ## 6  g_noise_3  noise    FALSE
    ## 7  g_noise_4  noise    FALSE
    ## 8  g_noise_5  noise     TRUE
    ## 9  g_noise_6  noise    FALSE

    cat("\ncausal exposures kept :", sum(causal_exposures %in% early_selected_G), "of 3\n")

    ##
    ## causal exposures kept : 3 of 3

    cat("noise  exposures kept :",
        sum(!(early_selected_G %in% causal_exposures)), "of 6\n")

    ## noise  exposures kept : 1 of 6

    # The omics side is unpenalized in this screening fit, so every feature is
    # retained. Section 11.3 penalizes the omics separately and shows why.
    cat("omics features retained:", sum(get_selected_Z(early_fit_pen)),
        "of", length(get_selected_Z(early_fit_pen)), "(Rho_Z_Mu = 0 here)\n")

    ## omics features retained: 30 of 30 (Rho_Z_Mu = 0 here)

### 7.3 Zero-penalty selected-only refit

Why this step:

- We keep the selected feature space,
- then estimate final coefficients without shrinkage bias,
- and use this model as the bootstrap target.

    set.seed(1102)

    # Build selected-only inputs.
    early_inputs_refit <- prepare_early_selected_inputs(early_fit_pen, G, Z_early_miss)

    # Refit with all penalties set to zero.
    early_fit_refit <- refit_selected(
      "early",
      fit_pen = early_fit_pen,
      inputs = early_inputs_refit,
      Y = Y,
      CoG = CoG,
      CoY = CoY,
      seed = 1102,
      verbose = TRUE
    )

    ## Intializing imputation of missing values in 'Z' via LOD / sqrt(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 = -3325.675
    ## iteration 2 : E-step finished.
    ## iteration 2: log-likelihood = -3316.160
    ## iteration 3 : E-step finished.
    ## iteration 3: log-likelihood = -3314.785
    ## iteration 4 : E-step finished.
    ## iteration 4: log-likelihood = -3314.499
    ## iteration 5 : E-step finished.
    ## iteration 5: log-likelihood = -3314.437
    ## iteration 6 : E-step finished.
    ## iteration 6: log-likelihood = -3314.423
    ## iteration 7 : E-step finished.
    ## iteration 7: log-likelihood = -3314.420
    ## Success: LUCID early model converged!
    ##
    ## Finished LUCID early model: log-likelihood = -3314.420.

    summary(early_fit_refit)

    ##
    ## ====================================================
    ## LUCID Early Integration: Model Summary
    ## ====================================================
    ##
    ## Model specification
    ##   Family                 : normal
    ##   Number of observations : 90
    ##   Number of clusters (K) : 2
    ##
    ## Missing-data profile
    ##   Listwise missing rows  : 1 / 90 (1.1%)
    ##   Sporadic missing rows  : 4 / 90 (4.4%)
    ##   Missing cells total    : 34 / 2700 (1.3%)
    ##
    ## Feature selection overview
    ##   G features selected    : 4 / 4 (100.0%)
    ##   Z features selected    : 30 / 30 (100.0%)
    ##
    ## Model fit statistics
    ##   Log-likelihood         : -3314.42
    ##   BIC                    : 11142.15
    ##   Number of parameters   : 1003
    ##
    ## 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)            1.7218477
    ## cluster2               0.0000000
    ## hs_child_age_yrs_None -0.1654292
    ## sex_male               0.1034161
    ##
    ## (2) Z: mean of omics data for each latent cluster
    ##                    mu_cluster1   mu_cluster2
    ## cg_GRHL3          1.3734684264  1.3734684264
    ## cg_BTF3L4         1.3816913508  1.3816913508
    ## cg_AL358472.7     1.3987916378  1.3987916378
    ## cg_HDGF           0.0042733091  0.0042733091
    ## cg_TDRD5         -0.0042363512 -0.0042363512
    ## cg_CSRNP3         0.0021582533  0.0021582533
    ## cg_HSPD1         -0.0073919764 -0.0073919764
    ## cg_EPM2AIP1      -0.0003366158 -0.0003366158
    ## cg_AC025171.1    -0.0158767778 -0.0158767778
    ## cg_VTRNA1_3      -0.0110528294 -0.0110528294
    ## tc_TC01006069_nc -1.3850242700 -1.3850242700
    ## tc_SLC9A4        -1.3826521909 -1.3826521909
    ## tc_RAB6C_AS1     -1.3938046667 -1.3938046667
    ## tc_LOC100129029   0.0145762574  0.0145762574
    ## tc_BRE            0.0019586229  0.0019586229
    ## tc_TC03001220_nc -0.0230442854 -0.0230442854
    ## tc_TC04002114_nc  0.0079963157  0.0079963157
    ## tc_TC04002369_nc  0.0022743484  0.0022743484
    ## tc_BEND4          0.0043763535  0.0043763535
    ## tc_SLC9A3         0.0172973964  0.0172973964
    ## miR.101.3p        1.3807686597  1.3807686597
    ## miR.125a.5p       1.3900145832  1.3900145832
    ## miR.125b.1.3p     1.3870740149  1.3870740149
    ## miR.127.3p        0.0082765431  0.0082765431
    ## miR.140.5p        0.0006754416  0.0006754416
    ## miR.142.3p       -0.0063514452 -0.0063514452
    ## miR.144.5p       -0.0063899974 -0.0063899974
    ## miR.19a.3p       -0.0030368478 -0.0030368478
    ## miR.19b.3p       -0.0012956967 -0.0012956967
    ## miR.21.5p         0.0035885183  0.0035885183
    ##
    ## (3) E: intercept and odds ratio of being assigned to each latent cluster for each exposure
    ##                                         beta           OR
    ## (Intercept).cluster2            9.000000e+00 8.103084e+03
    ## g_causal_1.cluster2            -1.804112e-16 1.000000e+00
    ## g_causal_2.cluster2            -6.036838e-16 1.000000e+00
    ## g_causal_3.cluster2             0.000000e+00 1.000000e+00
    ## g_noise_5.cluster2             -2.997602e-16 1.000000e+00
    ## hs_child_age_yrs_None.cluster2  6.431098e+01 8.509462e+27
    ## sex_male.cluster2               4.600000e+00 9.948432e+01

Compare this summary against the penalized fit’s in 7.1: the coefficients on the selected exposures/features should be of similar sign and rough magnitude (the model didn’t change what it found, only stopped shrinking it), while any exposure/feature that was zeroed out in the screening step is now simply absent from the model rather than penalized toward zero. This refit – not the penalized screening fit – is the model bootstrapped next.

### 7.4 Bootstrap CI + summary

    set.seed(1103)

    # Bootstrap on zero-penalty refit for CI inference.
    early_boot <- boot_lucid(
      G = early_inputs_refit$G,
      Z = early_inputs_refit$Z,
      Y = Y,
      CoG = CoG,
      CoY = CoY,
      model = early_fit_refit,
      R = 2,
      conf = 0.90
    )

    # Print summary with CI columns integrated into parameter tables.
    summary(early_fit_refit, boot.se = early_boot)

    ##
    ## ====================================================
    ## LUCID Early Integration: Model Summary
    ## ====================================================
    ##
    ## Model specification
    ##   Family                 : normal
    ##   Number of observations : 90
    ##   Number of clusters (K) : 2
    ##
    ## Missing-data profile
    ##   Listwise missing rows  : 1 / 90 (1.1%)
    ##   Sporadic missing rows  : 4 / 90 (4.4%)
    ##   Missing cells total    : 34 / 2700 (1.3%)
    ##
    ## Feature selection overview
    ##   G features selected    : 4 / 4 (100.0%)
    ##   Z features selected    : 30 / 30 (100.0%)
    ##
    ## Model fit statistics
    ##   Log-likelihood         : -3314.42
    ##   BIC                    : 11142.15
    ##   Number of parameters   : 1003
    ##
    ## 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)            1.7218477  1.2385705  3.01142394   *
    ## cluster2               0.0000000 -1.5553103  0.36962057
    ## hs_child_age_yrs_None -0.1654292 -0.3358611 -0.04702722   *
    ## sex_male               0.1034161 -0.3357313  0.49041869
    ##
    ## (2) Z: mean of omics data for each latent cluster
    ##                                estimate  norm_lower   norm_upper sig
    ## cg_GRHL3.cluster1          1.3734684264  1.90846980  2.099752509   *
    ## cg_BTF3L4.cluster1         1.3816913508  1.83254870  1.989602820   *
    ## cg_AL358472.7.cluster1     1.3987916378  1.94137047  2.383747404   *
    ## cg_HDGF.cluster1           0.0042733091 -0.11058955 -0.036376678   *
    ## cg_TDRD5.cluster1         -0.0042363512 -0.09103763  0.065854846
    ## cg_CSRNP3.cluster1         0.0021582533 -0.09549347  0.328479723
    ## cg_HSPD1.cluster1         -0.0073919764 -0.06766347  0.234889561
    ## cg_EPM2AIP1.cluster1      -0.0003366158 -0.02304176  0.095071686
    ## cg_AC025171.1.cluster1    -0.0158767778 -0.19708100  0.231882365
    ## cg_VTRNA1_3.cluster1      -0.0110528294 -0.27535309  0.518373678
    ## tc_TC01006069_nc.cluster1 -1.3850242700 -1.91767623 -1.843572366   *
    ## tc_SLC9A4.cluster1        -1.3826521909 -2.62942724 -1.706706039   *
    ## tc_RAB6C_AS1.cluster1     -1.3938046667 -2.12519331 -1.866292914   *
    ## tc_LOC100129029.cluster1   0.0145762574  0.09153418  0.200623157   *
    ## tc_BRE.cluster1            0.0019586229 -0.04628868  0.206457674
    ## tc_TC03001220_nc.cluster1 -0.0230442854 -0.18159018  0.037166591
    ## tc_TC04002114_nc.cluster1  0.0079963157 -0.65693964  0.444745777
    ## tc_TC04002369_nc.cluster1  0.0022743484 -0.10928348  0.526175843
    ## tc_BEND4.cluster1          0.0043763535 -0.18605279  0.108222928
    ## tc_SLC9A3.cluster1         0.0172973964 -0.02750211  0.355561877
    ## miR.101.3p.cluster1        1.3807686597  1.33868063  2.689737042   *
    ## miR.125a.5p.cluster1       1.3900145832  1.46813780  2.367798998   *
    ## miR.125b.1.3p.cluster1     1.3870740149  1.75393173  2.092505734   *
    ## miR.127.3p.cluster1        0.0082765431 -0.11139779  0.312129120
    ## miR.140.5p.cluster1        0.0006754416 -0.22314247  0.093229150
    ## miR.142.3p.cluster1       -0.0063514452 -0.16926488  0.306768637
    ## miR.144.5p.cluster1       -0.0063899974 -0.06030234 -0.002823870   *
    ## miR.19a.3p.cluster1       -0.0030368478 -0.55901496  0.448739731
    ## miR.19b.3p.cluster1       -0.0012956967 -0.28906636  0.210747748
    ## miR.21.5p.cluster1         0.0035885183  0.01047006  0.016498018   *
    ## cg_GRHL3.cluster2          1.3734684264 -1.55720482  1.883631792
    ## cg_BTF3L4.cluster2         1.3816913508 -0.28523845  1.029418606
    ## cg_AL358472.7.cluster2     1.3987916378 -1.55863437  1.605542467
    ## cg_HDGF.cluster2           0.0042733091 -0.16774990  0.105639717
    ## cg_TDRD5.cluster2         -0.0042363512 -0.81023079  1.081631057
    ## cg_CSRNP3.cluster2         0.0021582533 -0.33869197 -0.253422427   *
    ## cg_HSPD1.cluster2         -0.0073919764 -0.19332569  0.503579948
    ## cg_EPM2AIP1.cluster2      -0.0003366158 -0.78257057  0.944107967
    ## cg_AC025171.1.cluster2    -0.0158767778 -0.36138130  0.021457404
    ## cg_VTRNA1_3.cluster2      -0.0110528294 -1.36369709  1.174824402
    ## tc_TC01006069_nc.cluster2 -1.3850242700 -1.85290408  1.112239037
    ## tc_SLC9A4.cluster2        -1.3826521909 -0.34755772 -0.230606697   *
    ## tc_RAB6C_AS1.cluster2     -1.3938046667 -2.31253644  1.528463219
    ## tc_LOC100129029.cluster2   0.0145762574 -1.17838339  1.050708786
    ## tc_BRE.cluster2            0.0019586229 -0.67894553  0.992384542
    ## tc_TC03001220_nc.cluster2 -0.0230442854 -0.10425668  0.047516633
    ## tc_TC04002114_nc.cluster2  0.0079963157 -0.78174865  0.478257313
    ## tc_TC04002369_nc.cluster2  0.0022743484 -0.58289999  0.471529202
    ## tc_BEND4.cluster2          0.0043763535  0.04912507  0.166455194   *
    ## tc_SLC9A3.cluster2         0.0172973964  0.08586256  0.201022279   *
    ## miR.101.3p.cluster2        1.3807686597 -0.46277417  0.580242183
    ## miR.125a.5p.cluster2       1.3900145832 -0.64905520  1.667808828
    ## miR.125b.1.3p.cluster2     1.3870740149 -1.69930091  1.964266389
    ## miR.127.3p.cluster2        0.0082765431 -0.35641390  0.428374949
    ## miR.140.5p.cluster2        0.0006754416 -0.46042607  0.007964872
    ## miR.142.3p.cluster2       -0.0063514452 -0.49059440  0.096466692
    ## miR.144.5p.cluster2       -0.0063899974 -0.45356660  0.550907473
    ## miR.19a.3p.cluster2       -0.0030368478 -0.72332598  0.583055530
    ## miR.19b.3p.cluster2       -0.0012956967 -0.31362693  0.134213604
    ## miR.21.5p.cluster2         0.0035885183 -0.29477439  0.178501284
    ##
    ## (3) E: intercept and odds ratio of being assigned to each latent cluster for each exposure
    ##                          estimate norm_lower norm_upper sig
    ## g_causal_1.cluster2 -1.804112e-16 -0.6976451  0.3321579
    ## g_causal_2.cluster2 -6.036838e-16 -0.2419068  2.4689701
    ## g_causal_3.cluster2  0.000000e+00 -0.8445345 -0.1721251   *
    ## g_noise_5.cluster2  -2.997602e-16 -0.7957648  1.2030635

Each coefficient table above now carries a normal-theory confidence interval plus the `sig` column: `"*"` marks a row whose interval excludes 0, i.e. an effect the bootstrap distinguishes from no effect at all. With only `R = 2` replicates here the intervals themselves are not trustworthy – this is a code-pattern demonstration, not a real inference; a real analysis needs `R` in the hundreds, as noted in section 3’s hyperparameter guide.

## 8) Parallel Model Tutorial

### 8.1 Penalized screening fit

Interpretation target:

- Layer-specific cluster structure and missingness,
- exposure selection union/per-layer behavior,
- layer-specific omics selection.

    set.seed(1201)

    parallel_fit_pen <- estimate_lucid(
      lucid_model = "parallel",
      G = G,
      Z = Z_parallel_miss,
      Y = Y,
      CoG = CoG,
      CoY = CoY,
      family = "normal",
      K = c(2, 2, 2),
      Rho_G = RHO_G,
      Rho_Z_Mu = 0,
      Rho_Z_Cov = 0,
      max_itr = 15,
      max_tot.itr = 40,
      tol = 1e-2,
      seed = 1201,
      verbose = FALSE
    )

    ## Fitting LUCID parallel model (3 layers)...
    ## Finished LUCID parallel model. Selected G: 3/9; Selected Z by layer: 10/10, 10/10, 10/10.

    summary(parallel_fit_pen)

    ##
    ## ====================================================
    ## LUCID Parallel: Model Summary
    ## ====================================================
    ##
    ## Model specification
    ##   Family                 : gaussian
    ##   Number of observations : 90
    ##   Clusters per layer     : 2, 2, 2
    ##
    ## Missing-data profile by layer
    ##   Layer 1 listwise rows : 1 / 90 (1.1%)
    ##   Layer 1 sporadic rows : 0 / 90 (0.0%)
    ##   Layer 1 missing cells : 10 / 900 (1.1%)
    ##   Layer 2 listwise rows : 0 / 90 (0.0%)
    ##   Layer 2 sporadic rows : 1 / 90 (1.1%)
    ##   Layer 2 missing cells : 1 / 900 (0.1%)
    ##   Layer 3 listwise rows : 0 / 90 (0.0%)
    ##   Layer 3 sporadic rows : 1 / 90 (1.1%)
    ##   Layer 3 missing cells : 1 / 900 (0.1%)
    ##
    ## Feature selection overview
    ##   G features selected    : 3 / 9 (33.3%)
    ##   G features by layer
    ##     Layer 1              : 3 / 9 (33.3%)
    ##     Layer 2              : 3 / 9 (33.3%)
    ##     Layer 3              : 3 / 9 (33.3%)
    ##   Z features
    ##     Layer 1 selected     : 10 / 10 (100.0%)
    ##     Layer 1 multi-cluster: 10
    ##     Layer 2 selected     : 10 / 10 (100.0%)
    ##     Layer 2 multi-cluster: 10
    ##     Layer 3 selected     : 10 / 10 (100.0%)
    ##     Layer 3 multi-cluster: 10
    ##
    ## Model fit statistics
    ##   Log-likelihood         : -3417.55
    ##   BIC                    : 8702.52
    ##   Number of parameters   : 415
    ##
    ## Regularization
    ##   Rho_G                  : 0.050
    ##   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.90388527
    ## Layer1_LC2             0.52583758
    ## Layer2_LC2             0.10997526
    ## Layer3_LC2             0.73829435
    ## hs_child_age_yrs_None -0.12953921
    ## sex_male              -0.08827964
    ##
    ## (2) Z: mean of omics data for each latent cluster of each layer
    ## Layer 1
    ##
    ##               mu_cluster1 mu_cluster2
    ## cg_GRHL3       0.01944230  2.78671098
    ## cg_BTF3L4      0.02535503  2.77114802
    ## cg_AL358472.7 -0.05886647  2.85100872
    ## cg_HDGF        0.19363958 -0.18971710
    ## cg_TDRD5      -0.13897998  0.13379758
    ## cg_CSRNP3     -0.07415664  0.08033668
    ## cg_HSPD1       0.18552243 -0.20501715
    ## cg_EPM2AIP1   -0.03920856  0.03948454
    ## cg_AC025171.1 -0.07330782  0.04295667
    ## cg_VTRNA1_3    0.15846120 -0.18470622
    ##
    ## Layer 2
    ##
    ##                   mu_cluster1  mu_cluster2
    ## tc_TC01006069_nc -0.151915803 -2.761778697
    ## tc_SLC9A4         0.061894410 -3.024122495
    ## tc_RAB6C_AS1     -0.175486361 -2.736060974
    ## tc_LOC100129029   0.043355812 -0.047305319
    ## tc_BRE            0.068234642 -0.074450491
    ## tc_TC03001220_nc -0.027376915  0.029870821
    ## tc_TC04002114_nc  0.028645224 -0.031254667
    ## tc_TC04002369_nc -0.121803704  0.132899439
    ## tc_BEND4          0.079874133 -0.087150285
    ## tc_SLC9A3         0.007825153 -0.008537987
    ##
    ## Layer 3
    ##
    ##                mu_cluster1  mu_cluster2
    ## miR.101.3p    -0.006887544  3.007913039
    ## miR.125a.5p    0.187538247  2.785670575
    ## miR.125b.1.3p  0.027408679  2.968675795
    ## miR.127.3p    -0.107628701  0.123004230
    ## miR.140.5p     0.090862876 -0.103843286
    ## miR.142.3p    -0.004784527  0.005468031
    ## miR.144.5p     0.080101772 -0.091544883
    ## miR.19a.3p     0.060465036 -0.069102898
    ## miR.19b.3p     0.108156640 -0.123607589
    ## miR.21.5p      0.102286783 -0.116899180
    ##
    ## (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.24677085 0.7813197
    ## g_causal_1.cluster2   0.81378250 2.2564268
    ## g_causal_2.cluster2  -0.77415373 0.4610938
    ## g_causal_3.cluster2   0.05586156 1.0574513
    ##
    ## Layer 2
    ##
    ##                            beta        OR
    ## (Intercept).cluster2 -0.8436717 0.4301283
    ## g_causal_1.cluster2   0.9246242 2.5209208
    ## g_causal_2.cluster2  -0.8914857 0.4100461
    ## g_causal_3.cluster2   0.2035521 1.2257490
    ##
    ## Layer 3
    ##
    ##                            beta        OR
    ## (Intercept).cluster2 -1.3654847 0.2552569
    ## g_causal_1.cluster2   0.9934557 2.7005507
    ## g_causal_2.cluster2  -0.8493218 0.4277049
    ## g_causal_3.cluster2   0.1937766 1.2138251

### 8.1b Object structure: what’s in the fitted object

    names(parallel_fit_pen)

    ##  [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"

Compared to the early fit in section 7.1b, two fields are new and two common fields change shape:

- `N` (sample size) is present here but not for early.
- `z` is the joint E-step responsibility array across all layers, before it is marginalized into `inclusion.p` – present for parallel only; nothing else in the package reads it, so it is shown here purely for completeness.
- `select` gains layer structure: `selectG_layer` (per-layer exposure selection, alongside the `selectG` union already seen for early), and `selectZ` is now a list, one entry per omics layer.
- `res_Beta`/`res_Mu`/`res_Sigma`/`inclusion.p` are lists indexed by layer rather than single matrices.

    cat("log-likelihood:", parallel_fit_pen$likelihood, "\n")

    ## log-likelihood: -3417.549

    cat("sample size (N):", parallel_fit_pen$N, "\n")

    ## sample size (N): 90

    cat("exposures selected per layer:\n")

    ## exposures selected per layer:

    print(sapply(seq_along(parallel_fit_pen$K), function(i) sum(get_selected_G(parallel_fit_pen, layer = i))))

    ## [1] 3 3 3

### 8.2 Inspect selected features (union + per-layer)

    # Exposure selection union across layers.
    parallel_selected_G_union <- names(which(get_selected_G(parallel_fit_pen)))

    # Exposure selection per layer.
    parallel_selected_G_layer <- lapply(seq_along(parallel_fit_pen$K), function(i) {
      names(which(get_selected_G(parallel_fit_pen, layer = i)))
    })

    # Omics selection per layer (get_selected_Z already collapses any
    # vector/matrix selection encoding to one logical value per feature).
    parallel_selected_Z_layer <- lapply(get_selected_Z(parallel_fit_pen), function(s) names(which(s)))

    parallel_selected_G_union

    ## [1] "g_causal_1" "g_causal_2" "g_causal_3"

    parallel_selected_G_layer

    ## [[1]]
    ## [1] "g_causal_1" "g_causal_2" "g_causal_3"
    ##
    ## [[2]]
    ## [1] "g_causal_1" "g_causal_2" "g_causal_3"
    ##
    ## [[3]]
    ## [1] "g_causal_1" "g_causal_2" "g_causal_3"

    parallel_selected_Z_layer

    ## $methylome
    ##  [1] "cg_GRHL3"      "cg_BTF3L4"     "cg_AL358472.7" "cg_HDGF"
    ##  [5] "cg_TDRD5"      "cg_CSRNP3"     "cg_HSPD1"      "cg_EPM2AIP1"
    ##  [9] "cg_AC025171.1" "cg_VTRNA1_3"
    ##
    ## $transcriptome
    ##  [1] "tc_TC01006069_nc" "tc_SLC9A4"        "tc_RAB6C_AS1"     "tc_LOC100129029"
    ##  [5] "tc_BRE"           "tc_TC03001220_nc" "tc_TC04002114_nc" "tc_TC04002369_nc"
    ##  [9] "tc_BEND4"         "tc_SLC9A3"
    ##
    ## $miRNA
    ##  [1] "miR.101.3p"    "miR.125a.5p"   "miR.125b.1.3p" "miR.127.3p"
    ##  [5] "miR.140.5p"    "miR.142.3p"    "miR.144.5p"    "miR.19a.3p"
    ##  [9] "miR.19b.3p"    "miR.21.5p"

### 8.3 Zero-penalty selected-only refit

    set.seed(1202)

    parallel_inputs_refit <- prepare_parallel_selected_inputs(parallel_fit_pen, G, Z_parallel_miss)
    parallel_fit_refit <- refit_selected(
      "parallel",
      fit_pen = parallel_fit_pen,
      inputs = parallel_inputs_refit,
      Y = Y,
      CoG = CoG,
      CoY = CoY,
      seed = 1202,
      verbose = TRUE
    )

    ## Intializing imputation of missing values in 'Z' via LOD / sqrt(2)
    ##
    ## Intializing imputation of missing values in 'Z' via LOD / sqrt(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) (K = 2, Rho_G = 0, Rho_Z_Mu = 0, Rho_Z_Cov = 0)
    ## iteration 1 : E-step finished.
    ## iteration 1: log-likelihood = -3404.888
    ## iteration 2 : E-step finished.
    ## iteration 2: log-likelihood = -3403.244
    ## iteration 3 : E-step finished.
    ## iteration 3: log-likelihood = -3403.223
    ## iteration 4 : E-step finished.
    ## iteration 4: log-likelihood = -3403.223
    ## Success: LUCID parallel model converged!
    ##
    ## Finished LUCID parallel model: log-likelihood = -3403.223.

    summary(parallel_fit_refit)

    ##
    ## ====================================================
    ## LUCID Parallel: Model Summary
    ## ====================================================
    ##
    ## Model specification
    ##   Family                 : gaussian
    ##   Number of observations : 90
    ##   Clusters per layer     : 2, 2, 2
    ##
    ## Missing-data profile by layer
    ##   Layer 1 listwise rows : 1 / 90 (1.1%)
    ##   Layer 1 sporadic rows : 0 / 90 (0.0%)
    ##   Layer 1 missing cells : 10 / 900 (1.1%)
    ##   Layer 2 listwise rows : 0 / 90 (0.0%)
    ##   Layer 2 sporadic rows : 1 / 90 (1.1%)
    ##   Layer 2 missing cells : 1 / 900 (0.1%)
    ##   Layer 3 listwise rows : 0 / 90 (0.0%)
    ##   Layer 3 sporadic rows : 1 / 90 (1.1%)
    ##   Layer 3 missing cells : 1 / 900 (0.1%)
    ##
    ## Feature selection overview
    ##   G features selected    : 3 / 3 (100.0%)
    ##   G features by layer
    ##     Layer 1              : 3 / 3 (100.0%)
    ##     Layer 2              : 3 / 3 (100.0%)
    ##     Layer 3              : 3 / 3 (100.0%)
    ##   Z features
    ##     Layer 1 selected     : 10 / 10 (100.0%)
    ##     Layer 1 multi-cluster: 10
    ##     Layer 2 selected     : 10 / 10 (100.0%)
    ##     Layer 2 multi-cluster: 10
    ##     Layer 3 selected     : 10 / 10 (100.0%)
    ##     Layer 3 multi-cluster: 10
    ##
    ## Model fit statistics
    ##   Log-likelihood         : -3403.22
    ##   BIC                    : 8673.87
    ##   Number of parameters   : 415
    ##
    ## 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.90197504
    ## Layer1_LC2             0.55468259
    ## Layer2_LC2             0.08656150
    ## Layer3_LC2             0.73329966
    ## hs_child_age_yrs_None -0.12940582
    ## sex_male              -0.08911495
    ##
    ## (2) Z: mean of omics data for each latent cluster of each layer
    ## Layer 1
    ##
    ##               mu_cluster1 mu_cluster2
    ## cg_GRHL3       0.01911047  2.78541234
    ## cg_BTF3L4      0.02478402  2.77010682
    ## cg_AL358472.7 -0.05983045  2.85027248
    ## cg_HDGF        0.19299008 -0.18882557
    ## cg_TDRD5      -0.13874086  0.13339143
    ## cg_CSRNP3     -0.07401232  0.08009755
    ## cg_HSPD1       0.18605910 -0.20533509
    ## cg_EPM2AIP1   -0.03848329  0.03869584
    ## cg_AC025171.1 -0.07288049  0.04245059
    ## cg_VTRNA1_3    0.15835252 -0.18439186
    ##
    ## Layer 2
    ##
    ##                   mu_cluster1  mu_cluster2
    ## tc_TC01006069_nc -0.150718477 -2.761644116
    ## tc_SLC9A4         0.062093261 -3.022512916
    ## tc_RAB6C_AS1     -0.174660692 -2.735548496
    ## tc_LOC100129029   0.043334559 -0.047232147
    ## tc_BRE            0.067975103 -0.074088904
    ## tc_TC03001220_nc -0.027151255  0.029593287
    ## tc_TC04002114_nc  0.029476766 -0.032127959
    ## tc_TC04002369_nc -0.122166463  0.133154331
    ## tc_BEND4          0.079580265 -0.086737855
    ## tc_SLC9A3         0.007320192 -0.007978583
    ##
    ## Layer 3
    ##
    ##                mu_cluster1  mu_cluster2
    ## miR.101.3p    -0.006887544  3.007913039
    ## miR.125a.5p    0.187538247  2.785670575
    ## miR.125b.1.3p  0.027408679  2.968675795
    ## miR.127.3p    -0.107628701  0.123004230
    ## miR.140.5p     0.090862876 -0.103843286
    ## miR.142.3p    -0.004784527  0.005468031
    ## miR.144.5p     0.080101772 -0.091544883
    ## miR.19a.3p     0.060465036 -0.069102898
    ## miR.19b.3p     0.108156640 -0.123607589
    ## miR.21.5p      0.102286783 -0.116899180
    ##
    ## (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 -1.1631418 0.3125028
    ## g_causal_1.cluster2   1.4640897 4.3236056
    ## g_causal_2.cluster2  -1.4811806 0.2273691
    ## g_causal_3.cluster2   0.4026573 1.4957941
    ##
    ## Layer 2
    ##
    ##                            beta        OR
    ## (Intercept).cluster2 -2.2697448 0.1033385
    ## g_causal_1.cluster2   1.8277068 6.2196073
    ## g_causal_2.cluster2  -1.9585513 0.1410626
    ## g_causal_3.cluster2   0.6732859 1.9606694
    ##
    ## Layer 3
    ##
    ##                            beta         OR
    ## (Intercept).cluster2 -3.2332090 0.03943076
    ## g_causal_1.cluster2   1.9557658 7.06933069
    ## g_causal_2.cluster2  -1.9426942 0.14331731
    ## g_causal_3.cluster2   0.6703573 1.95493576

Unlike the early refit in 7.3, this summary prints one `G -> X` and one `X -> Z` block per layer – each layer’s cluster variable is its own model, refit unpenalized on that layer’s own selected features.

### 8.4 Bootstrap CI + summary

    set.seed(1203)

    parallel_boot <- boot_lucid(
      G = parallel_inputs_refit$G,
      Z = parallel_inputs_refit$Z,
      Y = Y,
      CoG = CoG,
      CoY = CoY,
      model = parallel_fit_refit,
      R = 2,
      conf = 0.90
    )

    summary(parallel_fit_refit, boot.se = parallel_boot)

    ##
    ## ====================================================
    ## LUCID Parallel: Model Summary
    ## ====================================================
    ##
    ## Model specification
    ##   Family                 : gaussian
    ##   Number of observations : 90
    ##   Clusters per layer     : 2, 2, 2
    ##
    ## Missing-data profile by layer
    ##   Layer 1 listwise rows : 1 / 90 (1.1%)
    ##   Layer 1 sporadic rows : 0 / 90 (0.0%)
    ##   Layer 1 missing cells : 10 / 900 (1.1%)
    ##   Layer 2 listwise rows : 0 / 90 (0.0%)
    ##   Layer 2 sporadic rows : 1 / 90 (1.1%)
    ##   Layer 2 missing cells : 1 / 900 (0.1%)
    ##   Layer 3 listwise rows : 0 / 90 (0.0%)
    ##   Layer 3 sporadic rows : 1 / 90 (1.1%)
    ##   Layer 3 missing cells : 1 / 900 (0.1%)
    ##
    ## Feature selection overview
    ##   G features selected    : 3 / 3 (100.0%)
    ##   G features by layer
    ##     Layer 1              : 3 / 3 (100.0%)
    ##     Layer 2              : 3 / 3 (100.0%)
    ##     Layer 3              : 3 / 3 (100.0%)
    ##   Z features
    ##     Layer 1 selected     : 10 / 10 (100.0%)
    ##     Layer 1 multi-cluster: 10
    ##     Layer 2 selected     : 10 / 10 (100.0%)
    ##     Layer 2 multi-cluster: 10
    ##     Layer 3 selected     : 10 / 10 (100.0%)
    ##     Layer 3 multi-cluster: 10
    ##
    ## Model fit statistics
    ##   Log-likelihood         : -3403.22
    ##   BIC                    : 8673.87
    ##   Number of parameters   : 415
    ##
    ## 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.90197504 -1.82927556 3.19573424
    ## Layer1_LC2             0.55468259  0.14853235 0.57423132   *
    ## Layer2_LC2             0.08656150 -2.29068484 0.82333900
    ## Layer3_LC2             0.73329966 -2.91408296 2.11115240
    ## hs_child_age_yrs_None -0.12940582  0.00806368 0.06297141   *
    ## sex_male              -0.08911495 -0.32395406 0.66050824
    ##
    ## (2) Z: mean of omics data for each latent cluster of each layer
    ## Layer 1
    ##
    ##                                  estimate  norm_lower  norm_upper sig
    ## Layer1.cg_GRHL3.cluster1       0.01911047 -4.61289075  0.42172539
    ## Layer1.cg_BTF3L4.cluster1      0.02478402 -4.87904227  0.71550321
    ## Layer1.cg_AL358472.7.cluster1 -0.05983045 -5.46219016  0.95522043
    ## Layer1.cg_HDGF.cluster1        0.19299008  0.14880101  0.35476226   *
    ## Layer1.cg_TDRD5.cluster1      -0.13874086 -0.50361142 -0.15483002   *
    ## Layer1.cg_CSRNP3.cluster1     -0.07401232 -1.10895254  0.49673336
    ## Layer1.cg_HSPD1.cluster1       0.18605910  0.32061011  0.45740944   *
    ## Layer1.cg_EPM2AIP1.cluster1   -0.03848329 -0.42730073  0.21512999
    ## Layer1.cg_AC025171.1.cluster1 -0.07288049 -0.51415546 -0.03944904   *
    ## Layer1.cg_VTRNA1_3.cluster1    0.15835252 -0.28020161  1.27019123
    ## Layer1.cg_GRHL3.cluster2       2.78541234  1.41491171  6.11778337   *
    ## Layer1.cg_BTF3L4.cluster2      2.77010682  1.78935186  5.70847126   *
    ## Layer1.cg_AL358472.7.cluster2  2.85027248  0.23304003  7.08251062   *
    ## Layer1.cg_HDGF.cluster2       -0.18882557 -0.64494552 -0.14308088   *
    ## Layer1.cg_TDRD5.cluster2       0.13339143 -0.15858406  0.67798636
    ## Layer1.cg_CSRNP3.cluster2      0.08009755 -0.35050717  1.19247352
    ## Layer1.cg_HSPD1.cluster2      -0.20533509 -0.71952057  0.10876138
    ## Layer1.cg_EPM2AIP1.cluster2    0.03869584 -0.01253431  0.03733736
    ## Layer1.cg_AC025171.1.cluster2  0.04245059  0.10650105  0.23746119   *
    ## Layer1.cg_VTRNA1_3.cluster2   -0.18439186 -1.27107973  0.78344846
    ##
    ## Layer 2
    ##
    ##                                      estimate  norm_lower  norm_upper sig
    ## Layer2.tc_TC01006069_nc.cluster1 -0.150718477  2.05128170  2.81316525   *
    ## Layer2.tc_SLC9A4.cluster1         0.062093261  2.56829266  3.96163161   *
    ## Layer2.tc_RAB6C_AS1.cluster1     -0.174660692  2.12434391  3.43675381   *
    ## Layer2.tc_LOC100129029.cluster1   0.043334559 -0.37341577  0.31185468
    ## Layer2.tc_BRE.cluster1            0.067975103  0.14597243  0.25298525   *
    ## Layer2.tc_TC03001220_nc.cluster1 -0.027151255 -0.52708619  0.41881040
    ## Layer2.tc_TC04002114_nc.cluster1  0.029476766 -0.11392084  0.52713969
    ## Layer2.tc_TC04002369_nc.cluster1 -0.122166463 -0.94294136  0.38815009
    ## Layer2.tc_BEND4.cluster1          0.079580265  0.21247640  0.45229697   *
    ## Layer2.tc_SLC9A3.cluster1         0.007320192 -0.42049017  0.29267780
    ## Layer2.tc_TC01006069_nc.cluster2 -2.761644116 -5.84711840 -4.12000693   *
    ## Layer2.tc_SLC9A4.cluster2        -3.022512916 -6.42428076 -5.10371608   *
    ## Layer2.tc_RAB6C_AS1.cluster2     -2.735548496 -5.71382338 -4.14727572   *
    ## Layer2.tc_LOC100129029.cluster2  -0.047232147 -0.51475117  0.49454267
    ## Layer2.tc_BRE.cluster2           -0.074088904 -0.21328387 -0.07772781   *
    ## Layer2.tc_TC03001220_nc.cluster2  0.029593287  0.02042346  0.11443281   *
    ## Layer2.tc_TC04002114_nc.cluster2 -0.032127959 -0.11897252 -0.08119528   *
    ## Layer2.tc_TC04002369_nc.cluster2  0.133154331 -0.37614047  0.80057170
    ## Layer2.tc_BEND4.cluster2         -0.086737855 -0.29839622 -0.07253692   *
    ## Layer2.tc_SLC9A3.cluster2        -0.007978583 -0.22786222  0.36205382
    ##
    ## Layer 3
    ##
    ##                                   estimate  norm_lower norm_upper sig
    ## Layer3.miR.101.3p.cluster1    -0.006887544 -0.85835674 0.63349326
    ## Layer3.miR.125a.5p.cluster1    0.187538247 -0.12054812 0.54531006
    ## Layer3.miR.125b.1.3p.cluster1  0.027408679 -0.11224394 0.26777031
    ## Layer3.miR.127.3p.cluster1    -0.107628701 -0.33956237 0.16003987
    ## Layer3.miR.140.5p.cluster1     0.090862876 -0.43461362 0.39456112
    ## Layer3.miR.142.3p.cluster1    -0.004784527 -0.90333386 0.55071060
    ## Layer3.miR.144.5p.cluster1     0.080101772 -0.48115276 0.23358977
    ## Layer3.miR.19a.3p.cluster1     0.060465036 -0.80183783 0.57872541
    ## Layer3.miR.19b.3p.cluster1     0.108156640 -0.71319277 0.63320509
    ## Layer3.miR.21.5p.cluster1      0.102286783 -0.52084664 0.54504734
    ## Layer3.miR.101.3p.cluster2     3.007913039  2.76591643 4.03964999   *
    ## Layer3.miR.125a.5p.cluster2    2.785670575  2.40835450 3.43823362   *
    ## Layer3.miR.125b.1.3p.cluster2  2.968675795  2.83195752 3.44316102   *
    ## Layer3.miR.127.3p.cluster2     0.123004230 -0.39451590 0.65348059
    ## Layer3.miR.140.5p.cluster2    -0.103843286  0.05693976 0.12198369   *
    ## Layer3.miR.142.3p.cluster2     0.005468031  0.07367142 0.40513048   *
    ## Layer3.miR.144.5p.cluster2    -0.091544883 -0.04645363 0.13132820
    ## Layer3.miR.19a.3p.cluster2    -0.069102898  0.02940357 0.24526051   *
    ## Layer3.miR.19b.3p.cluster2    -0.123607589 -0.06904772 0.09523343
    ## Layer3.miR.21.5p.cluster2     -0.116899180 -0.19704415 0.18960608
    ##
    ## (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 -1.1631418 -8.2214342 -3.3771064   *
    ## g_causal_1.cluster2   1.4640897  0.4389506  4.7566259   *
    ## g_causal_2.cluster2  -1.4811806 -6.3824366 -0.7601632   *
    ## g_causal_3.cluster2   0.4026573  0.4750914  1.6171105   *
    ##
    ## Layer 2
    ##
    ##                        estimate norm_lower norm_upper sig
    ## (Intercept).cluster2 -2.2697448  -8.216159  0.3394388
    ## g_causal_1.cluster2   1.8277068   3.384299  5.8233970   *
    ## g_causal_2.cluster2  -1.9585513  -6.068292 -4.3160274   *
    ## g_causal_3.cluster2   0.6732859   1.687756  1.8885930   *
    ##
    ## Layer 3
    ##
    ##                        estimate  norm_lower norm_upper sig
    ## (Intercept).cluster2 -3.2332090 -12.2463212  -5.925098   *
    ## g_causal_1.cluster2   1.9557658   1.9862321   3.295563   *
    ## g_causal_2.cluster2  -1.9426942  -2.3076720  -2.098619   *
    ## g_causal_3.cluster2   0.6703573  -0.1442928   1.294448

As in 7.4, the `sig` column flags rows whose normal-theory interval excludes 0 in each layer’s coefficient table; with layer-specific cluster variables, a feature can be significant in one layer’s table and not another’s, since each layer’s bootstrap resamples and refits that layer’s own model.

## 9) Serial Model Tutorial A: All-Early Stages

Serial model logic here:

- Stage 1: early on methylome
- Stage 2: early on transcriptome
- Stage 3: early on miRNA

Each stage uses previous latent information as designed by the serial pipeline.

### 9.1 Penalized screening fit

    set.seed(1301)

    # Serial structure: list of early-stage matrices.
    Z_serial_all_early <- list(
      methylome = Z_parallel_miss[[1]],
      transcriptome = Z_parallel_miss[[2]],
      miRNA = Z_parallel_miss[[3]]
    )

    serial_all_early_pen <- estimate_lucid(
      lucid_model = "serial",
      G = G,
      Z = Z_serial_all_early,
      Y = Y,
      CoG = CoG,
      CoY = CoY,
      family = "normal",
      K = list(2, 2, 2),
      Rho_G = RHO_G,
      Rho_Z_Mu = 0,
      Rho_Z_Cov = 0,
      max_itr = 15,
      max_tot.itr = 40,
      tol = 1e-2,
      seed = 1301,
      verbose = FALSE
    )

    ## Fitting LUCID serial model (3 stages)...
    ##   Stage 1/3 (early) finished: log-likelihood = -1202.568. Selected G: 3/9; Selected Z: 10/10.
    ##   Stage 2/3 (early) finished: log-likelihood = -1217.744.
    ##   Stage 3/3 (early) finished: log-likelihood = -1107.108.
    ## Finished LUCID serial model.

    summary(serial_all_early_pen)

    ##
    ## ====================================================
    ## LUCID Serial: Model Summary
    ## ====================================================
    ##
    ## Model specification
    ##   Family                 : normal
    ##   Number of observations : 90
    ##   Number of stages       : 3
    ##   Stage 1               : early (K = 2)
    ##   Stage 2               : early (K = 2)
    ##   Stage 3               : early (K = 2)
    ##
    ## Missing-data profile by stage
    ##   Stage 1
    ##     Listwise rows         : 1
    ##     Sporadic rows         : 0
    ##   Stage 2
    ##     Listwise rows         : 0
    ##     Sporadic rows         : 1
    ##   Stage 3
    ##     Listwise rows         : 0
    ##     Sporadic rows         : 1
    ##
    ## Model fit statistics
    ##   Log-likelihood         : -3527.42
    ##   BIC                    : 8881.76
    ##
    ## Regularization
    ##   Rho_G                  : 0.050
    ##   Rho_Z_Mu               : 0.000
    ##   Rho_Z_Cov              : 0.000
    ##
    ## Stage-wise detailed parameter estimates
    ##
    ## --- Stage 1 (early) ---
    ##
    ## ====================================================
    ## LUCID Early Integration: Model Summary
    ## ====================================================
    ##
    ## Model specification
    ##   Family                 : normal
    ##   Number of observations : 90
    ##   Number of clusters (K) : 2
    ##
    ## Missing-data profile
    ##   Listwise missing rows  : 1 / 90 (1.1%)
    ##   Sporadic missing rows  : 0 / 90 (0.0%)
    ##   Missing cells total    : 10 / 900 (1.1%)
    ##
    ## Feature selection overview
    ##   G features selected    : 3 / 9 (33.3%)
    ##   Z features selected    : 10 / 10 (100.0%)
    ##
    ## Model fit statistics
    ##   Log-likelihood         : -1202.57
    ##   BIC                    : 3017.11
    ##   Number of parameters   : 136
    ##
    ## Regularization
    ##   Rho_G                  : 0.050
    ##   Rho_Z_Mu               : 0.000
    ##   Rho_Z_Cov              : 0.000
    ##
    ## Detailed parameter estimates
    ## (1) Z: mean of omics data for each latent cluster
    ##               mu_cluster1 mu_cluster2
    ## cg_GRHL3       0.01961632  2.78733237
    ## cg_BTF3L4      0.02562603  2.77166380
    ## cg_AL358472.7 -0.05843567  2.85140812
    ## cg_HDGF        0.19392191 -0.19011728
    ## cg_TDRD5      -0.13906418  0.13396272
    ## cg_CSRNP3     -0.07422915  0.08045565
    ## cg_HSPD1       0.18520515 -0.20480481
    ## cg_EPM2AIP1   -0.03953897  0.03984596
    ## cg_AC025171.1 -0.07348596  0.04317287
    ## cg_VTRNA1_3    0.15848714 -0.18483199
    ##
    ## (2) E: intercept and odds ratio of being assigned to each latent cluster for each exposure
    ##                                        beta        OR
    ## (Intercept).cluster2           -0.233824182 0.7915010
    ## g_causal_1.cluster2             0.807455666 2.2421958
    ## g_causal_2.cluster2            -0.771406772 0.4623622
    ## g_causal_3.cluster2             0.053236931 1.0546795
    ## hs_child_age_yrs_None.cluster2 -0.004677383 0.9953335
    ## sex_male.cluster2               0.628371411 1.8745552
    ##
    ## --- Stage 2 (early) ---
    ##
    ## ====================================================
    ## LUCID Early Integration: Model Summary
    ## ====================================================
    ##
    ## Model specification
    ##   Family                 : normal
    ##   Number of observations : 90
    ##   Number of clusters (K) : 2
    ##
    ## Missing-data profile
    ##   Listwise missing rows  : 0 / 90 (0.0%)
    ##   Sporadic missing rows  : 1 / 90 (1.1%)
    ##   Missing cells total    : 1 / 900 (0.1%)
    ##
    ## Feature selection overview
    ##   G features selected    : 1 / 1 (100.0%)
    ##   Z features selected    : 10 / 10 (100.0%)
    ##
    ## Model fit statistics
    ##   Log-likelihood         : -1217.74
    ##   BIC                    : 3029.46
    ##   Number of parameters   : 132
    ##
    ## 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
    ##                   mu_cluster1  mu_cluster2
    ## tc_TC01006069_nc -2.770982315 -0.200404643
    ## tc_SLC9A4        -3.075730222  0.037563680
    ## tc_RAB6C_AS1     -2.807651590 -0.168319407
    ## tc_LOC100129029  -0.052357555  0.045812320
    ## tc_BRE           -0.112174120  0.098151196
    ## tc_TC03001220_nc  0.060238426 -0.052708001
    ## tc_TC04002114_nc -0.036339642  0.031796811
    ## tc_TC04002369_nc  0.078990829 -0.069116159
    ## tc_BEND4         -0.063253002  0.055345723
    ## tc_SLC9A3        -0.001011717  0.000885242
    ##
    ## (2) E: intercept and odds ratio of being assigned to each latent cluster for each cluster from previous serial stage
    ##                                beta           OR
    ## (Intercept).cluster2       7.781705 2.396358e+03
    ## Stage1.cluster2.cluster2 -10.462325 2.859368e-05
    ##
    ## --- Stage 3 (early) ---
    ##
    ## ====================================================
    ## LUCID Early Integration: Model Summary
    ## ====================================================
    ##
    ## Model specification
    ##   Family                 : normal
    ##   Number of observations : 90
    ##   Number of clusters (K) : 2
    ##
    ## Missing-data profile
    ##   Listwise missing rows  : 0 / 90 (0.0%)
    ##   Sporadic missing rows  : 1 / 90 (1.1%)
    ##   Missing cells total    : 1 / 900 (0.1%)
    ##
    ## Feature selection overview
    ##   G features selected    : 1 / 1 (100.0%)
    ##   Z features selected    : 10 / 10 (100.0%)
    ##
    ## Model fit statistics
    ##   Log-likelihood         : -1107.11
    ##   BIC                    : 2835.19
    ##   Number of parameters   : 138
    ##
    ## 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)            1.721848e+00
    ## cluster2               2.220446e-16
    ## hs_child_age_yrs_None -1.654292e-01
    ## sex_male               1.034161e-01
    ##
    ## (2) Z: mean of omics data for each latent cluster
    ##                 mu_cluster1   mu_cluster2
    ## miR.101.3p     1.399631e+00  1.399631e+00
    ## miR.125a.5p    1.400000e+00  1.400000e+00
    ## miR.125b.1.3p  1.400000e+00  1.400000e+00
    ## miR.127.3p     4.294969e-17  4.294969e-17
    ## miR.140.5p     7.000762e-17  7.000762e-17
    ## miR.142.3p     3.087227e-17  3.087227e-17
    ## miR.144.5p     1.571613e-17  1.571613e-17
    ## miR.19a.3p    -2.304965e-17 -2.304965e-17
    ## miR.19b.3p     2.827525e-17  2.827525e-17
    ## miR.21.5p     -1.181994e-17 -1.181994e-17
    ##
    ## (3) E: intercept and odds ratio of being assigned to each latent cluster for each cluster from previous serial stage
    ##                               beta           OR
    ## (Intercept).cluster2     -45.00000 2.862519e-20
    ## Stage2.cluster2.cluster2 -24.00013 3.774635e-11

    serial_selection_report(serial_all_early_pen)

    ## [[1]]
    ## [[1]]$stage
    ## [1] 1
    ##
    ## [[1]]$model
    ## [1] "early"
    ##
    ## [[1]]$selected_G
    ## [1] 3
    ##
    ## [[1]]$total_G
    ## [1] 9
    ##
    ## [[1]]$selected_Z
    ## [1] 10
    ##
    ## [[1]]$total_Z
    ## [1] 10
    ##
    ##
    ## [[2]]
    ## [[2]]$stage
    ## [1] 2
    ##
    ## [[2]]$model
    ## [1] "early"
    ##
    ## [[2]]$selected_G
    ## [1] NA
    ##
    ## [[2]]$total_G
    ## [1] NA
    ##
    ## [[2]]$selected_Z
    ## [1] 10
    ##
    ## [[2]]$total_Z
    ## [1] 10
    ##
    ##
    ## [[3]]
    ## [[3]]$stage
    ## [1] 3
    ##
    ## [[3]]$model
    ## [1] "early"
    ##
    ## [[3]]$selected_G
    ## [1] NA
    ##
    ## [[3]]$total_G
    ## [1] NA
    ##
    ## [[3]]$selected_Z
    ## [1] 10
    ##
    ## [[3]]$total_Z
    ## [1] 10

### 9.1b Object structure: what’s in the fitted object

    names(serial_all_early_pen)

    ##  [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"

Two fields are serial-only, beyond what early/parallel already showed:

- `submodel`: the fitted stage models themselves, one per stage, each a complete `early_lucid` or `lucid_parallel` object in its own right (with its own `res_Beta`, `select`, `likelihood`, etc.) – explored below.
- `res_Delta`: the between-stage transition coefficients, one element per transition (`n_stages - 1` of them), structured like `res_Beta` for the stage that estimated it.

`likelihood` and `select` at the top level are present for serial too, the same as early/parallel – but unlike those two, a serial fit has no single joint EM loop, so both are aggregates over stages rather than values from one fit:

    cat("top-level likelihood (sum over stages):", serial_all_early_pen$likelihood, "\n")

    ## top-level likelihood (sum over stages): -3527.42

    cat("per-stage likelihoods:",
        paste(round(sapply(serial_all_early_pen$submodel, `[[`, "likelihood"), 2),
              collapse = ", "), "\n")

    ## per-stage likelihoods: -1202.57, -1217.74, -1107.11

    cat("n_stages:", length(serial_all_early_pen$submodel), "\n")

    ## n_stages: 3

The fit’s top-level `select` is stage 1’s own selection only, not one combined across all three stages. That is deliberate: stage 1 is the only stage whose “G” is the cohort’s actual exposures. From stage 2 onward, “G” is the previous stage’s latent cluster-membership probabilities – there are no real exposures left to select among there, so `Rho_G` is forced to 0 for every stage after the first regardless of what penalty was requested, and that stage’s `select$selectG` is not a meaningful exposure-selection result. The complete per-stage record (including every stage’s real omics selection, `selectZ`) is always available via `serial_selection_report()` above, or directly at `fit$submodel[[i]]$select`.

    # Top-level select == stage 1's select
    identical(serial_all_early_pen$select, serial_all_early_pen$submodel[[1]]$select)

    ## [1] TRUE

    cat("Stage 1 exposures kept:",
        paste(names(which(get_selected_G(serial_all_early_pen))), collapse = ", "), "\n")

    ## Stage 1 exposures kept: g_causal_1, g_causal_2, g_causal_3

    # Rho_G is 0 from stage 2 on -- there is nothing there to select
    sapply(serial_all_early_pen$submodel, function(sm) sm$Rho$Rho_G)

    ## [1] 0.05 0.00 0.00

### 9.2 Zero-penalty selected-input refit

    set.seed(1302)

    serial_all_early_inputs_refit <- prepare_serial_selected_inputs(
      serial_all_early_pen, G, Z_serial_all_early
    )
    serial_all_early_refit <- refit_selected(
      "serial",
      fit_pen = serial_all_early_pen,
      inputs = serial_all_early_inputs_refit,
      Y = Y,
      CoG = CoG,
      CoY = CoY,
      seed = 1302,
      verbose = TRUE
    )

    ## Fitting LUCID serial model (Stage 1/3)...
    ## 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 = -1200.841
    ## iteration 2 : E-step finished.
    ## iteration 2: log-likelihood = -1198.968
    ## iteration 3 : E-step finished.
    ## iteration 3: log-likelihood = -1198.666
    ## iteration 4 : E-step finished.
    ## iteration 4: log-likelihood = -1198.662
    ## Success: LUCID early model converged!
    ##
    ## Finished LUCID early model: log-likelihood = -1198.662.
    ##
    ## Fitting LUCID serial model (Stage 2/3)...
    ## Intializing imputation of missing values in 'Z' via LOD / sqrt(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 = -1222.486
    ## iteration 2 : E-step finished.
    ## iteration 2: log-likelihood = -1221.061
    ## iteration 3 : E-step finished.
    ## iteration 3: log-likelihood = -1217.788
    ## iteration 4 : E-step finished.
    ## iteration 4: log-likelihood = -1217.538
    ## iteration 5 : E-step finished.
    ## iteration 5: log-likelihood = -1217.524
    ## iteration 6 : E-step finished.
    ## iteration 6: log-likelihood = -1217.524
    ## Success: LUCID early model converged!
    ##
    ## Finished LUCID early model: log-likelihood = -1217.524.
    ##
    ## Fitting LUCID serial model (Stage 3/3)...
    ## Intializing imputation of missing values in 'Z' via LOD / sqrt(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 = -1023.079
    ## iteration 2 : E-step finished.
    ## iteration 2: log-likelihood = -998.902
    ## iteration 3 : E-step finished.
    ## iteration 3: log-likelihood = -984.796
    ## iteration 4 : E-step finished.
    ## iteration 4: log-likelihood = -975.734
    ## iteration 5 : E-step finished.
    ## iteration 5: log-likelihood = -965.927
    ## iteration 6 : E-step finished.
    ## iteration 6: log-likelihood = -954.788
    ## iteration 7 : E-step finished.
    ## iteration 7: log-likelihood = -949.270
    ## iteration 8 : E-step finished.

    ## iteration 8: log-likelihood = -940.423
    ## iteration 9 : E-step finished.
    ## iteration 9: log-likelihood = -938.932
    ## iteration 10 : E-step finished.
    ## iteration 10: log-likelihood = -938.932
    ## Success: LUCID early model converged!
    ##
    ## Finished LUCID early model: log-likelihood = -938.932.
    ##
    ## Success: LUCID serial model constructed!

    summary(serial_all_early_refit)

    ##
    ## ====================================================
    ## LUCID Serial: Model Summary
    ## ====================================================
    ##
    ## Model specification
    ##   Family                 : normal
    ##   Number of observations : 90
    ##   Number of stages       : 3
    ##   Stage 1               : early (K = 2)
    ##   Stage 2               : early (K = 2)
    ##   Stage 3               : early (K = 2)
    ##
    ## Missing-data profile by stage
    ##   Stage 1
    ##     Listwise rows         : 1
    ##     Sporadic rows         : 0
    ##   Stage 2
    ##     Listwise rows         : 0
    ##     Sporadic rows         : 1
    ##   Stage 3
    ##     Listwise rows         : 0
    ##     Sporadic rows         : 1
    ##
    ## Model fit statistics
    ##   Log-likelihood         : -3355.12
    ##   BIC                    : 8537.16
    ##
    ## Regularization
    ##   Rho_G                  : 0.000
    ##   Rho_Z_Mu               : 0.000
    ##   Rho_Z_Cov              : 0.000
    ##
    ## Stage-wise detailed parameter estimates
    ##
    ## --- Stage 1 (early) ---
    ##
    ## ====================================================
    ## LUCID Early Integration: Model Summary
    ## ====================================================
    ##
    ## Model specification
    ##   Family                 : normal
    ##   Number of observations : 90
    ##   Number of clusters (K) : 2
    ##
    ## Missing-data profile
    ##   Listwise missing rows  : 1 / 90 (1.1%)
    ##   Sporadic missing rows  : 0 / 90 (0.0%)
    ##   Missing cells total    : 10 / 900 (1.1%)
    ##
    ## Feature selection overview
    ##   G features selected    : 3 / 3 (100.0%)
    ##   Z features selected    : 10 / 10 (100.0%)
    ##
    ## Model fit statistics
    ##   Log-likelihood         : -1198.66
    ##   BIC                    : 3009.30
    ##   Number of parameters   : 136
    ##
    ## 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
    ##               mu_cluster1 mu_cluster2
    ## cg_GRHL3       0.01914989  2.78568555
    ## cg_BTF3L4      0.02487383  2.77032608
    ## cg_AL358472.7 -0.05976249  2.85053277
    ## cg_HDGF        0.19307368 -0.18895441
    ## cg_TDRD5      -0.13876544  0.13344743
    ## cg_CSRNP3     -0.07408488  0.08018928
    ## cg_HSPD1       0.18585941 -0.20517509
    ## cg_EPM2AIP1   -0.03862822  0.03885292
    ## cg_AC025171.1 -0.07293658  0.04252108
    ## cg_VTRNA1_3    0.15834572 -0.18442375
    ##
    ## (2) E: intercept and odds ratio of being assigned to each latent cluster for each exposure
    ##                                      beta        OR
    ## (Intercept).cluster2           -1.1615235 0.3130090
    ## g_causal_1.cluster2             1.4615405 4.3125979
    ## g_causal_2.cluster2            -1.4789587 0.2278748
    ## g_causal_3.cluster2             0.4021624 1.4950541
    ## hs_child_age_yrs_None.cluster2  0.1161985 1.1232188
    ## sex_male.cluster2               1.0246714 2.7861797
    ##
    ## --- Stage 2 (early) ---
    ##
    ## ====================================================
    ## LUCID Early Integration: Model Summary
    ## ====================================================
    ##
    ## Model specification
    ##   Family                 : normal
    ##   Number of observations : 90
    ##   Number of clusters (K) : 2
    ##
    ## Missing-data profile
    ##   Listwise missing rows  : 0 / 90 (0.0%)
    ##   Sporadic missing rows  : 1 / 90 (1.1%)
    ##   Missing cells total    : 1 / 900 (0.1%)
    ##
    ## Feature selection overview
    ##   G features selected    : 1 / 1 (100.0%)
    ##   Z features selected    : 10 / 10 (100.0%)
    ##
    ## Model fit statistics
    ##   Log-likelihood         : -1217.52
    ##   BIC                    : 3029.02
    ##   Number of parameters   : 132
    ##
    ## 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
    ##                   mu_cluster1   mu_cluster2
    ## tc_TC01006069_nc -2.770982428 -0.2004050509
    ## tc_SLC9A4        -3.075852912  0.0375632835
    ## tc_RAB6C_AS1     -2.807651889 -0.1683196643
    ## tc_LOC100129029  -0.052357769  0.0458124873
    ## tc_BRE           -0.112174378  0.0981513802
    ## tc_TC03001220_nc  0.060238498 -0.0527080414
    ## tc_TC04002114_nc -0.036339702  0.0317968501
    ## tc_TC04002369_nc  0.078990703 -0.0691160198
    ## tc_BEND4         -0.063252955  0.0553456586
    ## tc_SLC9A3        -0.001011683  0.0008852122
    ##
    ## (2) E: intercept and odds ratio of being assigned to each latent cluster for each cluster from previous serial stage
    ##                               beta           OR
    ## (Intercept).cluster2      35.89575 3.884436e+15
    ## Stage1.cluster2.cluster2 -38.55656 1.799286e-17
    ##
    ## --- Stage 3 (early) ---
    ##
    ## ====================================================
    ## LUCID Early Integration: Model Summary
    ## ====================================================
    ##
    ## Model specification
    ##   Family                 : normal
    ##   Number of observations : 90
    ##   Number of clusters (K) : 2
    ##
    ## Missing-data profile
    ##   Listwise missing rows  : 0 / 90 (0.0%)
    ##   Sporadic missing rows  : 1 / 90 (1.1%)
    ##   Missing cells total    : 1 / 900 (0.1%)
    ##
    ## Feature selection overview
    ##   G features selected    : 1 / 1 (100.0%)
    ##   Z features selected    : 10 / 10 (100.0%)
    ##
    ## Model fit statistics
    ##   Log-likelihood         : -938.93
    ##   BIC                    : 2498.84
    ##   Number of parameters   : 138
    ##
    ## 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)            1.0162700
    ## cluster2               1.3386950
    ## hs_child_age_yrs_None -0.1394871
    ## sex_male              -0.1010743
    ##
    ## (2) Z: mean of omics data for each latent cluster
    ##                mu_cluster1  mu_cluster2
    ## miR.101.3p    -0.006807900  3.007913039
    ## miR.125a.5p    0.187538247  2.785670575
    ## miR.125b.1.3p  0.027408679  2.968675795
    ## miR.127.3p    -0.107628701  0.123004230
    ## miR.140.5p     0.090862876 -0.103843286
    ## miR.142.3p    -0.004784527  0.005468031
    ## miR.144.5p     0.080101772 -0.091544883
    ## miR.19a.3p     0.060465036 -0.069102898
    ## miR.19b.3p     0.108156640 -0.123607589
    ## miR.21.5p      0.102286783 -0.116899180
    ##
    ## (3) E: intercept and odds ratio of being assigned to each latent cluster for each cluster from previous serial stage
    ##                               beta           OR
    ## (Intercept).cluster2      20.28449 6.448276e+08
    ## Stage2.cluster2.cluster2 -33.53361 2.732356e-15

As with the other architectures, this refit’s coefficients are unshrunk versions of the screening fit’s; a serial model additionally reports one `X -> Z`/`G -> X` block per stage, plus `res_Delta`, the transition coefficients linking each stage’s cluster to the next stage’s.

### 9.3 Bootstrap CI + summary

    set.seed(1303)

    serial_all_early_boot <- boot_lucid(
      G = serial_all_early_inputs_refit$G,
      Z = serial_all_early_inputs_refit$Z,
      Y = Y,
      CoG = CoG,
      CoY = CoY,
      model = serial_all_early_refit,
      R = 2,
      conf = 0.90
    )

    summary(serial_all_early_refit, boot.se = serial_all_early_boot)

    ##
    ## ====================================================
    ## LUCID Serial: Model Summary
    ## ====================================================
    ##
    ## Model specification
    ##   Family                 : normal
    ##   Number of observations : 90
    ##   Number of stages       : 3
    ##   Stage 1               : early (K = 2)
    ##   Stage 2               : early (K = 2)
    ##   Stage 3               : early (K = 2)
    ##
    ## Missing-data profile by stage
    ##   Stage 1
    ##     Listwise rows         : 1
    ##     Sporadic rows         : 0
    ##   Stage 2
    ##     Listwise rows         : 0
    ##     Sporadic rows         : 1
    ##   Stage 3
    ##     Listwise rows         : 0
    ##     Sporadic rows         : 1
    ##
    ## Model fit statistics
    ##   Log-likelihood         : -3355.12
    ##   BIC                    : 8537.16
    ##
    ## Regularization
    ##   Rho_G                  : 0.000
    ##   Rho_Z_Mu               : 0.000
    ##   Rho_Z_Cov              : 0.000
    ##
    ## Stage-wise detailed parameter estimates
    ##
    ## --- Stage 1 (early) ---
    ##
    ## ====================================================
    ## LUCID Early Integration: Model Summary
    ## ====================================================
    ##
    ## Model specification
    ##   Family                 : normal
    ##   Number of observations : 90
    ##   Number of clusters (K) : 2
    ##
    ## Missing-data profile
    ##   Listwise missing rows  : 1 / 90 (1.1%)
    ##   Sporadic missing rows  : 0 / 90 (0.0%)
    ##   Missing cells total    : 10 / 900 (1.1%)
    ##
    ## Feature selection overview
    ##   G features selected    : 3 / 3 (100.0%)
    ##   Z features selected    : 10 / 10 (100.0%)
    ##
    ## Model fit statistics
    ##   Log-likelihood         : -1198.66
    ##   BIC                    : 3009.30
    ##   Number of parameters   : 136
    ##
    ## 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
    ##                           estimate   norm_lower  norm_upper sig
    ## cg_GRHL3.cluster1       0.01915096 -0.560219385  0.13496290
    ## cg_BTF3L4.cluster1      0.02487535 -1.331234082  0.40616099
    ## cg_AL358472.7.cluster1 -0.05976023 -1.616162333  0.23940750
    ## cg_HDGF.cluster1        0.19307498  0.187939832  0.47980106   *
    ## cg_TDRD5.cluster1      -0.13876549 -0.183559301 -0.15556152   *
    ## cg_CSRNP3.cluster1     -0.07408533 -0.055335868  0.16184040
    ## cg_HSPD1.cluster1       0.18585798 -0.161082010  0.20993766
    ## cg_EPM2AIP1.cluster1   -0.03862977 -0.508224892 -0.21767360   *
    ## cg_AC025171.1.cluster1 -0.07293744 -0.646622364  0.34345353
    ## cg_VTRNA1_3.cluster1    0.15834580 -0.163113495 -0.03726939   *
    ## cg_GRHL3.cluster2       2.78568846  3.009673503  3.30757464   *
    ## cg_BTF3L4.cluster2      2.77032851  2.848517574  3.66324114   *
    ## cg_AL358472.7.cluster2  2.85053469  2.900072512  3.81622141   *
    ## cg_HDGF.cluster2       -0.18895629 -0.198831953 -0.15492008   *
    ## cg_TDRD5.cluster2       0.13344788  0.039140341  0.55098278   *
    ## cg_CSRNP3.cluster2      0.08018997 -0.506828004  0.03702560
    ## cg_HSPD1.cluster2      -0.20517418 -0.694250160  0.09944692
    ## cg_EPM2AIP1.cluster2    0.03885462  0.001790297  0.40901997   *
    ## cg_AC025171.1.cluster2  0.04252212 -0.200343129  0.12291526
    ## cg_VTRNA1_3.cluster2   -0.18442433 -0.474583848  0.41718925
    ##
    ## (2) E: intercept and odds ratio of being assigned to each latent cluster for each exposure
    ##                                  estimate    norm_lower norm_upper sig
    ## (Intercept).cluster2           -1.1615304 -21.172239882  9.5377330
    ## g_causal_1.cluster2             1.4615242  -2.836796335  5.1504014
    ## g_causal_2.cluster2            -1.4789441  -3.907556125  0.8367722
    ## g_causal_3.cluster2             0.4021613   0.692934691  2.2174983   *
    ## hs_child_age_yrs_None.cluster2  0.1161995  -1.919663072  3.2306015
    ## sex_male.cluster2               1.0246523  -0.000351442  2.1629915
    ##
    ## --- Stage 2 (early) ---
    ##
    ## ====================================================
    ## LUCID Early Integration: Model Summary
    ## ====================================================
    ##
    ## Model specification
    ##   Family                 : normal
    ##   Number of observations : 90
    ##   Number of clusters (K) : 2
    ##
    ## Missing-data profile
    ##   Listwise missing rows  : 0 / 90 (0.0%)
    ##   Sporadic missing rows  : 1 / 90 (1.1%)
    ##   Missing cells total    : 1 / 900 (0.1%)
    ##
    ## Feature selection overview
    ##   G features selected    : 1 / 1 (100.0%)
    ##   Z features selected    : 10 / 10 (100.0%)
    ##
    ## Model fit statistics
    ##   Log-likelihood         : -1217.52
    ##   BIC                    : 3029.02
    ##   Number of parameters   : 132
    ##
    ## 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
    ##                                estimate   norm_lower  norm_upper sig
    ## tc_TC01006069_nc.cluster1 -2.7709824276 -4.190443506 -2.18817793   *
    ## tc_SLC9A4.cluster1        -3.0758531121 -5.058980233 -1.92956811   *
    ## tc_RAB6C_AS1.cluster1     -2.8076518895 -3.997619035 -2.42525369   *
    ## tc_LOC100129029.cluster1  -0.0523577689 -0.246014096 -0.04538028   *
    ## tc_BRE.cluster1           -0.1121743783 -0.245962867 -0.16861658   *
    ## tc_TC03001220_nc.cluster1  0.0602384985 -0.390340063  0.58835541
    ## tc_TC04002114_nc.cluster1 -0.0363397019  0.115294247  0.16294518   *
    ## tc_TC04002369_nc.cluster1  0.0789907033 -0.383389214  0.15683807
    ## tc_BEND4.cluster1         -0.0632529551 -0.330267409 -0.29236447   *
    ## tc_SLC9A3.cluster1        -0.0010116834  0.023803222  0.33148801   *
    ## tc_TC01006069_nc.cluster2 -0.2004050509 -0.616093851  1.31924221
    ## tc_SLC9A4.cluster2         0.0375632835 -0.127558724  1.08815992
    ## tc_RAB6C_AS1.cluster2     -0.1683196643 -1.009193180  1.41397788
    ## tc_LOC100129029.cluster2   0.0458124873 -0.407162907  0.55574912
    ## tc_BRE.cluster2            0.0981513803 -0.144741630  0.40124340
    ## tc_TC03001220_nc.cluster2 -0.0527080414 -1.019652531  0.58263383
    ## tc_TC04002114_nc.cluster2  0.0317968501 -1.218955229  0.80815419
    ## tc_TC04002369_nc.cluster2 -0.0691160198  0.024541724  0.24264268   *
    ## tc_BEND4.cluster2          0.0553456586  0.003256134  0.57778269   *
    ## tc_SLC9A3.cluster2         0.0008852121 -0.730096272  0.11598471
    ##
    ## (2) 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      35.88846   69.82898   72.19481   *
    ## Stage1.cluster2.cluster2 -38.54926  -76.64323  -74.27716   *
    ##
    ## --- Stage 3 (early) ---
    ##
    ## ====================================================
    ## LUCID Early Integration: Model Summary
    ## ====================================================
    ##
    ## Model specification
    ##   Family                 : normal
    ##   Number of observations : 90
    ##   Number of clusters (K) : 2
    ##
    ## Missing-data profile
    ##   Listwise missing rows  : 0 / 90 (0.0%)
    ##   Sporadic missing rows  : 1 / 90 (1.1%)
    ##   Missing cells total    : 1 / 900 (0.1%)
    ##
    ## Feature selection overview
    ##   G features selected    : 1 / 1 (100.0%)
    ##   Z features selected    : 10 / 10 (100.0%)
    ##
    ## Model fit statistics
    ##   Log-likelihood         : -938.93
    ##   BIC                    : 2498.84
    ##   Number of parameters   : 138
    ##
    ## 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)            1.7218477 -0.8888016  6.9577278
    ## Y.LC2                    0.0000000 -5.2062644  2.0757824
    ## Y.hs_child_age_yrs_None -0.1654292 -0.2352059 -0.0500037   *
    ## Y.sex_male               0.1034161 -0.3582500  0.7067403
    ##
    ## (2) Z: mean of omics data for each latent cluster
    ##                             estimate   norm_lower norm_upper sig
    ## miR.101.3p.cluster1     1.399631e+00  0.310403894 3.87707042   *
    ## miR.125a.5p.cluster1    1.400000e+00  0.987206375 2.39048372   *
    ## miR.125b.1.3p.cluster1  1.400000e+00  0.591519533 2.93104695   *
    ## miR.127.3p.cluster1     4.294969e-17 -0.538066124 0.92990821
    ## miR.140.5p.cluster1     7.000762e-17 -0.997425443 0.55933364
    ## miR.142.3p.cluster1     3.087227e-17 -0.853795594 0.41383900
    ## miR.144.5p.cluster1     1.571613e-17 -2.590246247 1.09638121
    ## miR.19a.3p.cluster1    -2.304965e-17 -0.525985944 0.23234210
    ## miR.19b.3p.cluster1     2.827525e-17 -0.843952480 0.42582390
    ## miR.21.5p.cluster1     -1.181994e-17 -1.035149405 0.62064189
    ## miR.101.3p.cluster2     1.399631e+00  1.092541036 1.91539430   *
    ## miR.125a.5p.cluster2    1.400000e+00  1.312499636 1.57461655   *
    ## miR.125b.1.3p.cluster2  1.400000e+00  1.240922694 1.30228058   *
    ## miR.127.3p.cluster2     4.294969e-17 -0.167841650 0.00134918
    ## miR.140.5p.cluster2     7.000762e-17  0.085588025 0.19077469   *
    ## miR.142.3p.cluster2     3.087227e-17  0.007181168 0.16614184   *
    ## miR.144.5p.cluster2     1.571613e-17 -0.022043374 0.21487153
    ## miR.19a.3p.cluster2    -2.304965e-17 -0.102841413 0.31468694
    ## miR.19b.3p.cluster2     2.827525e-17 -0.032782855 0.30627791
    ## miR.21.5p.cluster2     -1.181994e-17  0.132396412 0.18941780   *
    ##
    ## (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     45.00000   26.61747   91.10516   *
    ## Stage2.cluster2.cluster2 24.00014  -10.49577   90.94478

Each stage’s coefficient tables get their own `sig` markers, independently of the other stages – a stage-2 effect being significant says nothing about stage 3, since each stage’s bootstrap resamples and refits that stage’s own conditional model.

## 10) Serial Model Tutorial B: Mixed Parallel + Early

Serial mixed architecture used here:

- Stage 1: parallel submodel (methylome + transcriptome together)
- Stage 2: early submodel (miRNA)

### 10.1 Penalized screening fit

    set.seed(1401)

    # Nested list signals a parallel submodel at stage 1, followed by early stage 2.
    Z_serial_mixed <- list(
      list(
        methylome = Z_parallel_miss[[1]],
        transcriptome = Z_parallel_miss[[2]]
      ),
      miRNA = Z_parallel_miss[[3]]
    )

    serial_mixed_pen <- estimate_lucid(
      lucid_model = "serial",
      G = G,
      Z = Z_serial_mixed,
      Y = Y,
      CoG = CoG,
      CoY = CoY,
      family = "normal",
      K = list(list(2, 2), 2),
      Rho_G = RHO_G,
      Rho_Z_Mu = 0,
      Rho_Z_Cov = 0,
      max_itr = 15,
      max_tot.itr = 40,
      tol = 1e-2,
      seed = 1401,
      verbose = FALSE
    )

    ## Fitting LUCID serial model (2 stages)...
    ##   Stage 1/2 (parallel) finished: log-likelihood = -2444.642. Selected G: 3/9; Selected Z by layer: 10/10, 10/10.
    ##   Stage 2/2 (early) finished: log-likelihood = -1107.108.
    ## Finished LUCID serial model.

    summary(serial_mixed_pen)

    ##
    ## ====================================================
    ## LUCID Serial: Model Summary
    ## ====================================================
    ##
    ## Model specification
    ##   Family                 : normal
    ##   Number of observations : 90
    ##   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         : 0
    ##     Sporadic rows         : 1
    ##
    ## Model fit statistics
    ##   Log-likelihood         : -3551.75
    ##   BIC                    : 8952.92
    ##
    ## Regularization
    ##   Rho_G                  : 0.050
    ##   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 : 90
    ##   Clusters per layer     : 2, 2
    ##
    ## Missing-data profile by layer
    ##   Layer 1 listwise rows : 1 / 90 (1.1%)
    ##   Layer 1 sporadic rows : 0 / 90 (0.0%)
    ##   Layer 1 missing cells : 10 / 900 (1.1%)
    ##   Layer 2 listwise rows : 0 / 90 (0.0%)
    ##   Layer 2 sporadic rows : 1 / 90 (1.1%)
    ##   Layer 2 missing cells : 1 / 900 (0.1%)
    ##
    ## Feature selection overview
    ##   G features selected    : 3 / 9 (33.3%)
    ##   G features by layer
    ##     Layer 1              : 3 / 9 (33.3%)
    ##     Layer 2              : 3 / 9 (33.3%)
    ##   Z features
    ##     Layer 1 selected     : 10 / 10 (100.0%)
    ##     Layer 1 multi-cluster: 10
    ##     Layer 2 selected     : 10 / 10 (100.0%)
    ##     Layer 2 multi-cluster: 10
    ##
    ## Model fit statistics
    ##   Log-likelihood         : -2444.64
    ##   BIC                    : 6113.23
    ##   Number of parameters   : 272
    ##
    ## Regularization
    ##   Rho_G                  : 0.050
    ##   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
    ## cg_GRHL3       0.01960667  2.78730293
    ## cg_BTF3L4      0.02561217  2.77163898
    ## cg_AL358472.7 -0.05845803  2.85138969
    ## cg_HDGF        0.19390770 -0.19009725
    ## cg_TDRD5      -0.13906115  0.13395573
    ## cg_CSRNP3     -0.07422600  0.08045023
    ## cg_HSPD1       0.18521867 -0.20481312
    ## cg_EPM2AIP1   -0.03952313  0.03982859
    ## cg_AC025171.1 -0.07347676  0.04316178
    ## cg_VTRNA1_3    0.15848563 -0.18482556
    ##
    ## Layer 2
    ##
    ##                  mu_cluster1  mu_cluster2
    ## tc_TC01006069_nc -2.76171772 -0.151431669
    ## tc_SLC9A4        -3.02348512  0.061983212
    ## tc_RAB6C_AS1     -2.73581445 -0.175182553
    ## tc_LOC100129029  -0.04727047  0.043342620
    ## tc_BRE           -0.07428108  0.068108837
    ## tc_TC03001220_nc  0.02974284 -0.027271414
    ## tc_TC04002114_nc -0.03156595  0.028943037
    ## tc_TC04002369_nc  0.13303025 -0.121976355
    ## tc_BEND4         -0.08701134  0.079781296
    ## tc_SLC9A3        -0.00834082  0.007647755
    ##
    ## (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.23378217 0.7915342
    ## g_causal_1.cluster2   0.80753183 2.2423666
    ## g_causal_2.cluster2  -0.77145952 0.4623378
    ## g_causal_3.cluster2   0.05322757 1.0546696
    ##
    ## Layer 2
    ##
    ##                            beta        OR
    ## (Intercept).cluster2  0.8377907 2.3112551
    ## g_causal_1.cluster2  -0.9250451 0.3965136
    ## g_causal_2.cluster2   0.8918749 2.4396996
    ## g_causal_3.cluster2  -0.2028611 0.8163916
    ##
    ##
    ## --- Stage 2 (early) ---
    ##
    ## ====================================================
    ## LUCID Early Integration: Model Summary
    ## ====================================================
    ##
    ## Model specification
    ##   Family                 : normal
    ##   Number of observations : 90
    ##   Number of clusters (K) : 2
    ##
    ## Missing-data profile
    ##   Listwise missing rows  : 0 / 90 (0.0%)
    ##   Sporadic missing rows  : 1 / 90 (1.1%)
    ##   Missing cells total    : 1 / 900 (0.1%)
    ##
    ## Feature selection overview
    ##   G features selected    : 2 / 2 (100.0%)
    ##   Z features selected    : 10 / 10 (100.0%)
    ##
    ## Model fit statistics
    ##   Log-likelihood         : -1107.11
    ##   BIC                    : 2839.69
    ##   Number of parameters   : 139
    ##
    ## 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)            1.7218477
    ## cluster2               0.0000000
    ## hs_child_age_yrs_None -0.1654292
    ## sex_male               0.1034161
    ##
    ## (2) Z: mean of omics data for each latent cluster
    ##                 mu_cluster1   mu_cluster2
    ## miR.101.3p     1.399631e+00  1.399631e+00
    ## miR.125a.5p    1.400000e+00  1.400000e+00
    ## miR.125b.1.3p  1.400000e+00  1.400000e+00
    ## miR.127.3p     4.294969e-17  4.294969e-17
    ## miR.140.5p     7.000762e-17  7.000762e-17
    ## miR.142.3p     3.087227e-17  3.087227e-17
    ## miR.144.5p     1.571613e-17  1.571613e-17
    ## miR.19a.3p    -2.304965e-17 -2.304965e-17
    ## miR.19b.3p     2.827525e-17  2.827525e-17
    ## miR.21.5p     -1.181994e-17 -1.181994e-17
    ##
    ## (3) E: intercept and odds ratio of being assigned to each latent cluster for each cluster from previous serial stage
    ##                                     beta           OR
    ## (Intercept).cluster2            45.00000 3.493427e+19
    ## Stage1.Layer1.cluster2.cluster2 22.42754 5.497402e+09
    ## Stage1.Layer2.cluster2.cluster2 23.47467 1.566466e+10

    serial_selection_report(serial_mixed_pen)

    ## [[1]]
    ## [[1]]$stage
    ## [1] 1
    ##
    ## [[1]]$model
    ## [1] "parallel"
    ##
    ## [[1]]$selected_G
    ## [1] 3
    ##
    ## [[1]]$total_G
    ## [1] 9
    ##
    ## [[1]]$selected_Z_by_layer
    ##     methylome transcriptome
    ##            10            10
    ##
    ## [[1]]$total_Z_by_layer
    ##     methylome transcriptome
    ##            10            10
    ##
    ##
    ## [[2]]
    ## [[2]]$stage
    ## [1] 2
    ##
    ## [[2]]$model
    ## [1] "early"
    ##
    ## [[2]]$selected_G
    ## [1] NA
    ##
    ## [[2]]$total_G
    ## [1] NA
    ##
    ## [[2]]$selected_Z
    ## [1] 10
    ##
    ## [[2]]$total_Z
    ## [1] 10

### 10.2 Zero-penalty selected-input refit

    set.seed(1402)

    serial_mixed_inputs_refit <- prepare_serial_selected_inputs(serial_mixed_pen, G, Z_serial_mixed)
    serial_mixed_refit <- refit_selected(
      "serial",
      fit_pen = serial_mixed_pen,
      inputs = serial_mixed_inputs_refit,
      Y = Y,
      CoG = CoG,
      CoY = CoY,
      seed = 1402,
      verbose = FALSE
    )

    ## Fitting LUCID serial model (2 stages)...
    ##   Stage 1/2 (parallel) finished: log-likelihood = -2435.579.
    ##   Stage 2/2 (early) finished: log-likelihood = -1107.108.
    ## Finished LUCID serial model.

    summary(serial_mixed_refit)

    ##
    ## ====================================================
    ## LUCID Serial: Model Summary
    ## ====================================================
    ##
    ## Model specification
    ##   Family                 : normal
    ##   Number of observations : 90
    ##   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         : 0
    ##     Sporadic rows         : 1
    ##
    ## Model fit statistics
    ##   Log-likelihood         : -3542.69
    ##   BIC                    : 8934.80
    ##
    ## 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 : 90
    ##   Clusters per layer     : 2, 2
    ##
    ## Missing-data profile by layer
    ##   Layer 1 listwise rows : 1 / 90 (1.1%)
    ##   Layer 1 sporadic rows : 0 / 90 (0.0%)
    ##   Layer 1 missing cells : 10 / 900 (1.1%)
    ##   Layer 2 listwise rows : 0 / 90 (0.0%)
    ##   Layer 2 sporadic rows : 1 / 90 (1.1%)
    ##   Layer 2 missing cells : 1 / 900 (0.1%)
    ##
    ## Feature selection overview
    ##   G features selected    : 3 / 3 (100.0%)
    ##   G features by layer
    ##     Layer 1              : 3 / 3 (100.0%)
    ##     Layer 2              : 3 / 3 (100.0%)
    ##   Z features
    ##     Layer 1 selected     : 10 / 10 (100.0%)
    ##     Layer 1 multi-cluster: 10
    ##     Layer 2 selected     : 10 / 10 (100.0%)
    ##     Layer 2 multi-cluster: 10
    ##
    ## Model fit statistics
    ##   Log-likelihood         : -2435.58
    ##   BIC                    : 6095.11
    ##   Number of parameters   : 272
    ##
    ## 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
    ## cg_GRHL3       0.01912123  2.78560954
    ## cg_BTF3L4      0.02483414  2.77026216
    ## cg_AL358472.7 -0.05982001  2.85048084
    ## cg_HDGF        0.19304072 -0.18890613
    ## cg_TDRD5      -0.13876550  0.13343713
    ## cg_CSRNP3     -0.07407272  0.08017097
    ## cg_HSPD1       0.18589857 -0.20520028
    ## cg_EPM2AIP1   -0.03858839  0.03880921
    ## cg_AC025171.1 -0.07291502  0.04249461
    ## cg_VTRNA1_3    0.15834446 -0.18440941
    ##
    ## Layer 2
    ##
    ##                   mu_cluster1  mu_cluster2
    ## tc_TC01006069_nc -2.761572789 -0.150145472
    ## tc_SLC9A4        -3.021703480  0.062198525
    ## tc_RAB6C_AS1     -2.735259250 -0.174299961
    ## tc_LOC100129029  -0.047193030  0.043320800
    ## tc_BRE           -0.073892278  0.067829351
    ## tc_TC03001220_nc  0.029444093 -0.027028179
    ## tc_TC04002114_nc -0.032501823  0.029835020
    ## tc_TC04002369_nc  0.133308600 -0.122370510
    ## tc_BEND4         -0.086573422  0.079469996
    ## tc_SLC9A3        -0.007742854  0.007107546
    ##
    ## (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 -1.1612442 0.3130964
    ## g_causal_1.cluster2   1.4619640 4.3144246
    ## g_causal_2.cluster2  -1.4793132 0.2277941
    ## g_causal_3.cluster2   0.4022122 1.4951286
    ##
    ## Layer 2
    ##
    ##                            beta        OR
    ## (Intercept).cluster2  2.2583148 9.5669530
    ## g_causal_1.cluster2  -1.8289379 0.1605840
    ## g_causal_2.cluster2   1.9595122 7.0958651
    ## g_causal_3.cluster2  -0.6722936 0.5105363
    ##
    ##
    ## --- Stage 2 (early) ---
    ##
    ## ====================================================
    ## LUCID Early Integration: Model Summary
    ## ====================================================
    ##
    ## Model specification
    ##   Family                 : normal
    ##   Number of observations : 90
    ##   Number of clusters (K) : 2
    ##
    ## Missing-data profile
    ##   Listwise missing rows  : 0 / 90 (0.0%)
    ##   Sporadic missing rows  : 1 / 90 (1.1%)
    ##   Missing cells total    : 1 / 900 (0.1%)
    ##
    ## Feature selection overview
    ##   G features selected    : 2 / 2 (100.0%)
    ##   Z features selected    : 10 / 10 (100.0%)
    ##
    ## Model fit statistics
    ##   Log-likelihood         : -1107.11
    ##   BIC                    : 2839.69
    ##   Number of parameters   : 139
    ##
    ## 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)            1.7218477
    ## cluster2               0.0000000
    ## hs_child_age_yrs_None -0.1654292
    ## sex_male               0.1034161
    ##
    ## (2) Z: mean of omics data for each latent cluster
    ##                 mu_cluster1   mu_cluster2
    ## miR.101.3p     1.399631e+00  1.399631e+00
    ## miR.125a.5p    1.400000e+00  1.400000e+00
    ## miR.125b.1.3p  1.400000e+00  1.400000e+00
    ## miR.127.3p     4.294969e-17  4.294969e-17
    ## miR.140.5p     7.000762e-17  7.000762e-17
    ## miR.142.3p     3.087227e-17  3.087227e-17
    ## miR.144.5p     1.571613e-17  1.571613e-17
    ## miR.19a.3p    -2.304965e-17 -2.304965e-17
    ## miR.19b.3p     2.827525e-17  2.827525e-17
    ## miR.21.5p     -1.181994e-17 -1.181994e-17
    ##
    ## (3) E: intercept and odds ratio of being assigned to each latent cluster for each cluster from previous serial stage
    ##                                     beta           OR
    ## (Intercept).cluster2            45.00000 3.493427e+19
    ## Stage1.Layer1.cluster2.cluster2 22.48854 5.843180e+09
    ## Stage1.Layer2.cluster2.cluster2 23.46155 1.546048e+10

Stage 1’s block here is itself a parallel-model summary (one `G -> X`/`X -> Z` per layer, as in section 8), nested inside the serial report; stage 2’s block looks like the single-layer early summary from section 7. This is the same mix-and-match structure `estimate_lucid()` fit – the summary just mirrors it.

### 10.3 Bootstrap CI + summary

    set.seed(1403)

    serial_mixed_boot <- boot_lucid(
      G = serial_mixed_inputs_refit$G,
      Z = serial_mixed_inputs_refit$Z,
      Y = Y,
      CoG = CoG,
      CoY = CoY,
      model = serial_mixed_refit,
      R = 2,
      conf = 0.90
    )

    summary(serial_mixed_refit, boot.se = serial_mixed_boot)

    ##
    ## ====================================================
    ## LUCID Serial: Model Summary
    ## ====================================================
    ##
    ## Model specification
    ##   Family                 : normal
    ##   Number of observations : 90
    ##   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         : 0
    ##     Sporadic rows         : 1
    ##
    ## Model fit statistics
    ##   Log-likelihood         : -3542.69
    ##   BIC                    : 8934.80
    ##
    ## 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 : 90
    ##   Clusters per layer     : 2, 2
    ##
    ## Missing-data profile by layer
    ##   Layer 1 listwise rows : 1 / 90 (1.1%)
    ##   Layer 1 sporadic rows : 0 / 90 (0.0%)
    ##   Layer 1 missing cells : 10 / 900 (1.1%)
    ##   Layer 2 listwise rows : 0 / 90 (0.0%)
    ##   Layer 2 sporadic rows : 1 / 90 (1.1%)
    ##   Layer 2 missing cells : 1 / 900 (0.1%)
    ##
    ## Feature selection overview
    ##   G features selected    : 3 / 3 (100.0%)
    ##   G features by layer
    ##     Layer 1              : 3 / 3 (100.0%)
    ##     Layer 2              : 3 / 3 (100.0%)
    ##   Z features
    ##     Layer 1 selected     : 10 / 10 (100.0%)
    ##     Layer 1 multi-cluster: 10
    ##     Layer 2 selected     : 10 / 10 (100.0%)
    ##     Layer 2 multi-cluster: 10
    ##
    ## Model fit statistics
    ##   Log-likelihood         : -2435.58
    ##   BIC                    : 6095.11
    ##   Number of parameters   : 272
    ##
    ## 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.cg_GRHL3.cluster1       0.01912123 -1.76591782  1.08957449
    ## Layer1.cg_BTF3L4.cluster1      0.02483414 -1.56392335  0.60882501
    ## Layer1.cg_AL358472.7.cluster1 -0.05982001 -2.49983992  1.51412113
    ## Layer1.cg_HDGF.cluster1        0.19304072 -0.13676063  0.53007265
    ## Layer1.cg_TDRD5.cluster1      -0.13876550  0.04279999  0.08967088   *
    ## Layer1.cg_CSRNP3.cluster1     -0.07407272 -0.38744792 -0.02713626   *
    ## Layer1.cg_HSPD1.cluster1       0.18589857  0.09732106  0.56600143   *
    ## Layer1.cg_EPM2AIP1.cluster1   -0.03858839 -0.20314251  0.40283484
    ## Layer1.cg_AC025171.1.cluster1 -0.07291502 -0.79468524  0.69947818
    ## Layer1.cg_VTRNA1_3.cluster1    0.15834446  0.16124472  0.52044475   *
    ## Layer1.cg_GRHL3.cluster2       2.78560954  2.11508584  3.77154458   *
    ## Layer1.cg_BTF3L4.cluster2      2.77026216  0.94043816  4.31284257   *
    ## Layer1.cg_AL358472.7.cluster2  2.85048084  2.89302945  3.04909715   *
    ## Layer1.cg_HDGF.cluster2       -0.18890613 -0.35700217  0.13189109
    ## Layer1.cg_TDRD5.cluster2       0.13343713 -0.78098123  0.54432161
    ## Layer1.cg_CSRNP3.cluster2      0.08017097  0.35478659  0.67236357   *
    ## Layer1.cg_HSPD1.cluster2      -0.20520028 -1.17934214 -0.25929254   *
    ## Layer1.cg_EPM2AIP1.cluster2    0.03880921 -0.63207854  0.27824886
    ## Layer1.cg_AC025171.1.cluster2  0.04249461 -0.86892123  0.99941966
    ## Layer1.cg_VTRNA1_3.cluster2   -0.18440941 -0.40993676 -0.15188080   *
    ##
    ## Layer 2
    ##
    ##                                      estimate  norm_lower  norm_upper sig
    ## Layer2.tc_TC01006069_nc.cluster1 -2.761572789 -2.63646793 -2.55846630   *
    ## Layer2.tc_SLC9A4.cluster1        -3.021703480 -2.80613353 -2.71283205   *
    ## Layer2.tc_RAB6C_AS1.cluster1     -2.735259250 -2.35323755 -2.35210713   *
    ## Layer2.tc_LOC100129029.cluster1  -0.047193030 -0.50490186 -0.00620643   *
    ## Layer2.tc_BRE.cluster1           -0.073892278 -0.06750852  0.72599118
    ## Layer2.tc_TC03001220_nc.cluster1  0.029444093 -0.12820967  0.24614538
    ## Layer2.tc_TC04002114_nc.cluster1 -0.032501823 -0.48694290 -0.20798121   *
    ## Layer2.tc_TC04002369_nc.cluster1  0.133308600 -0.08003833  0.15248825
    ## Layer2.tc_BEND4.cluster1         -0.086573422 -0.50500831  0.04879925
    ## Layer2.tc_SLC9A3.cluster1        -0.007742854 -0.52199443  0.32625656
    ## Layer2.tc_TC01006069_nc.cluster2 -0.150145472 -0.06294660  0.30806436
    ## Layer2.tc_SLC9A4.cluster2         0.062198525  0.52038606  0.86236798   *
    ## Layer2.tc_RAB6C_AS1.cluster2     -0.174299961  0.34637690  0.35208448   *
    ## Layer2.tc_LOC100129029.cluster2   0.043320800 -0.34898041  0.43435215
    ## Layer2.tc_BRE.cluster2            0.067829351 -0.20572483 -0.11546113   *
    ## Layer2.tc_TC03001220_nc.cluster2 -0.027028179 -0.38169361  0.26800311
    ## Layer2.tc_TC04002114_nc.cluster2  0.029835020 -0.02338799  0.35627002
    ## Layer2.tc_TC04002369_nc.cluster2 -0.122370510 -0.42281979  0.08865890
    ## Layer2.tc_BEND4.cluster2          0.079469996 -0.11664746  0.84758795
    ## Layer2.tc_SLC9A3.cluster2         0.007107546 -0.34541849  0.26602623
    ##
    ## (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 -1.1612442 -17.796723 10.9478773
    ## g_causal_1.cluster2   1.4619640   1.302864  1.3483204   *
    ## g_causal_2.cluster2  -1.4793132  -2.723976 -1.5981707   *
    ## g_causal_3.cluster2   0.4022122  -0.686069  0.2639282
    ##
    ## Layer 2
    ##
    ##                        estimate norm_lower norm_upper sig
    ## (Intercept).cluster2  2.2583148  -7.018743 -2.0453165   *
    ## g_causal_1.cluster2  -1.8289379  -2.843784 -1.7261267   *
    ## g_causal_2.cluster2   1.9595122   1.922211  2.8339436   *
    ## g_causal_3.cluster2  -0.6722936  -1.732719  0.4350202
    ##
    ##
    ## --- Stage 2 (early) ---
    ##
    ## ====================================================
    ## LUCID Early Integration: Model Summary
    ## ====================================================
    ##
    ## Model specification
    ##   Family                 : normal
    ##   Number of observations : 90
    ##   Number of clusters (K) : 2
    ##
    ## Missing-data profile
    ##   Listwise missing rows  : 0 / 90 (0.0%)
    ##   Sporadic missing rows  : 1 / 90 (1.1%)
    ##   Missing cells total    : 1 / 900 (0.1%)
    ##
    ## Feature selection overview
    ##   G features selected    : 2 / 2 (100.0%)
    ##   Z features selected    : 10 / 10 (100.0%)
    ##
    ## Model fit statistics
    ##   Log-likelihood         : -1107.11
    ##   BIC                    : 2839.69
    ##   Number of parameters   : 139
    ##
    ## 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.27612790 -1.86384979  0.5409223
    ## Y.LC2                    1.67962912  1.30028679  4.1801879   *
    ## Y.hs_child_age_yrs_None  0.00697466  0.06486525  0.2240818   *
    ## Y.sex_male              -0.03484710 -0.25917976  0.4412278
    ##
    ## (2) Z: mean of omics data for each latent cluster
    ##                           estimate  norm_lower  norm_upper sig
    ## miR.101.3p.cluster1     1.00767143 -0.46848145 3.551966074
    ## miR.125a.5p.cluster1    1.02214199 -0.40740233 3.124545028
    ## miR.125b.1.3p.cluster1  0.98660078 -0.38859882 2.716618132
    ## miR.127.3p.cluster1    -0.17566318 -0.51279702 0.009874648
    ## miR.140.5p.cluster1    -0.04203931 -0.31183601 0.744567222
    ## miR.142.3p.cluster1    -0.04467781 -0.16472906 0.582082049
    ## miR.144.5p.cluster1    -0.02912828  0.07280313 0.105660280   *
    ## miR.19a.3p.cluster1    -0.03719136 -0.01165770 0.472771626
    ## miR.19b.3p.cluster1    -0.03995137 -0.11679661 0.586949609
    ## miR.21.5p.cluster1     -0.03855807 -0.15688253 0.561810413
    ## miR.101.3p.cluster2     3.32767225  3.44634186 6.063642015   *
    ## miR.125a.5p.cluster2    3.25335736  3.43046073 5.712266716   *
    ## miR.125b.1.3p.cluster2  3.42768359  3.80266354 6.007112575   *
    ## miR.127.3p.cluster2     0.86161108  1.29605083 1.876082340   *
    ## miR.140.5p.cluster2     0.20619878  0.21960284 0.591897300   *
    ## miR.142.3p.cluster2     0.21914038  0.15850210 0.742165439   *
    ## miR.144.5p.cluster2     0.14287142  0.12514977 0.565944157   *
    ## miR.19a.3p.cluster2     0.18242005 -0.02549459 0.801350043
    ## miR.19b.3p.cluster2     0.19595766 -0.04709506 0.795938426
    ## miR.21.5p.cluster2      0.18912365 -0.11024086 0.747990863
    ##
    ## (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            -5.910562 -85.615328  15.397543
    ## Stage1.Layer1.cluster2.cluster2  5.350285  -6.741763  15.912604
    ## Stage1.Layer2.cluster2.cluster2 -8.116317 -64.446051   6.508083

With this, every architecture the package supports (early, parallel, a serial chain of early stages, and a serial chain mixing a parallel stage with an early stage) has been fit, refit unpenalized, bootstrapped, and summarized with significance markers – the full pipeline this tutorial set out to demonstrate.

## 11) Missing-Data Handling: Diagnostics and Verification

Sections 7-10 injected missingness and let `summary()` display the resulting profile. That shows the model noticed the missing values; it does not show it handled them correctly. This section checks the handling.

LUCID classifies every subject, per omics layer, into one of three states:

code | state | meaning
--- | --- | ---
1 | complete | every feature observed
2 | sporadic | some features observed, some missing
3 | listwise | no feature observed for that layer

The distinction matters: imputation is triggered by sporadic subjects only. A subject missing an entire layer has no within-layer information to condition on, so the omics term drops out of that subject’s likelihood and they contribute through the exposure and outcome arms instead. Inventing values for them would be fabricating data.

    na_early <- check_na(Z_early_miss, lucid_model = "early")

    cat("subjects by missingness code (1 complete, 2 sporadic, 3 listwise):\n")

    ## subjects by missingness code (1 complete, 2 sporadic, 3 listwise):

    print(table(na_early$indicator_na))

    ##
    ##  1  2  3
    ## 85  4  1

    cat("\nimpute_flag (TRUE only when sporadic subjects exist):",
        na_early$impute_flag, "\n")

    ##
    ## impute_flag (TRUE only when sporadic subjects exist): TRUE

`analyze_missing_pattern()` gives the per-feature and per-subject view. The count of distinct patterns is the number worth watching: the observed-data likelihood is evaluated once per distinct missingness pattern, so pattern diversity, not sparsity alone, is what drives fitting cost.

    pat <- analyze_missing_pattern(Z_early_miss)

    cat("distinct missingness patterns :", pat$n_patterns, "\n")

    ## distinct missingness patterns : 4

    cat("complete subjects             :", pat$n_complete, "\n")

    ## complete subjects             : 85

    cat("overall proportion missing    :", round(pat$total_missing, 4), "\n")

    ## overall proportion missing    : 0.0126

### 11.1 Verify the handling, do not just display it

The two claims above are checkable against the fitted object, so we check them rather than assert them in prose. Recall the injection at section 5: row 1 of `Z_early` was blanked entirely (listwise), and a few cells in rows 2-5 were blanked individually (sporadic).

    fitted_Z <- early_fit_pen$Z

    # The listwise subject must still be NA after fitting.
    stopifnot(all(is.na(fitted_Z[1, ])))

    # The sporadic cells must have been filled.
    stopifnot(!anyNA(fitted_Z[2:5, ]))

    # Observed values must never have been altered.
    obs <- !is.na(Z_early_miss)
    stopifnot(isTRUE(all.equal(fitted_Z[obs], Z_early_miss[obs])))

    cat("verified: listwise row still NA, sporadic cells filled,",
        "observed values unchanged\n")

    ## verified: listwise row still NA, sporadic cells filled, observed values unchanged

One asymmetry to know about when you go looking for imputed values: `fit$Z` holds the fitted omics for early and parallel models, but for a serial model it holds the omics as supplied, because imputation happens inside each stage. The imputed data for serial stage `i` is at `fit$submodel[[i]]$Z`.

    cat("early  fit$Z still has NAs? ", anyNA(early_fit_pen$Z), "\n")

    ## early  fit$Z still has NAs?  TRUE

    cat("serial fit$Z still has NAs? ", anyNA(serial_all_early_pen$Z[[1]]),
        "  (imputed data lives on submodel[[i]]$Z)\n")

    ## serial fit$Z still has NAs?  TRUE   (imputed data lives on submodel[[i]]$Z)

### 11.2 Single-value imputation, and scoring it

`safe_impute()` is the cruder alternative: fill each feature with one summary of its observed values. It is what LUCID uses to initialize before the EM algorithm takes over with model-based imputation, and it is exported so you can inspect that starting point. `check_imputation_quality()` scores any imputation by asking whether the filled values have shifted the centre or spread of the data relative to what was observed.

    Z_mean_filled <- safe_impute(Z_early_miss, method = "mean")

    qual <- check_imputation_quality(Z_early_miss, Z_mean_filled)
    cat("mean-filled  : valid =", qual$is_valid,
        " mean shift =", round(qual$mean_diff, 3),
        " sd ratio =", round(qual$sd_ratio, 3), "\n")

    ## mean-filled  : valid = FALSE  mean shift = 0  sd ratio = 0

An `sd_ratio` well below 1 is the signature of mean-filling: every imputed value sits exactly at the centre, so the imputed cells carry no variation. The model-based imputation inside the EM loop conditions on each subject’s observed coordinates and on their cluster, which is why it recovers the withheld values more accurately than this baseline.

### 11.3 Penalizing the omics: what the two penalties actually do

`Rho_G` and `Rho_Z_Mu` are both called LASSO penalties, but they act on different parts of the model and they live on completely different scales. This is the single easiest thing to get wrong.

`Rho_G` penalizes the exposure coefficients in the `G -> X` model. Values in the region of 0.01-0.1 are meaningful, which is why this tutorial uses `RHO_G = 0.05`.

`Rho_Z_Mu` penalizes the cluster-specific omics means, and it is compared against those means directly. Our signal features are separated by roughly three standard deviations, so a penalty of 0.02 – the value this tutorial used before – is far too small to threshold anything at all.

    sweep_rho_z <- function(rz) {
      invisible(capture.output(
        f <- estimate_lucid(
          lucid_model = "early", G = G, Z = Z_early_miss, Y = Y, CoG = CoG, CoY = CoY,
          family = "normal", K = 2, init_omic.data.model = "EEV",
          Rho_G = 0, Rho_Z_Mu = rz, Rho_Z_Cov = if (rz > 0) 0.02 else 0,
          max_itr = 30, max_tot.itr = 90, tol = 1e-2, seed = 1101
        )
      ))
      kept <- which(get_selected_Z(f))
      acc <- mean(get_cluster_assignment(f) == x_true + 1); acc <- max(acc, 1 - acc)
      data.frame(
        Rho_Z_Mu       = rz,
        features_kept  = length(kept),
        signal_kept    = sum(kept %in% signal_cols_early),
        converged      = isTRUE(f$em_control$converged),
        subgroup_recovery = round(acc, 2)
      )
    }

    do.call(rbind, lapply(c(0, 2, 10), sweep_rho_z))

    ##   Rho_Z_Mu features_kept signal_kept converged subgroup_recovery
    ## 1        0            30           9      TRUE              0.74
    ## 2        2            28           9     FALSE              0.61
    ## 3       10             7           2     FALSE              0.61

Two things to take from that table, and the second is the reason this tutorial leaves the omics unpenalized.

The penalty does eventually select. At 0.02 – the value this tutorial used before – nothing is dropped at all. It takes a penalty two to three orders of magnitude larger before the feature count moves.

But look at `signal_kept` as it does. The penalty is not preferentially keeping the features that carry the subgroup signal; it drops those alongside the rest. On this data it is shrinking indiscriminately rather than selecting.

And it costs cluster recovery. Watch the `subgroup_recovery` column against the known `x_true`. Unpenalized, the model recovers the planted subgroup well above chance. As the omics penalty rises, the cluster means are shrunk toward each other, the subgroups become harder to tell apart, and recovery falls back toward 0.5 while the fit stops converging.

That is not a bug; it is the bias-variance trade the penalty exists to make, and here the variance saving is not worth the bias. Two practical rules follow:

- Tune `Rho_Z_Mu` on the scale of your omics means, not by analogy with `Rho_G`. The documented working range is roughly 1-100.
- Check that penalization has not destroyed the structure you are modelling. `em_control$converged` and the cluster sizes are the cheap diagnostics; with simulated data you also have the truth, as here.

## 12) Prediction and G-Computation

Fitting is only half of it. `predict_lucid()` assigns clusters and predicts outcomes, either on new data or – which is how you extract the fitted cluster memberships – on the training data.

    # CoG and CoY must be supplied here exactly as they were at fitting time: the
    # G -> X design includes the CoG columns, so omitting them makes the design
    # matrix too narrow for the fitted coefficients.
    # lucid_model is auto-detected from early_fit_refit's own class.
    pred_train <- predict_lucid(
      model = early_fit_refit,
      G = early_inputs_refit$G, Z = early_inputs_refit$Z, Y = Y,
      CoG = CoG, CoY = CoY
    )

    # Predicting on the training data reproduces the fit's own assignment.
    stopifnot(identical(
      as.numeric(pred_train$pred.x),
      as.numeric(get_cluster_assignment(early_fit_refit))
    ))

    cat("cluster sizes:\n"); print(table(pred_train$pred.x))

    ## cluster sizes:

    ##
    ##  2
    ## 90

Cluster labels run `1..K`. Versions before 3.1.0 returned `0..K-1`, so code written against the old convention needs its `+ 1` removed.

### 12.1 What you may omit, and why

`predict_lucid()` takes both `Z` and `Y`, and both look optional from the signature. Only one of them is.

| `Y` supplied | `Y` omitted
--- | --- | ---
`Z` supplied | works – supervised | works – unsupervised
`Z` omitted | error | error
`Z` omitted, `g_computation = TRUE` | works | works

The asymmetry follows from what the E-step actually does. Cluster membership is a posterior formed from three likelihood terms: the exposures, the omics, and (optionally) the outcome. Dropping `Y` removes one term and leaves a perfectly well-defined posterior over the other two – that is unsupervised prediction. Dropping `Z` removes the term the clusters are defined by, and there is nothing left to condition on.

`g_computation = TRUE` is not a way around that. It is a different estimator: it discards the omics and outcome terms entirely and derives the posterior from the exposure path alone, which is exactly why it can run with no omics data and why it is the only mode that returns `pred.z`. Passing `Z` or `Y` to it is accepted but they are ignored, with a printed notice to that effect.

Rather than take the table on trust, run it:

    legal <- function(expr) {
      r <- try(suppressWarnings(suppressMessages(invisible(capture.output(force(expr))))),
               silent = TRUE)
      if (inherits(r, "try-error")) "error" else "works"
    }

    Gp <- early_inputs_refit$G
    Zp <- early_inputs_refit$Z
    call_p <- function(...) predict_lucid(model = early_fit_refit,
                                          G = Gp, CoG = CoG, CoY = CoY, ...)

    data.frame(
      Z              = c("supplied", "supplied", "omitted", "omitted", "omitted"),
      Y              = c("supplied", "omitted",  "supplied", "omitted", "omitted"),
      g_computation  = c(FALSE, FALSE, FALSE, FALSE, TRUE),
      result         = c(
        legal(call_p(Z = Zp, Y = Y)),
        legal(call_p(Z = Zp)),
        legal(call_p(Z = NULL, Y = Y)),
        legal(call_p(Z = NULL)),
        legal(call_p(Z = NULL, g_computation = TRUE))
      )
    )

    ##          Z        Y g_computation result
    ## 1 supplied supplied         FALSE  works
    ## 2 supplied  omitted         FALSE  works
    ## 3  omitted supplied         FALSE  error
    ## 4  omitted  omitted         FALSE  error
    ## 5  omitted  omitted          TRUE  works

And this is the message you get if you forget it, which names the one mode that relaxes the rule:

    cat(tryCatch(
      call_p(Z = NULL, Y = Y),
      error = function(e) conditionMessage(e)
    ))

    ## Input data 'Z' is required for prediction. Omit it only with g_computation = TRUE, which predicts from the exposures alone.

Two traps worth knowing before the sections below.

Pass `CoG` and `CoY` exactly as you fitted them. The `G -> X` design includes the `CoG` columns, so omitting them at prediction leaves the design matrix narrower than the fitted coefficients and you get `non-conformable arguments` – an error that says nothing about the actual mistake.

A single-stage serial model cannot be predicted. It fits without complaint, but a one-stage chain is a fully equivalent early or parallel model, and prediction declines it with a message saying so. Fit it as the model it is.

### 12.2 Prediction across all three architectures

The rule above applies identically to every model type. Here it is exercised on the three models this tutorial has already fitted, reporting what was actually predicted rather than only that the call returned.

    # 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)
      }))
    }

    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)
    }

    pred_early_m <- predict_lucid(
      model = early_fit_refit,
      G = early_inputs_refit$G, Z = early_inputs_refit$Z, Y = Y,
      CoG = CoG, CoY = CoY
    )
    pred_parallel_m <- predict_lucid(
      model = parallel_fit_refit,
      G = parallel_inputs_refit$G, Z = parallel_inputs_refit$Z, Y = Y,
      CoG = CoG, CoY = CoY
    )
    pred_serial_m <- predict_lucid(
      model = serial_all_early_refit,
      G = serial_all_early_inputs_refit$G, Z = serial_all_early_inputs_refit$Z, Y = Y,
      CoG = CoG, CoY = CoY
    )

    rbind(
      cluster_sizes(pred_early_m$pred.x,    "early"),
      cluster_sizes(pred_parallel_m$pred.x, "parallel (per layer)"),
      cluster_sizes(pred_serial_m$pred.x,   "serial (per stage)")
    )

    ##                   model block cluster  n
    ## 1                 early     -       2 90
    ## 2  parallel (per layer)     1       1 45
    ## 3  parallel (per layer)     1       2 45
    ## 4  parallel (per layer)     2       1 47
    ## 5  parallel (per layer)     2       2 43
    ## 6  parallel (per layer)     3       1 48
    ## 7  parallel (per layer)     3       2 42
    ## 8    serial (per stage)     1       1 45
    ## 9    serial (per stage)     1       2 45
    ## 10   serial (per stage)     2       1 42
    ## 11   serial (per stage)     2       2 48
    ## 12   serial (per stage)     3       1 48
    ## 13   serial (per stage)     3       2 42

Predicted outcomes for the same three models:

    rbind(
      outcome_summary(pred_early_m$pred.y,    "early"),
      outcome_summary(pred_parallel_m$pred.y, "parallel"),
      outcome_summary(pred_serial_m$pred.y,   "serial"),
      outcome_summary(Y,                      "observed outcome")
    )

    ##              model  n  mean    sd    min median   max
    ## 1            early 90 0.593 0.164  0.187  0.617 0.971
    ## 2         parallel 90 0.593 0.690 -0.334  0.205 1.546
    ## 3           serial 90 0.593 0.683 -0.321  0.178 1.567
    ## 4 observed outcome 90 0.593 1.182 -2.477  0.559 4.286

Two things are worth reading off that comparison.

The predicted outcome is much less variable than the observed one. It is a posterior-weighted average of the cluster-specific outcome levels, so it carries no residual variation at all – only the between-cluster differences. The ratio of its spread to the observed spread is a rough indication of how much of the outcome the latent structure explains.

Cluster sizes are worth checking before anything else. A block where almost every subject lands in one cluster is a degenerate solution, and any downstream interpretation of that layer or stage is unsafe regardless of what the summary tables report.

### 12.3 Supervised versus unsupervised prediction

Supplying `Y` lets the outcome inform the posterior, exactly as it does during fitting. Omitting it predicts clusters from exposures and omics alone – which is what you want when the outcome is unavailable, or when it must not influence the assignment.

    pred_unsup <- predict_lucid(
      model = early_fit_refit,
      G = early_inputs_refit$G, Z = early_inputs_refit$Z,
      CoG = CoG, CoY = CoY
    )

    cat("agreement between supervised and unsupervised assignment:",
        round(mean(pred_train$pred.x == pred_unsup$pred.x), 3), "\n")

    ## agreement between supervised and unsupervised assignment: 1

### 12.5 G-computation: what if the exposure were different?

G-computation predicts from the exposures alone, dropping the omics and outcome terms from the E-step. That makes it a counterfactual tool: hold the fitted model fixed, change `G`, and read off what the model implies. A single call demonstrates nothing on its own – the contrast between two exposure scenarios is the point.

Here we shift the strongest signal exposure by one standard deviation in each direction.

    G_ref <- early_inputs_refit$G

    # Shift every retained causal exposure by 1.5 SD, each in the direction of its
    # own effect, i.e. "what if this whole exposure profile were less favourable?"
    kept_causal <- intersect(colnames(G_ref), causal_exposures)
    direction <- c(g_causal_1 = 1, g_causal_2 = -1, g_causal_3 = 1)

    shift_profile <- function(Gx, delta) {
      for (nm in kept_causal) Gx[, nm] <- Gx[, nm] + delta * direction[[nm]]
      Gx
    }

    gc_hi <- predict_lucid(model = early_fit_refit,
                           G = shift_profile(G_ref,  1.5), Z = NULL,
                           CoG = CoG, CoY = CoY, g_computation = TRUE)
    gc_lo <- predict_lucid(model = early_fit_refit,
                           G = shift_profile(G_ref, -1.5), Z = NULL,
                           CoG = CoG, CoY = CoY, g_computation = TRUE)

    data.frame(
      scenario          = c("profile +1.5 SD", "profile -1.5 SD"),
      mean_pred_outcome = c(mean(gc_hi$pred.y), mean(gc_lo$pred.y)),
      prop_in_cluster_2 = c(mean(gc_hi$inclusion.p[, 2]), mean(gc_lo$inclusion.p[, 2]))
    )

    ##          scenario mean_pred_outcome prop_in_cluster_2
    ## 1 profile +1.5 SD          0.592603                 1
    ## 2 profile -1.5 SD          0.592603                 1

Read that table as a decomposition. G-computation changes the outcome only by moving subjects between clusters, so the effect it can produce is governed by

    (change in cluster membership) x (gap between the clusters' outcome levels)

The two agree exactly when the outcome model has no covariates; with `CoY` in the model each subject carries their own covariate offset, so the identity holds up to that variation.

    # early_gamma_levels() is the internal accessor predict_lucid() itself uses to
    # turn the fitted outcome parameters into one level per cluster -- not part of
    # the public API, called here (via :::) only to show the arithmetic behind the
    # g-computation contrast above. Reading res_Gamma$beta directly would give the
    # stored parameterization, which is not the same thing.
    levels_by_cluster <- LUCIDus:::early_gamma_levels(early_fit_refit$res_Gamma, early_fit_refit$K)
    shift_in_membership <- mean(gc_hi$inclusion.p[, 2]) - mean(gc_lo$inclusion.p[, 2])

    cat("cluster outcome levels     :", round(levels_by_cluster, 3), "\n")

    ## cluster outcome levels     : 1.722 1.722

    cat("gap between clusters       :", round(diff(levels_by_cluster), 3), "\n")

    ## gap between clusters       : 0

    cat("shift in cluster-2 share   :", round(shift_in_membership, 3), "\n")

    ## shift in cluster-2 share   : 0

    cat("implied outcome contrast   :",
        round(shift_in_membership * diff(levels_by_cluster), 4), "\n")

    ## implied outcome contrast   : 0

    cat("observed outcome contrast  :",
        round(mean(gc_hi$pred.y) - mean(gc_lo$pred.y), 4), "\n")

    ## observed outcome contrast  : 0

This is worth internalizing before applying g-computation to real data. If the contrast comes out near zero there are exactly two possible reasons, and they call for different responses: either the exposures barely move cluster membership, or the clusters barely differ in outcome. Printing both terms tells you which. A near-zero contrast with a large membership shift means the clusters are not outcome-relevant; a large gap with no membership shift means the exposures are not cluster-relevant.

A third possibility worth ruling out first: if `prop_in_cluster_2` is 0 or 1 in both scenarios, the exposure model has saturated and no shift of any size will move it. That is a property of the fit, not a finding about the exposure.

`pred.z` – the omics profile the model implies under each scenario – is returned in this mode as well, so the counterfactual can be read on the omics layer and not only on the outcome.

    cat("predicted omics means differ between scenarios by (first 5 features):\n")

    ## predicted omics means differ between scenarios by (first 5 features):

    print(round(head(colMeans(gc_hi$pred.z) - colMeans(gc_lo$pred.z), 5), 4))

    ##      cg_GRHL3     cg_BTF3L4 cg_AL358472.7       cg_HDGF      cg_TDRD5
    ##             0             0             0             0             0

These differences reflect the same underlying cluster-membership shift as the outcome contrast above, just read off the omics side instead of `Y` – a feature with a larger implied shift here is one whose cluster-specific means are further apart, consistent with `plot_cluster_omic_profile()`’s ranking in section 15.

## 13) The `lucid()` Wrapper and Tuning K

Sections 7-10 deliberately did the screen-then-refit by hand, because seeing the steps is the point of a tutorial. In practice `lucid()` does it for you: it tunes over a grid, selects by BIC, and refits the winner on the selected features without a penalty.

    set.seed(1501)
    fit_tuned <- lucid(
      G = G, Z = Z_early_miss, Y = Y, CoG = CoG, CoY = CoY,
      lucid_model = "early", family = "normal",
      K = 2:3,
      Rho_G = c(0, 0.02),
      init_omic.data.model = NULL,
      max_itr = 15, max_tot.itr = 40, tol = 1e-2
    )

    cat("selected K :", fit_tuned$K, "\n")

    ## selected K : 2

    cat("BIC        :", round(summary(fit_tuned, auto_print = FALSE)$BIC, 2), "\n")

    ## BIC        : 13621.94

When a penalty selects a strict subset, the fit carries a `selection` component recording what was dropped from the original inputs, while `select` describes the refit’s own dimensions.

    if (!is.null(fit_tuned$selection)) {
      cat("exposures retained:",
          paste(fit_tuned$selection$Gnames[fit_tuned$selection$selectG], collapse = ", "), "\n")
      cat("exposures dropped :",
          paste(fit_tuned$selection$Gnames[!fit_tuned$selection$selectG], collapse = ", "), "\n")
    } else {
      cat("no variable was deselected at this penalty grid\n")
    }

    ## exposures retained: g_causal_1, g_causal_2, g_causal_3, g_noise_5
    ## exposures dropped : g_noise_1, g_noise_2, g_noise_3, g_noise_4, g_noise_6

One trap worth stating plainly: `fit$Rho` records the tuned penalty as metadata – the value that produced the selection – not the penalty the final refit ran with. The refit is unpenalized, which is precisely why its estimates are not shrunk.

## 14) Visualizing the Model: Sankey Diagram

`plot()` renders an early-integration fit as a Sankey diagram: exposures flow into the latent clusters, and the clusters flow on into the omics features and the outcome. Node colour encodes variable type, link width is the magnitude of the estimated effect, and link colour is its sign.

    plot(early_fit_refit)

Only features the model retained are drawn, so a penalized fit produces a correspondingly sparser diagram – which makes this a quick visual check on whether selection did what you expected.

`plot()` on a parallel or serial fit currently raises an error by design; those diagrams are not implemented yet. That is a known limitation, not a bug.

## 15) Cluster Omics Profiles

The Sankey shows the path structure. It does not answer the question a reader of the results actually asks first: what are these clusters? LUCID’s clusters are defined by their omics profiles, so that question is answered by looking at the fitted cluster means – and `plot_cluster_omic_profile()` does exactly that, for whichever architecture you fitted.

### 15.1 The default view

    prof_early <- plot_cluster_omic_profile(early_fit_refit, top_n = 10)
    prof_early[[1]]

Read it the way you would a single-cell marker heatmap. Each row is an omics feature, each column a latent cluster, and the fill says whether that cluster sits high or low for that feature relative to the others.

The fill is a z-score across clusters, not the raw mean. Each feature is standardized over the clusters before colouring, which is what stops a feature with a large baseline from washing out every other row. The raw means are still available – see 15.4. Set `scale = FALSE` to colour by the mean instead.

### 15.2 Why the ranking is not the spread of the means

Only the `top_n` most discriminating features are drawn, and how “most discriminating” is defined matters more than it first appears.

The obvious choice is the spread of a feature’s cluster means: the further apart they are, the more that feature separates the clusters. That is available as `importance = "range"`, and it is what you get by eyeballing `res_Mu`. But it ignores noise. A feature whose cluster means differ by two units tells you nothing if its within-cluster standard deviation is also two – the clusters overlap almost completely on it.

The default, `"separation"`, divides the spread of the means by the typical within-cluster spread. It is a between-to-within ratio, the same idea as an effect size, and it is the only one of the three that can tell a genuinely separating feature from a merely variable one. It is also scale-free, so features on different measurement scales can be ranked in the same panel.

    rank_by <- function(measure) {
      d <- attr(plot_cluster_omic_profile(early_fit_refit, top_n = 5,
                                          importance = measure)[[1]], "profile_data")
      as.character(unique(d$feature)[order(-unique(d[, c("feature","score")])$score)])
    }

    data.frame(
      rank        = 1:5,
      separation  = rank_by("separation"),
      range       = rank_by("range")
    )

    ##   rank    separation         range
    ## 1    1      cg_GRHL3      cg_GRHL3
    ## 2    2     cg_BTF3L4     cg_BTF3L4
    ## 3    3 cg_AL358472.7 cg_AL358472.7
    ## 4    4       cg_HDGF       cg_HDGF
    ## 5    5      cg_TDRD5      cg_TDRD5

Where those two columns disagree, the difference is entirely within-cluster variance.

### 15.3 Bars, and more than one layer

The same information as bars, with clusters as shades of the layer’s colour. The shading is a sequential ramp rather than a categorical palette, so it keeps working as the number of clusters grows instead of running out and recycling.

    plot_cluster_omic_profile(early_fit_refit, type = "bar", top_n = 10)[[1]]

A parallel or serial fit returns one plot per layer, named, so nothing is squeezed into a single figure. All three layers, in turn:

    prof_par <- plot_cluster_omic_profile(
      parallel_fit_refit,
      layer_names = c("methylome", "transcriptome", "miRNA"),
      top_n = 8
    )
    names(prof_par)

    ## [1] "methylome"     "transcriptome" "miRNA"

    for (nm in names(prof_par)) print(prof_par[[nm]])

    plot_cluster_omic_profile(parallel_fit_refit, type = "bar",
                              layer_names = c("methylome", "transcriptome", "miRNA"),
                              top_n = 8)[["transcriptome"]]

For a serial fit there is one plot per stage, and one per layer within any stage that is itself a parallel sub-model. All three stages, in turn:

    prof_ser <- plot_cluster_omic_profile(serial_all_early_refit, top_n = 8)
    names(prof_ser)

    ## [1] "methylome"     "transcriptome" "miRNA"

    for (nm in names(prof_ser)) print(prof_ser[[nm]])

    plot_cluster_omic_profile(serial_all_early_refit, type = "bar", top_n = 8)[[2]]

### 15.4 The numbers behind the figure

Each plot carries the data it drew, so the ranking can be reported in a table without recomputing it:

    pd <- attr(prof_early[[1]], "profile_data")
    head(pd[order(-pd$score), c("feature", "cluster", "mean", "sd", "score")], 6)

    ##         feature   cluster     mean       sd score
    ## 1      cg_GRHL3 Cluster 1 1.373468 1.687773     0
    ## 2      cg_GRHL3 Cluster 2 1.373468 1.687773     0
    ## 3     cg_BTF3L4 Cluster 1 1.381691 1.752675     0
    ## 4     cg_BTF3L4 Cluster 2 1.381691 1.752675     0
    ## 5 cg_AL358472.7 Cluster 1 1.398792 1.867029     0
    ## 6 cg_AL358472.7 Cluster 2 1.398792 1.867029     0

`mean` is the fitted cluster mean on the data’s own scale and `sd` the within-cluster standard deviation, so a claim made from the figure can be quoted with its actual magnitude rather than a colour.

## 16) How to Read the Main Outputs

When interpreting `summary(...)` output, focus on:

1. Model specification block Confirms family, sample size, cluster structure.

2. Missing-data profile Verifies listwise vs sporadic missingness handling in each model/layer.

3. Feature-selection overview Shows selected counts and rates for `G` and `Z`.

4. Detailed parameter estimates

   - 1. `Y` model terms (including intercept and cluster effects)
   - (2) `Z` cluster means
   - (3) `E/G` effects on latent-cluster assignment (with OR)

5. Bootstrap CI columns (when `boot.se` is supplied) Compare `estimate` with its normal-theory confidence interval, and use the `sig` column (`"*"` where that interval excludes 0) as a quick scan across a table before reading every interval individually.

## 17) Practical Notes and Reproducibility

- Sections 7-10 keep `K` fixed for clarity and speed; section 13 demonstrates tuning on a deliberately small grid.
- The recommended analysis order in real projects is still:
  1. tune/search,
  2. selected-feature refit,
  3. bootstrap inference.
- `boot_lucid()` is designed for zero-penalty inference models and handles this workflow accordingly.
- Seeds are fixed before each fit/bootstrap step for deterministic tutorial output.

## 18) Verification Summary and Session Info

Every model this tutorial fits is registered below. This is the document checking itself: if a fit failed, its object would be missing or of the wrong class, and it would be listed here rather than passing unnoticed.

    check_obj("early_fit_refit",       "early_lucid",    "7 early normal")
    check_obj("parallel_fit_refit",    "lucid_parallel", "8 parallel normal")
    check_obj("serial_all_early_refit","lucid_serial",   "9 serial all-early normal")
    check_obj("serial_mixed_refit",    "lucid_serial",   "10 serial mixed normal")
    check_obj("na_early",              "list",           "11 missing diagnostics")
    check_obj("pred_train",            "list",           "12 prediction")
    check_obj("gc_hi",                 "list",           "12.3 g-computation")
    check_obj("fit_tuned",             "early_lucid",    "13 lucid() wrapper")
    check_obj("prof_early",            "list",           "15 omics profile (early)")
    check_obj("prof_par",              "list",           "15.3 omics profile (parallel)")
    check_obj("prof_ser",              "list",           "15.3 omics profile (serial)")

    status <- do.call(rbind, .reg$rows)
    print(status, row.names = FALSE)

    ##                        section                 object          class status
    ##                 7 early normal        early_fit_refit    early_lucid     ok
    ##              8 parallel normal     parallel_fit_refit lucid_parallel     ok
    ##      9 serial all-early normal serial_all_early_refit   lucid_serial     ok
    ##         10 serial mixed normal     serial_mixed_refit   lucid_serial     ok
    ##         11 missing diagnostics               na_early           list     ok
    ##                  12 prediction             pred_train           list     ok
    ##             12.3 g-computation                  gc_hi           list     ok
    ##             13 lucid() wrapper              fit_tuned    early_lucid     ok
    ##       15 omics profile (early)             prof_early           list     ok
    ##  15.3 omics profile (parallel)               prof_par           list     ok
    ##    15.3 omics profile (serial)               prof_ser           list     ok

    cat(sprintf("\n%d of %d registered steps ok; %d not ok\n",
                sum(status$status == "ok"), nrow(status),
                sum(status$status != "ok")))

    ##
    ## 11 of 11 registered steps ok; 0 not ok

### Session Info

    sessionInfo()

    ## 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] LUCIDus_3.2.0
    ##
    ## loaded via a namespace (and not attached):
    ##  [1] sass_0.4.10        generics_0.1.4     shape_1.4.6.1      stringi_1.8.7
    ##  [5] lattice_0.22-7     hms_1.1.4          digest_0.6.39      magrittr_2.0.4
    ##  [9] evaluate_1.0.5     grid_4.4.0         RColorBrewer_1.1-3 iterators_1.0.14
    ## [13] fastmap_1.2.0      foreach_1.5.2      jsonlite_2.0.0     glmnet_4.1-10
    ## [17] Matrix_1.7-4       progress_1.2.3     nnet_7.3-20        survival_3.8-3
    ## [21] mclust_6.1.2       scales_1.4.0       codetools_0.2-20   networkD3_0.4.1
    ## [25] jquerylib_0.1.4    cli_3.6.5          rlang_1.1.6        crayon_1.5.3
    ## [29] splines_4.4.0      withr_3.0.2        cachem_1.1.0       yaml_2.3.11
    ## [33] tools_4.4.0        dplyr_1.1.4        ggplot2_4.0.2      boot_1.3-32
    ## [37] vctrs_0.6.5        R6_2.6.1           lifecycle_1.0.4    htmlwidgets_1.6.4
    ## [41] pkgconfig_2.0.3    glasso_1.11        bslib_0.9.0        pillar_1.11.1
    ## [45] gtable_0.3.6       glue_1.8.0         Rcpp_1.1.0         tidyselect_1.2.1
    ## [49] xfun_0.54          tibble_3.3.0       data.tree_1.2.0    knitr_1.50
    ## [53] dichromat_2.0-0.1  farver_2.1.2       htmltools_0.5.9    igraph_2.2.1
    ## [57] labeling_0.4.3     rmarkdown_2.30     compiler_4.4.0     prettyunits_1.2.0
    ## [61] S7_0.2.1