Skip to contents

Aligned MFA estimates a shared score matrix for a set of *latent reference rows* when multiple blocks have different row sets. Each block `X[[k]]` is linked to the shared rows via an integer index vector `row_index[[k]]`, but **no single block is privileged** as an anchor.

This is the symmetric counterpart to [anchored_mfa()], which uses an explicit reference block `Y` and estimates scores in the row space of `Y`.

Usage

aligned_mfa(
  X,
  row_index,
  N = NULL,
  preproc = multivarious::center(),
  ncomp = 2,
  normalization = c("MFA", "None", "custom"),
  alpha = NULL,
  score_constraint = c("none", "orthonormal"),
  feature_groups = NULL,
  feature_lambda = 0,
  feature_lambda_scale = c("raw", "block"),
  max_iter = 50,
  tol = 1e-06,
  ridge = 1e-08,
  verbose = FALSE,
  use_future = FALSE,
  strategy = c("auto", "in_memory", "gram", "stream"),
  gram_tol = 1e-10,
  gram_rank = NULL,
  ...
)

Arguments

X

A list of numeric matrices/data.frames. Each element `X[[k]]` is `n_k × p_k`. Alternatively a [block_source()], which is read one column chunk at a time so the blocks never need to be in memory together (see *Out-of-core fitting*).

row_index

A list of integer vectors. `row_index[[k]]` has length `n_k` and maps rows of `X[[k]]` to shared rows in `1..N`.

N

Optional integer specifying the number of shared rows. If `NULL` (default), `N` is inferred as `max(unlist(row_index))`.

preproc

A `multivarious` preprocessing pipeline (a `pre_processor`/`prepper`) or a list of them. If a list, it must have length `length(X)` and will be applied to `X` in that order.

ncomp

Integer number of components to extract.

normalization

Block weighting scheme. `"MFA"` uses inverse squared first singular value per block; `"None"` uses uniform weights; `"custom"` uses `alpha`.

alpha

Optional numeric vector of per-block weights (length `length(X)`), used when `normalization = "custom"`.

score_constraint

Identification strategy for the shared score matrix. `"none"` uses the historical unconstrained update followed by QR normalization inside each ALS iteration. `"orthonormal"` treats `S transpose S = I` as part of the model and updates `S` with a constrained majorization/polar step.

feature_groups

Feature prior specification. One of: * `NULL` (no feature prior), * `"colnames"` to group features with identical column names across blocks, * a `data.frame` with columns `block`, `feature`, `group` and optional `weight`.

feature_lambda

Non-negative scalar controlling strength of the feature prior.

feature_lambda_scale

How the feature prior is measured. `"raw"` (default, historical) penalizes `sum w ||V_k[j, ] - c_g||^2` on the loadings' own scale, so useful `feature_lambda` values depend on the size and scale of the data. `"block"` compares loadings in each block's MFA-normalized units, `sum w ||sqrt(alpha_k) V_k[j, ] - c_g||^2`, which makes `feature_lambda` unitless (relative to the block-weighted data term) and the fit invariant to rescaling any block (up to the absolute `ridge`, which is not scale-free).

max_iter

Maximum number of alternating least-squares iterations.

tol

Relative tolerance on the objective for convergence.

ridge

Non-negative ridge stabilization added to normal equations.

verbose

Logical; if `TRUE`, prints iteration progress.

use_future

Logical; if `TRUE`, block-wise computations that do not depend on one another may be performed via `furrr::future_map()` when available. The main alternating-least-squares loop is intrinsically sequential and is unaffected. Accepted here primarily for interface parity with [aligned_mcca()] and [anchored_mfa()].

strategy

How the data are accessed. `"auto"` (default) fits in memory when `X` is a list; for a [block_source()] it uses the Gram path, or the streaming path when a feature prior is supplied. `"in_memory"` loads every block (reading a block source in full). `"gram"` and `"stream"` force the corresponding out-of-core path for either input.

gram_tol

Relative eigenvalue tolerance used to drop numerically null directions when compressing each block's Gram matrix (Gram path only).

gram_rank

