#' Define a Multivariate Functional Data Object
#'
#' This is the constructor function for objects of the mfd class.
#' It is a wrapper to \code{fda::\link[fda]{fd}},
#' but it forces the coef argument to be
#' a three-dimensional array of coefficients even if
#' the functional data is univariate.
#' Moreover, it allows to include the original raw data from which
#' you get the smooth functional data.
#' Finally, it also includes the matrix of precomputed inner products
#' of the basis functions, which can be useful to speed up computations
#' when calculating inner products between functional observations
#'
#' @param coef
#' A three-dimensional array of coefficients:
#'
#' * the first dimension corresponds to basis functions.
#'
#' * the second dimension corresponds to the number of
#' multivariate functional observations.
#'
#' * the third dimension corresponds to variables.
#' @param basisobj
#' A functional basis object defining the basis,
#' as provided to \code{fda::\link[fda]{fd}}, but there is no default.
#' @param fdnames
#' A list of length 3, each member being a string vector
#' containing labels for the levels of the corresponding dimension
#' of the discrete data.
#'
#' The first dimension is for a single character indicating the argument
#' values,
#' i.e. the variable on the functional domain.
#'
#' The second is for replications, i.e. it denotes the functional observations.
#'
#' The third is for functional variables' names.
#' @param raw
#' A data frame containing the original discrete data.
#' Default is NULL, however, if provided, it must contain:
#'
#' a column (indicated by the \code{id_var} argument)
#' denoting the functional observations,
#' which must correspond to values in \code{fdnames[[2]]},
#'
#' a column named as \code{fdnames[[1]]},
#' returning the argument values of each function
#'
#' as many columns as the functional variables,
#' named as in \code{fdnames[[3]]},
#' containing the discrete functional values for each variable.
#' @param id_var
#' A single character value indicating the column
#' in the \code{raw} argument
#' containing the functional observations (as in \code{fdnames[[2]]}),
#' default is NULL.
#' @param B
#' A matrix with the inner products of the basis functions.
#' If NULL, it is calculated from the basis object provided.
#' Default is NULL.
#'
#' @return
#' A multivariate functional data object
#' (i.e., having class \code{mfd}),
#' which is a list with components named
#' \code{coefs}, \code{basis}, and \code{fdnames},
#' as for class \code{fd},
#' with possibly in addition the components \code{raw} and \code{id_var}.
#'
#' @details
#' To check that an object is of this class, use function is.mfd.
#'
#' @references
#' Ramsay, James O., and Silverman, Bernard W. (2006),
#' \emph{Functional Data Analysis}, 2nd ed., Springer, New York.
#'
#' Ramsay, James O., and Silverman, Bernard W. (2002),
#' \emph{Applied Functional Data Analysis}, Springer, New York.
#'
#' @export
#'
#' @examples
#' library(funcharts)
#' library(fda)
#' set.seed(0)
#' nobs <- 5
#' nbasis <- 10
#' nvar <- 2
#' coef <- array(rnorm(nobs * nbasis * nvar), dim = c(nbasis, nobs, nvar))
#' bs <- create.bspline.basis(rangeval = c(0, 1), nbasis = nbasis)
#' mfdobj <- mfd(coef = coef, basisobj = bs)
#' plot_mfd(mfdobj)
#'
mfd <- function(coef,
basisobj,
fdnames = NULL,
raw = NULL,
id_var = NULL,
B = NULL) {
if (is.null(fdnames)) {
fdnames <- list(
"t",
paste0("rep", seq_len(dim(coef)[2])),
paste0("var", seq_len(dim(coef)[3]))
)
}
if (sum(is.null(raw), is.null(id_var)) == 1) {
stop("If either raw or id_var are not NULL, both must be not NULL")
}
if (!is.null(raw)) {
# if (!(fdnames[[1]] %in% names(raw))) {
# stop("fdnames[[1]] must be the name of a column of the raw data.")
# }
if (!is.data.frame(raw)) {
stop("raw must be a data frame.")
}
if (!(is.character(id_var) & length(id_var) == 1)) {
stop ("id_var must be a single character")
}
if (!(class(raw[[id_var]]) %in% c("character", "factor"))) {
stop("column id_var of raw data frame must be a character or factor.")
}
if (!(setequal(unique(raw[[id_var]]), fdnames[[2]]))) {
stop(paste0("column id_var of raw data frame must contain",
"the same values as fdnames[[2]]."))
}
}
if (!(basisobj$type %in% c("bspline", "fourier", "const", "expon",
"monom", "polygonal", "power"))) {
stop("supported basis systems are bspline, fourier, constant,
exponential, monomial, polygonal, power")
}
if (!is.array(coef)) {
stop("'coef' is not array")
}
coefd <- dim(coef)
ndim <- length(coefd)
if (ndim != 3) {
stop("'coef' not of dimension 3")
}
if (coefd[1] != basisobj$nbasis) {
stop("1st dimension of coef must be equal to basisobj$nbasis")
}
fdobj <- fda::fd(coef, basisobj, fdnames)
fdobj$raw <- raw
fdobj$id_var <- id_var
nb <- basisobj$nbasis
if (is.null(B)) {
if (basisobj$type == "bspline") {
B <- fda::inprod.bspline(fda::fd(diag(nb), basisobj))
}
if (basisobj$type == "fourier") {
B <- diag(nb)
}
if (basisobj$type == "const") {
B <- matrix(diff(basisobj$rangeval))
}
if (basisobj$type == "expon") {
out_mat <- outer(basisobj$params, basisobj$params, "+")
exp_out_mat <- exp(out_mat)
B <- (exp_out_mat ^ basisobj$rangeval[2] -
exp_out_mat ^ basisobj$rangeval[1]) / out_mat
B[out_mat == 0] <- diff(basisobj$rangeval)
}
if (basisobj$type == "monom") {
out_mat <- outer(basisobj$params, basisobj$params, "+") + 1
B <- (basisobj$rangeval[2] ^ out_mat -
basisobj$rangeval[1] ^ out_mat) / out_mat
}
if (basisobj$type == "polygonal") {
B <- inprod_fd(fda::fd(diag(basisobj$nbasis), basisobj),
fda::fd(diag(basisobj$nbasis), basisobj))
}
if (basisobj$type == "power") {
out_mat <- outer(basisobj$params, basisobj$params, "+") + 1
B <- (basisobj$rangeval[2] ^ out_mat -
basisobj$rangeval[1] ^ out_mat) / out_mat
B[out_mat == 0] <- log(basisobj$rangeval[2]) -
log(basisobj$rangeval[1])
}
}
if (!is.matrix(B)) stop("B must be a matrix")
if (nrow(B) != nb | ncol(B) != nb)
stop("B must have the right number of rows and columns")
fdobj$basis$B <- B
class(fdobj) <- c("mfd", "fd")
fdobj
}
#' Confirm Object has Class \code{mfd}
#'
#' Check that an argument is a multivariate
#' functional data object of class \code{mfd}.
#'
#' @param mfdobj An object to be checked.
#'
#' @return a logical value: TRUE if the class is correct, FALSE otherwise.
#' @export
#'
is.mfd <- function(mfdobj) if (inherits(mfdobj, "mfd")) TRUE else FALSE
#' Simulate multivariate functional data
#'
#' Simulate random coefficients and create a multivariate functional data
#' object of class `mfd`.
#' It is mainly for internal use, to check that the package functions
#' work.
#'
#' @param nobs Number of functional observations to be simulated.
#' @param nbasis Number of basis functions.
#' @param nvar Number of functional covariates.
#' @param seed Deprecated: use \code{set.seed()} before calling
#' the function for reproducibility.
#'
#' @return
#' A simulated object of class `mfd`.
#' @export
#'
#' @examples
#' library(funcharts)
#' data_sim_mfd()
data_sim_mfd <- function(nobs = 5,
nbasis = 5,
nvar = 2,
seed) {
if (!missing(seed)) {
warning(paste0("argument seed is deprecated; ",
"please use set.seed()
before calling the function instead."),
call. = FALSE)
}
coef <- array(stats::rnorm(nobs * nbasis * nvar),
dim = c(nbasis, nobs, nvar))
bs <- fda::create.bspline.basis(rangeval = c(0, 1), nbasis = nbasis)
mfd(coef = coef, basisobj = bs)
}
#' Extract observations and/or variables from \code{mfd} objects.
#'
#' @param mfdobj An object of class \code{mfd}.
#' @param i
#' Index specifying functional observations to extract or replace.
#' They can be numeric, character,
#' or logical vectors or empty (missing) or NULL.
#' Numeric values are coerced to integer as by as.integer
#' (and hence truncated towards zero).
#' The can also be negative integers,
#' indicating functional observations to leave out of the selection.
#' Logical vectors indicate TRUE for the observations to select.
#' Character vectors will be matched
#' to the argument \code{fdnames[[2]]} of \code{mfdobj},
#' i.e. to functional observations' names.
#' @param j
#' Index specifying functional variables to extract or replace.
#' They can be numeric, logical,
#' or character vectors or empty (missing) or NULL.
#' Numeric values are coerced to integer as by as.integer
#' (and hence truncated towards zero).
#' The can also be negative integers,
#' indicating functional variables to leave out of the selection.
#' Logical vectors indicate TRUE for the variables to select.
#' Character vectors will be matched
#' to the argument \code{fdnames[[3]]} of \code{mfdobj},
#' i.e. to functional variables' names.
#'
#' @return a \code{mfd} object with selected observations and variables.
#'
#' @details
#' This function adapts the \code{fda::"[.fd"}
#' function to be more robust and suitable
#' for the \code{mfd} class.
#' In fact, whatever the number of observations
#' or variables you want to extract,
#' it always returns a \code{mfd} object with a three-dimensional coef array.
#' In other words, it behaves as you would
#' always use the argument \code{drop=FALSE}.
#' Moreover, you can extract observations
#' and variables both by index numbers and by names,
#' as you would normally do when using
#' \code{`[`} with standard vector/matrices.
#'
#' @export
#' @examples
#' library(funcharts)
#' library(fda)
#'
#' # In the following, we extract the first one/two observations/variables
#' # to see the difference with `[.fd`.
#' mfdobj <- data_sim_mfd()
#' fdobj <- fd(mfdobj$coefs, mfdobj$basis, mfdobj$fdnames)
#'
#' # The argument `coef` in `fd`
#' # objects is converted to a matrix when possible.
#' dim(fdobj[1, 1]$coef)
#' # Not clear what is the second dimension:
#' # the number of replications or the number of variables?
#' dim(fdobj[1, 1:2]$coef)
#' dim(fdobj[1:2, 1]$coef)
#'
#' # The argument `coef` in `mfd` objects is always a three-dimensional array.
#' dim(mfdobj[1, 1]$coef)
#' dim(mfdobj[1, 1:2]$coef)
#' dim(mfdobj[1:2, 1]$coef)
#'
#' # Actually, `[.mfd` works as `[.fd` when passing also `drop = FALSE`
#' dim(fdobj[1, 1, drop = FALSE]$coef)
#' dim(fdobj[1, 1:2, drop = FALSE]$coef)
#' dim(fdobj[1:2, 1, drop = FALSE]$coef)
#'
"[.mfd" <- function(mfdobj, i = TRUE, j = TRUE) {
if (!(is.mfd(mfdobj))) {
stop(paste0("First argument must be a ",
"multivariate functional data object."))
}
coefs <- mfdobj$coefs[, i, j, drop = FALSE]
fdnames <- mfdobj$fdnames
fdnames[[2]] <- dimnames(coefs)[[2]]
fdnames[[3]] <- dimnames(coefs)[[3]]
if (is.null(mfdobj$raw))
raw_filtered <- id_var <- NULL else {
raw <- mfdobj$raw
id_var <- mfdobj$id_var
raw_filtered <- raw[raw[[id_var]] %in% fdnames[[2]], , drop = FALSE]
}
mfd(coef = coefs, basisobj = mfdobj$basis, fdnames = fdnames,
raw = raw_filtered, id_var = id_var, B = mfdobj$basis$B)
}
#' Inner products of functional data contained in \code{mfd} objects.
#'
#' @param mfdobj1
#' A multivariate functional data object of class \code{mfd}.
#' @param mfdobj2
#' A multivariate functional data object of class \code{mfd}.
#' It must have the same functional variables as \code{mfdobj1}.
#' If NULL, it is equal to \code{mfdobj1}.
#'
#' @return
#' a three-dimensional array of \emph{L^2} inner products.
#' The first dimension is the number of functions in argument mfdobj1,
#' the second dimension is the same thing for argument mfdobj2,
#' the third dimension is the number of functional variables.
#' If you sum values over the third dimension,
#' you get a matrix of inner products
#' in the product Hilbert space of multivariate functional data.
#'
#' @details
#' Note that \emph{L^2} inner products are not calculated
#' for couples of functional data
#' from different functional variables.
#' This function is needed to calculate the
#' inner product in the product Hilbert space
#' in the case of multivariate functional data,
#' which for each observation is the sum of the \emph{L^2}
#' inner products obtained for each functional variable.
#'
#' @export
#' @examples
#' library(funcharts)
#' set.seed(123)
#' mfdobj1 <- data_sim_mfd()
#' mfdobj2 <- data_sim_mfd()
#' inprod_mfd(mfdobj1)
#' inprod_mfd(mfdobj1, mfdobj2)
inprod_mfd <- function(mfdobj1, mfdobj2 = NULL) {
if (!(is.mfd(mfdobj1))) {
stop("First argument is not a multivariate functional data object.")
}
if (!is.null(mfdobj2)) {
if (!(is.mfd(mfdobj2))) {
stop("Second argument is not a multivariate functional data object.")
}
if (length(mfdobj1$fdnames[[3]]) != length(mfdobj2$fdnames[[3]])) {
stop(paste0("mfdobj1 and mfdobj2 do not have the ",
"same number of functional variables."))
}
} else mfdobj2 <- mfdobj1
variables <- mfdobj1$fdnames[[3]]
n_var <- length(variables)
ids1 <- mfdobj1$fdnames[[2]]
ids2 <- mfdobj2$fdnames[[2]]
n_obs1 <- length(ids1)
n_obs2 <- length(ids2)
bs1 <- mfdobj1$basis
bs2 <- mfdobj2$basis
inprods <- array(NA, dim = c(n_obs1, n_obs2, n_var),
dimnames = list(ids1, ids2, variables))
C1 <- mfdobj1$coefs
C2 <- mfdobj2$coefs
inprods[] <- vapply(seq_len(n_var), function(jj) {
C1jj <- matrix(C1[, , jj], nrow = dim(C1)[1], ncol = dim(C1)[2])
C2jj <- matrix(C2[, , jj], nrow = dim(C2)[1], ncol = dim(C2)[2])
if (identical(bs1, bs2)) {
if (bs1$type == "fourier") {
out <- as.matrix(t(C1jj) %*% C2jj)
}
if (bs1$type %in% c("bspline", "expon", "monom", "polygonal", "power")) {
W <- bs1$B
out <- as.matrix(t(C1jj) %*% W %*% C2jj)
}
if (bs1$type == "const") {
W <- bs1$B
out <- t(C1jj) %*% C2jj * diff(bs1$rangeval)
}
} else {
fdobj1_jj <- fda::fd(C1jj, bs1)
fdobj2_jj <- fda::fd(C2jj, bs2)
out <- inprod_fd(fdobj1_jj, fdobj2_jj)
}
out
}, numeric(dim(mfdobj1$coefs)[2] * dim(mfdobj2$coefs)[2]))
inprods
}
#' Norm of Multivariate Functional Data
#'
#' Norm of multivariate functional data contained
#' in a \code{mfd} object.
#'
#' @param mfdobj A multivariate functional data object of class \code{mfd}.
#'
#' @return
#' A vector of length equal to the number of replications
#' in \code{mfdobj},
#' containing the norm of each multivariate functional observation
#' in the product Hilbert space,
#' i.e. the sum of \emph{L^2} norms for each functional variable.
#' @export
#' @examples
#' library(funcharts)
#' mfdobj <- data_sim_mfd()
#' norm.mfd(mfdobj)
#'
norm.mfd <- function(mfdobj) {
if (!(is.mfd(mfdobj))) {
stop("Input is not a multivariate functional data object.")
}
inprods <- inprod_mfd_diag(mfdobj)
sqrt(rowSums(inprods))
}
#' Inner product of two multivariate functional data objects,
#' for each observation
#'
#' @param mfdobj1 A multivariate functional data object of class \code{mfd}.
#' @param mfdobj2 A multivariate functional data object of class \code{mfd},
#' with the same number of functional variables and observations
#' as \code{mfdobj1}.
#' If NULL, then \code{mfdobj2=mfdobj1}. Default is NULL.
#'
#' @return It calculates the inner product of two
#' multivariate functional data objects.
#' The main function \code{inprod} of the package \code{fda}
#' calculates inner products among
#' all possible couples of observations.
#' This means that, if \code{mfdobj1} has \code{n1} observations
#' and \code{mfdobj2} has \code{n2} observations,
#' then for each variable \code{n1 X n2} inner products are calculated.
#' However, often one is interested only in calculating
#' the \code{n} inner products
#' between the \code{n} observations of \code{mfdobj1} and
#' the corresponding \code{n}
#' observations of \code{mfdobj2}. This function provides
#' this "diagonal" inner products only,
#' saving a lot of computation with respect to using
#' \code{fda::inprod} and then extracting the
#' diagonal elements.
#' Note that the code of this function calls a modified version
#' of \code{fda::inprod()}.
#' @export
#'
#' @examples
#' mfdobj <- data_sim_mfd()
#' inprod_mfd_diag(mfdobj)
#'
inprod_mfd_diag <- function(mfdobj1, mfdobj2 = NULL) {
if (!is.mfd(mfdobj1)) {
stop("Only mfd class allowed for mfdobj1 input")
}
if (!(fda::is.fd(mfdobj2) | is.null(mfdobj2))) {
stop("mfdobj2 input must be of class mfd, or NULL")
}
nvar1 <- dim(mfdobj1$coefs)[3]
nobs1 <- dim(mfdobj1$coefs)[2]
if (fda::is.fd(mfdobj2)) {
nvar2 <- dim(mfdobj2$coefs)[3]
if (nvar1 != nvar2) {
stop("mfdobj1 and mfdobj2 must have the same number of variables")
}
nobs2 <- dim(mfdobj2$coefs)[2]
if (nobs1 != nobs2) {
stop("mfdobj1 and mfdobj2 must have the same number of observations")
}
}
if (is.null(mfdobj2)) mfdobj2 <- mfdobj1
bs1 <- mfdobj1$basis
bs2 <- mfdobj2$basis
if (identical(bs1, bs2)) {
if (bs1$type == "fourier") {
inprods <- vapply(seq_len(nvar1), function(jj) {
C1jj <- mfdobj1$coefs[, , jj]
C2jj <- mfdobj2$coefs[, , jj]
colSums(as.matrix(C1jj * C2jj))
}, numeric(dim(mfdobj1$coefs)[2]))
}
if (bs1$type %in% c("bspline", "expon", "monom", "polygonal", "power")) {
inprods <- vapply(seq_len(nvar1), function(jj) {
C1jj <- mfdobj1$coefs[, , jj]
C2jj <- mfdobj2$coefs[, , jj]
rowSums(as.matrix(t(C1jj) %*% bs1$B * t(C2jj)))
}, numeric(dim(mfdobj1$coefs)[2]))
}
if (bs1$type == "const") {
inprods <- vapply(seq_len(nvar1), function(jj) {
C1jj <- mfdobj1$coefs[, , jj]
C2jj <- mfdobj2$coefs[, , jj]
t(C1jj) %*% C2jj * diff(bs1$rangeval)
as.numeric(C1jj * C2jj * diff(bs1$rangeval))
}, numeric(dim(mfdobj1$coefs)[2]))
}
} else {
inprods <- vapply(seq_len(nvar1), function(jj) {
fdobj1_jj <- fda::fd(matrix(mfdobj1$coefs[, , jj],
nrow = dim(mfdobj1$coefs)[1],
ncol = dim(mfdobj1$coefs)[2]),
bs1)
fdobj2_jj <- fda::fd(matrix(mfdobj2$coefs[, , jj],
nrow = dim(mfdobj2$coefs)[1],
ncol = dim(mfdobj2$coefs)[2]),
bs2)
out <- inprod_fd_diag_single(fdobj1_jj, fdobj2_jj)
}, numeric(dim(mfdobj1$coefs)[2]))
}
if (nobs1 == 1) inprods <- matrix(inprods, nrow = 1)
inprods
}
#' @noRd
#'
inprod_fd_diag_single <- function(fdobj1, fdobj2 = NULL) {
if (!fda::is.fd(fdobj1)) {
stop("Only fd class allowed for fdobj1 input")
}
nrep1 <- dim(fdobj1$coefs)[2]
coef1 <- fdobj1$coefs
basisobj1 <- fdobj1$basis
type1 <- basisobj1$type
range1 <- basisobj1$rangeval
if (!(fda::is.fd(fdobj2) | is.null(fdobj2))) {
stop("fdobj2 input must be of class fd, or NULL")
}
if (fda::is.fd(fdobj2)) {
type_calculated <- "inner_product"
nrep2 <- dim(fdobj2$coefs)[2]
if (nrep1 != nrep2) {
stop("fdobj1 and fdobj2 must have the same number of observations")
}
coef2 <- fdobj2$coefs
basisobj2 <- fdobj2$basis
type2 <- basisobj2$type
range2 <- basisobj2$rangeval
if (!all(range1 == range2)) {
stop("fdobj1 and fdobj2 must have the same domain")
}
}
if (is.null(fdobj2)) {
type_calculated <- "norm"
nrep2 <- nrep1
coef2 <- coef1
basisobj2 <- basisobj1
type2 <- type1
range2 <- range1
}
iter <- 0
rngvec <- range1
if ((all(c(coef1) == 0) || all(c(coef2) == 0))) {
return(numeric(nrep1))
}
JMAX <- 25
JMIN <- 5
EPS <- 1e-6
inprodmat <- numeric(nrep1)
nrng <- length(rngvec)
for (irng in 2:nrng) {
rngi <- c(rngvec[irng - 1], rngvec[irng])
if (irng > 2)
rngi[1] <- rngi[1] + 1e-10
if (irng < nrng)
rngi[2] <- rngi[2] - 1e-10
iter <- 1
width <- rngi[2] - rngi[1]
JMAXP <- JMAX + 1
h <- rep(1, JMAXP)
h[2] <- 0.25
s <- vector(mode = "list", length = JMAXP)
fx1 <- fda::eval.fd(rngi, fdobj1)
if (type_calculated == "inner_product") {
fx2 <- fda::eval.fd(rngi, fdobj2)
s[[1]] <- width * colSums(fx1 * fx2) / 2
}
if (type_calculated == "norm") {
s[[1]] <- width * colSums(fx1^2) / 2
}
tnm <- 0.5
for (iter in 2:JMAX) {
tnm <- tnm * 2
if (iter == 2) {
x <- mean(rngi)
} else {
del <- width/tnm
x <- seq(rngi[1] + del/2, rngi[2] - del/2, del)
}
fx1 <- fda::eval.fd(x, fdobj1)
if (type_calculated == "inner_product") {
fx2 <- fda::eval.fd(x, fdobj2)
chs <- width * colSums(fx1 * fx2) / tnm
}
if (type_calculated == "norm") {
chs <- width * colSums(fx1^2) / tnm
}
s[[iter]] <- (s[[iter - 1]] + chs) / 2
if (iter >= 5) {
ind <- (iter - 4):iter
ya <- s[ind]
xa <- h[ind]
absxa <- abs(xa)
absxamin <- min(absxa)
ns <- min((seq_along(absxa))[absxa == absxamin])
cs <- ya
ds <- ya
y <- ya[[ns]]
ns <- ns - 1
for (m in 1:4) {
for (i in 1:(5 - m)) {
ho <- xa[i]
hp <- xa[i + m]
w <- (cs[[i + 1]] - ds[[i]])/(ho - hp)
ds[[i]] <- hp * w
cs[[i]] <- ho * w
}
if (2 * ns < 5 - m) {
dy <- cs[[ns + 1]]
} else {
dy <- ds[[ns]]
ns <- ns - 1
}
y <- y + dy
}
ss <- y
errval <- max(abs(dy))
ssqval <- max(abs(ss))
if (all(ssqval > 0)) {
crit <- errval/ssqval
} else {
crit <- errval
}
if (crit < EPS && iter >= JMIN) break
}
s[[iter + 1]] <- s[[iter]]
h[iter + 1] <- 0.25 * h[iter]
if (iter == JMAX)
warning("Failure to converge.")
}
inprodmat <- inprodmat + ss
}
names(inprodmat) <- NULL
inprodmat
}
#' @noRd
#'
inprod_fd <- function (fdobj1,
fdobj2 = NULL,
Lfdobj1 = fda::int2Lfd(0),
Lfdobj2 = fda::int2Lfd(0),
rng = range1, wtfd = 0) {
result1 <- fdchk(fdobj1)
nrep1 <- result1[[1]]
fdobj1 <- result1[[2]]
coef1 <- fdobj1$coefs
basisobj1 <- fdobj1$basis
type1 <- basisobj1$type
range1 <- basisobj1$rangeval
if (is.null(fdobj2)) {
tempfd <- fdobj1
tempbasis <- tempfd$basis
temptype <- tempbasis$type
temprng <- tempbasis$rangeval
if (temptype == "bspline") {
basis2 <- fda::create.bspline.basis(temprng, 1, 1)
}
else {
if (temptype == "fourier")
basis2 <- fda::create.fourier.basis(temprng, 1)
else basis2 <- fda::create.constant.basis(temprng)
}
fdobj2 <- fda::fd(1, basis2)
}
result2 <- fdchk(fdobj2)
nrep2 <- result2[[1]]
fdobj2 <- result2[[2]]
coef2 <- fdobj2$coefs
basisobj2 <- fdobj2$basis
type2 <- basisobj2$type
range2 <- basisobj2$rangeval
if (rng[1] < range1[1] || rng[2] > range1[2])
stop("Limits of integration are inadmissible.")
if (fda::is.fd(fdobj1) && fda::is.fd(fdobj2) && type1 == "bspline" &&
type2 == "bspline" && is.eqbasis(basisobj1, basisobj2) &&
is.integer(Lfdobj1) && is.integer(Lfdobj2) &&
length(basisobj1$dropind) ==
0 && length(basisobj1$dropind) == 0 && wtfd == 0 &&
all(rng == range1)) {
inprodmat <- fda::inprod.bspline(fdobj1,
fdobj2,
Lfdobj1$nderiv,
Lfdobj2$nderiv)
return(inprodmat)
}
Lfdobj1 <- fda::int2Lfd(Lfdobj1)
Lfdobj2 <- fda::int2Lfd(Lfdobj2)
iter <- 0
rngvec <- rng
knotmult <- numeric(0)
if (type1 == "bspline")
knotmult <- knotmultchk(basisobj1, knotmult)
if (type2 == "bspline")
knotmult <- knotmultchk(basisobj2, knotmult)
if (length(knotmult) > 0) {
knotmult <- sort(unique(knotmult))
knotmult <- knotmult[knotmult > rng[1] && knotmult <
rng[2]]
rngvec <- c(rng[1], knotmult, rng[2])
}
if ((all(c(coef1) == 0) || all(c(coef2) == 0)))
return(matrix(0, nrep1, nrep2))
JMAX <- 25
JMIN <- 5
EPS <- 1e-06
inprodmat <- matrix(0, nrep1, nrep2)
nrng <- length(rngvec)
for (irng in 2:nrng) {
rngi <- c(rngvec[irng - 1], rngvec[irng])
if (irng > 2)
rngi[1] <- rngi[1] + 1e-10
if (irng < nrng)
rngi[2] <- rngi[2] - 1e-10
iter <- 1
width <- rngi[2] - rngi[1]
JMAXP <- JMAX + 1
h <- rep(1, JMAXP)
h[2] <- 0.25
s <- array(0, c(JMAXP, nrep1, nrep2))
sdim <- length(dim(s))
fx1 <- fda::eval.fd(rngi, fdobj1, Lfdobj1)
fx2 <- fda::eval.fd(rngi, fdobj2, Lfdobj2)
if (!is.numeric(wtfd)) {
wtd <- fda::eval.fd(rngi, wtfd, 0)
fx2 <- matrix(wtd, dim(wtd)[1], dim(fx2)[2]) * fx2
}
s[1, , ] <- width * matrix(crossprod(fx1, fx2), nrep1,
nrep2)/2
tnm <- 0.5
for (iter in 2:JMAX) {
tnm <- tnm * 2
if (iter == 2) {
x <- mean(rngi)
}
else {
del <- width/tnm
x <- seq(rngi[1] + del/2, rngi[2] - del/2, del)
}
fx1 <- fda::eval.fd(x, fdobj1, Lfdobj1)
fx2 <- fda::eval.fd(x, fdobj2, Lfdobj2)
if (!is.numeric(wtfd)) {
wtd <- fda::eval.fd(wtfd, x, 0)
fx2 <- matrix(wtd, dim(wtd)[1], dim(fx2)[2]) *
fx2
}
chs <- width * matrix(crossprod(fx1, fx2), nrep1,
nrep2)/tnm
s[iter, , ] <- (s[iter - 1, , ] + chs)/2
if (iter >= 5) {
ind <- (iter - 4):iter
ya <- s[ind, , ]
ya <- array(ya, c(5, nrep1, nrep2))
xa <- h[ind]
absxa <- abs(xa)
absxamin <- min(absxa)
ns <- min((seq_along(absxa))[absxa == absxamin])
cs <- ya
ds <- ya
y <- ya[ns, , ]
ns <- ns - 1
for (m in 1:4) {
for (i in 1:(5 - m)) {
ho <- xa[i]
hp <- xa[i + m]
w <- (cs[i + 1, , ] - ds[i, , ])/(ho - hp)
ds[i, , ] <- hp * w
cs[i, , ] <- ho * w
}
if (2 * ns < 5 - m) {
dy <- cs[ns + 1, , ]
}
else {
dy <- ds[ns, , ]
ns <- ns - 1
}
y <- y + dy
}
ss <- y
errval <- max(abs(dy))
ssqval <- max(abs(ss))
if (all(ssqval > 0)) {
crit <- errval/ssqval
}
else {
crit <- errval
}
if (crit < EPS && iter >= JMIN)
break
}
s[iter + 1, , ] <- s[iter, , ]
h[iter + 1] <- 0.25 * h[iter]
if (iter == JMAX)
warning("Failure to converge.")
}
inprodmat <- inprodmat + ss
}
if (length(dim(inprodmat) == 2)) {
return(as.matrix(inprodmat))
}
else {
return(inprodmat)
}
}
#' @noRd
#'
fdchk <- function (fdobj) {
if (inherits(fdobj, "fd")) {
coef <- fdobj$coefs
}
else {
if (inherits(fdobj, "basisfd")) {
coef <- diag(rep(1, fdobj$nbasis - length(fdobj$dropind)))
fdobj <- fda::fd(coef, fdobj)
}
else {
stop("FDOBJ is not an FD object.")
}
}
coefd <- dim(as.matrix(coef))
if (length(coefd) > 2)
stop("Functional data object must be univariate")
nrep <- coefd[2]
basisobj <- fdobj$basis
return(list(nrep, fdobj))
}
#' @noRd
#'
knotmultchk <- function (basisobj, knotmult) {
type <- basisobj$type
if (type == "bspline") {
params <- basisobj$params
nparams <- length(params)
norder <- basisobj$nbasis - nparams
if (norder == 1) {
knotmult <- c(knotmult, params)
}
else {
if (nparams > 1) {
for (i in 2:nparams) if (params[i] == params[i -
1])
knotmult <- c(knotmult, params[i])
}
}
}
return(knotmult)
}
#' @noRd
#'
is.eqbasis <- function (basisobj1, basisobj2) {
eqwrd <- TRUE
if (basisobj1$type != basisobj2$type) {
eqwrd <- FALSE
return(eqwrd)
}
if (any(basisobj1$rangeval != basisobj2$rangeval)) {
eqwrd <- FALSE
return(eqwrd)
}
if (basisobj1$nbasis != basisobj2$nbasis) {
eqwrd <- FALSE
return(eqwrd)
}
if (any(basisobj1$params != basisobj2$params)) {
eqwrd <- FALSE
return(eqwrd)
}
if (any(basisobj1$dropind != basisobj2$dropind)) {
eqwrd <- FALSE
return(eqwrd)
}
return(eqwrd)
}
#' Get Multivariate Functional Data from a data frame
#'
#' @param dt
#' A \code{data.frame} containing the discrete data.
#' For each functional variable, a single column,
#' whose name is provided in the argument \code{variables},
#' contains discrete values of that variable for all functional observation.
#' The column indicated by the argument \code{id}
#' denotes which is the functional observation in each row.
#' The column indicated by the argument \code{arg}
#' gives the argument value at which
#' the discrete values of the functional variables are observed for each row.
#' @param domain
#' A numeric vector of length 2 defining
#' the interval over which the functional data object
#' can be evaluated.
#' @param arg
#' A character variable, which is the name of
#' the column of the data frame \code{dt}
#' giving the argument values at which the functional variables
#' are evaluated for each row.
#' @param id
#' A character variable indicating
#' which is the functional observation in each row.
#' @param variables
#' A vector of characters of the column names
#' of the data frame \code{dt}
#' indicating the functional variables.
#' @param n_basis
#' An integer variable specifying the number of basis functions;
#' default value is 30.
#' See details on basis functions.
#' @param n_order
#' An integer specifying the order of b-splines,
#' which is one higher than their degree.
#' The default of 4 gives cubic splines.
#' @param basisobj
#' An object of class \code{basisfd} defining
#' the basis function expansion.
#' Default is \code{NULL}, which means that
#' a \code{basisfd} object is created by doing
#' \code{create.bspline.basis(rangeval = domain,
#' nbasis = n_basis, norder = n_order)}
#' @param Lfdobj
#' An object of class \code{Lfd} defining a
#' linear differential operator of order m.
#' It is used to specify a roughness penalty through \code{fdPar}.
#' Alternatively, a nonnegative integer
#' specifying the order m can be given and is
#' passed as \code{Lfdobj} argument to the function \code{fdPar},
#' which indicates that the derivative of order m is penalized.
#' Default value is 2, which means that the
#' integrated squared second derivative is penalized.
#' @param lambda
#' A non-negative real number.
#' If you want to use a single specified smoothing parameter
#' for all functional data objects in the dataset,
#' this argument is passed to the function \code{fda::fdPar}.
#' Default value is NULL, in this case the smoothing parameter is chosen
#' by minimizing the generalized cross-validation (GCV)
#' criterion over the grid of values given by the argument.
#' See details on how smoothing parameters work.
#' @param lambda_grid
#' A vector of non-negative real numbers.
#' If \code{lambda} is provided as a single number, this argument is ignored.
#' If \code{lambda} is NULL, then this provides the grid of values
#' over which the optimal smoothing parameter is
#' searched. Default value is \code{10^seq(-10,1,l=20)}.
#' @param ncores
#' If you want parallelization, give the number of cores/threads
#' to be used when doing GCV separately on all observations.
#'
#' @details
#' Basis functions are created with
#' \code{fda::create.bspline.basis(domain, n_basis)}, i.e.
#' B-spline basis functions of order 4 with equally spaced knots
#' are used to create \code{mfd} objects.
#'
#' The smoothing penalty lambda is provided as
#' \code{fda::fdPar(bs, 2, lambda)},
#' where bs is the basis object and 2 indicates
#' that the integrated squared second derivative is penalized.
#'
#' Rather than having a data frame with long format,
#' i.e. with all functional observations in a single column
#' for each functional variable,
#' if all functional observations are observed on a common equally spaced grid,
#' discrete data may be available in matrix form for each functional variable.
#' In this case, see \code{get_mfd_list}.
#'
#' @seealso \code{\link{get_mfd_list}}
#'
#' @return An object of class \code{mfd}.
#' See also \code{?mfd} for additional details on the
#' multivariate functional data class.
#' @export
#' @examples
#' library(funcharts)
#'
#' x <- seq(1, 10, length = 25)
#' y11 <- cos(x)
#' y21 <- cos(2 * x)
#' y12 <- sin(x)
#' y22 <- sin(2 * x)
#' df <- data.frame(id = factor(rep(1:2, each = length(x))),
#' x = rep(x, times = 2),
#' y1 = c(y11, y21),
#' y2 = c(y12, y22))
#'
#' mfdobj <- get_mfd_df(dt = df,
#' domain = c(1, 10),
#' arg = "x",
#' id = "id",
#' variables = c("y1", "y2"),
#' lambda = 1e-5)
#'
get_mfd_df <- function(dt,
domain,
arg,
id,
variables,
n_basis = 30,
n_order = 4,
basisobj = NULL,
Lfdobj = 2,
lambda = NULL,
lambda_grid = 10^seq(-10, 1, length.out = 10),
ncores = 1) {
if (!(is.data.frame(dt))) {
stop("dt must be a data.frame")
}
if (!(arg %in% names(dt))) {
stop("domain_var must be the name of a column of dt")
}
if (!(id %in% names(dt))) {
stop("id must be the name of a column of dt")
}
if (sum(variables %in% names(dt)) < length(variables)) {
stop("variables must contain only names of columns of dt")
}
if (!is.numeric(domain) | length(domain) != 2) {
stop("domain must be a vector with two numbers.")
}
if (!is.null(basisobj) & !fda::is.basis(basisobj)) {
stop("basisobj must be NULL or a basisfd object")
}
if (!is.null(basisobj) & !all(basisobj$rangeval == domain)) {
stop("if basisobj is provided, basisobj$rangeval must be equal to domain")
}
if (!is.null(Lfdobj) & !fda::is.Lfd(Lfdobj)) {
if (is.numeric(Lfdobj)) {
if (Lfdobj != abs(round(Lfdobj))) {
stop("Lfdobj must be a positive integer or a Lfd object")
}
} else {
stop("Lfdobj must be a positive integer or a Lfd object")
}
}
if (fda::is.basis(basisobj)) {
message("basisobj is provided, n_basis and n_order are ignored")
n_basis <- basisobj$nbasis
}
if (is.null(basisobj)) {
basisobj <- fda::create.bspline.basis(rangeval = domain,
nbasis = n_basis,
norder = n_order)
}
ids <- levels(factor(dt[[id]]))
n_obs <- length(ids)
n_var <- length(variables)
lambda_search <- if (!is.null(lambda)) lambda else lambda_grid
n_lam <- length(lambda_search)
ncores <- min(ncores, n_obs)
fun_gcv <- function(ii) {
rows_i <- dt[[id]] == ids[ii] &
dt[[arg]] >= domain[1] &
dt[[arg]] <= domain[2]
dt_i <- dt[rows_i, , drop = FALSE]
x <- dt_i[[arg]]
y <- data.matrix(dt_i[, variables])
# Generalized cross validation to choose smoothing parameter
coefList <- list()
gcv <- matrix(data=NA,n_var,n_lam)
rownames(gcv) <- variables
colnames(gcv) <- lambda_search
for (h in seq_len(n_lam)) {
fdpenalty <- fda::fdPar(basisobj, Lfdobj, lambda_search[h])
smoothObj <- fda::smooth.basis(x, y, fdpenalty)
coefList[[h]] <- smoothObj$fd$coefs
gcv[, h] <- smoothObj$gcv
}
# If only NA in a row,
# consider as optimal smoothing parameter the first one in the sequence.
gcvmin <- apply(gcv, 1, function(x) {
if(sum(is.na(x)) < n_lam) which.min(x) else 1
})
opt_lam <- lambda_search[gcvmin]
names(opt_lam) <- variables
coefs <- vapply(seq_len(n_var),
function(jj) coefList[[gcvmin[jj]]][, jj],
numeric(n_basis))
if (basisobj$nbasis == 1) coefs <- matrix(as.numeric(coefs), nrow = 1)
colnames(coefs) <- variables
coefs
}
# You need to perform gcv separately for each observation
if (ncores == 1) {
coefs_list <- lapply(seq_len(n_obs), fun_gcv)
} else {
if (.Platform$OS.type == "unix") {
coefs_list <- parallel::mclapply(seq_len(n_obs), fun_gcv, mc.cores = ncores)
} else {
cl <- parallel::makeCluster(ncores)
parallel::clusterExport(cl,
c("dt",
"ids",
"domain",
"variables",
"n_var",
"n_lam",
"lambda_search"),
envir = environment())
coefs_list <- parallel::parLapply(cl, seq_len(n_obs), fun_gcv)
parallel::stopCluster(cl)
}
}
names(coefs_list) <- ids
coefs <- array(do.call(rbind, coefs_list),
dim = c(basisobj$nbasis, n_obs, n_var),
dimnames = list(basisobj$names, ids, variables))
fdObj <- mfd(coefs, basisobj, list(arg, ids, variables),
dt[, c(id, arg, variables)], id)
fdObj
}
#' Get Multivariate Functional Data from a list of matrices
#'
#' @param data_list
#' A named list of matrices.
#' Names of the elements in the list denote the functional variable names.
#' Each matrix in the list corresponds to a functional variable.
#' All matrices must have the same dimension, where
#' the number of rows corresponds to replications, while
#' the number of columns corresponds to the argument values at which
#' functions are evaluated.
#' @param grid
#' A numeric vector, containing the argument values at which
#' functions are evaluated.
#' Its length must be equal to the number of columns
#' in each matrix in data_list.
#' Default is NULL, in this case a vector equally spaced numbers
#' between 0 and 1 is created,
#' with as many numbers as the number of columns in each matrix in data_list.
#' @param n_basis
#' An integer variable specifying the number of basis functions;
#' default value is 30.
#' See details on basis functions.
#' @param n_order
#' An integer specifying the order of B-splines,
#' which is one higher than their degree.
#' The default of 4 gives cubic splines.
#' @param basisobj
#' An object of class \code{basisfd} defining the
#' B-spline basis function expansion.
#' Default is \code{NULL}, which means that
#' a \code{basisfd} object is created by doing
#' \code{create.bspline.basis(rangeval = domain,
#' nbasis = n_basis, norder = n_order)}
#' @param Lfdobj
#' An object of class \code{Lfd} defining a linear
#' differential operator of order m.
#' It is used to specify a roughness penalty through \code{fdPar}.
#' Alternatively, a nonnegative integer specifying
#' the order m can be given and is
#' passed as \code{Lfdobj} argument to the function \code{fdPar},
#' which indicates that the derivative of order m is penalized.
#' Default value is 2, which means that the integrated
#' squared second derivative is penalized.
#' @param lambda
#' A non-negative real number.
#' If you want to use a single specified smoothing parameter
#' for all functional data objects in the dataset,
#' this argument is passed to the function \code{fda::fdPar}.
#' Default value is NULL, in this case the smoothing parameter is chosen
#' by minimizing the generalized cross-validation (GCV) criterion
#' over the grid of values given by the argument.
#' See details on how smoothing parameters work.
#' @param lambda_grid
#' A vector of non-negative real numbers.
#' If \code{lambda} is provided as a single number, this argument is ignored.
#' If \code{lambda} is NULL, then this provides
#' the grid of values over which the optimal smoothing parameter is
#' searched. Default value is \code{10^seq(-10,1,l=20)}.
#' @param ncores
#' Deprecated.
#'
#'
#' @details
#' Basis functions are created with
#' \code{fda::create.bspline.basis(domain, n_basis)}, i.e.
#' B-spline basis functions of order 4 with equally spaced knots
#' are used to create \code{mfd} objects.
#'
#' The smoothing penalty lambda is provided as
#' \code{fda::fdPar(bs, 2, lambda)},
#' where bs is the basis object and 2 indicates that
#' the integrated squared second derivative is penalized.
#'
#' Rather than having a list of matrices,
#' you may have a data frame with long format,
#' i.e. with all functional observations in a single column
#' for each functional variable.
#' In this case, see \code{get_mfd_df}.
#'
#' @return
#' An object of class \code{mfd}.
#' See also \code{\link{mfd}} for additional details
#' on the multivariate functional data class.
#'
#' @seealso \code{\link{mfd}},
#' \code{\link{get_mfd_list}},
#' \code{\link{get_mfd_array}}
#'
#' @export
#'
#' @examples
#' library(funcharts)
#' data("air")
#' # Only take first 5 multivariate functional observations
#' # and only two variables from air
#' air_small <- lapply(air[c("NO2", "CO")], function(x) x[1:5, ])
#' mfdobj <- get_mfd_list(data_list = air_small)
#'
get_mfd_list <- function(data_list,
grid = NULL,
n_basis = 30,
n_order = 4,
basisobj = NULL,
Lfdobj = 2,
lambda = NULL,
lambda_grid = 10^seq(-10, 1, length.out = 10),
ncores = 1) {
if (!missing(ncores)) {
warning("argument ncores is deprecated.", call. = FALSE)
}
if (!(is.list(data_list))) {
stop("data_list must be a list of matrices")
}
if (is.null(names(data_list))) {
stop("data_list must be a named list")
}
if (length(unique(lapply(data_list, dim))) > 1) {
stop("data_list must be a list of matrices all of the same dimensions")
}
n_args <- ncol(data_list[[1]])
if (!is.null(grid) & (length(grid) != n_args)) {
stop(paste0(
"grid length, ", length(grid),
" has not the same length as number of ",
"observed data per functional observation, ",
ncol(data_list[[1]])))
}
data_array <- simplify2array(data_list)
get_mfd_array(data_array = aperm(data_array, c(2, 1, 3)),
grid = grid,
n_basis = n_basis,
n_order = n_order,
basisobj = basisobj,
Lfdobj = Lfdobj,
lambda = lambda,
lambda_grid = lambda_grid)
}
#' Get Multivariate Functional Data from a three-dimensional array
#'
#'
#' @param data_array
#' A three-dimensional array.
#' The first dimension corresponds to argument values,
#' the second to replications,
#' and the third to variables within replications.
#' @param grid
#' See \code{\link{get_mfd_list}}.
#' @param n_basis
#' See \code{\link{get_mfd_list}}.
#' @param n_order
#' #' See \code{\link{get_mfd_list}}.
#' @param basisobj
#' #' See \code{\link{get_mfd_list}}.
#' @param Lfdobj
#' #' See \code{\link{get_mfd_list}}.
#' @param lambda
#' See \code{\link{get_mfd_list}}.
#' @param lambda_grid
#' See \code{\link{get_mfd_list}}.
#' @param ncores
#' Deprecated. See \code{\link{get_mfd_list}}.
#'
#' @return
#' An object of class \code{mfd}.
#' See also \code{?mfd} for additional details on the
#' multivariate functional data class.
#' @export
#' @seealso
#' \code{\link{get_mfd_list}}, \code{\link{get_mfd_df}}
#'
#' @examples
#' library(funcharts)
#' library(fda)
#' data("CanadianWeather")
#' mfdobj <- get_mfd_array(CanadianWeather$dailyAv[, 1:10, ],
#' lambda = 1e-5)
#' plot_mfd(mfdobj)
#'
get_mfd_array <- function(data_array,
grid = NULL,
n_basis = 30,
n_order = 4,
basisobj = NULL,
Lfdobj = 2,
lambda = NULL,
lambda_grid = 10^seq(- 10, 1, length.out = 10),
ncores = 1) {
name <- value <- id <- NULL
if (!missing(ncores)) {
warning("argument ncores is deprecated.", call. = FALSE)
}
n_var <- dim(data_array)[3]
n_args <- dim(data_array)[1]
if (!is.null(grid) & (length(grid) != n_args)) {
stop(paste0(
"grid length, ", length(grid),
" has not the same length as number of ",
"observed data per functional observation, ",
dim(data_array)[1]))
}
if (is.null(grid)) grid <- seq(0, 1, l = n_args)
domain <- range(grid)
if (!is.null(basisobj) & !fda::is.basis(basisobj)) {
stop("basisobj must be NULL or a basisfd object")
}
if (!is.null(basisobj) & !all(basisobj$rangeval == domain)) {
stop("if basisobj is provided, basisobj$rangeval must be equal to domain")
}
if (!is.null(Lfdobj) & !fda::is.Lfd(Lfdobj)) {
if (is.numeric(Lfdobj)) {
if (Lfdobj != abs(round(Lfdobj))) {
stop("Lfdobj must be a positive integer or a Lfd object")
}
} else {
stop("Lfdobj must be a positive integer or a Lfd object")
}
}
if (fda::is.basis(basisobj)) {
if (!(basisobj$type %in% c("bspline", "fourier", "const"))) {
stop("basisobj supported types are only bspline and fourier and constant")
}
message("basisobj is provided, n_basis and n_order are ignored")
n_basis <- basisobj$nbasis
}
if (is.null(basisobj)) {
basisobj <- fda::create.bspline.basis(rangeval = domain,
nbasis = n_basis,
norder = n_order)
}
variables <- dimnames(data_array)[[3]]
ids <- dimnames(data_array)[[2]]
if (is.null(ids)) ids <- as.character(seq_len(dim(data_array)[[2]]))
n_obs <- length(ids)
lambda_search <- if (!is.null(lambda)) lambda else lambda_grid
n_lam <- length(lambda_search)
coefList <- list()
gcv <- array(data = NA, dim = c(n_obs, n_var, n_lam))
dimnames(gcv)[[1]] <- ids
dimnames(gcv)[[2]] <- variables
dimnames(gcv)[[3]] <- lambda_search
for (h in seq_len(n_lam)) {
fdpenalty <- fda::fdPar(basisobj, Lfdobj, lambda_search[h])
smoothObj <- fda::smooth.basis(grid, data_array, fdpenalty)
cc <- smoothObj$fd$coefs
if (n_var == 1) {
cc <- array(cc, dim = c(dim(cc)[1], dim(cc)[2], 1))
}
coefList[[h]] <- cc
gcv[, , h] <- smoothObj$gcv
}
# If only NA in a row,
# consider as optimal smoothing parameter the first one in the sequence.
gcvmin <- apply(gcv, 1:2, function(x) {
if(sum(is.na(x)) < n_lam) which.min(x) else 1
})
coef <- array(NA, dim = c(n_basis, n_obs, n_var))
dimnames(coef)[[1]] <- as.character(basisobj$names)
dimnames(coef)[[2]] <- as.character(ids)
dimnames(coef)[[3]] <- as.character(variables)
for (ii in seq_len(n_obs)) {
for (jj in seq_len(n_var)) {
coef[, ii, jj] <- coefList[[gcvmin[ii, jj]]][, ii, jj]
}
}
df_raw <- dplyr::bind_cols(
data.frame(id = rep(ids, each = n_args)),
data.frame(t = rep(grid, n_obs)),
lapply(seq_len(dim(data_array)[3]), function(ii) {
data_array[, , ii] %>%
as.data.frame() %>%
stats::setNames(ids) %>%
tidyr::pivot_longer(dplyr::everything()) %>%
dplyr::mutate(name = factor(name, levels = ids)) %>%
dplyr::arrange(name) %>%
dplyr::select(value) %>%
stats::setNames(variables[ii])
}) %>%
dplyr::bind_cols()
) %>%
dplyr::mutate(id = factor(id, levels = ids))
fdObj <- mfd(coef = coef,
basisobj = basisobj,
fdnames = list("t",
as.character(ids),
as.character(variables)),
raw = df_raw,
id_var = "id")
fdObj
}
#' Convert a \code{fd} object into a Multivariate Functional Data object.
#'
#'
#' @param fdobj
#' An object of class fd.
#'
#' @return
#' An object of class \code{mfd}.
#' See also \code{?mfd} for additional details on the
#' multivariate functional data class.
#' @export
#' @seealso
#' \code{mfd}
#'
#' @examples
#' library(funcharts)
#' library(fda)
#' bs <- create.bspline.basis(nbasis = 10)
#' fdobj <- fd(coef = 1:10, basisobj = bs)
#' mfdobj <- get_mfd_fd(fdobj)
get_mfd_fd <- function(fdobj) {
if (length(fdobj$fdnames[[1]]) > 1) fdobj$fdnames[[1]] <- "time"
if (!fda::is.fd(fdobj)) {
stop("fdobj must be an object of class fd.")
}
coefs <- fdobj$coefs
if (length(dim(coefs)) == 2) {
if (!(identical(colnames(coefs), fdobj$fdnames[[2]]) |
identical(colnames(coefs), fdobj$fdnames[[3]]))) {
stop(paste0("colnames(fdobj$coefs) must correspond either to ",
"fdobj$fdnames[[2]] (i.e. replication names) or to ",
"fdobj$fdnames[[3]] (i.e. variable names)."))
}
if (identical(colnames(coefs), fdobj$fdnames[[2]])) {
coefs <- array(coefs, dim = c(nrow(coefs), ncol(coefs), 1))
dimnames(coefs) <- list()
dimnames(coefs)[[1]] <- dimnames(fdobj$coefs)[[1]]
dimnames(coefs)[[2]] <- fdobj$fdnames[[2]]
dimnames(coefs)[[3]] <- fdobj$fdnames[[3]]
}
if (identical(colnames(coefs), fdobj$fdnames[[3]])) {
coefs <- array(coefs, dim = c(nrow(coefs), 1, ncol(coefs)))
dimnames(coefs) <- list()
dimnames(coefs)[[1]] <- dimnames(fdobj$coefs)[[1]]
dimnames(coefs)[[2]] <- fdobj$fdnames[[2]]
dimnames(coefs)[[3]] <- fdobj$fdnames[[3]]
}
}
bs <- fdobj$basis
mfd(coefs,
fdobj$basis,
fdobj$fdnames)
}
#' Standardize Multivariate Functional Data.
#'
#' Scale multivariate functional data contained
#' in an object of class \code{mfd}
#' by subtracting the mean function and dividing
#' by the standard deviation function.
#'
#' @param mfdobj
#' A multivariate functional data object of class \code{mfd}.
#' @param center
#' A logical value, or a \code{fd} object.
#' When providing a logical value, if TRUE, \code{mfdobj} is centered,
#' i.e. the functional mean function is calculated and subtracted
#' from all observations in \code{mfdobj},
#' if FALSE, \code{mfdobj} is not centered.
#' If \code{center} is a \code{fd} object, then this function
#' is used as functional mean for centering.
#' @param scale
#' A logical value, or a \code{fd} object.
#' When providing a logical value, if TRUE, \code{mfdobj}
#' is scaled after possible centering,
#' i.e. the functional standard deviation is calculated
#' from all functional observations in \code{mfdobj} and
#' then the observations are divided by this calculated standard deviation,
#' if FALSE, \code{mfdobj} is not scaled.
#' If \code{scale} is a \code{fd} object,
#' then this function is used as standard deviation function for scaling.
#'
#' @return
#' A standardized object of class \code{mfd}, with two attributes,
#' if calculated,
#' \code{center} and \code{scale}, storing the mean and
#' standard deviation functions used for standardization.
#'
#' @details
#' This function has been written to work similarly
#' as the function \code{\link{scale}} for matrices.
#' When calculated, attributes \code{center} and \code{scale}
#' are of class \code{fd}
#' and have the same structure you get
#' when you use \code{fda::\link[fda]{mean.fd}}
#' and \code{fda::\link[fda]{sd.fd}}.
#' @export
#' @examples
#' library(funcharts)
#' mfdobj <- data_sim_mfd()
#' mfdobj_scaled <- scale_mfd(mfdobj)
scale_mfd <- function(mfdobj, center = TRUE, scale = TRUE) {
if (!is.mfd(mfdobj)) {
stop("Only mfd class allowed for mfdobj input")
}
bs <- mfdobj$basis
n_obs <- length(mfdobj$fdnames[[2]])
if (n_obs == 1 & (!fda::is.fd(scale) | !fda::is.fd(center))) {
stop("There is only one observation in the data set")
}
# Center
if (!(is.logical(center) | fda::is.fd(center))) {
stop("Only logical or fd classes allowed for center input")
}
mean_fd <- NULL
if (is.logical(center) && center == FALSE) {
cen_fd <- mfdobj
} else {
if (is.logical(center) && center == TRUE) {
mean_fd <- fda::mean.fd(mfdobj)
}
if (fda::is.fd(center)) {
mean_fd <- center
}
mean_fd_coefs <- array(mean_fd$coefs[, 1, ],
dim = c(dim(mean_fd$coefs)[c(1, 3)], n_obs))
mean_fd_coefs <- aperm(mean_fd_coefs, c(1, 3, 2))
mean_fd_rep <- fda::fd(mean_fd_coefs, bs, mfdobj$fdnames)
cen_fd <- fda::minus.fd(mfdobj, mean_fd_rep)
}
# Scale
if (!(is.logical(scale) | fda::is.fd(scale))) {
stop("Only logical or fd classes allowed for scale input")
}
sd_fd <- NULL
if (is.logical(scale) && scale == FALSE) {
fd_std <- cen_fd
} else {
if (is.logical(scale) && scale == TRUE) {
sd_fd <- fda::sd.fd(mfdobj)
}
if (fda::is.fd(scale)) {
sd_fd <- scale
}
if (sd_fd$basis$nbasis < 15 | sd_fd$basis$type != "bspline") {
domain <- mfdobj$basis$rangeval
x_eval <- seq(domain[1], domain[2], length.out = 1000)
sd_eval <- fda::eval.fd(evalarg = x_eval, sd_fd)
bs_sd <- fda::create.bspline.basis(rangeval = domain, nbasis = 20)
fdpar_more_basis <- fda::fdPar(bs_sd, 2, 0)
sd_fd_more_basis <- fda::smooth.basis(x_eval, sd_eval,
fdpar_more_basis)$fd
sd_inv <- sd_fd_more_basis^(-1)
sd_inv_eval <- fda::eval.fd(evalarg = x_eval, sd_inv)
sd_inv <- fda::smooth.basis(x_eval, sd_inv_eval, fda::fdPar(bs, 2, 0))$fd
} else {
sd_inv <- sd_fd^(-1)
}
sd_inv_coefs <- array(sd_inv$coefs, dim = c(dim(sd_inv$coefs), n_obs))
sd_inv_coefs <- aperm(sd_inv_coefs, c(1, 3, 2))
sd_inv_rep <- fda::fd(sd_inv_coefs, bs, mfdobj$fdnames)
fd_std <- fda::times.fd(cen_fd, sd_inv_rep, bs)
fd_std$fdnames <- mfdobj$fdnames
dimnames(fd_std$coefs) <- dimnames(mfdobj$coefs)
dimnames(fd_std$fdnames) <- NULL
}
attr(fd_std, "scaled:center") <- mean_fd
attr(fd_std, "scaled:scale") <- sd_fd
fd_std$raw <- NULL
fd_std$id_var <- mfdobj$id_var
class(fd_std) <- c("mfd", "fd")
fd_std
}
#' De-standardize a standardized Multivariate Functional Data object
#'
#' This function takes a scaled
#' multivariate functional data contained in an object of class \code{mfd},
#' multiplies it by function provided by the argument \code{scale}
#' and adds the function provided by the argument \code{center}.
#'
#' @param scaled_mfd
#' A scaled multivariate functional data object,
#' of class \code{mfd}.
#' @param center
#' A functional data object of class \code{fd},
#' having the same structure you get
#' when you use \code{fda::\link[fda]{mean.fd}}
#' over a \code{mfd} object.
#' @param scale
#' A functional data object of class \code{fd},
#' having the same structure you get
#' when you use \code{fda::\link[fda]{sd.fd}}
#' over a \code{mfd} object.
#'
#' @return
#' A de-standardized object of class \code{mfd},
#' obtained by multiplying \code{scaled_mfd} by \code{scale} and then
#' adding \code{center}.
#' @noRd
#'
descale_mfd <- function (scaled_mfd, center = FALSE, scale = FALSE) {
if (!is.mfd(scaled_mfd)) stop("scaled_mfd must be from class mfd")
basis <- scaled_mfd$basis
nbasis <- basis$nbasis
nobs <- length(scaled_mfd$fdnames[[2]])
nvar <- length(scaled_mfd$fdnames[[3]])
if (fda::is.fd(scale)) {
coef_sd_list <- lapply(seq_len(nvar), function(jj) {
matrix(scale$coefs[, jj], nrow = nbasis, ncol = nobs)
})
coef_sd <- simplify2array(coef_sd_list)
sd_fd <- fda::fd(coef_sd, scaled_mfd$basis)
centered <- fda::times.fd(scaled_mfd, sd_fd, basisobj = basis)
} else {
if (is.logical(scale) & !scale & length(scale) == 1) {
centered <- scaled_mfd
} else {
stop("scale must be either an mfd object or FALSE")
}
}
if (fda::is.fd(center)) {
descaled_mean_list <- lapply(seq_len(nvar), function(jj) {
out <- centered$coefs[, , jj, drop = FALSE] + as.numeric(center$coefs[, 1, jj])
matrix(out, dim(out)[1], dim(out)[2])
})
descaled_coef <- simplify2array(descaled_mean_list)
} else {
if (is.logical(center) & !center & length(center) == 1) {
descaled_coef <- centered$coef
} else {
stop("center must be either an mfd object or FALSE")
}
}
dimnames(descaled_coef) <- dimnames(scaled_mfd$coefs)
mfd(descaled_coef,
scaled_mfd$basis,
scaled_mfd$fdnames,
B = scaled_mfd$basis$B)
}
#' Tensor product of two Multivariate Functional Data objects
#'
#' This function returns the tensor product of two
#' Multivariate Functional Data objects.
#' Each object must contain only one replication.
#'
#' @param mfdobj1
#' A multivariate functional data object, of class \code{mfd},
#' having only one functional observation.
#' @param mfdobj2
#' A multivariate functional data object, of class \code{mfd},
#' having only one functional observation.
#' If NULL, it is set equal to \code{mfdobj1}. Default is NULL.
#'
#' @return
#' An object of class \code{bifd}.
#' If we denote with x(s)=(x_1(s),\dots,x_p(s))
#' the vector of p functions represented by \code{mfdobj1} and
#' with y(t)=(y_1(t),\dots,y_q(t)) the vector of q functions
#' represented by \code{mfdobj2},
#' the output is the
#' vector of pq bivariate functions
#'
#' f(s,t)=(x_1(s)y_1(t),\dots,x_1(s)y_q(t),
#' \dots,x_p(s)y_1(t),\dots,x_p(s)y_q(t)).
#'
#'
#' @export
#' @examples
#' library(funcharts)
#' mfdobj1 <- data_sim_mfd(nobs = 1, nvar = 3)
#' mfdobj2 <- data_sim_mfd(nobs = 1, nvar = 2)
#' tensor_product_mfd(mfdobj1)
#' tensor_product_mfd(mfdobj1, mfdobj2)
#'
tensor_product_mfd <- function(mfdobj1, mfdobj2 = NULL) {
if (!is.mfd(mfdobj1)) {
stop("First argument must be a mfd object.")
}
if (is.null(mfdobj2)) mfdobj2 <- mfdobj1
if (!is.mfd(mfdobj2)) {
stop("First argument must be a mfd object.")
}
obs1 <- mfdobj1$fdnames[[2]]
obs2 <- mfdobj2$fdnames[[2]]
nobs1 <- length(obs1)
nobs2 <- length(obs2)
if (nobs1 > 1) {
stop("mfdobj1 must have only 1 observation")
}
if (nobs2 > 1) {
stop("mfdobj2 must have only 1 observation")
}
variables1 <- mfdobj1$fdnames[[3]]
variables2 <- mfdobj2$fdnames[[3]]
nvar1 <- length(variables1)
nvar2 <- length(variables2)
coef1 <- mfdobj1$coefs
coef2 <- mfdobj2$coefs
coef1 <- matrix(coef1, nrow = dim(coef1)[1], ncol = dim(coef1)[3])
coef2 <- matrix(coef2, nrow = dim(coef2)[1], ncol = dim(coef2)[3])
basis1 <- mfdobj1$basis
basis2 <- mfdobj2$basis
nbasis1 <- basis1$nbasis
nbasis2 <- basis2$nbasis
coef <- outer(coef1, coef2)
coef <- aperm(coef, c(1, 3, 4, 2))
coef <- array(coef, dim = c(nbasis1, nbasis2, 1, nvar1 * nvar2))
dim_names <- expand.grid(variables1, variables2)
variables <- paste(dim_names[, 1], dim_names[, 2])
obs <- paste0("s.", obs1, " t.", obs2)
fdnames <- list(basis1$names,
basis2$names,
obs,
variables)
dimnames(coef) <- fdnames
fda::bifd(coef, basis1, basis2, fdnames)
}
#' Bind variables of two Multivariate Functional Data Objects
#'
#' @param mfdobj1
#' An object of class mfd, with the same number of replications of mfdobj2
#' and different variable names with respect to mfdobj2.
#' @param mfdobj2
#' An object of class mfd, with the same number of replications of mfdobj1,
#' and different variable names with respect to mfdobj1.
#'
#' @return
#' An object of class mfd, whose replications are the same of mfdobj1 and
#' mfdobj2 and whose functional variables are the union of the functional
#' variables in mfdobj1 and mfdobj2.
#' @export
#'
#' @examples
#' library(funcharts)
#' mfdobj1 <- data_sim_mfd(nvar = 3)
#' mfdobj2 <- data_sim_mfd(nvar = 2)
#' dimnames(mfdobj2$coefs)[[3]] <- mfdobj2$fdnames[[3]] <- c("var10", "var11")
#'
#' plot_mfd(mfdobj1)
#' plot_mfd(mfdobj2)
#' mfdobj_cbind <- cbind_mfd(mfdobj1, mfdobj2)
#' plot_mfd(mfdobj_cbind)
#'
cbind_mfd <- function(mfdobj1, mfdobj2) {
if (!is.null(mfdobj1) & !is.mfd(mfdobj1)) {
stop("mfdobj1 must be an object of class mfd if not NULL")
}
if (!is.null(mfdobj2) & !is.mfd(mfdobj2)) {
stop("mfdobj2 must be an object of class mfd if not NULL")
}
if (is.null(mfdobj2)) {
return(mfdobj1)
}
if (is.null(mfdobj1)) {
return(mfdobj2)
}
if (dim(mfdobj1$coefs)[2] != dim(mfdobj2$coefs)[2]) {
stop("mfdobj1 and mfdobj2 must have the same number of replications.")
}
if (!identical(mfdobj1$basis, mfdobj2$basis)) {
stop("mfdobj1 and mfdobj2 must have the same basis.")
}
mfd(coef = array(c(mfdobj1$coefs,
mfdobj2$coefs),
dim = c(dim(mfdobj1$coefs)[1],
dim(mfdobj1$coefs)[2],
dim(mfdobj1$coefs)[3] + dim(mfdobj2$coefs)[3])),
basisobj = mfdobj1$basis,
fdnames = list(mfdobj1$fdnames[[1]],
mfdobj1$fdnames[[2]],
c(mfdobj1$fdnames[[3]],
mfdobj2$fdnames[[3]])),
B = mfdobj1$basis$B)
}
#' Bind replications of two Multivariate Functional Data Objects
#'
#' @param mfdobj1
#' An object of class mfd, with the same variables of mfdobj2
#' and different replication names with respect to mfdobj2.
#' @param mfdobj2
#' An object of class mfd, with the same variables of mfdobj1,
#' and different replication names with respect to mfdobj1.
#'
#' @return
#' An object of class mfd, whose variables are the same of mfdobj1 and
#' mfdobj2 and whose replications are the union of the replications
#' in mfdobj1 and mfdobj2.
#' @export
#'
#' @examples
#' library(funcharts)
#' mfdobj1 <- data_sim_mfd(nvar = 3, nobs = 4)
#' mfdobj2 <- data_sim_mfd(nvar = 3, nobs = 5)
#' dimnames(mfdobj2$coefs)[[2]] <-
#' mfdobj2$fdnames[[2]] <-
#' c("rep11", "rep12", "rep13", "rep14", "rep15")
#' mfdobj_rbind <- rbind_mfd(mfdobj1, mfdobj2)
#' plot_mfd(mfdobj_rbind)
#'
rbind_mfd <- function(mfdobj1, mfdobj2) {
if (!is.null(mfdobj1) & !is.mfd(mfdobj1)) {
stop("mfdobj1 must be an object of class mfd if not NULL")
}
if (!is.null(mfdobj2) & !is.mfd(mfdobj2)) {
stop("mfdobj2 must be an object of class mfd if not NULL")
}
if (is.null(mfdobj2)) {
return(mfdobj1)
}
if (is.null(mfdobj1)) {
return(mfdobj2)
}
if (dim(mfdobj1$coefs)[3] != dim(mfdobj2$coefs)[3]) {
stop("mfdobj1 and mfdobj2 must have the same number of variables")
}
if (!identical(mfdobj1$basis, mfdobj2$basis)) {
stop("mfdobj1 and mfdobj2 must have the same basis.")
}
coef1_aperm <- aperm(mfdobj1$coefs, c(1, 3, 2))
coef2_aperm <- aperm(mfdobj2$coefs, c(1, 3, 2))
coef_aperm <- array(c(coef1_aperm, coef2_aperm),
dim = c(dim(mfdobj1$coefs)[1],
dim(mfdobj1$coefs)[3],
dim(mfdobj1$coefs)[2] + dim(mfdobj2$coefs)[2]))
coef <- aperm(coef_aperm, c(1, 3, 2))
mfd(coef = coef,
basisobj = mfdobj1$basis,
fdnames = list(mfdobj1$fdnames[[1]],
c(mfdobj1$fdnames[[2]],
mfdobj2$fdnames[[2]]),
mfdobj2$fdnames[[3]]),
B = mfdobj1$basis$B)
}
# Plots -------------------------------------------------------------------
#' Convert a Multivariate Functional Data Object into a data frame
#'
#' This function extracts the argument \code{raw} of an
#' object of class \code{mfd}.
#'
#' @param mfdobj A multivariate functional data object of class mfd.
#'
#' @return
#' A \code{data.frame} in the long format,
#' which has been used to create the object \code{mfdobj}
#' using the function \code{\link{get_mfd_df}},
#' or that has been created when creating \code{mfdobj}
#' using the function \code{\link{get_mfd_list}}.
#'
#' @seealso \code{\link{get_mfd_df}}, \code{\link{get_mfd_list}}
#' @noRd
mfd_to_df_raw <- function(mfdobj) {
id <- NULL
if (!(is.mfd(mfdobj))) {
stop("Input must be a multivariate functional data object.")
}
dt <- mfdobj$raw
id_var <- mfdobj$id_var
arg_var <- mfdobj$fdnames[[1]]
obs <- mfdobj$fdnames[[2]]
variables <- mfdobj$fdnames[[3]]
vars_to_select <- c(variables, id_var, arg_var)
dt %>%
dplyr::select(dplyr::all_of(vars_to_select)) %>%
dplyr::rename(id = !!id_var) %>%
dplyr::filter(id %in% !!obs) %>%
tidyr::pivot_longer(dplyr::all_of(variables), names_to = "var") %>%
dplyr::arrange("id", "var", !!arg_var) %>%
tidyr::drop_na()
}
#' Discretize a Multivariate Functional Data Object
#'
#' This function discretizes an object of class \code{mfd}
#' and stores it into a \code{data.frame} object in the long format.
#'
#' @param mfdobj A multivariate functional data object of class mfd.
#'
#' @return
#' A \code{data.frame} in the long format,
#' obtained by discretizing the functional values using
#' \code{fda::\link[fda]{eval.fd}}
#' on a grid of 200 equally spaced argument values
#' covering the functional domain.
#' @noRd
mfd_to_df <- function(mfdobj) {
pos <- NULL
n <- var <- NULL
if (!(is.mfd(mfdobj))) {
stop("Input must be a multivariate functional data object.")
}
n_obs <- length(mfdobj$fdnames[[2]])
arg_var <- mfdobj$fdnames[[1]]
range <- mfdobj$basis$rangeval
evalarg <- seq(range[1], range[2], l = 200)
X <- fda::eval.fd(evalarg, mfdobj)
id <- mfdobj$fdnames[[2]]
id <- data.frame(id = id) %>%
dplyr::mutate(pos = seq_len(dplyr::n())) %>%
dplyr::group_by(id) %>%
dplyr::mutate(n = dplyr::n()) %>%
dplyr::mutate(id = ifelse(n == 1,
id,
paste0(id, " rep", seq_len(dplyr::n())))) %>%
dplyr::arrange(pos) %>%
dplyr::pull(id)
variables <- mfdobj$fdnames[[3]]
lapply(seq_along(variables), function(jj) {
variable <- variables[jj]
.df <- as.data.frame(X[, , jj, drop = FALSE]) %>%
stats::setNames(id)
.df[[arg_var]] <- evalarg
.df$var <- variable
tidyr::pivot_longer(.df, -c(!!arg_var, var), names_to = "id")
}) %>%
dplyr::bind_rows() %>%
dplyr::mutate(var = factor(var, levels = !!variables),
id = factor(id, levels = !!id))
}
#' Plot a Multivariate Functional Data Object.
#'
#' Plot an object of class \code{mfd} using \code{ggplot2}
#' and \code{patchwork}.
#'
#' @param mfdobj
#' A multivariate functional data object of class mfd.
#' @param data
#' A \code{data.frame} providing columns
#' to create additional aesthetic mappings.
#' It must contain a factor column "id" with the replication values
#' as in \code{mfdobj$fdnames[[2]]}.
#' If it contains a column "var", this must contain
#' the functional variables as in \code{mfdobj$fdnames[[3]]}.
#' @param mapping
#' Set of aesthetic mappings additional
#' to \code{x} and \code{y} as passed to the function
#' \code{ggplot2::geom:line}.
#' @param stat
#' See \code{ggplot2::\link[ggplot2]{geom_line}}.
#' @param position
#' See \code{ggplot2::\link[ggplot2]{geom_line}}.
#' @param na.rm
#' See \code{ggplot2::\link[ggplot2]{geom_line}}.
#' @param orientation
#' See \code{ggplot2::\link[ggplot2]{geom_line}}.
#' @param show.legend
#' See \code{ggplot2::\link[ggplot2]{geom_line}}.
#' @param inherit.aes
#' See \code{ggplot2::\link[ggplot2]{geom_line}}.
#' @param type_mfd
#' A character value equal to "mfd" or "raw".
#' If "mfd", the smoothed functional data are plotted, if "raw",
#' the original discrete data are plotted.
#' @param y_lim_equal
#' A logical value. If \code{TRUE}, the limits of the y-axis
#' are the same for all functional variables.
#' If \code{FALSE}, limits are different for each variable.
#' Default value is \code{FALSE}.
#' @param ...
#' See \code{ggplot2::\link[ggplot2]{geom_line}}.
#'
#' @return
#' A plot of the multivariate functional data object.
#'
#' @export
#' @examples
#' library(funcharts)
#' library(ggplot2)
#' mfdobj <- data_sim_mfd()
#' ids <- mfdobj$fdnames[[2]]
#' df <- data.frame(id = ids, first_two_obs = ids %in% c("rep1", "rep2"))
#' plot_mfd(mapping = aes(colour = first_two_obs),
#' data = df,
#' mfdobj = mfdobj)
#'
plot_mfd <- function(mfdobj,
mapping = NULL,
data = NULL,
stat = "identity",
position = "identity",
na.rm = TRUE,
orientation = NA,
show.legend = NA,
inherit.aes = TRUE,
type_mfd = "mfd",
y_lim_equal = FALSE,
...) {
var <- id <- value <- NULL
if (!(is.mfd(mfdobj))) {
stop("First argument must be a multivariate functional data object.")
}
if (!(type_mfd %in% c("mfd", "raw"))) {
stop("type_mfd not 'mfd' or 'raw'")
}
if (type_mfd == "mfd") df <- mfd_to_df(mfdobj)
if (type_mfd == "raw") df <- mfd_to_df_raw(mfdobj)
if (!is.null(data)) {
join_vars <- "id"
if ("var" %in% names(data)) join_vars <- c(join_vars, "var")
df <- dplyr::inner_join(df, data, by = join_vars)
}
variables <- mfdobj$fdnames[[3]]
df$var <- factor(as.character(df$var), levels = variables)
arg_var <- mfdobj$fdnames[[1]]
if (grepl(" ", arg_var)) {
mapping1 <- ggplot2::aes(!!dplyr::sym(paste0("`", arg_var, "`")), y = value, group = id)
} else {
mapping1 <- ggplot2::aes(!!dplyr::sym(arg_var), y = value, group = id)
}
mapping_tot <- c(mapping1, mapping)
class(mapping_tot) <- "uneval"
ylim_common <- range(df$value)
for (kk in seq_along(mapping_tot)) {
column <- as.character(mapping_tot[kk])
column <- substr(column, 2, stringr::str_count(column))
mapping_type <- names(mapping_tot)[kk]
if (!(column %in% names(df))) {
df <- df %>%
dplyr::mutate(!!mapping_type := factor(eval(parse(text=column))))
}
}
plot_list <- list()
for (jj in seq_along(variables)) {
dat <- df %>%
dplyr::filter(var == variables[jj])
if (grepl(" ", arg_var)) {
mapping1 <- ggplot2::aes(!!dplyr::sym(paste0("`", arg_var, "`")), value, group = id)
} else {
mapping1 <- ggplot2::aes(!!dplyr::sym(arg_var), value, group = id)
}
mapping_tot <- c(mapping1, mapping)
class(mapping_tot) <- "uneval"
geom_line_obj <- ggplot2::geom_line(mapping = mapping_tot,
data = dat,
stat = stat,
position = position,
na.rm = na.rm,
orientation = orientation,
show.legend = show.legend,
inherit.aes = inherit.aes,
...)
if (is.null(geom_line_obj$aes_params$linewidth) & is.null(geom_line_obj$mapping$linewidth)) {
geom_line_obj$aes_params$linewidth <- 0.25
}
p <- ggplot2::ggplot() +
geom_line_obj +
ggplot2::ylab(variables[jj]) +
ggplot2::xlab(arg_var) +
ggplot2::theme_bw() +
ggplot2::scale_linetype_discrete(drop = FALSE) +
ggplot2::scale_colour_discrete(drop = FALSE)
if (!is.null(p$layers[[1]]$data[["colour"]])) {
if (is.numeric(p$layers[[1]]$data$colour)) {
p <- p + ggplot2::scale_colour_continuous(limits = range(dat$colour))
} else {
p <- p + ggplot2::scale_colour_discrete(drop = FALSE)
}
}
if (!is.null(p$layers[[1]]$data[["color"]])) {
if (is.numeric(p$layers[[1]]$data$color)) {
p <- p + ggplot2::scale_color_continuous(limits = range(dat$color))
} else {
p <- p + ggplot2::scale_color_discrete(drop = FALSE)
}
}
if (y_lim_equal) {
p <- p + ggplot2::ylim(ylim_common)
}
plot_list[[jj]] <- p
}
patchwork::wrap_plots(plot_list) #+ patchwork::plot_layout(guides = "collect")
}
#' Add the plot of a new multivariate functional data object to an existing
#' plot.
#'
#'
#' @param plot_mfd_obj
#' A plot produced by \code{link{plot_mfd}}
#' @param mfdobj_new
#' A new multivariate functional data object of class mfd to be plotted.
#' @param data
#' See \code{\link{plot_mfd}}.
#' @param mapping
#' See \code{\link{plot_mfd}}.
#' @param stat
#' See \code{\link{plot_mfd}}.
#' @param position
#' See \code{\link{plot_mfd}}.
#' @param na.rm
#' See \code{\link{plot_mfd}}.
#' @param orientation
#' See \code{\link{plot_mfd}}.
#' @param show.legend
#' See \code{\link{plot_mfd}}.
#' @param inherit.aes
#' See \code{\link{plot_mfd}}.
#' @param type_mfd
#' See \code{\link{plot_mfd}}.
#' @param y_lim_equal
#' See \code{\link{plot_mfd}}.
#' @param ...
#' See \code{\link{plot_mfd}}.
#'
#' @return
#' A plot of the multivariate functional data object added to the existing
#' one.
#'
#' @export
#' @examples
#' library(funcharts)
#' library(ggplot2)
#' mfdobj1 <- data_sim_mfd()
#' mfdobj2 <- data_sim_mfd()
#' p <- plot_mfd(mfdobj1)
#' lines_mfd(p, mfdobj_new = mfdobj2)
#'
lines_mfd <- function(plot_mfd_obj,
mfdobj_new,
mapping = NULL,
data = NULL,
stat = "identity",
position = "identity",
na.rm = TRUE,
orientation = NA,
show.legend = NA,
inherit.aes = TRUE,
type_mfd = "mfd",
y_lim_equal = FALSE,
...) {
nvars <- length(plot_mfd_obj$patches$plots) + 1
p2 <- plot_mfd(mfdobj = mfdobj_new,
mapping = mapping,
data = data,
stat = stat,
position = position,
na.rm = na.rm,
orientation = orientation,
show.legend = show.legend,
inherit.aes = inherit.aes,
type_mfd = type_mfd,
y_lim_equal = y_lim_equal,
... = ...)
# check obs names
obs_names <- list()
for (jj in seq_len(nvars)) {
obs_names[[jj]] <-
unique(as.character(plot_mfd_obj[[1]]$layers[[1]]$data$id))
}
obs_names <- unique(unlist(obs_names))
if (any(mfdobj_new$fdnames[[2]] %in% obs_names)) {
mfdobj_new$fdnames[[2]] <- paste0(mfdobj_new$fdnames[[2]], "_2")
dimnames(mfdobj_new$coefs)[[2]] <- paste0(mfdobj_new$fdnames[[2]], "_2")
}
if (!y_lim_equal) {
plot_mfd_obj$scales$scales[[2]] <- NULL
} else {
ylim_common2 <- p2$scales$scales[[2]]$limits
if (length(plot_mfd_obj$scales$scales) == 2) {
ylim_common <- plot_mfd_obj$scales$scales[[2]]$limits
plot_mfd_obj$scales$scales[[2]] <- NULL
} else {
ylim_common <- range(vapply(seq_len(nvars), function(jj) {
range(plot_mfd_obj[[jj]]$layers[[1]]$data$value)
}, c(0, 0)))
}
ylim_common_new <- range(c(ylim_common, ylim_common2))
}
p <- plot_mfd_obj
for (jj in seq_len(nvars)) {
p[[jj]] <- plot_mfd_obj[[jj]] +
p2[[jj]]$layers[[1]]
if (y_lim_equal) {
p[[jj]]$scales$scales[[2]] <- NULL
p[[jj]] <- p[[jj]] + ggplot2::ylim(ylim_common_new)
}
}
p
}
#' Plot a Bivariate Functional Data Object.
#'
#' Plot an object of class \code{bifd} using
#' \code{ggplot2} and \code{geom_tile}.
#' The object must contain only one single functional replication.
#'
#' @param bifd_obj A bivariate functional data object of class bifd,
#' containing one single replication.
#' @param type_plot a character value
#' If "raster", it plots the bivariate functional data object
#' as a raster image.
#' If "contour", it produces a contour plot.
#' If "perspective", it produces a perspective plot.
#' Default value is "raster".
#' @param phi
#' If \code{type_plot=="perspective"}, it is the \code{phi} argument
#' of the function \code{plot3D::persp3D}.
#' @param theta
#' If \code{type_plot=="perspective"}, it is the \code{theta} argument
#' of the function \code{plot3D::persp3D}.
#'
#' @return
#' A ggplot with a geom_tile layer providing a plot of the
#' bivariate functional data object as a heat map.
#' @export
#'
#' @examples
#' library(funcharts)
#' mfdobj <- data_sim_mfd(nobs = 1)
#' tp <- tensor_product_mfd(mfdobj)
#' plot_bifd(tp)
#'
plot_bifd <- function(bifd_obj,
type_plot = "raster",
phi = 40,
theta = 40) {
s <- t <- value <- NULL
if (!inherits(bifd_obj, "bifd")) {
stop("bifd_obj must be an object of class bifd")
}
if (length(dim(bifd_obj$coef)) != 4) {
stop("length of bifd_obj$coef must be 4")
}
if (dim(bifd_obj$coef)[3] != 1) {
stop("third dimension of bifd_obj$coef must be 1")
}
if (!(type_plot %in% c("raster", "contour", "perspective"))) {
stop("type_plot must be one of \"raster\", \"contour\", \"perspective\"")
}
s_eval <- seq(bifd_obj$sbasis$rangeval[1],
bifd_obj$sbasis$rangeval[2],
l = 100)
t_eval <- seq(bifd_obj$tbasis$rangeval[1],
bifd_obj$tbasis$rangeval[2],
l = 100)
X_eval <- fda::eval.bifd(s_eval, t_eval, bifd_obj)
variables <- bifd_obj$bifdnames[[4]]
nvar <- length(variables)
zlim <- c(- max(abs(X_eval)), max(abs(X_eval)))
if (type_plot == "perspective") {
phi <- 40
theta <- 40
nr <- ceiling(sqrt(nvar))
graphics::par(mfrow = c(nr, nr))
for (ii in seq_len(nvar)) {
graphics::persp(s_eval,
t_eval,
X_eval[,,1,ii],
phi = phi,
theta = theta,
main = variables[ii],
zlim = zlim,
xlab = "s",
ylab = "t",
zlab = "value")
}
graphics::par(mfrow = c(1, 1))
} else {
plot_list <- list()
for (ii in seq_along(variables)) {
p <- X_eval[, , , ii] %>%
data.frame() %>%
stats::setNames(t_eval) %>%
dplyr::mutate(s = s_eval) %>%
tidyr::pivot_longer(-s, names_to = "t", values_to = "value") %>%
dplyr::mutate(t = as.numeric(t),
variable = bifd_obj$bifdnames[[4]][ii]) %>%
ggplot2::ggplot() +
ggplot2::theme_bw() +
ggplot2::theme(panel.grid.major = ggplot2::element_blank(),
panel.grid.minor = ggplot2::element_blank(),
strip.background = ggplot2::element_blank(),
panel.border = ggplot2::element_rect(colour = "black")) +
ggplot2::ggtitle(variables[ii])
if (type_plot == "raster") {
p <- p +
ggplot2::geom_tile(ggplot2::aes(s, t, fill = value)) +
ggplot2::scale_fill_gradientn(
colours = c("blue", "white", "red"),
limits = zlim)
}
if (type_plot == "contour") {
p <- p +
ggplot2::geom_contour(ggplot2::aes(
s,
t,
z = value,
colour = ggplot2::after_stat(get("level")))) +
ggplot2::scale_color_gradientn(
colours = c("blue", "white", "red"),
limits = zlim) +
ggplot2::labs(colour = 'level')
}
plot_list[[ii]] <- p +
ggplot2::theme(plot.title = ggplot2::element_text(hjust = 0.5,
size = 9))
}
patchwork::wrap_plots(plot_list) +
patchwork::plot_layout(guides = "collect")
}
}
#' Mean Function for Multivariate Functional Data
#'
#' Computes the mean function for a multivariate functional data object of class `mfd`.
#'
#' @param mfdobj An object of class `mfd` representing the multivariate functional data.
#' It contains \eqn{N} observations of a \eqn{p}-dimensional multivariate functional variable.
#'
#' @return An `mfd` object representing the mean function of the input data. The output contains
#' one observation, representing the mean function for each dimension of the multivariate functional variable.
#'
#' @details
#' This function calculates the mean function for each dimension of the multivariate functional data
#' by averaging the coefficients across all observations. The resulting object is a single observation
#' containing the mean function for each dimension.
#'
#' @examples
#' \dontrun{
#' library(funcharts)
#' data(air)
#' mfdobj <- get_mfd_list(air)
#' mean_result <- mean_mfd(mfdobj)
#' plot_mfd(mean_result)
#' }
#'
#' @export
mean_mfd <- function(mfdobj) {
x <- mfdobj
coef_mean <- apply(x$coefs, c(1, 3), mean)
coef_mean <- array(coef_mean, dim = c(nrow(coef_mean), 1, ncol(coef_mean)))
fdnames <- list(x$fdnames[[1]], "sample mean", x$fdnames[[3]])
mfd(coef_mean, x$basis, fdnames)
}
#' Covariance Function for Multivariate Functional Data
#'
#' Computes the covariance function for two multivariate functional data objects of class `mfd`.
#'
#' @param mfdobj1 An object of class `mfd` representing the first multivariate functional data set.
#' It contains \eqn{N} observations of a \eqn{p}-dimensional multivariate functional variable.
#' @param mfdobj2 An object of class `mfd` representing the second multivariate functional data set.
#' Defaults to `mfdobj1`. If provided, it must also contain \eqn{N} observations of a \eqn{p}-dimensional
#' multivariate functional variable.
#'
#' @return A bifd object representing the covariance function of the two input objects. The output
#' is a collection of \eqn{p^2} functional surfaces, each corresponding to the covariance between
#' two components of the multivariate functional data.
#'
#' @details
#' The function calculates the covariance between all pairs of dimensions from the two multivariate
#' functional data objects. Each covariance is represented as a functional surface in the resulting
#' bifd object. The covariance function is useful for analyzing relationships between functional variables.
#'
#' @examples
#' \dontrun{
#' library(funcharts)
#' data("air")
#' x <- get_mfd_list(air[1:3])
#' cov_result <- cov_mfd(x)
#' plot_bifd(cov_result)
#' }
#'
#' @export
cov_mfd <- function(mfdobj1, mfdobj2 = mfdobj1) {
fdobj1 <- mfdobj1
fdobj2 <- mfdobj2
coefx <- fdobj1$coefs
coefy <- fdobj2$coefs
coefdobj1 <- dim(coefx)
coefdobj2 <- dim(coefy)
basisx <- fdobj1$basis
basisy <- fdobj2$basis
nbasisx <- basisx$nbasis
nbasisy <- basisy$nbasis
nvar <- coefdobj1[3]
coefvar <- array(0, c(nbasisx, nbasisx, 1, nvar^2))
varnames <- fdobj1$fdnames[[3]]
m <- 0
bivarnames <- vector("character", nvar^2)
for (i in 1:nvar) for (j in 1:nvar) {
m <- m + 1
coefvar[, , 1, m] <- stats::var(t(coefx[, , i]), t(coefx[, , j]))
bivarnames[m] <- paste(varnames[i], "vs", varnames[j])
}
bifdnames <- list()
bifdnames[[1]] <- fdobj1$names
bifdnames[[2]] <- fdobj2$names
bifdnames[[3]] <- "covariance"
bifdnames[[4]] <- bivarnames
varbifd <- fda::bifd(coefvar, basisx, basisx, bifdnames)
return(varbifd)
}
#' Correlation Function for Multivariate Functional Data
#'
#' Computes the correlation function for two multivariate functional data objects of class `mfd`.
#'
#' @param mfdobj1 An object of class `mfd` representing the first multivariate functional data set.
#' It contains \eqn{N} observations of a \eqn{p}-dimensional multivariate functional variable.
#' @param mfdobj2 An object of class `mfd` representing the second multivariate functional data set.
#' Defaults to `mfdobj1`. If provided, it must also contain \eqn{N} observations of a \eqn{p}-dimensional
#' multivariate functional variable.
#'
#' @return A bifd object representing the correlation function of the two input objects. The output
#' is a collection of \eqn{p^2} functional surfaces, each corresponding to the correlation between
#' two components of the multivariate functional data.
#'
#' @details
#' The function calculates the correlation between all pairs of dimensions from the two multivariate
#' functional data objects. The data is first scaled using \code{\link{scale_mfd}}, and the correlation
#' is then computed as the covariance of the scaled data using \code{\link{cov_mfd}}.
#'
#' @examples
#' \dontrun{
#' library(funcharts)
#' data("air")
#' x <- get_mfd_list(air[1:3])
#' cor_result <- cor_mfd(x)
#' plot_bifd(cor_result)
#' }
#'
#' @export
cor_mfd <- function(mfdobj1, mfdobj2 = mfdobj1) {
mfdobj1_scaled <- scale_mfd(mfdobj1)
mfdobj2_scaled <- scale_mfd(mfdobj2)
cor_result <- cov_mfd(mfdobj1_scaled, mfdobj2_scaled)
cor_result$bifdnames[[3]] <- "correlation"
return(cor_result)
}
#' @noRd
#'
# project_mfd <- function(X, basisobj) {
#
# fdnames <- NULL
# if (is.mfd(X)) {
# p <- dim(X$coef)[3]
# bs <- X$basis
# rb <- bs$rangeval
# xseq <- seq(rb[1], rb[2], l = 300)
# Xeval <- fda::eval.fd(xseq, X)
# Xeval <- aperm(Xeval, c(2, 1, 3))
# fdnames <- X$fdnames
# }
# if (is.array(X)) {
# rb <- c(0, 1)
# xseq <- seq(rb[1], rb[2], l = dim(X)[2])
# Xeval <- X
# Xeval <- aperm(Xeval, c(2, 1, 3))
# }
# if (is.list(X) & !is.mfd(X)) {
# rb <- c(0, 1)
# xseq <- seq(rb[1], rb[2], l = ncol(X[[1]]))
# Xeval <- simplify2array(X)
# Xeval <- aperm(Xeval, c(2, 1, 3))
# }
#
# coefList <- fda::project.basis(Xeval, argvals = xseq, basisobj = basisobj)
# X_mfd <- mfd(coefList, basisobj = basisobj, fdnames = fdnames)
# X_mfd
#
# }
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.