Skip to contents

Getting started · Class overview · Changelog · Issue tracker

multidesign is an R package for multivariate experimental data where the matrix alone is not enough. It keeps observation-level design metadata and variable-level metadata attached to the data, so subsetting, grouping, splitting, and cross-validation stay aligned.

Status: Pre-release and source-only. The API may change before the first stable release.

Installation

multidesign is not on CRAN yet. Install the development version from GitHub:

install.packages("remotes")
remotes::install_github("bbuchsbaum/multidesign")

Quick start

library(multidesign)

X <- matrix(seq_len(24), nrow = 6)
md <- multidesign(
  X,
  tibble::tibble(condition = rep(c("face", "house"), each = 3)),
  tibble::tibble(network = rep(c("visual", "control"), each = 2))
)

face_visual <- md |>
  subset(condition == "face") |>
  select_variables(network == "visual")

xdata(face_visual)
#>      [,1] [,2]
#> [1,]    1    7
#> [2,]    2    8
#> [3,]    3    9

The matrix, row design, and column metadata stay synchronized through both operations. This is the ordinary single-block workflow; use hyperdesign when the data naturally contains several related blocks.

Why use multidesign?

  • Keep the data matrix, row design, optional column metadata, and explicit cell-observation mask together.
  • Filter observations and variables without manually realigning metadata or masks.
  • Split, summarize, and build cross-validation folds from experimental design variables.
  • Represent corresponding entities across blocks without changing the package’s row-centric matrix orientation.
  • Scale to multi-subject, multi-session, or lazy-loading workflows with hyperdesign and multiframe.

Core objects

Class Use it when…
multidesign You have one data matrix plus row-wise design information and optional column metadata.
hyperdesign You have multiple related blocks, optionally linked by corresponding entity IDs.
multiframe Your observations are expensive to materialize and you want lazy evaluation.
multiblock You need lower-level stacked matrix operations without the full design abstraction.

Most workflows start with multidesign and move to hyperdesign when the data naturally breaks into subject-, session-, or modality-level blocks.

Corresponding rows and partial observations

Rows usually represent trials, scans, or other independent observations. They can also represent known entities—such as landmarks or tasks—that recur across blocks. A hyperdesign can declare that second role with an entity key:

shape_a <- multidesign(
  matrix(c(NA, 0, 1, 0, 0, 1), ncol = 2, byrow = TRUE),
  tibble::tibble(landmark = c("A", "B", "C")),
  cells = matrix(
    c(TRUE, TRUE, TRUE, FALSE, FALSE, FALSE),
    ncol = 2,
    byrow = TRUE
  )
)
shape_b <- multidesign(
  matrix(c(1, 0, 0, 1, 1, 1), ncol = 2, byrow = TRUE),
  tibble::tibble(landmark = c("B", "C", "D"))
)

shapes <- hyperdesign(
  list(shape_a = shape_a, shape_b = shape_b),
  id = "landmark",
  space = "common"
)

corr <- correspondence(shapes)
corr$global_ids
#> [1] "A" "B" "C" "D"
do.call(rbind, corr$row_map)
#>         [,1] [,2] [,3]
#> shape_a    1    2    3
#> shape_b    2    3    4
corr$overlap$pair_n
#>         shape_a shape_b
#> shape_a       3       2
#> shape_b       2       3

aligned <- align_by_id(shapes)
aligned$observed
#>   shape_a shape_b
#> A    TRUE   FALSE
#> B    TRUE    TRUE
#> C    TRUE    TRUE
#> D   FALSE    TRUE
aligned$cells[, , "shape_a"]
#>       variable
#> entity     1     2
#>      A  TRUE  TRUE
#>      B  TRUE FALSE
#>      C FALSE FALSE
#>      D FALSE FALSE
aligned$x[, , "shape_a"]
#>       variable
#> entity  1  2
#>      A NA  0
#>      B  1 NA
#>      C NA NA
#>      D NA NA

correspondence(shapes, sort_ids = TRUE)$global_ids
#> [1] "A" "B" "C" "D"

