Multi-omics studies increasingly combine several assay platforms on the same patients – for example RNA-seq, DNA methylation, and proteomics run on the same cohort. Each platform produces an assay block: the complete set of features measured by that platform for a given subject (all genes for RNA-seq, all CpG sites for methylation, and so on). In practice, not every subject has every block. A blood draw can fail, an imaging visit can be missed, a sample can be too degraded to run on a given platform, and the result is that a subject is missing an entire block at once, not a handful of scattered values within it. This is different from ordinary feature-level missingness (a few dropped genes here and there), which most tools already handle; block-level missingness removes a whole modality for that subject in one go.
Methods built for fully-paired data, such as
mixOmics::DIABLO (Singh et al. 2019), require every subject
to have every block: a patient missing even one platform is dropped
before the analysis begins. This has a real cost. If the patients who
end up missing a block are not a random subset (for example, sicker
patients who could not complete a full imaging workup), then the
subjects who survive this filtering are a biased sample of the cohort,
not a representative one, and any downstream comparison inherits that
bias.
WOVEN (Weighted Omics View Embedding via Nystrom) is a supervised multi-omics integration method that avoids this intersection requirement. It fits a shared latent space using only the anchor subjects – those observed in every block – and then places every other subject into that same space using whatever blocks they do have, via a linear form of the Nystrom method. The Nystrom method was originally developed to solve integral equations numerically, by approximating them using a small representative set of points. In spectral and kernel methods it has since been repurposed as an out-of-sample extension: a way to place a new point into an already-fitted low-dimensional embedding without recomputing the whole eigendecomposition, by reusing the mapping learned from a reference subset of points (Bengio et al. 2003). WOVEN’s anchor subjects play that reference-subset role – the projection matrices learned from them are applied directly to whatever features a block-missing subject has, with no feature-level imputation of the missing block.
woven() takes a named list of matrices — one per omics
platform — where:
NAWOVEN expects pre-processed, approximately continuous
data. Apply standard platform-specific normalization before
passing data to woven():
| Platform | Recommended pre-processing |
|---|---|
| RNA-seq (bulk) | log1p(CPM) then scale() per gene |
| DNA methylation | M-values (log2(beta/(1-beta))), then
scale() |
| Proteomics (LC-MS) | log2(intensity), median-center per sample |
| Metabolomics | log2 transform, then scale() |
| Microbiome (16S) | CLR transform (e.g. compositions::clr()) |
Raw counts, raw beta values, or raw intensities will produce poorly
scaled projection matrices. When in doubt, scale() each
modality matrix so all features have mean 0 and SD 1.
library(woven)
set.seed(42)
n <- 150 # total subjects
K <- 3 # latent dimensions to learn
n_groups <- 3
# True group labels (e.g. CN, MCI, Dementia)
Y <- factor(rep(c("CN", "MCI", "Dementia"), each = n / n_groups),
levels = c("CN", "MCI", "Dementia"))
# Simulate three pre-processed modality matrices with group signal
make_modality <- function(p, Y, signal = 3) {
X <- matrix(rnorm(length(Y) * p), length(Y), p)
for (g in levels(Y))
X[Y == g, seq_len(8)] <- X[Y == g, seq_len(8)] + signal
colnames(X) <- paste0("Feature_", seq_len(p))
X
}
X_rna <- make_modality(300, Y, signal = 3)
X_meth <- make_modality(120, Y, signal = 4)
X_prot <- make_modality(40, Y, signal = 5)set.seed(7)
V <- 3
miss_mask <- matrix(runif(n * V) < 0.35, n, V)
# Every subject must retain at least one modality
for (i in which(rowSums(miss_mask) == V))
miss_mask[i, sample(V, 1)] <- FALSE
# Apply missingness: entire row -> NA for missing modalities
X_rna[ miss_mask[, 1], ] <- NA
X_meth[miss_mask[, 2], ] <- NA
X_prot[miss_mask[, 3], ] <- NA
n_anchors <- sum(rowSums(miss_mask) == 0)
cat(sprintf(
"Anchors (all 3 views): %d / %d (%.0f%%)\n",
n_anchors, n, 100 * n_anchors / n
))
#> Anchors (all 3 views): 47 / 150 (31%)Pass a named list so modality labels appear in all
plots automatically. anchor_idx is optional – WOVEN detects
anchors from the NA pattern.
fit <- woven(
X_list = list(RNA = X_rna, Methylation = X_meth, Proteomics = X_prot),
Y = Y,
K = K,
lambdas = 0.01, # graph Laplacian regularization (robust to this)
gamma_y = 5.0, # label supervision strength (any value > 0 works)
k_nn = 10L # k-NN graph for Laplacian
)
fit # prints summary + guided next steps
#> WOVEN fit
#> Modalities : 3 Subjects: 150 Dimensions: 3
#> Anchors : 47 (31%) Scored: 150 (100%)
#> Solver : mcca_dual (closed-form, globally optimal)
#> gamma_y : 5.00 lambda: 0.01/0.01/0.01 k_nn: 10
#> Singular values: 5.719, 4.845, 1.999
#> Classes : CN, MCI, Dementia
#> Modalities : RNA, Methylation, Proteomics
#>
#> -- Next steps --
#> plot(fit, labels = Y) # latent space scatter
#> woven_plot_vip(fit, modality = "RNA") # top features by VIP
#> woven_plot_loadings(fit, dim = 1) # loadings per modality
#> woven_plot_variance(fit) # variance per dimension
#> woven_metrics(fit, Y) # silhouette, NMI, ESS
#> woven_predict(fit, X_list_new) # predict on new dataplot() shows all scored subjects colored by group. Solid
points are anchors; faded points are block-missing subjects projected
from their available views. Ellipses show the 68% confidence region per
group (anchors only).
WOVEN latent space. All subjects are scored regardless of missingness.
Variable Importance in Projection (VIP) scores rank features by their contribution to the shared latent space across all K dimensions. VIP > 1 indicates above-average importance.
Top RNA features by VIP score.
Plot VIP for a different modality:
Top methylation features by VIP score.
Loadings show which features push subjects in the positive
vs. negative direction along a given latent dimension. Equivalent to
plotLoadings() in DIABLO.
Top feature loadings for latent dimension 1 across all three modalities.
Use this to verify that your chosen K captures most of the shared signal. If the last dimension still explains substantial variance, increase K.
Proportion of shared variance explained per latent dimension.
woven_metrics() computes silhouette score,
Davies-Bouldin index, NMI, and effective sample size (ESS) retention
directly from the fit object.
woven_metrics(fit, Y)
#> Silhouette Davies-Bouldin NMI ESS
#> 0.02735447 4.49789867 0.08722509 1.00000000ESS = 1.00 means every subject with at least one observed modality was scored. Compare this to DIABLO, which can only score subjects present in all modalities.
# Simulate 20 new subjects with partial missingness
set.seed(99)
X_new <- list(
RNA = make_modality(300, Y[1:20], signal = 3),
Methylation = make_modality(120, Y[1:20], signal = 4),
Proteomics = make_modality(40, Y[1:20], signal = 5)
)
# Some new subjects are missing a modality
X_new$RNA[1:5, ] <- NA
X_new$Proteomics[6:10, ] <- NA
pred <- woven_predict(fit, X_new)
head(pred[, 1:3])
#> predicted_class confidence p_CN
#> 1 MCI 0.7543819 0.1195400
#> 2 MCI 0.5249843 0.1749973
#> 3 CN 0.7015899 0.7015899
#> 4 Dementia 0.4288839 0.2685680
#> 5 Dementia 0.6187182 0.2533136
#> 6 Dementia 0.3840129 0.2787230predicted_class returns original label names (“CN”,
“MCI”, “Dementia”), not integer codes.
The key advantage of WOVEN over mixOmics::DIABLO is
effective sample size (ESS). DIABLO requires every
subject to have every modality; WOVEN scores all subjects with at least
one observed view.
n_anchors <- length(anchorIdx(fit)) # what DIABLO would use
n_woven <- sum(!is.na(Z(fit)[, 1])) # what WOVEN scores
cat(sprintf("DIABLO ESS: %d / %d (%.0f%%)\n",
n_anchors, n, round(100 * n_anchors / n)))
#> DIABLO ESS: 47 / 150 (31%)
cat(sprintf("WOVEN ESS: %d / %d (%.0f%%)\n",
n_woven, n, round(100 * n_woven / n)))
#> WOVEN ESS: 150 / 150 (100%)In real cohorts, the subjects DIABLO discards are often the sickest patients (those who miss a blood draw or imaging scan due to disease severity), introducing systematic bias in comparative effectiveness estimates.
The fit is an S4 object (class ). Accessor functions expose the internal quantities most useful for custom downstream analyses; see for the complete slot list if you need something an accessor does not cover.
dim(Z(fit)) # n x K consensus latent scores for ALL subjects
#> [1] 150 3
names(WList(fit)) # one projection matrix per modality
#> [1] "RNA" "Methylation" "Proteomics"
dim(WList(fit)$RNA) # p_rna x K
#> [1] 300 3
# Row names on Z come from row names of your input matrices
# (set rownames on X_list matrices to get named rows in Z(fit))Z(fit) can be passed directly to survival analysis,
clustering, or any downstream model as a low-dimensional representation
of the full cohort.
sessionInfo()
#> R version 4.6.1 (2026-06-24)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 26.04.1 LTS
#>
#> Matrix products: default
#> BLAS: /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
#> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.32.so; LAPACK version 3.12.0
#>
#> locale:
#> [1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C
#> [3] LC_TIME=en_US.UTF-8 LC_COLLATE=en_US.UTF-8
#> [5] LC_MONETARY=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8
#> [7] LC_PAPER=en_US.UTF-8 LC_NAME=C
#> [9] LC_ADDRESS=C LC_TELEPHONE=C
#> [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C
#>
#> time zone: Etc/UTC
#> tzcode source: system (glibc)
#>
#> attached base packages:
#> [1] stats graphics grDevices utils datasets methods base
#>
#> other attached packages:
#> [1] woven_0.99.3 rmarkdown_2.32
#>
#> loaded via a namespace (and not attached):
#> [1] Matrix_1.7-6 gtable_0.3.6 jsonlite_2.0.0
#> [4] dplyr_1.2.1 compiler_4.6.1 tidyselect_1.2.1
#> [7] parallel_4.6.1 cluster_2.1.8.3 jquerylib_0.1.4
#> [10] scales_1.4.0 BiocParallel_1.47.0 yaml_2.3.12
#> [13] fastmap_1.2.0 lattice_0.23-1 ggplot2_4.0.3
#> [16] R6_2.6.1 labeling_0.4.3 generics_0.1.4
#> [19] knitr_1.52 tibble_3.3.1 MatrixGenerics_1.25.0
#> [22] maketools_1.3.2 pillar_1.11.1 bslib_0.12.0
#> [25] RColorBrewer_1.1-3 rlang_1.3.0 cachem_1.1.0
#> [28] RANN_2.6.3 xfun_0.61 sass_0.4.10
#> [31] sys_3.4.3 S7_0.2.2 otel_0.2.0
#> [34] cli_3.6.6 withr_3.0.3 magrittr_2.0.5
#> [37] digest_0.6.39 grid_4.6.1 lifecycle_1.0.5
#> [40] vctrs_0.7.3 evaluate_1.0.5 glue_1.8.1
#> [43] farver_2.1.2 codetools_0.2-20 buildtools_1.0.0
#> [46] pkgconfig_2.0.3 matrixStats_1.5.0 tools_4.6.1
#> [49] htmltools_0.5.9