# Multiblock basics: one projector, many tables

# 1. Why multiblock?

Many studies collect several tables on the same samples – e.g. transcriptomics + metabolomics, or multiple sensor blocks. Most single-table reductions (PCA, ICA, NMF, …) ignore that structure. `multiblock_projector` is a thin wrapper that keeps track of which original columns belong to which block, so you can

- drop-in any existing decomposition (PCA, SVD, NMF, …)
- still know “these five loadings belong to block A, those three to block B”
- project or reconstruct per block effortlessly.

We demonstrate with a minimal two-block toy-set.

    set.seed(1)
    n  <- 100
    pA <- 7; pB <- 5                    # two blocks, different widths

    XA <- matrix(rnorm(n * pA), n, pA)
    XB <- matrix(rnorm(n * pB), n, pB)
    X  <- cbind(XA, XB)                 # global data matrix
    blk_idx <- list(A = 1:pA, B = (pA + 1):(pA + pB)) # Named list is good practice

# 2. Wrap a single PCA as a multiblock projector

    # 2-component centred PCA (using base SVD for brevity)
    preproc_fitted <- fit(center(), X)
    Xc        <- transform(preproc_fitted, X)          # Centered data
    svd_res   <- svd(Xc, nu = 0, nv = 2)               # only V (loadings)
    mb        <- multiblock_projector(
      v             = svd_res$v,                       # p × k loadings
      preproc       = preproc_fitted,                  # remembers centering
      block_indices = blk_idx
    )

    print(mb)
    #> Projector object:
    #>   Input dimension: 12
    #>   Output dimension: 2
    #>   With pre-processing:
    #> A finalized pre-processing pipeline:
    #>  Step  1 : center

## 2.1 Project the whole data

    scores_all <- project(mb, X)                       # n × 2
    head(round(scores_all, 3))
    #>        [,1]   [,2]
    #> [1,] -0.815 -1.159
    #> [2,]  1.075 -3.326
    #> [3,] -0.068  1.124
    #> [4,] -0.055 -0.788
    #> [5,] -0.554  1.005
    #> [6,] -0.942  1.565

## 2.2 Project one block only

    # Project using only data from block A (requires original columns)
    scores_A <- project_block(mb, XA, block = 1)
    # Project using only data from block B
    scores_B <- project_block(mb, XB, block = 2)

    cor(scores_all[,1], scores_A[,1])                  # high (they coincide)
    #> [1] 0.7228449

Because the global PCA treats all columns jointly, projecting only block A gives exactly the same latent coordinates as when the whole matrix is available – useful when a block is missing at prediction time.

## 2.3 Partial feature projection

Need to use just three variables from block B?

    # Get the global indices for the first 3 columns of block B
    sel_cols_global <- blk_idx[["B"]][1:3]
    # Extract the corresponding data columns from the full matrix or block B
    part_XB_data  <- X[, sel_cols_global, drop = FALSE] # Data must match global indices

    scores_part <- partial_project(mb, part_XB_data,
                                   colind = sel_cols_global)  # Use global indices
    head(round(scores_part, 3))
    #>        [,1]   [,2]
    #> [1,] -2.546 -0.723
    #> [2,]  1.594 -3.584
    #> [3,]  0.815  0.912
    #> [4,] -0.329 -0.648
    #> [5,] -2.223  1.394
    #> [6,] -2.628  0.972

# 3. Adding scores → multiblock_biprojector

If you also keep the sample scores (from the original fit) you get two-way functionality: re-construct data, measure error, run permutation tests, etc. That is one extra line when creating the object:

    bi <- multiblock_biprojector(
      v             = svd_res$v,
      s             = Xc %*% svd_res$v,    # Calculate scores: Xc %*% V
      sdev          = svd_res$d[1:2] / sqrt(n-1), # SVD d are related to sdev
      preproc       = preproc_fitted,
      block_indices = blk_idx
    )
    print(bi)
    #> Multiblock Bi-Projector object:
    #>   Projection matrix dimensions:  12 x 2
    #>   Block indices:
    #>     Block 1: 1,2,3,4,5,6,7
    #>     Block 2: 8,9,10,11,12