Optional maximum number of Gram eigenpairs kept per block (one value, or one per block) on the Gram path. `NULL` (default) keeps all of them, which is exact. A smaller rank makes the fit approximate but bounds each compressed block at `n_k × gram_rank`; the fraction of each block's variance left out is returned as `gram_discarded`.

...

Unused (reserved for future extensions).

Value

An object inheriting from `multivarious::multiblock_biprojector` with additional class `"aligned_mfa"`. The object contains global scores in `s`, concatenated loadings in `v`, and block mappings in `block_indices`. Additional fields include `V_list`, `row_index`, `alpha_blocks`, and `objective_trace`.

Details

## Model The fitted model has the form: $$X_k \approx S[\mathrm{idx}_k,] V_k^\top$$ where `S` is `N × ncomp`, each `V_k` is `p_k × ncomp`, and `idx_k` maps rows of `X_k` to `1..N`. Repeated indices are allowed (e.g., repeated measures), and contributions are aggregated in the score updates.

## Feature similarity prior When `feature_lambda > 0` and `feature_groups` is supplied, Aligned MFA applies the same group-shrinkage penalty as [anchored_mfa()], pulling loading vectors of grouped features toward a shared group center.

Out-of-core fitting

Without a feature prior, the alternating least-squares fit depends on each preprocessed block `X_k` only through its row Gram matrix `G_k = X_k X_k^T` (`n_k × n_k`): the loadings are ridge regressions on the scores and so lie in the row space of `X_k`, and the MFA weight is the inverse of the largest eigenvalue of `G_k`. The Gram path therefore (1) reads each block once, chunk by chunk, to accumulate its column-local preprocessing statistics and `G_k`; (2) runs the ordinary fit on compressed blocks `R_k` with `R_k R_k^T = G_k` (at most `n_k` columns each); and (3) reads each block once more to map the fitted loadings and correlation loadings back to the original features (with `X_k = U_k D_k W_k^T`, loadings `V_R` on `R_k = U_k D_k` become `W_k V_R`). Peak memory is one chunk, one block's Gram matrix (`n_k^2` values, built and compressed one block at a time), the compressed blocks, and the loadings; `gram_rank` caps the compressed blocks for subjects with many rows. Scores, loadings, and diagnostics match the in-memory fit; with `score_constraint = "none"` they can differ by a rotation within the same subspace because that initialization depends on an SVD sign. The same engine serves the other methods that accept block sources (`mfa()`, `mcca()`, `aligned_mcca()`, `anchored_mcca()`, `anchored_mfa()`, `graph_anchored_mfa()`, `coupled_graph_anchored_mfa()`, `response_aligned_mfa()`, `aligned_interbattery()`).

The feature prior (`feature_lambda > 0` with `feature_groups`) couples features across blocks, so the Gram shortcut does not apply. The streaming path instead runs the same alternating least squares as the in-memory fit with one read of the data per iteration: for each chunk it updates the loadings (feature-wise, given the group centers) and accumulates `X_k V_k` for the score step, evaluating the previous iteration's objective in the same read. It holds the loadings (two copies) and the per-row normal equations but never the data, and needs about `iterations + 3` passes. Feature groups given by name need column names on the source (`block_source(colnames = )`).

Both out-of-core paths support column-local preprocessing only (`NULL`, `multivarious::pass()`, `center()`, `standardize()`). Resampling works on block-source fits: permutations shuffle `row_index` only, and each bootstrap replicate is a derived source that reads the original one and keeps the resampled rows, so every replicate refit re-reads the data.

Examples

# \donttest{
set.seed(1)
N <- 30
X1 <- matrix(rnorm(20 * 10), 20, 10)
X2 <- matrix(rnorm(15 * 8), 15, 8)
idx1 <- sample.int(N, nrow(X1), replace = FALSE)
idx2 <- sample.int(N, nrow(X2), replace = FALSE)

fit <- aligned_mfa(list(X1 = X1, X2 = X2), list(X1 = idx1, X2 = idx2), ncomp = 2)
#> Applying the same preprocessor definition independently to each block.
stopifnot(nrow(multivarious::scores(fit)) == N)
# }