stopifnot(all(vapply(seq_along(shapes), function(i) {
  identical(corr$global_ids[corr$row_map[[i]]], corr$ids[[i]])
}, logical(1))))

The aligned result distinguishes three states that can all display as NA in aligned$x: landmark D is absent from shape_a (observed is FALSE), landmark C is present but has an all-FALSE cell-mask row, and landmark A’s first coordinate is observed even though its stored value is NA. The fill argument changes only the displayed value for absent or masked cells; consult observed and cells for their meaning. A missing value in x never creates a mask implicitly.

This is a role of the row key, not a second matrix orientation. Do not store features × observations in x to satisfy a left-action formula. A solver that uses left-action notation should transpose at its boundary.

The declarations answer different questions: common_vars records design columns shared by every block (schema), id names values that join rows (correspondence), and space says whether columns are shared axes ("common") or block-specific variables ("block"). as_multidesign() always row-stacks; it never joins by id, and it refuses space = "block". align_by_id() is the explicit join: it returns common-space arrays on the global entity universe, but it does not fit an alignment model.

Cell masks move with ordinary row and column subsets. When masked rows or duplicate entity keys must be combined, aggregation is explicit: summarize_by(x, group, aggregate = "mean") and hyperdesign(blocks, id = "landmark", aggregate = "mean"). A custom scalar-returning function may replace "mean". Stacking promotes an unmasked block to all observed when another block has a mask. Preprocessing and dimensionality reduction reject partial masks because those operations do not declare how the mask itself transforms.

These contracts let downstream packages validate data for generalized Procrustes analysis without putting transforms, gauges, fitting, or certification in multidesign. Likewise, fold_over(shapes, landmark) holds an entity out from all blocks where it occurs; it is an ordinary data split, not a specialized Bai–Bartoli/LBW cross-validation engine. Use positional = TRUE only when equal row positions really are the correspondence and that fragile assumption is intentional.

Design-aware cross-validation

set.seed(42)

# Simulate a trial-by-feature matrix
X <- matrix(rnorm(40 * 8), nrow = 40, ncol = 8)

# Design metadata for each observation
design_df <- tibble::tibble(
  condition = rep(c("face", "house"), each = 20),
  run = rep(1:4, each = 10),
  trial = 1:40
)

# Metadata for each variable / column
column_df <- tibble::tibble(
  roi = paste0("ROI_", 1:8),
  network = rep(c("visual", "frontal"), each = 4)
)

# Build a multidesign object
md <- multidesign(X, design_df, column_df)

# Row-wise and column-wise access stay synchronized
face_trials <- subset(md, condition == "face")
visual_rois <- select_variables(md, network == "visual")

# Summarize by an experimental factor
condition_means <- summarize_by(md, condition)

# Create design-aware cross-validation folds
folds <- fold_over(md, run)

# Run a simple CV pipeline
cv <- cross_validate(
  folds,
  fit_fn = function(analysis) {
    list(mean = colMeans(xdata(analysis)))
  },
  score_fn = function(model, assessment) {
    mat <- xdata(assessment)
    pred <- matrix(
      model$mean,
      nrow = nrow(mat),
      ncol = ncol(mat),
      byrow = TRUE
    )
    c(mse = mean((mat - pred)^2))
  }
)

summary(cv)

Typical workflow

  1. Create a multidesign from a matrix and a design table.
  2. Subset rows by design variables and columns by variable metadata.
  3. Summarize or split by condition, run, subject, or any other design factor.
  4. Generate folds with fold_over() or cv_rows().
  5. Pass those folds into cross_validate() with your model-fitting and scoring functions.
  6. Move to hyperdesign when the analysis spans multiple related blocks.

Albers theme

This package uses the albersdown theme. Existing vignette theme hooks are replaced so albers.css and local albers.js render consistently on CRAN and GitHub Pages. The defaults are configured via params$family and params$preset (family = “ochre”, preset = “homage”). The pkgdown site uses template: { package: albersdown }.