Author: Frédéric Bertrand

# Fast Principal Component Analysis for Big Data with bigPCAcpp

#### Frédéric Bertrand

## Introduction

The bigPCAcpp package provides principal component analysis (PCA) routines that operate directly on `bigmemory::big.matrix` objects. This vignette walks through a complete analysis workflow and compares the results against the reference implementation from base R’s `prcomp()` to demonstrate numerical agreement.

We will use the classic `iris` measurement data as a small, in-memory example. Even for larger data sets stored on disk, the workflow is identical once a `big.matrix` descriptor has been created.

## Preparing a `big.matrix`

    library(bigmemory)
    library(bigPCAcpp)

    iris_mat <- as.matrix(iris[, 1:4])
    big_iris <- as.big.matrix(iris_mat, type = "double")

Every bigPCAcpp entry point accepts the `big.matrix` object directly (and, for compatibility, still works with external pointers via the `@address` slot), allowing analyses without copying data into regular R matrices.

## Running PCA with bigPCAcpp

    big_pca <- pca_bigmatrix(
      xpMat = big_iris,
      center = TRUE,
      scale = TRUE,
      ncomp = 4L,
      block_size = 128L
    )
    str(big_pca)
    #> List of 10
    #>  $ sdev               : num [1:4] 1.708 0.956 0.383 0.144
    #>  $ rotation           : num [1:4, 1:4] 0.521 -0.269 0.58 0.565 0.377 ...
    #>  $ center             : num [1:4] 5.84 3.06 3.76 1.2
    #>  $ scale              : num [1:4] 0.828 0.436 1.765 0.762
    #>  $ column_sd          : num [1:4] 0.828 0.436 1.765 0.762
    #>  $ eigenvalues        : num [1:4] 2.9185 0.914 0.1468 0.0207
    #>  $ explained_variance : num [1:4] 0.72962 0.22851 0.03669 0.00518
    #>  $ cumulative_variance: num [1:4] 0.73 0.958 0.995 1
    #>  $ covariance         : num [1:4, 1:4] 1 -0.118 0.872 0.818 -0.118 ...
    #>  $ nobs               : num 150
    #>  - attr(*, "backend")= chr "bigmemory"
    #>  - attr(*, "class")= chr [1:3] "bigpca_bigmemory" "bigpca" "list"

The returned list mirrors the structure of a `prcomp` object: singular values (`sdev`), rotation matrix (`rotation`), optional centering and scaling vectors, and additional diagnostics including the covariance matrix and explained variance ratios.

## Comparing against `prcomp`

    base_pca <- prcomp(iris_mat, center = TRUE, scale. = TRUE)

    align_columns <- function(reference, target) {
      aligned <- target
      cols <- min(ncol(reference), ncol(target))
      for (j in seq_len(cols)) {
        ref <- reference[, j]
        tgt <- target[, j]
        if (sum((ref - tgt)^2) > sum((ref + tgt)^2)) {
          aligned[, j] <- -tgt
        }
      }
      aligned
    }

    rotation_aligned <- align_columns(base_pca$rotation, big_pca$rotation)
    max_rotation_error <- max(abs(rotation_aligned - base_pca$rotation))
    max_sdev_error <- max(abs(big_pca$sdev - base_pca$sdev))

    big_scores <- pca_scores_bigmatrix(
      xpMat = big_iris,
      rotation = big_pca$rotation,
      center = big_pca$center,
      scale = big_pca$scale,
      block_size = 128L
    )

    scores_aligned <- align_columns(base_pca$x, big_scores)
    max_score_error <- max(abs(scores_aligned - base_pca$x))

    c(
      rotation = max_rotation_error,
      sdev = max_sdev_error,
      scores = max_score_error
    )
    #>     rotation         sdev       scores
    #> 3.247402e-15 2.914335e-15 7.993606e-15

The maximum absolute deviations between the base implementation and bigPCAcpp are negligible (on the order of numerical precision), showing that the out-of-core algorithm faithfully reproduces the same components and scores.

## Variable relationships

The exported helpers expose common PCA diagnostics without requiring the original data matrix in memory.

    loadings <- pca_variable_loadings(big_pca$rotation, big_pca$sdev)
    correlations <- pca_variable_correlations(
      big_pca$rotation,
      big_pca$sdev,
      big_pca$column_sd,
      big_pca$scale
    )
    contributions <- pca_variable_contributions(loadings)

    head(loadings)
    #>            [,1]       [,2]        [,3]        [,4]
    #> [1,]  0.8901688 0.36082989  0.27565767 -0.03760602
    #> [2,] -0.4601427 0.88271627 -0.09361987  0.01777631
    #> [3,]  0.9915552 0.02341519 -0.05444699  0.11534978
    #> [4,]  0.9649790 0.06399985 -0.24298265 -0.07535950
    head(correlations)
    #>            [,1]       [,2]        [,3]        [,4]
    #> [1,]  0.8901688 0.36082989  0.27565767 -0.03760602
    #> [2,] -0.4601427 0.88271627 -0.09361987  0.01777631
    #> [3,]  0.9915552 0.02341519 -0.05444699  0.11534978
    #> [4,]  0.9649790 0.06399985 -0.24298265 -0.07535950
    head(contributions)
    #>            [,1]         [,2]       [,3]       [,4]
    #> [1,] 0.27150969 0.1424440565 0.51777574 0.06827052
    #> [2,] 0.07254804 0.8524748749 0.05972245 0.01525463
    #> [3,] 0.33687936 0.0005998389 0.02019990 0.64232089
    #> [4,] 0.31906291 0.0044812296 0.40230191 0.27415396
    range(correlations)
    #> [1] -0.4601427  0.9915552

