--- title: "fastPLS User Guide" output: rmarkdown::html_vignette header-includes: - \usepackage{array} vignette: > %\VignetteIndexEntry{fastPLS User Guide} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>") options(width = 68) library(fastPLS) ``` ## Overview `fastPLS` combines partial least squares (PLS) modelling with compiled numerical implementations and optional accelerator execution. This vignette is a practical guide to choosing a model, compiling the package, fitting and evaluating models, and running single or nested cross-validation. Detailed derivations and pseudocode are reserved for the accompanying software-methods manuscript. ## Choosing a Model `fastPLS` provides four related PLS families through one fitting interface. The choice should follow the scientific question and the size of the data rather than the execution backend. The direct PLS-SVD method uses singular value decomposition (SVD) to obtain its latent directions. | Method | Use it when | Main consideration | |:--|:--|:--| | `plssvd` | A direct low-rank model is suitable, especially for multivariate responses. | One decomposition supplies every requested component prefix. | | `simpls` | A general linear PLS model is required for regression or classification. | This is the optimized SIMPLS-family estimator used by the package. | | `opls` | Response-orthogonal variation in the predictors should be separated before prediction. | The orthogonal filter adds sequential work and must be applied to new observations. | | `kernelpls` | A nonlinear relation is plausible and the number of observations is moderate. | Nonlinear kernels require a training Gram matrix with quadratic storage in the sample count. | Classification can decode PLS scores by the largest dummy-response score (`classifier = "argmax"`) or by linear discriminant analysis (LDA) in the retained score space (`classifier = "lda"`). LDA is the default. It can improve discrimination when class boundaries are not well represented by the largest raw PLS score. All public methods use the package's native randomized SVD. It is an approximation, so the fitted object records the seed and effective numerical controls. ## Installation and Compilation Install the released package with: ```{r install-cran, eval = FALSE} install.packages("fastPLS") ``` To compile the development version from source, install `remotes` and use a fresh R session: ```{r install-github, eval = FALSE} install.packages("remotes") remotes::install_github( "tkcaccia/fastPLS", upgrade = "never", force = TRUE, build_vignettes = TRUE ) ``` The package always builds a CPU backend. CUDA and Metal are optional and are included only when their toolchains are available at compilation time. An unavailable accelerator produces an error when requested; it does not silently fall back to the CPU. ### macOS Install Apple's command-line developer tools before a source build: ```{sh macos-command-line-tools, eval = FALSE} xcode-select --install ``` macOS uses Apple Accelerate for CPU BLAS/LAPACK. Metal is detected automatically on supported Apple systems. To require Metal and stop installation if it cannot be compiled, set the following before the source installation: ```{r require-metal, eval = FALSE} Sys.setenv(FASTPLS_USE_METAL = "1") ``` In the benchmarks conducted on the Apple M3 system used for package testing, the Metal backend did not provide a meaningful computational-speed improvement over the macOS CPU backend. Performance may differ with the matrix dimensions, model configuration, and Apple hardware generation. ### Ubuntu and Debian Install the compiler toolchain and OpenBLAS development files before building the package: ```{sh ubuntu-openblas, eval = FALSE} sudo apt update sudo apt install build-essential gfortran pkg-config libopenblas-dev ``` Then require OpenBLAS during the source build. This prevents an unnoticed fallback to the BLAS/LAPACK supplied by R: ```{r require-openblas-linux, eval = FALSE} Sys.setenv(FASTPLS_USE_OPENBLAS = "1") install.packages("fastPLS", type = "source") ``` Ubuntu and Debian releases may provide different OpenBLAS versions, compilation options, threading implementations, and CPU kernels. Consequently, identical fastPLS calls can have substantially different runtimes across distributions or OpenBLAS installations without indicating a change in the statistical model. For reproducible timing, use a current build compiled for the target processor and record the version and selected kernel reported by `fastPLS_blas()`. Detecting the OpenBLAS family alone does not establish that an architecture-appropriate kernel is active. ### Fedora Install the corresponding development packages, then use the same R command shown above: ```{sh fedora-openblas, eval = FALSE} sudo dnf install gcc gcc-c++ gcc-gfortran make pkgconf-pkg-config \ openblas-devel ``` ### Windows x86-64 Install the Rtools release that matches the installed R version. A source build can use R's BLAS/LAPACK, but OpenBLAS is recommended for the Linux and Windows performance routes evaluated with fastPLS. One option is to install an x86-64 OpenBLAS archive from an MSYS2 UCRT64 terminal: ```{sh windows-openblas, eval = FALSE} pacman -S --needed mingw-w64-ucrt-x86_64-openblas ``` Point the package configuration to the matching prefix and require OpenBLAS: ```{r require-openblas-windows, eval = FALSE} Sys.setenv( FASTPLS_USE_OPENBLAS = "1", OPENBLAS_ROOT = "C:/msys64/ucrt64" ) remotes::install_github( "tkcaccia/fastPLS", upgrade = "never", force = TRUE, build_vignettes = TRUE ) ``` The OpenBLAS library must match the target architecture. In particular, a Windows ARM64 build must not use an x86-64 Rtools or MSYS2 archive. If a compatible ARM64 OpenBLAS development package is unavailable, leave automatic detection enabled and allow the build to use R's BLAS/LAPACK instead. Windows OpenBLAS distributions can also differ in version, compiler options, threading implementation, and processor-specific kernel. These differences can materially affect runtime even when the fastPLS version and model settings are unchanged. For reproducible benchmarks, record the OpenBLAS version, selected kernel, thread count, and library path reported by `fastPLS_blas()`. If OpenBLAS is installed in a nonstandard location on Linux or Windows, set `OPENBLAS_ROOT` to its installation prefix. Set `FASTPLS_USE_OPENBLAS = "0"` only when an R-supplied BLAS build is explicitly desired. ### Verify the Compiled Libraries After installation, restart R and verify the selected CPU library and optional accelerators: ```{r verify-compiled-libraries, eval = FALSE} library(fastPLS) fastPLS_blas() has_cuda() has_metal() ``` `fastPLS_blas()` reports the backend together with the OpenBLAS version, configuration, selected CPU core, parallel runtime, active thread count, and resolved library path when available. Linux and Windows benchmarks should proceed only when `fastPLS_blas()$backend` is `"OpenBLAS"` and the expected `version` and `core` are active. Use `fastPLS_blas(details = FALSE)` when only the former scalar backend name is required. The campaign tools in `fastPLS-extra` validate the detailed report before running fastPLS timing stages. The separate installation vignette contains CUDA toolkit requirements, environment variables, and troubleshooting for architecture or linker errors. ## Backend and CPU Configuration CPU is the default backend. After choosing a mathematical model, users can set one execution backend for the R session when several calls should use the same hardware: ```r options(backend = "cuda") fit <- pls(X, y, method = "simpls") # CUDA session default fit.cpu <- pls(X, y, backend = "cpu") # explicit override ``` Batch jobs may use `FASTPLS_BACKEND=cpu|cuda|metal`. Precedence is an explicit function argument, `options(backend = ...)`, `FASTPLS_BACKEND`, and finally CPU. For CPU execution, `options(n.cores = 4L)` requests four threads from the linked BLAS/OpenMP runtime. Eligible matrix operations may use these threads, but sequential PLS deflation remains serial and multicore acceleration depends on matrix shape and the installed numerical library. An explicit `n.cores =` argument takes precedence over the option. The first operation that resolves an unavailable option or environment value raises an error; CPU is never substituted. Prediction must use the backend that fitted the model; therefore, a model fitted with CUDA or Metal requires the same explicit or session-level backend selection during prediction. ## Supported Computational Routes The terms in this table distinguish implementation coverage and data residency. `Tested` denotes a route covered by fixed-seed numerical tests. `Approximate` identifies rSVD execution. `Native` means that preprocessing, cross-products, decomposition, component updates, prediction, and an optional LDA head execute on the selected device after input transfer. `Unavailable` combinations stop; they do not silently fall back to CPU. The table uses T for tested, A for approximate, N for device-native, H for an explicit host/device hybrid, and U for unavailable. | Model or operation | CPU | CUDA | Metal | |---|---:|---:|---:| | PLS-SVD, float64 | T/A | T/A/N | U | | SIMPLS family, float64 | T/A | T/A/N | U | | OPLS, float64 | T/A | T/A/N | U | | Linear kernel PLS, float64 | T/A | T/A/N | U | | Nonlinear kernel PLS, float64 | T/A | T/A/N | U | | PLS-SVD, float32 | T/A | T/A/N | T/A/H | | SIMPLS family, float32 | T/A | T/A/N | T/A/H | | OPLS, float32 | T/A | T/A/N | T/A/H | | Linear kernel PLS, float32 | T/A | T/A/N | T/A/H | | Nonlinear kernel PLS, float32 | T/A | T/A/N | T/A/H | | Argmax PLS-DA | T | T/N | T/H | | Latent-space LDA | T | T/N | T/H | Float32 support is route and platform dependent, as described in the Float32 Input section. CUDA uses a device-native route, whereas Metal uses the fixed CPU/Metal operation split. Metal float64 requests stop because the hardware does not provide native double-precision arithmetic. Nonlinear kernel workloads that exceed the guarded device-memory budget stop before allocation and never fall back to another method or backend. ## Public API and Data Inputs Users select the mathematical method and implementation through the public functions; the internal C++, CUDA, Metal, and benchmarking helpers are not called directly. | Function | Purpose | |---|---| | `pls()` | Fit PLS models for regression or classification. | | `predict()` | Predict from fitted `fastPLS`, OPLS, or kernel PLS models. | | `plot()` | Plot PLS scores and optional ellipses. | | `plot.permutation()` | Plot R2/Q2 diagnostics from a PLS permutation test. | | `pls.single.cv()` | Select components by grouped cross-validation. | | `pls.double.cv()` | Run nested cross-validation. | | `evaluate()` | Evaluate classification or regression predictions. | | `fastsvd()` | Run the stand-alone native CPU randomized SVD. | | `fastcor()` | Compute fast Pearson-style correlations. | | `ViP()` | Compute variable-importance-in-projection trajectories. | | `fastPLS_blas()` | Report the CPU library, version, configuration, and runtime core. | | `has_cuda()` | Check whether CUDA-native fastPLS support is available. | | `has_metal()` | Check for Apple Metal support. | ### Choosing a Starting Model | Goal | Suggested call | |---|---| | Fast exploratory modelling | SIMPLS-family estimator with CPU rSVD | | Repeatable CPU analysis | `backend = "cpu", seed = 1` | | Direct cross-covariance PLS-SVD | `method = "plssvd"` | | Remove response-unrelated structured variation | `method = "opls"` | | Nonlinear relationships | Kernel PLS with an RBF or polynomial kernel | | Classification: latent-space discriminant analysis (default) | `classifier = "lda"` | | Classification: argmax PLS-DA decoding | `classifier = "argmax"` | | Small or moderate data | `backend = "cpu"` | | Large dense matrices | CUDA when available | | Apple Silicon acceleration | Metal when available | | Float32 CPU input | `float::fl(X)`, `backend = "cpu"` | The public classification choices are `"argmax"` and `"lda"`; regression ignores `classifier`. LDA uses the fixed scale-normalized Cholesky fallback sequence described above and does not expose a ridge-tuning argument. ## Classification Tasks For classification, responses are supplied as factors. `fastPLS` handles the PLS-DA response encoding internally and returns predicted class labels. The `classifier` argument is used only for this type of task and selects the classification head: `argmax` or `lda`. These options are not regression models and are not used for numeric responses. The examples in this section use `iris` for compact multiclass classification. ```{r chunk-002} set.seed(100) X <- as.matrix(iris[, 1:4]) Y_cls <- iris$Species cls_test_id <- sample(seq_len(nrow(X)), 30) Xtrain <- X[-cls_test_id, , drop = FALSE] Xtest <- X[cls_test_id, , drop = FALSE] Ytrain_cls <- Y_cls[-cls_test_id] Ytest_cls <- Y_cls[cls_test_id] ``` ### Fit And Predict A Classifier The `method` argument selects the PLS algorithm, `backend` selects the implementation, and `classifier` selects the classification head. The default classification head is `classifier = "lda"`, which fits linear discriminant analysis in the retained PLS score space. Use `classifier = "argmax"` explicitly to predict the class with the largest PLS-DA response score. ```{r chunk-003} fit_cls <- pls( Xtrain, Ytrain_cls, Xtest, Ytest_cls, ncomp = 1:2, fit = TRUE, return_variance = FALSE, seed = 101 ) fit_cls$accuracy ``` Classification models can also be fitted once and predicted later. `predict()` returns only the predicted class by default and can optionally return ranked classes by setting `top` to a positive integer. The ordinary prediction in `Ypred` is always the rank-1 class. When `top > 1`, `Ypred_top` contains the ordered candidate labels for each sample: `rank1` is the predicted class, `rank2` is the next most likely class, and so on. If available, `Ypred_top_score` contains the corresponding class scores used to create that ranking. For both float64 and float32 models, the default `raw_scores = FALSE` evaluates ranked predictions in bounded row blocks and retains only the requested ranks. Set `raw_scores = TRUE` only when the complete class-score arrays are needed, because those arrays can be substantially larger. ```{r chunk-004} fit_cls_train_only <- pls( Xtrain, Ytrain_cls, ncomp = 1:2, classifier = "lda", fit = TRUE, return_variance = FALSE, seed = 101 ) pred_cls_later <- predict( fit_cls_train_only, Xtest, Ytest = Ytest_cls, top = 2, raw_scores = TRUE ) pred_cls_later$accuracy pred_cls_later$metrics$metrics head(pred_cls_later$Ypred_top[["ncomp=2"]]) ``` ### Evaluate Classification Predictions Use `evaluate()` to summarize predicted class labels, ranked labels, class-score matrices, or a complete object returned by `predict()`. The task and requested rank are inferred from these inputs. For classification, `lift_accuracy` is accuracy divided by the `no_information_rate`, the accuracy obtained by always predicting the most frequent observed class. Values above one therefore improve on this simple majority-class baseline. For classification, the complete output includes global metrics, per-class metrics, and the confusion matrix. ```{r chunk-005} eval_cls_path <- evaluate( observed = Ytest_cls, predicted = pred_cls_later ) eval_cls <- eval_cls_path$by_component[["ncomp=2"]] eval_cls ``` The same complete evaluation is also stored automatically in a fitted PLS object when observed test responses are supplied. Results are grouped first by data role and then by component count. ```{r chunk-005a} fit_cls$metrics$test[["ncomp=2"]]$metrics ``` Rows of the confusion matrix are predicted labels and columns are observed labels. The confusion matrix is returned as an ordinary R table. ```{r chunk-006} eval_cls$confusion ```
When class-score matrices are available, `evaluate()` can also report top-k accuracy. Top-k accuracy asks whether the true class appears anywhere among the first `k` ranked labels. Thus top-1 accuracy is ordinary classification accuracy, while top-5 accuracy gives credit when the correct class appears among the five highest-scoring alternatives. ```{r chunk-007} score_last <- pred_cls_later$LDA_scores[ , , dim(pred_cls_later$LDA_scores)[3L] ] evaluate( observed = Ytest_cls, predicted = score_last ) ``` ### Classification Heads The same `pls()` interface exposes two classification-specific heads for factor responses: argmax PLS-DA and latent-space LDA. They are decoders applied after the PLS model has produced class-response scores or latent scores. For regression, leave `classifier` at its default; numeric responses are predicted directly as continuous values. ```{r chunk-008} fit_cls_plssvd <- pls( Xtrain, Ytrain_cls, Xtest, Ytest_cls, ncomp = 1:2, method = "plssvd", seed = 100 ) head(fit_cls_plssvd$Ypred) evaluate( observed = Ytest_cls, predicted = fit_cls_plssvd$Ypred[["ncomp=2"]] )$confusion ``` For PLS-DA with an LDA prediction head, use `classifier = "lda"`. On systems with GPU support, the backend is selected through `backend = "cuda"` or `backend = "metal"` where available: ```{r chunk-009, eval = FALSE} fit_cls_lda_gpu <- pls( Xtrain, Ytrain_cls, Xtest, Ytest_cls, ncomp = 1:2, method = "plssvd", backend = "cuda", classifier = "lda" ) ``` An unavailable CUDA or Metal selection raises an error; fastPLS never changes the request silently to CPU. To run on the CPU, select it explicitly: ```{r chunk-010} fit_cls_lda_cpu <- pls( Xtrain, Ytrain_cls, Xtest, Ytest_cls, ncomp = 1:2, method = "plssvd", backend = "cpu", seed = 100, classifier = "lda" ) head(fit_cls_lda_cpu$Ypred) ``` ### Kernel PLS For Classification Kernel PLS changes the representation of the samples before the inner PLS fit. The `linear` kernel is equivalent to an ordinary inner-product representation and is useful as a fast baseline. The `rbf` kernel uses a radial-basis similarity; the `poly` kernel uses polynomial interactions among features. ```{r chunk-012} kernel_fits <- lapply(c("linear", "rbf", "poly"), function(k) { pls( Xtrain, Ytrain_cls, Xtest, Ytest_cls, ncomp = 1:2, method = "kernelpls", kernel = k, degree = 2, seed = 102 ) }) names(kernel_fits) <- c("linear", "rbf", "poly") kernel_accuracy <- vapply(kernel_fits, function(fit) { mean(fit$Ypred[["ncomp=2"]] == Ytest_cls) }, numeric(1)) kernel_accuracy ``` ### Classification Score Plots `plot()` can visualize stored PLS score maps. Refit with `fit = TRUE` to store training scores, or predict with `proj = TRUE` to store test scores. Ellipses can be ordinary confidence ellipses or Hotelling T2 ellipses. ```{r chunk-013, fig.width = 5, fig.height = 4} plot( fit_cls, groups = Ytrain_cls, ellipse = TRUE, ellipse.type = "confidence" ) ``` The following example fits OPLS to the three-class `iris` split defined at the beginning of this section, with two orthogonal components (`north = 2`). The same `plot()` interface is available for PLS-SVD, the SIMPLS-family estimator, OPLS, and kernel PLS. ```{r iris-three-class-opls, fig.width = 7.5, fig.height = 4} opls_three <- pls( Xtrain, Ytrain_cls, Xtest, Ytest_cls, ncomp = 1:2, method = "opls", fit = TRUE, proj = TRUE, north = 2L, seed = 201 ) old_par <- par(mfrow = c(1, 2), mar = c(4.2, 4.2, 2.2, 0.8)) plot( opls_three, score.set = "train", groups = Ytrain_cls, ellipse = TRUE, ellipse.type = "hotelling", main = "OPLS training, north = 2" ) plot( opls_three, score.set = "test", groups = opls_three$Ypred[["ncomp=2"]], xlim = c(-0.16, 0.42), main = "OPLS test prediction" ) par(old_par) ``` ## Regression Tasks For regression, responses are supplied as numeric vectors or matrices. The examples in this section use `mtcars` to demonstrate univariate regression, multivariate regression, and OPLS. Regression does not use `classifier = "argmax"` or `classifier = "lda"`: those are classification heads for factor responses. For numeric responses, `pls()` returns continuous predictions and regression metrics such as `R2Y`, `Q2Y`, and `RMSD`. ```{r chunk-016} set.seed(100) Xreg <- as.matrix(mtcars[, c("disp", "hp", "wt", "qsec", "drat")]) Y_reg <- mtcars$mpg reg_test_id <- sample(seq_len(nrow(Xreg)), 8) Xreg_train <- Xreg[-reg_test_id, , drop = FALSE] Xreg_test <- Xreg[reg_test_id, , drop = FALSE] Ytrain_reg <- Y_reg[-reg_test_id] Ytest_reg <- Y_reg[reg_test_id] ``` ### Univariate Regression For a univariate task, supply the response as a numeric vector. The simplest workflow is to provide both the training data and an independent test set directly to `pls()`. The fitted object then contains test-set predictions and, when `Ytest` is supplied, predictive metrics. ```{r chunk-017} fit_reg <- pls( Xreg_train, Ytrain_reg, Xreg_test, Ytest_reg, ncomp = 1:3, fit = TRUE, return_variance = FALSE ) fit_reg$Q2Y ``` ### Float64 and Float32 Standard R numeric matrices use float64, whereas a `float::float32` object requests float32 execution. Float64 stores each value in eight bytes and retains approximately 15--16 decimal digits of precision. Float32 stores each value in four bytes and retains approximately seven decimal digits. Float32 can therefore halve the representation size of a matrix and reduce data-transfer and memory-bandwidth costs, but it also introduces more rounding error. | Property | float64 | float32 | |:--|:--|:--| | R input | Standard numeric matrix | `float::fl()` matrix | | Storage per value | 8 bytes | 4 bytes | | Approximate decimal precision | 15--16 digits | 7 digits | | fastPLS backends | CPU and CUDA | CPU, CUDA, and Metal | | Suggested role | Baseline and confirmatory analyses | Reduced-storage or accelerated analyses after numerical comparison | Reduced representation size does not guarantee a faster fit or lower peak process memory. Temporary workspaces, decomposition costs, host-device transfers, and backend-specific kernels can dominate the calculation. Runtime, incremental host memory, and device memory may therefore increase or decrease when float32 replaces float64. The lower precision can affect nearly tied latent directions, selected component counts, predictions, or classifications close to a decision boundary. For a new scientific workflow, compare float32 and float64 using the same split, seed, component grid, and model settings, and examine prediction differences as well as the final performance metric. Float64 remains the baseline precision when small numerical differences could alter the scientific conclusion. CUDA keeps PLS-SVD, the SIMPLS-family estimator, OPLS, nonlinear kernel construction, prediction, and LDA on the selected GPU. Metal accepts float32 only and uses the fixed CPU/Metal operation split described above; CUDA supports float32 and float64. Nonlinear kernel PLS requires an explicit `n` by `n` Gram matrix, so the package checks the estimated live device or unified-memory storage before fitting. The package warns once for measured-risk regimes. These include precision-sensitive SIMPLS-family and linear kernel-PLS classification, nonlinear kernels, and multivariate regression with at least 10,000 response columns and 50 components. In the latter regime, runtime, memory, and numerical behavior must be checked against float64 before the float32 result is used for scientific interpretation. Unsupported combinations stop before allocation and are never silently promoted to float64. On Windows, the standard R toolchain does not expose the single-precision BLAS/LAPACK symbols used by the Unix-like compiled kernels. The Windows CPU route therefore combines float-package rSVD and Cholesky operations with portable C++ float kernels. It supports PLS-SVD, the SIMPLS-family estimator, OPLS, linear and nonlinear kernel PLS, argmax, and latent-space LDA when `backend = "cpu"` and rSVD is selected automatically. Model factors, LDA buffers, kernel matrices, and predictions remain float32, but this portable route can be slower than the native Unix-like implementation. Windows float32 accelerator requests stop with an unsupported-combination error rather than silently converting data to float64. ```{r chunk-018} Xreg32 <- float::fl(as.matrix(Xreg_train)) Yreg32 <- float::fl(matrix(Ytrain_reg, ncol = 1)) fit_reg32 <- pls( Xreg32, Yreg32, float::fl(as.matrix(Xreg_test)), float::fl(matrix(Ytest_reg, ncol = 1)), ncomp = 1:2 ) fit_reg32$Q2Y ``` For standard double-precision regression, `Ypred` is a numeric prediction array with one slice for each requested number of components. For float32 input, `Ypred` is kept as a named list of `float::float32` prediction matrices. In the standard example below, the last array slice corresponds to the largest requested component count (`ncomp = 3`). The metric vectors are named by component count, for example `fit_reg$Q2Y["ncomp=3"]`. ```{r chunk-019, fig.width=4.8, fig.height=4} reg_component <- dim(fit_reg$Ypred)[3L] pred_mpg <- fit_reg$Ypred[, , reg_component] plot( Ytest_reg, pred_mpg, pch = 21, bg = "#4E79A7", col = "black", xlab = "Observed mpg", ylab = "Predicted mpg", main = "Regression: observed vs predicted" ) abline(0, 1, col = "#D55E00", lwd = 2) ``` The alternative workflow is to fit the model once without a test set, then call `predict()` later. This is useful when the same model must be applied to several independent datasets. `predict()` automatically applies the centering/scaling stored in the fitted object. When `Ytest` is supplied, it also passes the predictions to `evaluate()` and stores the complete evaluation under `metrics`. ```{r chunk-020} fit_reg_train_only <- pls( Xreg_train, Ytrain_reg, ncomp = 1:3, fit = TRUE, return_variance = FALSE ) pred_reg_later <- predict( fit_reg_train_only, Xreg_test, Ytest = Ytest_reg, proj = TRUE ) pred_reg_later$Q2Y pred_reg_later$metrics$metrics head(pred_reg_later$Ttest) ``` ### Evaluate Regression Predictions For numeric regression, `evaluate()` reports R2, Q2, RMSD/RMSE, MAE, bias, median relative error percentage, RPD, and correlations. If `ytrain` is supplied, Q2 is calculated relative to the training-set response mean, which is the preferred setting for independent test-set evaluation. For a response matrix, each response column is centered on its corresponding training mean before the denominator sums are aggregated. If `ytrain` is omitted, Q2 is returned as `NA`; it is not silently replaced by an R2 calculation. For wide multivariate responses, `pls()`, `pls.single.cv()`, and `pls.double.cv()` return aggregate evaluation metrics by default. Set `bycol = TRUE` to also calculate the full response-wise metric table; this is useful for inspecting individual spectral bins, but can be expensive for NMR data with many response columns. ```{r chunk-021} eval_reg <- evaluate( observed = Ytest_reg, predicted = pred_mpg, ytrain = Ytrain_reg ) eval_reg$task eval_reg$metrics lapply(eval_reg$metric_definitions, strwrap, width = 56) eval_reg$per_response ``` ### Multivariate Regression For multivariate regression, supply a numeric response matrix with one column per outcome. The following example predicts `mpg`, `qsec`, and `drat` simultaneously from a common predictor matrix. `bycol = TRUE` requests both the aggregate multivariate metrics and a response-wise summary. ```{r multivariate-regression} Xmulti <- as.matrix(mtcars[, c("disp", "hp", "wt", "gear", "carb")]) Ymulti <- as.matrix(mtcars[, c("mpg", "qsec", "drat")]) Xmulti_train <- Xmulti[-reg_test_id, , drop = FALSE] Xmulti_test <- Xmulti[reg_test_id, , drop = FALSE] Ymulti_train <- Ymulti[-reg_test_id, , drop = FALSE] Ymulti_test <- Ymulti[reg_test_id, , drop = FALSE] fit_multi <- pls( Xtrain = Xmulti_train, Ytrain = Ymulti_train, Xtest = Xmulti_test, Ytest = Ymulti_test, ncomp = 1:3, method = "plssvd", bycol = TRUE, return_variance = FALSE, seed = 102 ) fit_multi$metrics$test[["ncomp=3"]]$metrics fit_multi$metrics$test[["ncomp=3"]]$per_response ``` Component selection uses the same interface. With a numeric response matrix, the default criterion is aggregate held-out RMSD across all response columns. ```{r multivariate-regression-cv} cv_multi <- pls.single.cv( Xdata = Xmulti_train, Ydata = Ymulti_train, ncomp = 1:3, kfold = 3, method = "plssvd", fit = FALSE, bycol = TRUE, return_splits = TRUE, seed = 102 ) cv_multi$best_ncomp cv_multi$best_metric_value head(cv_multi$split_index) ``` ### OPLS For Regression OPLS is accessed through the same `pls()` function with `method = "opls"`. It is often used to separate predictive variation from response-orthogonal variation. `ncomp` counts predictive components, separately from the orthogonal components removed by `north`. Each removed direction reduces the available predictor rank. For example, with eight independent predictors and one removed direction, at most seven predictive components remain. A request exceeding the remaining rank raises an error; the same restriction applies within each CV training fold. ```{r chunk-022} fit_opls <- pls( Xreg_train, Ytrain_reg, Xreg_test, Ytest_reg, ncomp = 1:2, method = "opls", seed = 101 ) class(fit_opls) fit_opls$Q2Y ``` ## Cross-Validation `fastPLS` provides two cross-validation helpers. Use `pls.single.cv()` for grouped k-fold or leave-one-group-out validation; pass a scalar `ncomp` for a fixed-component CV or a vector of candidates when the number of components should be selected from a grid. Use `pls.double.cv()` for nested validation, where an inner CV chooses the number of components and an outer CV estimates predictive performance. Both helpers support regression and classification and dispatch to compiled CPU or accelerator routes when available. CUDA SIMPLS-family CV keeps task data and fold workspaces on the device. CUDA library handles are created once per CV call and reused across folds rather than reconstructed for every fit. CUDA PLS-SVD CV also uses a resident route for large multivariate regression responses, while classification and smaller-response tasks retain the faster fold-local CUDA route. This choice is automatic and does not change fold assignment, fold-specific preprocessing, requested components, or prediction semantics. For large multivariate responses, predictor and response marginal moments are computed once. Each training fold obtains its own moments by subtracting the held-out contribution before fold-specific centering and scaling. The large cross-covariance remains matrix-free, so this reuse does not introduce a dense predictor-by-response cache. When its cost and storage model is favorable, the compiled CPU engine can also form a bounded response Gram matrix once for wide-response SIMPLS-family problems and extract an exactly double-centred training submatrix for each fold. The Gram kernel is selected between GEMM and SYRK by platform, precision, and matrix shape; a SYRK route retains one triangle until a complete fold matrix is required. Fold-specific preprocessing, seeds, and model fitting remain unchanged. Aggregate regression metrics are computed directly from the native out-of-fold predictions before those predictions are formatted for R. On CPU and Metal, the complete nested coordinator is compiled when inner selection uses accuracy, balanced accuracy (or its identical macro-recall definition), dummy-response Q2Y, or regression RMSD/Q2Y. Other documented criteria, such as macro F1, MAE, RPD, or correlation, retain R-level outer coordination around compiled single-CV and fitting kernels because their full metric paths are assembled by `evaluate()`. CUDA likewise uses R-level nested coordination around its native CUDA kernels. These routes return the same documented fold structure, but the fully compiled coordinator avoids repeated R calls and model-object assembly and is therefore preferable when its metric matches the scientific objective. ### Ordinary k-fold CV In ordinary k-fold CV, samples are split directly into `kfold` folds. The example below validates a fixed two-component SIMPLS-family classifier with five folds. ```{r chunk-023} cv_kfold <- pls.single.cv( Xdata = Xtrain, Ydata = Ytrain_cls, ncomp = 2, kfold = 5, return_splits = TRUE, seed = 103 ) cv_kfold$metrics$cross_validated[["ncomp=2"]]$metrics head(cv_kfold$split_index) ``` `metrics$cross_validated` contains the complete `evaluate()` output for each tested component count, while `selection_metrics` is the compact internal table used to select the best setting. When `fit = TRUE`, `metrics$fitted` evaluates the corresponding model fitted to the full dataset. With `return_splits = TRUE`, `split_index` has one row per input sample and one column per fold; entries identify whether that sample was used for training or testing. Nested CV uses the same optional field for all outer and inner splits; `outer_test` marks samples that are unavailable to a given inner CV cycle. ### Grouped k-fold CV with `constrain` The `constrain` argument controls grouped splitting. It is a vector with one entry per sample; samples with the same value are assigned to the same fold. In practice, this prevents leakage when multiple rows come from the same patient, subject, batch, or technical replicate. For example, if two spectra come from the same patient, giving them the same patient identifier in `constrain` ensures that both spectra are placed either in the training set or in the test set, never one in each. ```{r chunk-024} patient_id <- rep( seq_len(ceiling(nrow(Xtrain) / 2)), each = 2 )[seq_len(nrow(Xtrain))] cv_grouped <- pls.single.cv( Xdata = Xtrain, Ydata = Ytrain_cls, constrain = patient_id, ncomp = 2, kfold = 4, return_splits = TRUE, seed = 104 ) c(n_folds = length(unique(cv_grouped$fold)), n_patient_groups = length(unique(patient_id))) head(data.frame( sample_index = seq_len(nrow(Xtrain)), patient_id = patient_id, cv_grouped$split_index, check.names = FALSE ), 8) patient_rows <- split(seq_along(patient_id), patient_id) patients_kept_together <- vapply( seq_len(ncol(cv_grouped$split_index)), function(fold) { all(vapply(patient_rows, function(rows) { length(unique(cv_grouped$split_index[rows, fold])) == 1L }, logical(1L))) }, logical(1L) ) stopifnot(all(patients_kept_together)) data.frame( fold = colnames(cv_grouped$split_index), patients_kept_together = patients_kept_together ) ``` The displayed rows show that the two samples from each patient receive the same `training` or `test` label within a fold. The executable `stopifnot()` check applies this rule to every patient and every fold, so the vignette build fails if grouped splitting ever places samples from one patient on opposite sides of a split. ### Leave-one-out CV and leave-one-group-out CV Leave-one-out CV is requested with `kfold = "loocv"`. When `constrain` is not supplied, each sample is held out once. When `constrain` is supplied, LOOCV becomes leave-one-constraint-group-out CV, so a whole patient, subject, batch, or replicate group is held out together at each iteration. Numeric `kfold` values greater than or equal to the number of constraint groups are also treated as leave-one-group-out CV. ```{r chunk-025} cv_loocv <- pls.single.cv( Xdata = Xtrain, Ydata = Ytrain_cls, constrain = patient_id, ncomp = 1, kfold = "loocv", seed = 105 ) c(n_loocv_folds = length(unique(cv_loocv$fold)), n_patient_groups = length(unique(patient_id))) ``` ### Component and hyperparameter optimization CV `pls.single.cv()` repeats the same CV splitting strategy over several candidate component counts and returns the best value according to the predictive metric: accuracy for classification and RMSD for regression. Fold-aware Q2Y, full-data fitted R2Y, balanced accuracy, and the other task-specific metrics documented below can instead be requested explicitly through `selection`. Predictive arguments can also be supplied as vectors to tune a compact grid. Fold-control arguments such as `kfold` remain single settings for the whole CV run. For LDA classification, a rare class may be absent from an inner training fold. fastPLS fits LDA to the classes represented in that fold and maps the predictions back to the original factor levels. Held-out observations from an absent class are retained in the accuracy and balanced-accuracy calculations, although that class cannot be predicted by a model that did not observe it. When otherwise identical argmax and LDA configurations are requested together, fastPLS calculates the PLS component path and fold projection once. The two classification heads retain separate predictions, metric paths, and selected component counts. For sufficiently tall SIMPLS classification problems, full-data predictor and class moments are also computed once. Each training fold obtains its moments by subtracting the held-out contribution before applying fold-specific centering and scaling; the PLS model, LDA model, and predictions are still fitted independently within every fold. This avoids repeatedly materializing large training-score matrices without leaking held-out information into model fitting. The same sufficient statistics can assemble eligible PLS-SVD folds through the fold-specific predictor Gram and class cross-product, without materializing the training-score matrix. On the Metal route, small reduced products are evaluated with Accelerate while the large sample-matrix products assigned to Metal remain on the GPU. Some grouped or highly imbalanced folds have lower effective rank than the requested component path. In that case, fastPLS fits only the estimable score prefix and repeats its prediction and metric for larger requested counts. The requested path is preserved, while `effective_ncomp` records the prefix used in each fold. For LDA, a fold with no estimable PLS direction uses its empirical training-class priors and finite log-prior scores; a training fold containing one class predicts that class. These fallbacks are reported through the fold `status` field rather than surfacing as a matrix-dimension error. The same rule applies to a direct regression fit. If fewer directions are estimable than requested, `effective_ncomp` records the usable prefix and later paths repeat the last estimable prediction and coefficient matrix. If the training response is constant, no direction is estimable: predictions equal the training-response mean, coefficient paths are zero, and response-variance metrics with a zero denominator are `NA`. ```{r chunk-026} cv_opt <- pls.single.cv( Xdata = Xreg_train, Ydata = Ytrain_reg, ncomp = 1:3, kfold = 5 ) cv_opt$best_ncomp ``` ### Interpreting `R2Y`, `Q2Y`, and `RMSD` For regression models, `R2Y`, `Q2Y`, and `RMSD` answer different questions and should not usually be identical. In `pls()`, `R2Y` is training-set R2 and independent-test `Q2Y` uses the training-response mean in its denominator. For multivariate responses, each response is centered separately before the sums of squares are aggregated. In `pls.single.cv()`, each held-out prediction is evaluated relative to the corresponding fold-training response means. `pls.double.cv()` applies the same rule to the outer folds. `RMSD` is also calculated from held-out predictions and is reported on the response scale, so lower values are better. `R2Y` is a training-set explained-variance estimate from one additional model fitted on the full dataset; set `fit = FALSE` to skip this extra fit when only cross-validated performance is needed. For classification, `Q2Y` is calculated from held-out dummy-coded PLS-DA response scores using fold-training class proportions, `accuracy` reports decoded-label accuracy, and `R2Y` is calculated from the full-data PLS-DA fit on the dummy-coded response scores. Dummy-response `Q2Y` and `R2Y` are not classification accuracy. The exact convention used by each function is also reported in its `metrics$definitions` element. `RMSD` is not used for classification. Use `selection = "R2Y"` to select from the full-data fitted-response path or `selection = "Q2Y"` to select from out-of-fold predictions standardized by each fold's training-response mean. Selecting R2Y automatically enables and returns the fitted path even if `fit = FALSE`; because it is a training criterion, Q2Y or another held-out metric is usually preferable for choosing model complexity. The former names `"r2"` and `"q2"` are rejected because they did not identify these two different quantities unambiguously. Classification settings can be selected by `accuracy`, `balanced_accuracy`, `AUROC`, `lift_accuracy`, `macro_precision`, `macro_recall`, `macro_f1`, `kappa`, R2Y, or Q2Y. Binary AUROC pools continuous held-out class scores across folds and treats the second factor level as positive. Use balanced accuracy when unequal class frequencies make majority-class accuracy misleading. Regression settings can be selected by R2Y, Q2Y, `RMSD`, `MAE`, `MAPE_percent`, `RPD`, `Pearson_r`, or `Spearman_r`. These additional aggregate metrics are useful for high-dimensional multivariate responses such as spectra. RMSD, MAE, and MAPE_percent are minimized; the other criteria are maximized. Signed bias and signed `MRE_percent` remain available from `evaluate()` but are not tuning criteria because they have no unambiguous one-sided optimization direction. The definitions match `evaluate()`, and an incompatible choice such as classification accuracy for a regression response raises an error before model fitting. ```{r chunk-027} data.frame( ncomp = cv_opt$ncomp, training_R2Y = round(cv_opt$R2Y, 3), heldout_Q2Y = round(cv_opt$Q2Y, 3), heldout_RMSD = round(cv_opt$RMSD, 3) ) ``` ### Permutation-test p-values `fastPLS` provides two permutation-test procedures. In `pls()`, the permutation test is a single train/test procedure: the rows of `Xtrain` are randomly permuted, the model is refitted, and the permuted test-set `Q2Y` values are compared with the observed `Q2Y` values component by component. The returned `pval` is the corrected Monte Carlo upper-tail value `(b + 1) / (B + 1)`, where `b` counts successful null fits at least as extreme as observed and `B` counts successful null fits. It can therefore never be zero. Because `pls()` has no grouping argument, one training row is the permutation unit. The full permutation table is stored in `permutation` and can be visualized with `plot.permutation()`, where the x-axis is the correlation between the original and permuted response structure and the y-axis shows R2 and Q2. ```{r chunk-028} perm_fit <- pls( Xtrain = Xreg_train, Ytrain = Ytrain_reg, Xtest = Xreg_test, Ytest = Ytest_reg, ncomp = 2, fit = TRUE, perm.test = TRUE, return_variance = FALSE, seed = 108 ) perm_fit$pval plot.permutation(perm_fit, ncomp = 2) ``` In `pls.double.cv()`, the permutation test repeats the complete nested cross-validation workflow. Independent rows are exchanged individually. When `constrain` identifies repeated observations, complete constraint blocks are exchanged only between groups with the same number of rows. Thus group sizes, within-group response structure, and class frequencies are preserved exactly. The observed and permuted analyses use identical outer and inner folds and the same randomized-SVD seeds, so null variation reflects the exchange operation rather than new folds or sketches. Model selection and permutation inference use the same `selection`. For imbalanced classification, `selection = "balanced_accuracy"` therefore selects the model by mean class-specific recall and tests that statistic against its permutation distribution; it does not substitute dummy-response Q2. The output records the statistic in `permutation_metric`, `permutation_observed`, and `permutation_sampled`. Larger predictive metrics use the upper permutation tail, whereas losses such as RMSD use the lower tail. Both tails use `(b + 1) / (B + 1)`. Failed null fits are stored in `permutation_errors`, omitted from `B`, and summarized by `permutation_completed` and `permutation_failed`. ```{r chunk-029} dcv_perm <- pls.double.cv( Xdata = Xreg_train[1:20, ], Ydata = Ytrain_reg[1:20], ncomp = 1:2, kfold_inner = 2, kfold_outer = 2, perm.test = TRUE, seed = 109 ) data.frame( permutation_metric = dcv_perm$permutation_metric, observed = dcv_perm$permutation_observed, p_value = dcv_perm$p.value, completed = dcv_perm$permutation_completed, failed = dcv_perm$permutation_failed ) ``` For an imbalanced classification analysis, the corresponding call is: ```{r balanced-permutation-example, eval=FALSE} dcv_balanced <- pls.double.cv( Xdata = Xtrain, Ydata = Ytrain_cls, ncomp = 1:5, kfold_inner = 5, kfold_outer = 5, constrain = patient_id, selection = "balanced_accuracy", perm.test = TRUE, times = 100, seed = 109 ) dcv_balanced$balanced_accuracy dcv_balanced$metrics$permutation ``` Cross-validation results do not retain the training matrices. To fit the final model, combine the selected configuration with `best_parameters` and pass the original training data explicitly to `pls()`. This keeps the CV object compact and makes the data used for refitting unambiguous. ```{r chunk-030} cv_select <- pls.single.cv( Xdata = Xtrain, Ydata = Ytrain_cls, ncomp = 1:3, kfold = 5, seed = 106 ) selected <- utils::modifyList( cv_select$tuning_config, cv_select$best_parameters ) svd_controls <- selected$svd_dots selected$svd_dots <- NULL fit_selected <- do.call( pls, c( list( Xtrain = Xtrain, Ytrain = Ytrain_cls, Xtest = Xtest, Ytest = Ytest_cls, return_variance = FALSE ), selected, svd_controls ) ) data.frame( best_ncomp = cv_select$best_ncomp, test_accuracy = mean(fit_selected$Ypred[[1]] == Ytest_cls) ) ``` For example, `kernelpls` can select the best combination of component count and kernel setting. The selected values are returned in `best_parameters` and can be combined with `tuning_config` for the final explicit refit. ```{r chunk-031} cv_kernel <- pls.single.cv( Xdata = Xtrain, Ydata = Ytrain_cls, ncomp = 1:3, kfold = 5, method = "kernelpls", kernel = c("linear", "rbf"), gamma = c(0.1, 1), seed = 107 ) selected_kernel <- utils::modifyList( cv_kernel$tuning_config, cv_kernel$best_parameters ) svd_controls <- selected_kernel$svd_dots selected_kernel$svd_dots <- NULL fit_kernel <- do.call( pls, c( list( Xtrain = Xtrain, Ytrain = Ytrain_cls, Xtest = Xtest, Ytest = Ytest_cls, return_variance = FALSE ), selected_kernel, svd_controls ) ) data.frame( best_ncomp = cv_kernel$best_parameters$ncomp, best_kernel = cv_kernel$best_parameters$kernel, test_accuracy = mean(fit_kernel$Ypred[[1]] == Ytest_cls) ) ``` ### Nested or double CV Double cross-validation is a nested validation design for separating model optimization from the final estimate of predictive performance. This separation is especially important for PLS-DA because the number of latent variables and other modelling choices can otherwise be tuned on the same samples used to report performance, producing optimistic accuracy or Q2 estimates. In the terminology of Szymanska et al. (2012), the inner CV loop (`CV1`) is used to optimize model complexity, such as the number of latent variables, whereas the outer CV loop (`CV2`) holds out samples that are not used during optimization and therefore provides the performance estimate of the complete modelling strategy. In `fastPLS`, `pls.double.cv()` follows this idea using a reproducible grouped- fold plan constructed in R. For the eligible selection criteria described above, CPU and Metal use a compiled coordinator for inner selection, outer refitting, prediction, and metric accumulation. CUDA uses an R coordinator around native CUDA single-CV and outer-fit kernels. Both `kfold_inner` and `kfold_outer` can be ordinary fold counts or `"loocv"`, and both respect `constrain`. ```{r chunk-032} cv_double <- pls.double.cv( Xdata = Xtrain, Ydata = Ytrain_cls, constrain = patient_id, ncomp = 1:3, kfold_inner = 3, kfold_outer = 3, method = "simpls", classifier = "lda", selection = "balanced_accuracy", backend = "cpu", return_splits = TRUE, seed = 104 ) data.frame( selected_ncomp_mode = cv_double$bcomp, outer_metric = cv_double$metric_name, outer_accuracy = cv_double$accuracy, outer_balanced_accuracy = cv_double$balanced_accuracy, outer_Q2Y = cv_double$Q2Y, outer_R2Y = cv_double$R2Y ) data.frame( outer_fold = seq_along(cv_double$results[[1]]$best_ncomp), inner_selected_ncomp = cv_double$results[[1]]$best_ncomp ) head(cv_double$split_index) ``` Here, all rows from one illustrative patient remain together in both loops. If an inner training fold contains one class, fastPLS predicts that class and uses a finite constant score for every requested component. These predictions remain in the pooled inner metric. If an outer training partition contains one class, the same rule supplies its held-out prediction and the fold is marked as not estimable for discrimination. The returned `degenerate_inner_folds`, `constant_classifier_fallback`, minimum class counts, `component_selection_informative`, and `outer_discrimination_estimable` fields make these cases explicit. The same nested workflow accepts a numeric vector for univariate regression or a numeric matrix for multivariate regression. The default inner-loop criterion for both tasks is held-out RMSD; for a response matrix it is aggregated across response columns. ```{r nested-regression-examples} cv_double_uni <- pls.double.cv( Xdata = Xreg_train, Ydata = Ytrain_reg, ncomp = 1:2, kfold_inner = 2, kfold_outer = 2, method = "simpls", backend = "cpu", seed = 105 ) cv_double_multi <- pls.double.cv( Xdata = Xmulti_train, Ydata = Ymulti_train, ncomp = 1:2, kfold_inner = 2, kfold_outer = 2, method = "plssvd", backend = "cpu", bycol = TRUE, seed = 105 ) data.frame( task = c("univariate regression", "multivariate regression"), selected_ncomp = c(cv_double_uni$bcomp, cv_double_multi$bcomp), outer_RMSD = c(cv_double_uni$RMSD, cv_double_multi$RMSD) ) ``` For each outer fold, the inner loop selects the component count by balanced accuracy and the selected LDA model predicts samples that were not involved in that choice. Component counts may therefore differ among outer folds; `bcomp` reports their most frequent value, while `balanced_accuracy` and `Ypred` summarize the outer held-out predictions. The same interface can tune more than the component count. In this example the inner loop chooses the best combination of `ncomp`, `kernel`, and `gamma` for `kernelpls`. The outer loop then uses the selected combination for each held-out outer fold. ```{r chunk-033} cv_double_kernel <- pls.double.cv( Xdata = Xtrain, Ydata = Ytrain_cls, constrain = patient_id, ncomp = 1:2, kfold_inner = 3, kfold_outer = 3, method = "kernelpls", kernel = c("linear", "rbf"), gamma = c(0.1, 1), seed = 108 ) cv_double_kernel$results[[1]]$best_parameters ``` The most commonly used `pls.double.cv()` output fields are: | Field | Meaning | |---|---| | `bcomp` | Most frequently selected number of components. | | `accuracy`, `Q2Y`, `RMSD` | Outer-CV predictive performance. | | `Ypred` | Final cross-validated prediction for each sample. | | `conf` | Classification confusion matrix. | | `metrics` | Detailed outer-CV evaluation results. | | `results` | Detailed per-run and per-fold information. | Important elements returned by `pls.double.cv()` are: - `results`: one entry per repeated outer CV run. Each entry stores `Ypred` and `pred` for that run, the outer `fold` assignment, the `best_ncomp` selected inside each outer fold, the full `best_parameters` selected inside each outer fold, the complete inner-CV objects in `inner`, the run-level `metric_name` and `metric_value`, and the fitted `backend` and `method`. - `Ypred`: the final cross-validated prediction for each sample. For classification, repeated runs are combined by voting; for regression, predictions are averaged across repeated runs. - `acc_tot`: classification-only text summary of the total number and percentage of correctly classified samples. - `conf`: classification-only confusion matrix. Entries are printed as counts and column percentages so that class-wise errors can be inspected. - `vote_counts`: classification-only matrix with one row per sample and one column per class, showing how many repeated outer-CV runs voted for each class. - `accuracy`, `Q2Y`, `RMSD`, and `R2Y`: one value per repeated outer CV run. For classification, `accuracy` reports decoded-label accuracy, `Q2Y` reports held-out Q2 on dummy-coded PLS-DA response scores, `R2Y` reports the mean training-fit R2 of the selected outer-fold PLS-DA models, and `RMSD` is not used. For regression, `Q2Y` reports held-out Q2, `RMSD` reports held-out RMSD, and `R2Y` reports the mean training-fit R2 of the selected outer-fold models. - `metric_name`: the metric used for inner/outer model selection. It is based on held-out predictions except when `selection = "R2Y"` explicitly requests the fitted-response criterion. - `medianQ2Y`, `CI95Q2Y`, `medianR2Y`, `CI95R2Y`, `medianRMSD`, and `CI95RMSD`: summaries across repeated outer CV runs, returned only when `runn > 1`. They are omitted for the default `runn = 1` output. - `bcomp`: the most frequently selected number of components across all outer folds and repeated runs. - `backend` and `method`: the default backend and PLS method used by the call. If vector-valued methods or backends are tuned in the inner loop, the selected fold-level values are stored in `results[[run]]$best_parameters`. - `selection_metric`: the criterion used by the inner CV loop. The default `"auto"` means accuracy for classification and RMSD for regression. All task-compatible selection criteria listed above can also drive the nested permutation test. ## SVD Utility `fastsvd()` provides direct access to the native CPU randomized SVD used by PLS. It returns the left singular vectors (`u`), singular values (`d`), and right singular vectors (`v`) for users who want a truncated decomposition outside a PLS model. A standard R numeric matrix selects float64 computation. A matrix created with `float::fl()` selects the native float32 path automatically; its left and right singular vectors remain in float32 format. The function exposes no solver selector because rSVD is its only algorithm. Stand-alone CUDA and Metal SVD routes are not exposed; their reduced decompositions are not device-native for every matrix shape. ```{r chunk-034} s64 <- fastsvd(Xtrain, ncomp = 3, seed = 104) s32 <- fastsvd(float::fl(Xtrain), ncomp = 3, seed = 104) names(s64) s64$d inherits(s32$u, "float32") ``` ## CUDA and Metal Availability CUDA and Apple Metal are optional. Use `has_cuda()` before selecting `backend = "cuda"` and `has_metal()` before selecting `backend = "metal"`. An unavailable accelerator request stops with an informative error and is not redirected to CPU. ```{r chunk-038} has_cuda() has_metal() ``` ```{r chunk-039, eval = FALSE} if (has_cuda()) { fit_gpu <- pls( Xtrain, Ytrain_cls, Xtest, Ytest_cls, ncomp = 1:2, backend = "cuda" ) fit_gpu_lda <- pls( Xtrain, Ytrain_cls, Xtest, Ytest_cls, ncomp = 1:2, method = "plssvd", backend = "cuda", classifier = "lda" ) } ``` ```{r chunk-040, eval = FALSE} if (has_metal()) { Xtrain_metal <- float::fl(as.matrix(Xtrain)) Xtest_metal <- float::fl(as.matrix(Xtest)) fit_metal <- pls( Xtrain_metal, Ytrain_cls, Xtest_metal, Ytest_cls, ncomp = 1:2, backend = "metal" ) } ``` ## Helper Functions `fastcor()` computes fast Pearson-style correlations. `ViP()` returns variable importance in projection trajectories for direct SIMPLS-family fits, following the standard VIP interpretation used for PLS variable ranking (Wold, Sjostrom, and Eriksson, 2001; Chong and Jun, 2005). VIP is most useful when the columns of `X` are interpretable predictors, such as genes, metabolites, spectral bins, or clinical variables; larger values indicate stronger contribution to the fitted latent predictive model. Linear kernel PLS uses the same direct SIMPLS-family path and is also supported. PLS-SVD scores are not generally orthogonal and its stored response singular vectors are not the final latent regression map; OPLS applies an additional predictor filter; and nonlinear kernel weights index training samples rather than original predictors. `ViP()` therefore stops for these three cases instead of returning a quantity with a misleading variable- importance interpretation. The supported matrix calculation, like the metrics returned by `evaluate()`, is implemented in the dependency-free C++ core and is independent of CUDA or Metal availability; the R functions validate inputs and format the returned objects. ```{r chunk-041} C <- fastcor(Xtrain, byrow = FALSE, diag = FALSE) dim(C) ``` ```{r chunk-042} vip <- ViP(fit_reg) dim(vip) ``` ## Methodological Scope This vignette is a practical guide to fitting, validating, and predicting with fastPLS. The mathematical derivations and executable pseudocode for native rSVD, PLS-SVD, the SIMPLS-family estimator, OPLS, kernel PLS, compact prediction, and sufficient-statistics cross-validation are maintained in the future Journal of Statistical Software manuscript. Keeping those details in the methods article avoids repeating a long technical description in the package vignette while preserving a single place in which the equations can be checked against the shared C++ core. The fitted object still records the information needed to audit an analysis, including the executed method, backend, precision, randomized controls, effective component count, and route diagnostics. These fields should be saved with the analysis whenever numerical reproducibility is important. ## Method References The implementation in `fastPLS` is not a line-by-line copy of the papers below; rather, these papers define the statistical algorithms or numerical building blocks that the package implements and accelerates. - Barker, M. and Rayens, W. (2003). Partial least squares for discrimination. *Journal of Chemometrics*, 17, 166-173. - Boulesteix, A.-L. and Strimmer, K. (2007). Partial least squares: a versatile tool for the analysis of high-dimensional genomic data. *Briefings in Bioinformatics*, 8, 32-44. - Chong, I.-G. and Jun, C.-H. (2005). Performance of some variable selection methods when multicollinearity is present. *Chemometrics and Intelligent Laboratory Systems*, 78, 103-112. - de Jong, S. (1993). SIMPLS: an alternative approach to partial least squares regression. *Chemometrics and Intelligent Laboratory Systems*, 18, 251-263. - Fisher, R. A. (1936). The use of multiple measurements in taxonomic problems. *Annals of Eugenics*, 7, 179-188. - Geladi, P. and Kowalski, B. R. (1986). Partial least-squares regression: a tutorial. *Analytica Chimica Acta*, 185, 1-17. - Halko, N., Martinsson, P.-G., and Tropp, J. A. (2011). Finding structure with randomness: probabilistic algorithms for constructing approximate matrix decompositions. *SIAM Review*, 53, 217-288. - Mevik, B.-H. and Wehrens, R. (2007). The pls package: principal component and partial least squares regression in R. *Journal of Statistical Software*, 18, 1-23. - Rosipal, R. and Trejo, L. J. (2001). Kernel partial least squares regression in reproducing kernel Hilbert space. *Journal of Machine Learning Research*, 2, 97-123. - Szymanska, E., Saccenti, E., Smilde, A. K., and Westerhuis, J. A. (2012). Double-check: validation of diagnostic statistics for PLS-DA models in metabolomics studies. *Metabolomics*, 8, S3-S16. doi:10.1007/s11306-011-0330-3. - Trygg, J. and Wold, S. (2002). Orthogonal projections to latent structures (O-PLS). *Journal of Chemometrics*, 16, 119-128. - Wold, S., Sjostrom, M., and Eriksson, L. (2001). PLS-regression: a basic tool of chemometrics. *Chemometrics and Intelligent Laboratory Systems*, 58, 109-130. ## Session Information ```{r session-info} sessionInfo() ```