---
title: "Benchmarking PLS2 Implementations"
shorttitle: "Benchmarking PLS2 Implementations"
author:
- name: "Frédéric Bertrand"
  affiliation:
  - Cedric, Cnam, Paris
  email: frederic.bertrand@lecnam.net
date: "`r Sys.Date()`"
output:
  rmarkdown::html_vignette:
    toc: true
vignette: >
  %\VignetteIndexEntry{Benchmarking PLS2 Implementations}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup_ops, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  fig.path = "figures/benchmarking-pls2-",
  fig.width = 7,
  fig.height = 5,
  dpi = 150,
  message = FALSE,
  warning = FALSE
)

LOCAL <- identical(Sys.getenv("LOCAL"), "TRUE")
set.seed(2025)
```

```{r setup, message=FALSE}
library(bigPLSR)
library(bigmemory)
library(bench)
set.seed(456)
```

## Overview

The package offers dense (`pls2_dense`) and streaming (`pls2_stream`)
solvers for multi-response partial least squares regression (PLS2).
This vignette demonstrates how to benchmark both variants on a synthetic
dataset featuring three correlated response variables.

### Recent additions

Beyond the dense and streaming SIMPLS/NIPALS solvers, bigPLSR now ships
with Kalman-filter PLS (`algorithm = "kf_pls"`), double-RKHS modelling
(`algorithm = "rkhs_xy"`) and optional coefficient thresholding. The
resampling helpers (`pls_cross_validate()`, `pls_bootstrap()`) can also
leverage the [`future`](https://future.futureverse.org) ecosystem for
parallel execution.

To benchmark these variants simply change the `algorithm` parameter in
the chunks below, for example:

```{r, eval=FALSE}
bench::mark(
  dense = pls_fit(X[], Y_mat, ncomp = ncomp, algorithm = "rkhs_xy"),
  streaming = pls_fit(X, Y, ncomp = ncomp, backend = "bigmem",
                      algorithm = "kf_pls", chunk_size = 1024L)
)
```

and remember to reset your `future` plan after enabling parallelism:

```{r, eval=FALSE}
future::plan(future::multisession, workers = 2)
pls_cross_validate(X[], Y_mat, ncomp = 4, folds = 3,
                   parallel = TRUE)
future::plan(future::sequential)
```

Multi-response benchmarks follow the same principles as the PLS1 case.
We focus on the `pls_fit()` API and contrast its dense and streaming
backends before reporting the stored results against third-party
packages.

## Simulated data

```{r data-generation, eval=LOCAL, cache=TRUE}
n <- 1200
p <- 60
q <- 3
ncomp <- 4

X <- bigmemory::big.matrix(nrow = n, ncol = p, type = "double")
X[,] <- matrix(rnorm(n * p), nrow = n)

loading_matrix <- matrix(rnorm(p * q), nrow = p)
latent_scores <- matrix(rnorm(n * q), nrow = n)
Y_mat <- scale(latent_scores %*% t(loading_matrix[1:q, , drop = FALSE]) +
                 matrix(rnorm(n * q, sd = 0.5), nrow = n))

Y <- bigmemory::big.matrix(nrow = n, ncol = q, type = "double")
Y[,] <- Y_mat

X[1:6, 1:6]
Y[1:6, 1:min(6, q)]
```

## Internal benchmarks

```{r internal-benchmark, eval=LOCAL, cache=TRUE}
internal_bench <- bench::mark(
  dense_simpls = pls_fit(as.matrix(X[]), Y_mat, ncomp = ncomp,
                         backend = "arma", algorithm = "simpls"),
  streaming_simpls = pls_fit(X, Y, ncomp = ncomp, backend = "bigmem",
                             algorithm = "simpls", chunk_size = 512L),
  dense_nipals = pls_fit(as.matrix(X[]), Y_mat, ncomp = ncomp,
                         backend = "arma", algorithm = "nipals"),
  streaming_nipals = pls_fit(X, Y, ncomp = ncomp, backend = "bigmem",
                             algorithm = "nipals", chunk_size = 512L),
  iterations = 15,
  check = FALSE
)
internal_bench
```

The dense path again excels when memory allows, whereas the streaming
backend prioritises scalability via block-wise processing.

## External references

```{r external-benchmark}
data("external_pls_benchmarks", package = "bigPLSR")
subset(external_pls_benchmarks, task == "pls2")
```

The stored table mirrors the structure of the PLS1 benchmark and was
produced with the script in `inst/scripts/external_pls_benchmarks.R`.

## Key messages

* Dense SIMPLS remains the fastest option for well-sized dense matrices.
* Streaming NIPALS offers robustness when responses are numerous or when
  the predictor matrix is file-backed.
* External comparisons help position bigPLSR relative to established
  alternatives without adding heavyweight dependencies to the vignette.