Loadings match the scaled rotation matrix, correlations honour whether the PCA was computed on standardised variables and remain bounded between -1 and 1, and contributions report the relative importance of each variable within a component.

## Visualising PCA results

The companion plotting helpers make it straightforward to inspect the components returned by bigPCAcpp.

    pca_plot_scree(big_pca)

Scree plot of variance explained by each component.

The scree plot summarises how much variance each component explains and makes it easy to identify natural cutoffs.

    pca_plot_scores(
      big_iris,
      rotation = big_pca$rotation,
      center = big_pca$center,
      scale = big_pca$scale,
      max_points = nrow(big_iris),
      sample = "head"
    )

Scores for the first two principal components.

Score plots provide a quick way to compare sample relationships using the requested principal components without materialising the full score matrix.

    pca_plot_correlation_circle(
      correlations,
      components = c(1L, 2L)
    )

Correlation circle highlighting how variables align with the first two components.

Correlation circles visualise how each variable aligns with the chosen components, making it easy to spot groups of features that contribute in a similar direction.

    pca_plot_biplot(
      big_scores,
      loadings,
      components = c(1L, 2L)
    )

Biplot combining sample scores and variable loadings.

The biplot overlays sample scores with the variable loadings, providing a joint view of how observations cluster along the selected components and which features drive those separations.

## Singular value decomposition helpers

The package also exposes building blocks for singular value decomposition (SVD) that operate directly on `big.matrix` instances, which can be useful for custom pipelines that only need the singular vectors or values.

    svd_res <- svd_bigmatrix(big_iris, nu = 2L, nv = 2L, block_size = 128L)
    svd_res$d
    #> [1] 95.959914 17.761034  3.460931  1.884826

The `svd_bigmatrix()` helper mirrors base R’s `svd()` but streams data in manageable blocks, making it practical for large file-backed matrices.

## Robust PCA and SVD

When data contain outliers, the robust variants supplied by bigPCAcpp help stabilise the recovered components by down-weighting leverage points.

    robust_pca <- pca_robust(iris_mat, ncomp = 4L)
    robust_pca$sdev
    #> [1] 1.9647910 0.5053148 0.3058318 0.2349409
    robust_pca$robust_weights[1:10]
    #>  [1] 0.3130404 0.2549449 0.7021058 0.7068777 0.6394573 0.5986293 0.6464197
    #>  [8] 0.6390229 0.6953475 0.6492590

The robust PCA interface accepts regular dense matrices, computes robust estimates of location and scale, and returns component scores along with the per-row weights and iteration counts used by the re-weighted SVD.

The underlying solver is also exposed directly for bespoke workflows that only need a robust singular value decomposition.

    robust_svd <- svd_robust(iris_mat, ncomp = 3L)
    robust_svd$d
    #> [1] 85.31120 16.25951  3.14779
    robust_svd$weights[1:10]
    #>  [1] 1 1 1 1 1 1 1 1 1 1

The robust SVD returns singular values, left singular vectors, and the weights assigned to each observation, which can be combined with classical SVD results to assess the influence of individual rows.

## Next steps for larger data

For on-disk matrices created with `filebacked.big.matrix()`, pass the descriptor pointer to `pca_bigmatrix()` and the algorithm will stream data in blocks, keeping memory usage bounded. Component scores can likewise be generated in batches using `pca_scores_stream_bigmatrix()`.

    library(bigmemory)
    library(bigPCAcpp)

    path <- tempfile(fileext = ".bin")
    desc <- paste0(path, ".desc")

    bm <- filebacked.big.matrix(
      nrow = nrow(iris_mat),
      ncol = ncol(iris_mat),
      type = "double",
      backingfile = basename(path),
      backingpath = dirname(path),
      descriptorfile = basename(desc)
    )

    bm[,] <- iris_mat

    pca <- pca_bigmatrix(bm, center = TRUE, scale = TRUE, ncomp = 4)
    scores <- filebacked.big.matrix(
      nrow = nrow(bm),
      ncol = ncol(pca$rotation),
      type = "double",
      backingfile = "scores.bin",
      backingpath = dirname(path),
      descriptorfile = "scores.desc"
    )

    pca_scores_stream_bigmatrix(
      bm,
      scores,
      pca$rotation,
      center = pca$center,
      scale = pca$scale
    )
    #> <pointer: 0x1267a8600>

Once the scores are stored on disk, they can be sampled and plotted just like the in-memory workflow:

    pca_plot_scores(
      bm,
      rotation = pca$rotation,
      center = pca$center,
      scale = pca$scale,
      components = c(1L, 2L),
      max_points = nrow(bm),
      sample = "head"
    )

Scores streamed from a file-backed big.matrix.

    library(bigmemory)
    library(bigPCAcpp)

    path <- tempfile(fileext = ".bin")
    desc <- paste0(path, ".desc")

    bm <- filebacked.big.matrix(
      nrow = 5000,
      ncol = 50,
      type = "double",
      backingfile = basename(path),
      backingpath = dirname(path),
      descriptorfile = basename(desc)
    )

    pca <- pca_bigmatrix(bm, center = TRUE, scale = TRUE, ncomp = 5)
    scores <- filebacked.big.matrix(
      nrow = nrow(bm),
      ncol = ncol(pca$rotation),
      type = "double",
      backingfile = "scores.bin",
      backingpath = dirname(path),
      descriptorfile = "scores.desc"
    )

    pca_scores_stream_bigmatrix(
      bm,
      scores,
      pca$rotation,
      center = pca$center,
      scale = pca$scale
    )

With these building blocks, bigPCAcpp enables analyses that match the accuracy of in-memory PCA workflows while scaling to data sets that exceed RAM.