Now you can, for instance, test whether component-wise consensus between blocks is stronger than by chance.

    # Quick permutation test (use more permutations for real analyses)
    # use_rspectra=FALSE needed for this 2-block example; larger problems can use TRUE
    perm_res <- perm_test(bi, Xlist = list(A = XA, B = XB), nperm = 99, use_rspectra = FALSE)
    print(perm_res$component_results)
    #>   comp observed pval lower_ci upper_ci
    #> 1    1 84.25129  0.1 78.70594 88.96802

The `perm_test` method for `multiblock_biprojector` uses an eigen-based score consensus statistic to assess whether blocks share more variance than expected by chance.

# 4. Take-aways

- Any decomposition that delivers a loading matrix `v` (and optionally scores `s`) can become multiblock-aware by supplying `block_indices`.
- The wrapper introduces zero new maths – it only remembers the column grouping and plugs into the common verbs:

Verb | What it does in multiblock context
--- | ---
`project()` | whole-matrix projection (uses preprocessing)
`project_block()` | scores based on one block’s data
`partial_project()` | scores from an arbitrary subset of global columns
`coef(..., block=)` | retrieve loadings for a specific block
`perm_test()` | permutation test for block consensus (biprojector)

This light infrastructure lets you prototype block-aware analyses quickly, while still tapping into the entire `multiblock` toolkit (cross-validation, reconstruction metrics, composition with `compose_projector`, etc.).

    sessionInfo()
    #> R version 4.5.1 (2025-06-13)
    #> Platform: aarch64-apple-darwin20
    #> Running under: macOS Sonoma 14.3
    #>
    #> Matrix products: default
    #> BLAS:   /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRblas.0.dylib
    #> LAPACK: /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.1
    #>
    #> locale:
    #> [1] C/en_CA.UTF-8/en_CA.UTF-8/C/en_CA.UTF-8/en_CA.UTF-8
    #>
    #> time zone: America/Toronto
    #> tzcode source: internal
    #>
    #> attached base packages:
    #> [1] stats     graphics  grDevices utils     datasets  methods   base
    #>
    #> other attached packages:
    #> [1] glmnet_4.1-10      Matrix_1.7-3       knitr_1.51         tibble_3.3.1
    #> [5] dplyr_1.1.4        ggplot2_4.0.1      multivarious_0.3.1
    #>
    #> loaded via a namespace (and not attached):
    #>  [1] GPArotation_2025.3-1 utf8_1.2.6           sass_0.4.10
    #>  [4] future_1.68.0        generics_0.1.4       shape_1.4.6.1
    #>  [7] lattice_0.22-7       listenv_0.10.0       digest_0.6.39
    #> [10] magrittr_2.0.4       evaluate_1.0.5       grid_4.5.1
    #> [13] RColorBrewer_1.1-3   iterators_1.0.14     fastmap_1.2.0
    #> [16] foreach_1.5.2        jsonlite_2.0.0       ggrepel_0.9.6
    #> [19] RSpectra_0.16-2      survival_3.8-3       scales_1.4.0
    #> [22] pls_2.8-5            codetools_0.2-20     jquerylib_0.1.4
    #> [25] cli_3.6.5            crayon_1.5.3         rlang_1.1.7
    #> [28] chk_0.10.0           parallelly_1.45.1    future.apply_1.20.0
    #> [31] splines_4.5.1        withr_3.0.2          cachem_1.1.0
    #> [34] yaml_2.3.12          otel_0.2.0           tools_4.5.1
    #> [37] parallel_4.5.1       corpcor_1.6.10       globals_0.18.0
    #> [40] rsvd_1.0.5           assertthat_0.2.1     vctrs_0.7.0
    #> [43] R6_2.6.1             matrixStats_1.5.0    proxy_0.4-27
    #> [46] lifecycle_1.0.5      MASS_7.3-65          irlba_2.3.5.1
    #> [49] pkgconfig_2.0.3      pillar_1.11.1        bslib_0.9.0
    #> [52] geigen_2.3           gtable_0.3.6         glue_1.8.0
    #> [55] Rcpp_1.1.1           xfun_0.55            tidyselect_1.2.1
    #> [58] svd_0.5.8            farver_2.1.2         htmltools_0.5.9
    #> [61] labeling_0.4.3       rmarkdown_2.30       compiler_4.5.1
    #> [64] S7_0.2.1