Nothing
#' @title Memory-based learning (mbl)
#' @name mbl
#' @aliases mbl plot.mbl get_predictions
#' @description
#' \loadmathjax
#' Memory-based learning (a.k.a. instance-based learning or local regression)
#' is a non-linear lazy learning approach for predicting a response variable
#' from predictor variables. For each observation in a prediction set, a local
#' regression is fitted using a subset of similar observations (nearest
#' neighbors) from a reference set. This function does not produce a global
#' model.
#'
#' @usage
#' mbl(Xr, Yr, Xu, Yu = NULL,
#' neighbors,
#' diss_method = diss_pca(ncomp = ncomp_by_opc()),
#' diss_usage = c("none", "predictors", "weights"),
#' fit_method = fit_wapls(min_ncomp = 3, max_ncomp = 15),
#' spike = NULL, group = NULL,
#' gh = FALSE,
#' control = mbl_control(),
#' verbose = TRUE, seed = NULL, ...)
#'
#' @usage \method{plot}{mbl}(x, what = c("validation", "gh"), metric = "rmse", ncomp = c(1, 2), ...)
#'
#' @usage get_predictions(x)
#'
#' @param Xr A matrix of predictor variables for the reference data
#' (observations in rows, variables in columns). Column names are required.
#' @param Yr A numeric vector or single-column matrix of response values
#' corresponding to \code{Xr}. NA values are not permitted.
#' @param Xu A matrix of predictor variables for the data to be predicted
#' (observations in rows, variables in columns). Must have the same column
#' names as \code{Xr}.
#' @param Yu An optional numeric vector or single-column matrix of response
#' values corresponding to \code{Xu}. Used for computing prediction
#' statistics. Default is \code{NULL}.
#' @param neighbors A neighbor selection object specifying how to select
#' neighbors. Use \code{\link{neighbors_k}()} for fixed k-nearest neighbors
#' or \code{\link{neighbors_diss}()} for dissimilarity threshold-based
#' selection.
#' @param diss_method A dissimilarity method object or a precomputed
#' dissimilarity matrix. Available constructors:
#' \itemize{
#' \item \code{\link{diss_pca}()}: Mahalanobis distance in PCA score space.
#' This is the default where the number of components is optimized using
#' side information (see \code{ncomp_by_opc}()).
#' \item \code{\link{diss_pls}()}: Mahalanobis distance in PLS score space
#' \item \code{\link{diss_euclidean}()}: Euclidean distance
#' \item \code{\link{diss_mahalanobis}()}: Mahalanobis distance
#' \item \code{\link{diss_cosine}()}: Cosine dissimilarity
#' \item \code{\link{diss_correlation}()}: Correlation-based dissimilarity
#' }
#' A precomputed matrix can also be passed. When \code{diss_usage =
#' "predictors"}, it must be square with dimensions
#' \code{(nrow(Xr) + nrow(Xu))} and zeros on the diagonal. Otherwise, it must
#' have \code{nrow(Xr)} rows and \code{nrow(Xu)} columns.
#' @param diss_usage How dissimilarity information is used in local models:
#' \itemize{
#' \item \code{"none"} (default): dissimilarities used only for neighbor
#' selection
#' \item \code{"predictors"}: local dissimilarity matrix columns added as
#' predictors
#' \item \code{"weights"}: neighbors weighted by dissimilarity using a
#' tricubic function
#' }
#' @param fit_method A local fitting method object. Available constructors:
#' \itemize{
#' \item \code{\link{fit_pls}()}: Partial least squares regression
#' \item \code{\link{fit_wapls}()}: Weighted average PLS (default)
#' \item \code{\link{fit_gpr}()}: Gaussian process regression
#' }
#' @param spike An integer vector indicating indices of observations in
#' \code{Xr} to force into (positive values) or exclude from (negative values)
#' all neighborhoods. Default is \code{NULL}. Spiking does not change
#' neighborhood size; forced observations displace the most distant neighbors.
#' @param gh Logical indicating whether to compute global Mahalanobis (GH)
#' distances. Default is \code{FALSE}. GH distances measure how far each
#' observation lies from the center of the reference set in PLS score space.
#' The computation uses a fixed methodology: PLS projection with the number
#' of components selected via \code{\link{ncomp_by_opc}()} (capped at 40).
#' This is independent of the \code{diss_method} argument.
#' @param group An optional factor assigning group labels to \code{Xr}
#' observations (e.g., measurement batches). Used to avoid pseudo-replication
#' in cross-validation: when one observation is held out, all observations
#' from its group are also removed.
#' @param control A list from \code{\link{mbl_control}()} specifying validation
#' type, tuning options, and other settings.
#' @param verbose Logical indicating whether to display a progress bar.
#' Default is \code{TRUE}. Not shown during parallel execution.
#' @param seed An integer for random number generation, enabling reproducible
#' cross-validation results. Default is \code{NULL}.
#'
#' @param x An object of class \code{mbl} (as returned by \code{\link{mbl}}).
#' @param what Character vector specifying what to plot. Options are
#' \code{"validation"} (validation statistics) and/or \code{"gh"} (PLS scores
#' used for GH distance computation). Default is both.
#' @param metric Character string specifying which validation statistic to plot.
#' Options are \code{"rmse"}, \code{"st_rmse"}, or \code{"r2"}. Only used when
#' \code{"validation"} is in \code{what}.
#' @param ncomp Integer vector of length 1 or 2 specifying which PLS components
#' to plot. Default is \code{c(1, 2)}. Only used when \code{"gh"} is in
#' \code{what}.
#' @param ... Additional arguments passed to \code{\link[graphics]{plot}}.
#' Currently unused for `mbl()`.
#'
#' @param ... Additional arguments (currently unused).
#'
#' @details
#' ## Spiking
#' The \code{spike} argument forces specific reference observations into or out
#' of neighborhoods. Positive indices are always included; negative indices are
#' always excluded. When observations are forced in, the most distant neighbors
#' are displaced to maintain neighborhood size. See Guerrero et al. (2010).
#'
#' ## Dissimilarity usage
#' When \code{diss_usage = "predictors"}, the local dissimilarity matrix columns
#' are appended as additional predictor variables, which can improve predictions
#' (Ramirez-Lopez et al., 2013a).
#'
#' When \code{diss_usage = "weights"}, neighbors are weighted using a tricubic
#' function (Cleveland and Devlin, 1988; Naes et al., 1990):
#'
#' \mjdeqn{W_{j} = (1 - v^{3})^{3}}{W_j = (1 - v^3)^3}
#'
#' where \mjeqn{v = d(xr_i, xu_j) / \max(d)}{v = d(xr_i, xu_j) / max(d)}.
#'
#' ## GH distance
#' The global Mahalanobis distance (GH) measures how far each observation lies
#' from the center of the reference set. It is always computed using a PLS
#' projection with the number of components optimized via
#' \code{\link{ncomp_by_opc}()} (maximum 40 components or \code{nrow(Xr)},
#' whichever is smaller). This methodology is fixed and independent of the
#' \code{diss_method} specified for neighbor selection.
#'
#' GH distances are useful for identifying extrapolation: observations with
#' high GH values lie far from the calibration space and may yield unreliable
#' predictions.
#'
#' ## Grouping
#' The \code{group} argument enables leave-group-out cross-validation. When
#' \code{validation_type = "local_cv"} in \code{\link{mbl_control}()}, the
#' \code{p} parameter refers to the proportion of groups (not observations)
#' retained per iteration.
#'
#' ## Deprecated arguments
#' The following arguments from previous versions of \code{resemble} are no
#' longer supported and will throw an error if used: \code{k}, \code{k_diss},
#' \code{k_range}, \code{method}, \code{pc_selection}, \code{center},
#' \code{scale}, and \code{documentation}. See the current argument list for
#' their replacements.
#'
#' @return
#' ## mbl
#' For \code{mbl()}, a list of class \code{mbl} containing:
#' \itemize{
#' \item \code{control}: control parameters from \code{control}
#' \item \code{fit_method}: fit constructor from \code{fit_method}
#' \item \code{Xu_neighbors}: list with neighbor indices and dissimilarities
#' \item \code{dissimilarities}: dissimilarity method and matrix (if
#' \code{return_dissimilarity = TRUE} in \code{control})
#' \item \code{n_predictions}: number of predictions made
#' \item \code{gh}: GH distances for \code{Xr} and \code{Xu} (if
#' \code{gh = TRUE})
#' \item \code{validation_results}: validation statistics by method
#' \item \code{results}: list of data.frame objects with predictions, one per
#' neighborhood size
#' \item \code{seed}: the seed value used
#' }
#'
#' Each results table contains:
#' \itemize{
#' \item \code{o_index}: observation index
#' \item \code{k}: number of neighbors used
#' \item \code{k_diss}, \code{k_original}: (\code{neighbors_diss} only)
#' threshold and original count
#' \item \code{ncomp}: (\code{fit_pls} only) number of PLS components
#' \item \code{min_ncomp}, \code{max_ncomp}: (\code{fit_wapls} only)
#' component range
#' \item \code{yu_obs}, \code{pred}: observed and predicted values
#' \item \code{yr_min_obs}, \code{yr_max_obs}: response range in neighborhood
#' \item \code{index_nearest_in_Xr}, \code{index_farthest_in_Xr}: neighbor
#' indices
#' \item \code{y_nearest}, \code{y_farthest}: neighbor response values
#' \item \code{diss_nearest}, \code{diss_farthest}: neighbor dissimilarities
#' \item \code{y_nearest_pred}: (NNv validation) leave-one-out prediction
#' \item \code{loc_rmse_cv}, \code{loc_st_rmse_cv}: (local_cv validation) CV
#' statistics
#' \item \code{loc_ncomp}: (local dissimilarity only) components used locally
#' }
#'
#' ## Get predictions
#'
#' The \code{get_predictions()} function extracts predicted values from an
#' object of class \code{mbl}. It returns a \code{data.frame} containing the
#' predictions.
#'
#' @author
#' \href{https://orcid.org/0000-0002-5369-5120}{Leonardo Ramirez-Lopez} and
#' Antoine Stevens
#'
#' @references
#' Cleveland, W. S., and Devlin, S. J. 1988. Locally weighted regression: an
#' approach to regression analysis by local fitting. Journal of the American
#' Statistical Association 83:596-610.
#'
#' Guerrero, C., Zornoza, R., Gomez, I., Mataix-Beneyto, J. 2010. Spiking of
#' NIR regional models using observations from target sites: Effect of model
#' size on prediction accuracy. Geoderma 158:66-77.
#'
#' Naes, T., Isaksson, T., Kowalski, B. 1990. Locally weighted regression and
#' scatter correction for near-infrared reflectance data. Analytical Chemistry
#' 62:664-673.
#'
#' Ramirez-Lopez, L., Behrens, T., Schmidt, K., Stevens, A., Dematte, J.A.M.,
#' Scholten, T. 2013a. The spectrum-based learner: A new local approach for
#' modeling soil vis-NIR spectra of complex data sets. Geoderma 195-196:268-279.
#'
#' Ramirez-Lopez, L., Behrens, T., Schmidt, K., Viscarra Rossel, R., Dematte,
#' J.A.M., Scholten, T. 2013b. Distance and similarity-search metrics for use
#' with soil vis-NIR spectra. Geoderma 199:43-53.
#'
#' Rasmussen, C.E., Williams, C.K. 2006. Gaussian Processes for Machine
#' Learning. MIT Press.
#'
#' Shenk, J., Westerhaus, M., Berzaghi, P. 1997. Investigation of a LOCAL
#' calibration procedure for near infrared instruments. Journal of Near
#' Infrared Spectroscopy 5:223-232.
#'
#' @seealso
#' \code{\link{mbl_control}}, \code{\link{neighbors_k}},
#' \code{\link{neighbors_diss}}, \code{\link{diss_pca}}, \code{\link{diss_pls}},
#' \code{\link{fit_pls}}, \code{\link{fit_wapls}}, \code{\link{fit_gpr}},
#' \code{\link{search_neighbors}}
#'
#' @examples
#' \dontrun{
#' library(prospectr)
#' data(NIRsoil)
#'
#' # Preprocess: detrend + first derivative with Savitzky-Golay
#' sg_det <- savitzkyGolay(
#' detrend(NIRsoil$spc, wav = as.numeric(colnames(NIRsoil$spc))),
#' m = 1, p = 1, w = 7
#' )
#' NIRsoil$spc_pr <- sg_det
#'
#' # Split data
#' test_x <- NIRsoil$spc_pr[NIRsoil$train == 0 & !is.na(NIRsoil$CEC), ]
#' test_y <- NIRsoil$CEC[NIRsoil$train == 0 & !is.na(NIRsoil$CEC)]
#' train_x <- NIRsoil$spc_pr[NIRsoil$train == 1 & !is.na(NIRsoil$CEC), ]
#' train_y <- NIRsoil$CEC[NIRsoil$train == 1 & !is.na(NIRsoil$CEC)]
#'
#' # Example 1: Spectrum-based learner (Ramirez-Lopez et al., 2013)
#' ctrl <- mbl_control(validation_type = "NNv")
#'
#' sbl <- mbl(
#' Xr = train_x,
#' Yr = train_y,
#' Xu = test_x,
#' neighbors = neighbors_k(seq(40, 140, by = 20)),
#' diss_method = diss_pca(ncomp = ncomp_by_opc(40)),
#' fit_method = fit_gpr(),
#' control = ctrl
#' )
#' sbl
#' plot(sbl)
#' get_predictions(sbl)
#'
#' # Example 2: With known Yu
#' sbl_2 <- mbl(
#' Xr = train_x,
#' Yr = train_y,
#' Xu = test_x,
#' Yu = test_y,
#' neighbors = neighbors_k(seq(40, 140, by = 20)),
#' fit_method = fit_gpr(),
#' control = ctrl
#' )
#' plot(sbl_2)
#'
#' # Example 3: LOCAL algorithm (Shenk et al., 1997)
#' local_algo <- mbl(
#' Xr = train_x,
#' Yr = train_y,
#' Xu = test_x,
#' Yu = test_y,
#' neighbors = neighbors_k(seq(40, 140, by = 20)),
#' diss_method = diss_correlation(),
#' diss_usage = "none",
#' fit_method = fit_wapls(min_ncomp = 3, max_ncomp = 15),
#' control = ctrl
#' )
#' plot(local_algo)
#'
#' # Example 4: Using dissimilarity as predictors
#' local_algo_2 <- mbl(
#' Xr = train_x,
#' Yr = train_y,
#' Xu = test_x,
#' Yu = test_y,
#' neighbors = neighbors_k(seq(40, 140, by = 20)),
#' diss_method = diss_pca(ncomp = ncomp_by_opc(40)),
#' diss_usage = "predictors",
#' fit_method = fit_wapls(min_ncomp = 3, max_ncomp = 15),
#' control = ctrl
#' )
#' plot(local_algo_2)
#'
#' # Example 5: Parallel execution
#' library(doParallel)
#' n_cores <- min(2, parallel::detectCores() - 1)
#' clust <- makeCluster(n_cores)
#' registerDoParallel(clust)
#'
#' local_algo_par <- mbl(
#' Xr = train_x,
#' Yr = train_y,
#' Xu = test_x,
#' Yu = test_y,
#' neighbors = neighbors_k(seq(40, 140, by = 20)),
#' diss_method = diss_correlation(),
#' fit_method = fit_wapls(min_ncomp = 3, max_ncomp = 15),
#' control = ctrl
#' )
#'
#' registerDoSEQ()
#' try(stopCluster(clust))
#' }
#'
#' @export
######################################################################
# resemble
# Copyright (C) 2026 Leonardo Ramirez-Lopez and Antoine Stevens
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
######################################################################
mbl <- function(
Xr, Yr, Xu, Yu = NULL,
neighbors,
diss_method = diss_pca(ncomp = ncomp_by_opc()),
diss_usage = c("none", "predictors", "weights"),
fit_method = fit_wapls(min_ncomp = 3, max_ncomp = 15),
spike = NULL,
group = NULL,
gh = FALSE,
control = mbl_control(),
verbose = TRUE,
seed = NULL,
...
) {
f_call <- match.call()
dots <- list(...)
# ---------------------------------------------------------------------------
# Block removed arguments
# ---------------------------------------------------------------------------
if ("k" %in% names(dots) || "k_diss" %in% names(dots) || "k_range" %in% names(dots)) {
stop(
"Arguments 'k', 'k_diss', 'k_range' have been removed.\n",
"Use neighbors_k() or neighbors_diss() instead.\n",
"Example: mbl(..., neighbors = neighbors_k(c(40, 80, 120)))",
call. = FALSE
)
}
if ("method" %in% names(dots)) {
stop(
"Argument 'method' has been renamed to 'fit_method'.\n",
"Use fit_pls(), fit_wapls(), or fit_gpr() constructors.\n",
"Example: mbl(..., fit_method = fit_wapls(min_ncomp = 3, max_ncomp = 15))",
call. = FALSE
)
}
if ("pc_selection" %in% names(dots)) {
stop(
"Argument 'pc_selection' has been removed.\n",
"Component selection is now specified in diss_*() constructors.\n",
"Example: mbl(..., diss_method = diss_pca(ncomp = ncomp_by_opc(40)))",
call. = FALSE
)
}
if ("center" %in% names(dots) || "scale" %in% names(dots)) {
stop(
"Arguments 'center' and 'scale' have been removed.\n",
"These are now set in diss_*() and fit_*() constructors.\n",
"Example: diss_pca(center = TRUE, scale = FALSE)",
call. = FALSE
)
}
if ("documentation" %in% names(dots)) {
stop(
"Argument 'documentation' has been removed.",
call. = FALSE
)
}
# ---------------------------------------------------------------------------
# Validate constructor arguments
# ---------------------------------------------------------------------------
if (missing(neighbors)) {
stop("'neighbors' is required. Use neighbors_k() or neighbors_diss().",
call. = FALSE)
}
if (!inherits(neighbors, "neighbors")) {
stop(
"'neighbors' must be created by neighbors_k() or neighbors_diss().",
call. = FALSE
)
}
# ---------------------------------------------------------------------------
# Validate inputs
# ---------------------------------------------------------------------------
# --- control validation ---
if (!inherits(control, "mbl_control")) {
stop("'control' must be created by mbl_control()", call. = FALSE)
}
# ---------------------------------------------------------------------------
# Set BLAS threads to reduce overhead (restored on exit)
# ---------------------------------------------------------------------------
if (requireNamespace("RhpcBLASctl", quietly = TRUE)) {
old_blas_threads <- blas_get_num_procs()
if (old_blas_threads != control$blas_threads) {
blas_set_num_threads(control$blas_threads)
on.exit(blas_set_num_threads(old_blas_threads), add = TRUE)
}
} else if (Sys.info()["sysname"] == "Linux" && control$blas_threads == 1L) {
message(
"Tip: Install 'RhpcBLASctl' for optimal performance on Linux:\n",
" install.packages('RhpcBLASctl')"
)
}
if (!is.logical(verbose) || length(verbose) != 1L) {
stop("'verbose' must be TRUE or FALSE.", call. = FALSE)
}
if (!is.matrix(Xr)) {
Xr <- as.matrix(Xr)
}
if (!is.matrix(Xu)) {
Xu <- as.matrix(Xu)
}
if (!is.null(Yu)) {
if (!is.matrix(Yu)) {
Yu <- as.matrix(Yu)
}
}
if (!is.matrix(Yr)) {
Yr <- as.matrix(Yr)
}
n_xr <- nrow(Xr)
n_xu <- nrow(Xu)
n_total <- n_xr + n_xu
if (ncol(Xr) != ncol(Xu)) {
stop("ncol(Xr) must equal ncol(Xu).", call. = FALSE)
}
if (ncol(Xr) < 4L) {
stop("Xr must have at least 4 columns.", call. = FALSE)
}
if (NROW(Yr) != nrow(Xr)) {
stop("length(Yr) must equal nrow(Xr).", call. = FALSE)
}
if (any(is.na(Yr))) {
stop("NA values in Yr are not supported.", call. = FALSE)
}
if (!is.null(Yu) && NROW(Yu) != nrow(Xu)) {
stop("length(Yu) must equal nrow(Xu).", call. = FALSE)
}
# ---------------------------------------------------------------------------
# Validate diss_method
# ---------------------------------------------------------------------------
if (is.matrix(diss_method)) {
# User passed a precomputed dissimilarity matrix
diss_matrix <- diss_method
diss_method <- NULL
# center <- FALSE
# scale <- FALSE
} else if (!inherits(diss_method, "diss_method")) {
stop(
"'diss_method' must be a diss_*() constructor or a numeric matrix.\n",
"Available constructors: diss_pca(), diss_pls(), diss_correlation(), ",
"diss_euclidean(), diss_cosine(), diss_mahalanobis()",
call. = FALSE
)
} else {
# ---------------------------------------------------------------------------
# Validate ncomp against data dimensions
# ---------------------------------------------------------------------------
if (!is.null(diss_method$ncomp)) {
max_ncomp <- .get_max_ncomp(diss_method$ncomp)
max_allowed <- min(n_total, ncol(Xr))
if (max_ncomp > max_allowed) {
warning(
"Requested ncomp (", max_ncomp, ") exceeds max allowed by data dimensions (",
max_allowed, ").\nIt will be capped to ", max_allowed, ".",
call. = FALSE
)
}
}
diss_matrix <- NULL
# center <- if (!is.null(diss_method$center)) diss_method$center else TRUE
# scale <- if (!is.null(diss_method$scale)) diss_method$scale else FALSE
}
if (!inherits(fit_method, "fit_method")) {
stop(
"'fit_method' must be created by a fit_*() constructor.\n",
"Available: fit_pls(), fit_wapls(), fit_gpr()",
call. = FALSE
)
}
diss_usage <- match.arg(diss_usage)
# ---------------------------------------------------------------------------
# Convert neighbors to internal format
# ---------------------------------------------------------------------------
if (inherits(neighbors, "neighbors_k")) {
k <- neighbors$k
k_diss <- NULL
k_range <- NULL
k_max <- max(k)
k_diss_max <- NULL
# Validate k against data
if (k_max > n_xr) {
stop(
"k (", k_max, ") cannot exceed nrow(Xr) (", n_xr, ").",
call. = FALSE
)
}
} else {
# neighbors_diss
k <- NULL
k_diss <- neighbors$threshold
k_range <- c(neighbors$k_min, neighbors$k_max)
k_max <- NULL
k_diss_max <- max(k_diss)
if (is.infinite(k_range[2])) {
if (verbose) {
message(
"setting 'k_max' (", k_range[2], ") to nrow(Xr) (", n_xr, ").",
call. = FALSE
)
}
k_range[2] <- n_xr
}
# Validate k_range against data
if (k_range[2] > n_xr) {
stop(
"k_max (", k_range[2], ") cannot exceed nrow(Xr) (", n_xr, ").",
call. = FALSE
)
}
}
# ---------------------------------------------------------------------------
# Extract from fit_method constructor
# ---------------------------------------------------------------------------
is_fit_pls <- inherits(fit_method, "fit_pls")
is_fit_wapls <- inherits(fit_method, "fit_wapls")
is_fit_gpr <- inherits(fit_method, "fit_gpr")
# For fit_and_predict()
if (is_fit_pls) {
pred_method <- "pls"
pls_c <- fit_method$ncomp
} else if (is_fit_wapls) {
pred_method <- "wapls"
pls_c <- c(fit_method$min_ncomp, fit_method$max_ncomp)
} else {
pred_method <- "gpr"
pls_c <- NULL
}
noise_variance <- fit_method$noise_variance
fit_scale <- fit_method$scale
# ---------------------------------------------------------------------------
# Parallel setup
# ---------------------------------------------------------------------------
"%mydo%" <- get("%do%")
if (control$allow_parallel && getDoParRegistered()) {
"%mydo%" <- get("%dopar%")
}
# ---------------------------------------------------------------------------
# Validate and set names
# ---------------------------------------------------------------------------
if (is.null(colnames(Xr))) {
stop("Xr must have column names.", call. = FALSE)
}
if (is.null(colnames(Xu))) {
stop("Xu must have column names.", call. = FALSE)
}
if (!identical(colnames(Xr), colnames(Xu))) {
stop("Column names of Xr and Xu must be identical.", call. = FALSE)
}
rownames(Xr) <- seq_len(nrow(Xr))
rownames(Xu) <- seq_len(nrow(Xu))
# ---------------------------------------------------------------------------
# Validation type
# ---------------------------------------------------------------------------
validation_type <- control$validation_type
is_local_cv <- "local_cv" %in% validation_type
is_nnv <- "NNv" %in% validation_type
if (is_local_cv && is_nnv) {
validation_type <- "both"
}
# ---------------------------------------------------------------------------
# Validate group
# ---------------------------------------------------------------------------
if (!is.null(group)) {
if (length(group) != nrow(Xr)) {
stop("length(group) must equal nrow(Xr).", call. = FALSE)
}
group <- as.factor(group)
}
# ---------------------------------------------------------------------------
# Setup for Xu
# ---------------------------------------------------------------------------
if (is_nnv && n_xu < 3L) {
stop(
"Nearest neighbor validation requires at least 3 observations in Xu.",
call. = FALSE
)
}
if (!is.null(Yu)) {
Yu <- as.matrix(Yu)
if (nrow(Yu) != n_xu) {
stop("nrow(Yu) must equal nrow(Xu).", call. = FALSE)
}
}
pre_nms_ng <- "Xu_"
ln <- n_xu
first_nn <- 1L
y_output_name <- "yu_obs"
y_hat_output_name <- "pred"
val_summary_name <- "Yu_prediction_statistics"
# ---------------------------------------------------------------------------
# Validate fit_method ncomp against data dimensions
# ---------------------------------------------------------------------------
if (is_fit_pls) {
if (fit_method$ncomp > min(n_xr, ncol(Xr))) {
warning(
"Requested ncomp (", fit_method$ncomp, ") in fit_pls() exceeds max allowed (",
min(n_xr, ncol(Xr)), ").\nIt will be capped internally.",
call. = FALSE
)
}
} else if (is_fit_wapls) {
max_allowed <- min(n_xr, ncol(Xr))
if (fit_method$max_ncomp > max_allowed) {
warning(
"Requested max_ncomp (", fit_method$max_ncomp, ") in fit_wapls() exceeds max allowed (",
max_allowed, ").\nIt will be capped internally.",
call. = FALSE
)
}
}
has_projection <- FALSE
# Copy diss_method to avoid modifying user's input
diss_method_internal <- diss_method
# ---------------------------------------------------------------------------
# Compute neighborhoods
# ---------------------------------------------------------------------------
if (is.null(diss_matrix)) {
# Force return_projection = TRUE when needed for diss_usage = "predictors"
if (diss_usage == "predictors") {
if (inherits(diss_method_internal, "diss_pca") ||
inherits(diss_method_internal, "diss_pls")) {
if (!isTRUE(diss_method_internal$return_projection)) {
diss_method_internal$return_projection <- TRUE
}
}
}
# Using diss_method constructor
neighborhoods <- get_neighbor_info(
Xr = Xr,
Xu = Xu,
diss_method = diss_method_internal,
Yr = Yr,
neighbors = neighbors,
spike = spike,
return_dissimilarity = control$return_dissimilarity,
diss_usage = diss_usage
)
diss_xr_xu <- neighborhoods$dissimilarity
if (!is.null(neighborhoods$projection)) {
diss_projection <- neighborhoods$projection
has_projection <- TRUE
} else {
has_projection <- FALSE
}
} else {
# Using precomputed diss_matrix
has_projection <- FALSE
diss_xr_xr <- NULL
dim_diss <- dim(diss_matrix)
if (diss_usage == "predictors") {
if (dim_diss[1] != n_total || dim_diss[2] != n_total ||
any(diag(diss_matrix) != 0)) {
stop(
"When diss_usage = 'predictors', the dissimilarity matrix must be ",
"square (", n_total, " x ", n_total, ") with zeros on the diagonal.",
call. = FALSE
)
}
diss_xr_xr <- diss_matrix[1:n_xr, 1:n_xr]
diss_xr_xu <- diss_matrix[1:n_xr, (n_xr + 1):n_total]
} else {
# diss_usage %in% c("weights", "none")
if (dim_diss[1] != n_xr || dim_diss[2] != n_xu) {
stop(
"When diss_usage = 'weights' or 'none', the dissimilarity matrix ",
"must have nrow(Xr) (", n_xr, ") rows and nrow(Xu) (", n_xu, ") columns.",
call. = FALSE
)
}
diss_xr_xu <- diss_matrix
}
neighborhoods <- diss_to_neighbors(
diss_xr_xu,
k = k_max,
k_diss = k_diss_max,
k_range = k_range,
spike = spike,
return_dissimilarity = control$return_dissimilarity,
skip_first = FALSE
)
neighborhoods$diss_xr_xr <- diss_xr_xr
}
# ---------------------------------------------------------------------------
# Compute GH distances (independent of diss_method)
# ---------------------------------------------------------------------------
if (gh) {
# FIXME: this is not ideal, as it requires an additional projection step.
# in addition the optimisation of components is hard coded
gh_projection <- ortho_projection(
Xr = Xr,
Xu = Xu,
Yr = Yr,
ncomp = ncomp_by_opc(min(n_xr, 40L)),
method = "pls",
scale = fit_scale
)
gh_center <- colMeans(gh_projection$scores[1:n_xr, , drop = FALSE])
neighborhoods$gh <- list(
gh_Xr = as.vector(f_diss(
gh_projection$scores[1:n_xr, , drop = FALSE],
Xu = t(gh_center),
diss_method = "mahalanobis",
center = FALSE,
scale = FALSE
)),
gh_Xu = as.vector(f_diss(
gh_projection$scores[(n_xr + 1):n_total, , drop = FALSE],
Xu = t(gh_center),
diss_method = "mahalanobis",
center = FALSE,
scale = FALSE
)),
projection = gh_projection
)
} else {
neighborhoods$gh <- NULL
}
# ---------------------------------------------------------------------------
# Determine smallest neighborhood size
# ---------------------------------------------------------------------------
if (inherits(neighbors, "neighbors_k")) {
min_k <- min(neighbors$k)
smallest_neighborhood <- neighborhoods$neighbors[1:min_k, , drop = FALSE]
smallest_n_neighbors <- colSums(!is.na(smallest_neighborhood))
} else {
# neighbors_diss
min_threshold <- min(neighbors$threshold)
min_diss <- neighborhoods$neighbors_diss <= min_threshold
if (!is.null(spike)) {
spike_in <- sum(spike > 0)
if (spike_in > 0) {
min_diss[1:spike_in, ] <- TRUE
}
}
smallest_neighborhood <- neighborhoods$neighbors
smallest_neighborhood[!min_diss] <- NA
smallest_n_neighbors <- colSums(!is.na(smallest_neighborhood))
smallest_n_neighbors[smallest_n_neighbors < neighbors$k_min] <- neighbors$k_min
smallest_n_neighbors[smallest_n_neighbors > neighbors$k_max] <- neighbors$k_max
}
if (is_local_cv) {
min_n_samples <- floor(min(smallest_n_neighbors) * control$p) - 1L
if (inherits(neighbors, "neighbors_k")) {
min_neighborhood <- min(neighbors$k)
} else {
min_neighborhood <- neighbors$k_min
}
min_cv_samples <- floor(min_neighborhood * (1 - control$p))
if (min_cv_samples < 3L) {
stop(
"Local cross-validation requires at least 3 observations in ",
"the hold-out set. The current cross-validation parameters ",
"leave less than 3 observations in some neighborhoods.",
call. = FALSE
)
}
} else {
min_n_samples <- smallest_n_neighbors - 1L
}
# ---------------------------------------------------------------------------
# Validate PLS components vs neighborhood size
# ---------------------------------------------------------------------------
if (is_fit_pls || is_fit_wapls) {
if (is_fit_pls) {
max_pls <- fit_method$ncomp
} else {
max_pls <- fit_method$max_ncomp
}
if (any(min_n_samples < max_pls)) {
stop(
"More PLS components than observations in some neighborhoods.\n",
"If 'local_cv' is being used, consider that some observations ",
"in the neighborhoods are held out for local validation.",
call. = FALSE
)
}
}
# ---------------------------------------------------------------------------
# Prepare iteration neighborhoods
# ---------------------------------------------------------------------------
is_local_diss <- !is.null(diss_method) &&
(inherits(diss_method, "diss_local_pca") ||
inherits(diss_method, "diss_local_pls"))
if (is_local_diss) {
iter_neighborhoods <- ith_mbl_neighbor(
Xr = Xr, Xu = Xu, Yr = Yr, Yu = Yu,
diss_usage = "none",
neighbor_indices = neighborhoods$neighbors,
neighbor_diss = neighborhoods$neighbors_diss,
group = group
)
} else {
iter_neighborhoods <- ith_mbl_neighbor(
Xr = Xr, Xu = Xu, Yr = Yr, Yu = Yu,
diss_usage = diss_usage,
neighbor_indices = neighborhoods$neighbors,
neighbor_diss = neighborhoods$neighbors_diss,
diss_xr_xr = neighborhoods$diss_xr_xr,
group = group
)
}
r_fields <- c(
"o_index", "k_diss", "k_original", "k", "ncomp", "min_ncomp", "max_ncomp",
y_output_name, y_hat_output_name, "yr_min_obs", "yr_max_obs",
"index_nearest_in_Xr", "index_farthest_in_Xr",
"y_nearest", "y_nearest_pred",
"y_farthest", "diss_nearest", "diss_farthest",
"loc_rmse_cv", "loc_st_rmse_cv", "loc_ncomp", "rep"
)
n_ith_result <- ifelse(
inherits(neighbors, "neighbors_k"),
length(neighbors$k),
length(neighbors$threshold)
)
template_pred_results <- as.data.frame(
matrix(
NA, n_ith_result, length(r_fields),
dimnames = list(NULL, r_fields)
)
)
template_pred_results$rep[1] <- 0
if (inherits(neighbors, "neighbors_k")) {
template_pred_results$k <- neighbors$k
} else {
template_pred_results$k_diss <- neighbors$threshold
}
pg_bar_width <- 10
n_characters <- nchar(n_xu)
n_iter <- n_xu
to_erase <- pg_bar_width + (2 * n_characters) + 8
to_erase <- paste(rep(" ", to_erase), collapse = "")
if (verbose) {
cat("\033[32m\033[3mPredicting...\n\033[23m\033[39m")
}
pred_obs <- foreach(
i = seq_len(n_iter),
ith_observation = iter_neighborhoods,
.inorder = FALSE,
.export = c(
"dissimilarity", "fit_and_predict", "pls_cv",
"get_col_sds", "get_wapls_weights",
"sample_stratified", "gaussian_pr_cv"
),
.noexport = c("Xr", "Xu")
) %mydo% {
ith_pred_results <- template_pred_results
additional_results <- NULL
ith_pred_results$o_index[] <- i
# if (is_local_diss) {
# ith_observation <- get_ith_local_neighbors(
# ith_xr = ith_observation$ith_xr,
# ith_xu = ith_observation$ith_xu,
# ith_yr = ith_observation$ith_yr,
# ith_yu = ith_observation$ith_yu,
# diss_usage = diss_usage,
# ith_neig_indices = ith_observation$ith_neig_indices,
# neighbors = neighbors,
# spike = spike,
# diss_method = diss_method,
# ith_group = ith_observation$ith_group,
# mbl_is_parallel = control$allow_parallel
# )
# ith_pred_results$loc_ncomp[] <- ith_observation$ith_ncomp
# additional_results$ith_neig_indices <- ith_observation$ith_neig_indices
# additional_results$ith_neigh_diss <- ith_observation$ith_neigh_diss
# }
if (verbose) {
cat(paste0("\033[34m\033[3m", i, "/", n_iter, "\033[23m\033[39m"))
pb <- txtProgressBar(width = pg_bar_width, char = "\033[34m_\033[39m")
}
if (inherits(neighbors, "neighbors_diss")) {
ith_diss <- ith_observation$ith_neigh_diss
if (!is.null(spike)) {
n_spike_hold <- sum(spike > 0)
if (n_spike_hold > 0) {
ith_diss[seq_len(n_spike_hold)] <- 0
}
}
k_diss_vec <- neighbors$threshold
k_min <- neighbors$k_min
k_max <- neighbors$k_max
ith_pred_results$k_original <- sapply(k_diss_vec, FUN = function(x, d) sum(d < x), d = ith_diss)
ith_pred_results$k <- ith_pred_results$k_original
ith_pred_results$k[ith_pred_results$k_original < k_min] <- k_min
ith_pred_results$k[ith_pred_results$k_original > k_max] <- k_max
} else {
ith_pred_results$k <- neighbors$k
}
for (kk in seq_len(nrow(ith_pred_results))) {
if (verbose) {
setTxtProgressBar(pb, kk / nrow(ith_pred_results))
}
# If the sample has not been predicted before,
# then create a model and predict it (useful only when k_diss is used)
current_k <- ith_pred_results$k[kk]
# Skip refitting if neighborhood size unchanged from previous iteration
# (optimization for neighbors_diss where different thresholds may prorduce
# same k)
if (current_k != ifelse(kk == 1, 0, ith_pred_results$k[kk - 1])) {
# Subset predictors: when diss_usage == "predictors", include local
# dissimilarity columns alongside original spectral variables
if (diss_usage == "predictors") {
keep_cols <- c(
1:current_k,
(1 + ith_observation$n_k):ncol(ith_observation$ith_xr)
)
i_k_xr <- ith_observation$ith_xr[1:current_k, keep_cols]
i_k_xu <- ith_observation$ith_xu[, keep_cols, drop = FALSE]
} else {
i_k_xr <- ith_observation$ith_xr[1:current_k, ]
i_k_xu <- ith_observation$ith_xu
}
# for extracting some basic stats
i_k_yr <- ith_observation$ith_yr[first_nn:current_k, , drop = FALSE]
# i_k_yu <- ith_observation$ith_yu
kth_diss <- ith_observation$ith_neigh_diss[first_nn:current_k]
i_idx <- ith_observation$ith_neig_indices[first_nn:current_k]
ith_pred_results$rep[kk] <- 0
ith_yr_range <- range(i_k_yr)
ith_pred_results$yr_min_obs[kk] <- ith_yr_range[1]
ith_pred_results$yr_max_obs[kk] <- ith_yr_range[2]
ith_pred_results$diss_farthest[kk] <- max(kth_diss)
ith_pred_results$diss_nearest[kk] <- min(kth_diss)
ith_pred_results$y_farthest[kk] <- i_k_yr[which.max(kth_diss)]
ith_pred_results$y_nearest[kk] <- i_k_yr[which.min(kth_diss)]
ith_pred_results$index_nearest_in_Xr[kk] <- i_idx[which.min(kth_diss)]
ith_pred_results$index_farthest_in_Xr[kk] <- i_idx[which.max(kth_diss)]
# Full neighbor set for model fitting (including spiked observations).
# Stats above excluded spike via first_nn offset. This set is then
# subsetted at each k iteration.
i_k_yr <- ith_observation$ith_yr[1:current_k, , drop = FALSE]
if (!is.null(group)) {
i_k_group <- factor(ith_observation$ith_group[1:current_k])
} else {
i_k_group <- NULL
}
if (diss_usage == "weights") {
# Weights are defined according to a tricubic function
# as in Cleveland and Devlin (1988) and Naes and Isaksson (1990).
std_kth_diss <- kth_diss / max(kth_diss)
kth_weights <- (1 - (std_kth_diss^3))^3
kth_weights[which(kth_weights == 0)] <- 1e-04
} else {
kth_weights <- rep(1, current_k)
}
# Local model fit and prediction
i_k_pred <- fit_and_predict(
x = i_k_xr,
y = i_k_yr,
pred_method = pred_method,
scale = fit_scale,
pls_c = pls_c,
weights = kth_weights,
newdata = i_k_xu,
CV = is_local_cv,
tune = control$tune_locally,
group = i_k_group,
p = control$p,
number = control$number,
noise_variance = noise_variance,
range_prediction_limits = control$range_prediction_limits,
pls_max_iter = fit_method$max_iter,
pls_tol = fit_method$tol,
seed = seed,
algorithm = fit_method$method
)
ith_pred_results[[y_hat_output_name]][kk] <- i_k_pred$prediction
# Track selected PLS components for NNv validation
selected_pls <- NULL
if (is_local_cv) {
# Select best model: tuned or fixed components
if (control$tune_locally) {
best_row <- which.min(i_k_pred$validation$cv_results$rmse_cv)
} else {
best_row <- ifelse(is_fit_pls, fit_method$ncomp, 1)
}
# Store optimized component counts
if (is_fit_pls) {
ith_pred_results$ncomp[kk] <- i_k_pred$validation$cv_results$ncomp[best_row]
selected_pls <- ith_pred_results$ncomp[kk]
}
if (is_fit_wapls) {
ith_pred_results$min_ncomp[kk] <- i_k_pred$validation$cv_results$min_component[best_row]
ith_pred_results$max_ncomp[kk] <- i_k_pred$validation$cv_results$max_component[best_row]
selected_pls <- i_k_pred$validation$cv_results[best_row, 1:2]
}
# Store local CV statistics
ith_pred_results$loc_rmse_cv[kk] <- i_k_pred$validation$cv_results$rmse_cv[best_row]
ith_pred_results$loc_st_rmse_cv[kk] <- i_k_pred$validation$cv_results$st_rmse_cv[best_row]
} else {
# No local CV: use fixed component counts from fit_method
if (is_fit_pls) {
ith_pred_results$ncomp[kk] <- fit_method$ncomp
selected_pls <- ith_pred_results$ncomp[kk]
}
if (is_fit_wapls) {
ith_pred_results$min_ncomp[kk] <- fit_method$min_ncomp
ith_pred_results$max_ncomp[kk] <- fit_method$max_ncomp
selected_pls <- c(fit_method$min_ncomp, fit_method$max_ncomp)
}
}
if (is_nnv) {
# Leave-nearest-neighbor-out validation: exclude nearest neighbor (or its group)
# and predict its value using the remaining neighbors
if (!is.null(group)) {
out_group <- which(i_k_group == i_k_group[[ith_observation$local_index_nearest]])
} else {
out_group <- ith_observation$local_index_nearest
}
nearest_pred <- fit_and_predict(
x = i_k_xr[-out_group, ],
y = i_k_yr[-out_group, , drop = FALSE],
pred_method = pred_method,
scale = fit_scale,
pls_c = selected_pls,
noise_variance = noise_variance,
newdata = i_k_xr[ith_observation$local_index_nearest, , drop = FALSE],
CV = FALSE,
tune = FALSE,
range_prediction_limits = control$range_prediction_limits,
pls_max_iter = fit_method$max_iter,
pls_tol = fit_method$tol,
seed = seed,
algorithm = fit_method$method
)$prediction
ith_pred_results$y_nearest_pred[kk] <- nearest_pred / kth_weights[1]
}
} else {
# Neighborhood size unchanged: copy previous results
ith_k_diss <- ith_pred_results$k_diss[kk]
ith_pred_results[kk, ] <- ith_pred_results[kk - 1, ]
ith_pred_results$rep[kk] <- 1
ith_pred_results$k_diss[kk] <- ith_k_diss
}
}
if (verbose) {
if (kk == nrow(ith_pred_results) & i != n_iter) {
cat("\r", to_erase, "\r")
}
if (i == n_iter) {
cat("\n")
}
# do not use close() (it prints a new line)
## close(pb)
}
list(
results = ith_pred_results,
additional_results = additional_results
)
}
# Reorder results by observation index (foreach may return out of order)
iteration_order <- sapply(
pred_obs,
FUN = function(x) x$results$o_index[1]
)
pred_obs <- pred_obs[order(iteration_order, decreasing = FALSE)]
# Combine all iteration results into single table
results_table <- do.call(
"rbind", lapply(pred_obs, FUN = function(x) x$results)
)
# No k adjustment needed when Xu is provided (always the case now)
fix_k <- 0
if (is_local_diss) {
# Reconstruct local dissimilarity matrix from iteration results
diss_xr_xu <- do.call(
"cbind",
lapply(iteration_order,
FUN = function(ii, x, m) {
idc <- x[[ii]]$additional_results$ith_neig_indices
d <- x[[ii]]$additional_results$ith_neigh_diss
m[idc] <- d
m
},
x = pred_obs,
m = matrix(NA, nrow(Xr), 1)
)
)
class(diss_xr_xu) <- c("local_ortho_diss", "matrix")
dimnames(diss_xr_xu) <- list(
paste0("Xr_", seq_len(nrow(diss_xr_xu))),
paste0(pre_nms_ng, seq_len(ncol(diss_xr_xu)))
)
# Reconstruct neighbor indices from iteration results
neighborhoods$neighbors <- do.call(
"cbind", lapply(iteration_order,
FUN = function(ii, x, m) {
idc <- x[[ii]]$additional_results$ith_neig_indices
m[seq_len(length(idc))] <- idc
m
},
x = pred_obs,
m = matrix(NA, max(results_table$k), 1)
)
)
}
out <- c(
if (is.null(Yu)) "yu_obs",
if (all(is.na(results_table$k_original))) "k_original",
if (!is_nnv) "y_nearest_pred",
if (!is_fit_wapls) c("min_ncomp", "max_ncomp"),
if (!is_fit_pls) "ncomp",
if (!is_local_cv) c("loc_rmse_cv", "loc_st_rmse_cv"),
if (!is_local_diss) "loc_ncomp",
"rep"
)
results_table <- results_table[, !(names(results_table) %in% out), drop = FALSE]
if (inherits(neighbors, "neighbors_diss")) {
# Split results by dissimilarity threshold
k_diss_vec <- neighbors$threshold
results_table <- lapply(
k_diss_vec,
FUN = function(x, tbl, sel) tbl[tbl[[sel]] == x, ],
tbl = results_table,
sel = "k_diss"
)
names(results_table) <- paste0("k_diss_", k_diss_vec)
# Compute percentage of observations bounded by k_range
k_min <- neighbors$k_min
k_max <- neighbors$k_max
p_bounded <- sapply(
results_table,
FUN = function(x, k_min, k_max) {
sum(x$k_original <= k_min | x$k_original >= k_max)
},
k_min = k_min,
k_max = k_max
)
col_ks <- data.frame(
k_diss = k_diss_vec,
p_bounded = paste0(round(100 * p_bounded / n_xu, 3), "%")
)
} else {
# Split results by k
k_vec <- neighbors$k - fix_k
results_table <- lapply(
k_vec,
FUN = function(x, tbl, sel) tbl[tbl[[sel]] == x, ],
tbl = results_table,
sel = "k"
)
names(results_table) <- paste0("k_", k_vec)
col_ks <- data.frame(k = k_vec)
}
if (is_nnv) {
# Compute nearest neighbor validation statistics
nn_stats <- function(x) {
nn_rmse <- (mean((x$y_nearest - x$y_nearest_pred)^2))^0.5
nn_st_rmse <- nn_rmse / diff(range(x$y_nearest))
nn_rsq <- (cor(x$y_nearest, x$y_nearest_pred))^2
c(nn_rmse = nn_rmse, nn_st_rmse = nn_st_rmse, nn_rsq = nn_rsq)
}
loc_nn_res <- do.call("rbind", lapply(results_table, FUN = nn_stats))
loc_nn_res <- cbind(
col_ks,
rmse = loc_nn_res[, "nn_rmse"],
st_rmse = loc_nn_res[, "nn_st_rmse"],
r2 = loc_nn_res[, "nn_rsq"]
)
} else {
loc_nn_res <- NULL
}
if (is_local_cv) {
# Compute mean local cross-validation statistics
mean_loc_res <- function(x) {
mean_loc_rmse <- mean(x$loc_rmse_cv)
mean_loc_st_rmse <- mean(x$loc_st_rmse_cv)
c(loc_rmse = mean_loc_rmse, loc_st_rmse = mean_loc_st_rmse)
}
loc_res <- do.call("rbind", lapply(results_table, mean_loc_res))
loc_res <- cbind(
col_ks,
rmse = loc_res[, "loc_rmse"],
st_rmse = loc_res[, "loc_st_rmse"]
)
} else {
loc_res <- NULL
}
if (!is.null(Yu)) {
# Add observed values to results and compute prediction statistics
for (i in seq_along(results_table)) {
results_table[[i]][[y_output_name]] <- Yu
}
yu_stats <- function(x, y_hat, y) {
y_rmse <- mean((x[[y_hat]] - x[[y]])^2, na.rm = TRUE)^0.5
y_st_rmse <- y_rmse / diff(range(x[[y_hat]]), na.rm = TRUE)
y_rsq <- cor(x[[y_hat]], x[[y]], use = "complete.obs")^2
c(rmse = y_rmse, st_rmse = y_st_rmse, rsq = y_rsq)
}
pred_res <- do.call(
"rbind",
lapply(results_table, yu_stats, y_hat = y_hat_output_name, y = y_output_name)
)
pred_res <- cbind(
col_ks,
rmse = pred_res[, "rmse"],
st_rmse = pred_res[, "st_rmse"],
r2 = pred_res[, "rsq"]
)
} else {
pred_res <- NULL
}
diss_method_name <- if (is_local_diss) {
paste0(class(diss_method)[1], " (locally computed)")
} else {
class(diss_method)[1]
}
if (control$return_dissimilarity) {
diss_list <- list(
diss_method = diss_method,
diss_usage = diss_usage,
diss_xr_xu = diss_xr_xu
)
if (has_projection) {
diss_list$projection <- diss_projection
}
} else {
diss_method_return <- NULL
if (inherits(diss_method, "diss_method")) {
diss_method_return <- diss_method
}
diss_list <- list(
diss_method = diss_method_return,
diss_usage = diss_usage
)
}
colnames(neighborhoods$neighbors) <- paste0(pre_nms_ng, seq_len(ln))
rownames(neighborhoods$neighbors) <- paste0("k_", seq_len(nrow(neighborhoods$neighbors)))
val_list <- structure(
list(
loc_res,
loc_nn_res,
pred_res
),
names = c(
"local_cross_validation",
"nearest_neighbor_validation",
val_summary_name
)
)
results_list <- list(
control = control,
diss_usage = diss_usage,
fit_method = fit_method,
spike = ifelse(is.null(spike), FALSE, TRUE),
dissimilarities = diss_list,
Xu_neighbors = list(
neighbors = neighborhoods$neighbors,
neighbors_diss = neighborhoods$neighbors_diss
),
n_predictions = n_xu,
gh = neighborhoods$gh,
validation_results = val_list,
results = results_table,
seed = seed
)
attr(results_list, "call") <- f_call
class(results_list) <- c("mbl", "list")
results_list
}
#' @rdname mbl
#' @export
plot.mbl <- function(x,
what = c("validation", "gh"),
metric = "rmse",
ncomp = c(1, 2),
...) {
# Validate arguments
what <- match.arg(what, c("validation", "gh"), several.ok = TRUE)
metric <- match.arg(metric, c("rmse", "st_rmse", "r2"))
if (!inherits(x, "mbl")) {
stop("'x' must be an object of class 'mbl'.", call. = FALSE)
}
# Save and restore graphics parameters
opar <- par("mfrow", "mar")
on.exit(par(opar))
# Set up multi-panel if needed
if (length(what) == 2 && !is.null(x$gh)) {
par(mfrow = c(1, 2))
}
# Process plot arguments
plot_args <- .process_plot_args(list(...))
# Plot validation results
if ("validation" %in% what) {
.plot_validation(x, metric, plot_args)
}
# Plot GH scores
if ("gh" %in% what) {
if (is.null(x$gh)) {
message("GH distance not available in this object.")
} else {
.plot_gh_scores(x, ncomp, plot_args)
}
}
mtext(plot_args$main, outer = TRUE, cex = 2, line = -2)
invisible(x)
}
# =============================================================================
# Internal plotting helpers
# =============================================================================
#' Process plot arguments with defaults
#' @keywords internal
#' @noRd
.process_plot_args <- function(dots) {
args <- dots
# Extract or set main title
if ("main" %in% names(args)) {
main <- args$main
args$main <- NULL
} else {
main <- "Memory-based learning results"
}
# Set defaults
if (!"col.axis" %in% names(args)) {
args$col.axis <- grey(0.3)
}
if (!"pch" %in% names(args)) {
args$pch <- 16
}
# Remove arguments that are set internally
internal_args <- c("col", "xlab", "ylab", "type", "ylim", "xlim")
args <- args[!names(args) %in% internal_args]
args$main <- main
args
}
#' Plot validation results
#' @keywords internal
#' @noRd
.plot_validation <- function(object, metric, plot_args) {
# Collect validation results
val_data <- list()
colors <- character()
if (!is.null(object$validation_results$nearest_neighbor_validation)) {
val_data$NNv <- cbind(
object$validation_results$nearest_neighbor_validation,
val = "NNv"
)
colors <- c(colors, "dodgerblue")
}
if (!is.null(object$validation_results$local_cross_validation)) {
val_data$local_cv <- cbind(
object$validation_results$local_cross_validation,
r2 = NA,
val = "local_cv"
)
colors <- c(colors, "green4")
}
if (!is.null(object$validation_results$Yu_prediction_statistics)) {
val_data$Yu <- cbind(
object$validation_results$Yu_prediction_statistics,
val = "Yu prediction"
)
colors <- c(colors, "red")
}
if (!is.null(object$validation_results$Yr_fitted_statistics)) {
val_data$Yr <- cbind(
object$validation_results$Yr_fitted_statistics,
val = "Yr fitted"
)
colors <- c(colors, "orange")
}
if (length(val_data) == 0) {
par(mfrow = c(1, 1))
message("No validation results to plot.")
return(invisible(NULL))
}
tpl <- do.call(rbind, val_data)
col_names <- colnames(tpl)
# Determine ID variable
id_var <- if ("k" %in% col_names) "k" else "k_diss"
# Select columns for metric
keep_cols <- c(id_var, metric, "val")
keep_cols <- keep_cols[keep_cols %in% col_names]
tpl_subset <- data.frame(tpl)[, keep_cols, drop = FALSE]
# Reshape to wide format
to_plot <- reshape(
tpl_subset,
timevar = "val",
idvar = id_var,
direction = "wide"
)
colnames(to_plot) <- gsub("[.]", " ", colnames(to_plot))
# Remove r2 for local_cv (not available)
if (metric == "r2") {
drop_col <- grepl("local_cv", colnames(to_plot))
to_plot <- to_plot[, !drop_col, drop = FALSE]
colors <- colors[colors != "green4"]
}
# Plot
y_range <- range(to_plot[, -1], na.rm = TRUE)
y_range[2] <- y_range[2] * 1.1
do.call("matplot", c(
list(
x = to_plot[, 1],
y = to_plot[, -1, drop = FALSE],
type = "b",
xlab = id_var,
ylab = metric,
ylim = y_range,
col = colors
),
plot_args[!names(plot_args) %in% "main"]
))
grid(col = rgb(0.3, 0.3, 0.3, 0.1), lty = 1, lwd = 1)
mtext("Validation results", col = grey(0.3))
legend(
"topright",
legend = colnames(to_plot)[-1],
col = colors,
pch = plot_args$pch,
box.lty = 0,
bg = NA
)
invisible(NULL)
}
#' Plot GH distance scores
#' @keywords internal
#' @noRd
.plot_gh_scores <- function(object, ncomp, plot_args) {
xr_scores <- object$gh$projection$scores
n_components <- object$gh$projection$ncomp
# Transform to Mahalanobis space if multicomponent
if (n_components > 1) {
xr_scores <- euclid_to_mahal(xr_scores)
xr_scores <- sweep(xr_scores, 2, colMeans(xr_scores), "-")
}
# Split Xr and Xu scores
xu_idx <- grep("Xu_", rownames(xr_scores))
xr_idx <- grep("Xr_", rownames(xr_scores))
xu_scores <- xr_scores[xu_idx, , drop = FALSE]
xr_scores <- xr_scores[xr_idx, , drop = FALSE]
# Colors
xr_col <- rgb(0, 0, 0.4, 0.5)
xu_col <- rgb(1, 0, 0, 0.5)
if (n_components == 1) {
.plot_gh_1d(xr_scores, xu_scores, xr_col, xu_col, plot_args)
} else {
.plot_gh_2d(xr_scores, xu_scores, ncomp, xr_col, xu_col, plot_args)
}
invisible(NULL)
}
#' Plot 1D GH scores
#' @keywords internal
#' @noRd
.plot_gh_1d <- function(xr_scores, xu_scores, xr_col, xu_col, plot_args) {
scores_combined <- c(xr_scores[, 1], xu_scores[, 1])
set_labels <- c(rep("Xr", nrow(xr_scores)), rep("Xu", nrow(xu_scores)))
df <- data.frame(
index = seq_along(scores_combined),
score = scores_combined,
set = set_labels
)
df <- df[order(df$score), ]
df$index <- seq_len(nrow(df))
rng <- range(scores_combined)
rng <- 1.2 * c(-max(abs(rng)), max(abs(rng)))
do.call("plot", c(
list(
x = df$index[df$set == "Xr"],
y = df$score[df$set == "Xr"],
ylim = rng,
col = xr_col,
xlab = "Ordered PLS values",
ylab = "PLS 1"
),
plot_args[!names(plot_args) %in% "main"]
))
points(
df$index[df$set == "Xu"],
df$score[df$set == "Xu"],
col = xu_col,
pch = plot_args$pch
)
mtext("Partial least squares scores", col = grey(0.3))
grid(col = rgb(0.3, 0.3, 0.3, 0.1), lty = 1, lwd = 1)
legend("topleft", legend = c("Xr", "Xu"), col = c(xr_col, xu_col),
pch = plot_args$pch, cex = 0.8, box.lty = 0, bg = NA)
}
#' Plot 2D GH scores
#' @keywords internal
#' @noRd
.plot_gh_2d <- function(xr_scores, xu_scores, ncomp, xr_col, xu_col, plot_args) {
rng <- 1.2 * range(xr_scores[, ncomp], xu_scores[, ncomp])
xlab <- paste0("PLS ", ncomp[1], " (standardized)")
ylab <- paste0("PLS ", ncomp[2], " (standardized)")
do.call("plot", c(
list(
x = xr_scores[, ncomp[1]],
y = xr_scores[, ncomp[2]],
xlab = xlab,
ylab = ylab,
xlim = rng,
ylim = rng,
col = xr_col
),
plot_args[!names(plot_args) %in% "main"]
))
points(xu_scores[, ncomp, drop = FALSE], col = xu_col, pch = plot_args$pch)
mtext("Partial least squares (Mahalanobis space)", col = grey(0.3))
grid(col = rgb(0.3, 0.3, 0.3, 0.1), lty = 1, lwd = 1)
legend("topright", legend = c("Xr", "Xu"), col = c(xr_col, xu_col),
pch = plot_args$pch, cex = 0.8, box.lty = 0, bg = NA)
# Draw reference circles
max_score <- ceiling(max(abs(xr_scores[, ncomp])))
for (r in seq_len(max_score)) {
theta <- seq(0, 2 * pi, length.out = 101)
lines(r * cos(theta), r * sin(theta),
col = rgb(0.3, 0.3, 0.3, 0.3), lty = 1, lwd = 0.5)
}
}
#' @aliases mbl
#' @export
get_predictions <- function(x) {
if (is.na(match("mbl", class(x)))) {
stop("the object is not of class 'mbl'")
}
ext_pred <- function(x, ...) {
prediction <- x$pred
return(prediction)
}
predictions <- data.frame(sapply(x$results, ext_pred))
return(predictions)
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.