The R Project SVN R

Rev

Rev 87397 | Blame | Compare with Previous | Last modification | View Log | Download | RSS feed

#  File src/library/stats/R/smspline.R
#  Part of the R package, https://www.R-project.org
#
#  Copyright (C) 1995-2023 The R Core Team
#
#  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
#  (at your option) 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.
#
#  A copy of the GNU General Public License is available at
#  https://www.R-project.org/Licenses/

.nknots.smspl <- function(n) {
    ## Number of inner knots
    if(n < 50L) n
    else trunc({
        a1 <- log2( 50)
        a2 <- log2(100)
        a3 <- log2(140)
        a4 <- log2(200)
        if  (n < 200L) 2^(a1+(a2-a1)*(n-50)/150)
        else if (n < 800L) 2^(a2+(a3-a2)*(n-200)/600)
        else if (n < 3200L)2^(a3+(a4-a3)*(n-800)/2400)
        else  200 + (n-3200)^0.2
    })
}
## n.knots <- function(n) {
##     message(".nknots.smspl() is now exported; use it instead of n.knots()")
##     .nknots.smspl(n)
## }

smooth.spline <-
    function(x, y = NULL, w = NULL, df, spar = NULL, lambda = NULL, cv = FALSE,
             all.knots = FALSE, nknots = .nknots.smspl, keep.data = TRUE,
             df.offset = 0, penalty = 1, control.spar = list(),
         tol = 1e-6 * IQR(x),
         keep.stuff = FALSE) # was "fix" till R 3.3.x

{
    contr.sp <- list(low = -1.5, # low = 0.      was default till R 1.3.x
                     high = 1.5,
                     tol = 1e-4, # tol = 0.001   was default till R 1.3.x
                     eps = 2e-8, # eps = 0.00244 was default till R 1.3.x
                     maxit = 500, trace = getOption("verbose"))
    contr.sp[names(control.spar)] <- control.spar
    ctrl.Num <- contr.sp[1:4]
    if(!all(vapply(ctrl.Num, is.numeric, NA)) ||
       contr.sp$tol < 0 || contr.sp$eps <= 0 || contr.sp$maxit <= 0)
        stop("invalid 'control.spar'")

    xy <- xy.coords(x, y, setLab=FALSE)
    y <- xy$y
    x <- xy$x
    if(!all(is.finite(c(x, y))))
        stop("missing or infinite values in inputs are not allowed")
    n <- length(x)
    if(is.na(n)) stop("invalid number of points")
    no.wgts <- is.null(w)
    w <-
    if(no.wgts) 1 # rep_len(1, n)
    else {
        if(n != length(w)) stop("lengths of 'x' and 'w' must match")
        if(any(w < 0)) stop("all weights should be non-negative")
        if(all(w == 0)) stop("some weights should be positive")
        (w * sum(w > 0))/sum(w)
    } # now sum(w) == #{obs. with weight > 0} == sum(w > 0)

    ## Replace y[], w[] for same x[] (to a precision of 'tol') by their mean :
    if(!is.finite(tol) || tol <= 0)
        stop("'tol' must be strictly positive and finite")
    if(!match(keep.stuff, c(FALSE,TRUE))) stop("invalid 'keep.stuff'")
    xx <- round((x - mean(x))/tol)  # de-mean to avoid possible overflow
    uns.x <- is.unsorted(x)
    iOx <- if(uns.x) sort.list(x) else TRUE
    xxs <- xx[iOx] # xx sorted
    nd <- c(TRUE, xxs[-n] < xxs[-1L]) # === !duplicated(xxs)
    nx <- length(ux <- x[iOx][nd]) # ux := unique & sorted  x
    if(nx <= 3L) stop("need at least four unique 'x' values")
    if(nx == n) { # speedup
    ox <- if(uns.x) (function(p) { p[p] <- seq_along(p) ; p })(iOx) else TRUE
    tmp <- cbind(w, w*y, w*y^2)[iOx,]
    } else {
    ox <- match(xx, xxs[nd])
    ## Faster, much simplified version of tapply()
    tapply1 <- function (X, INDEX, FUN = NULL, ..., simplify = TRUE) {
        sapply(X = unname(split(X, INDEX)), FUN = FUN, ...,
           simplify = simplify, USE.NAMES = FALSE)
    }
    tmp <- matrix(unlist(tapply1(seq_len(n), ox,
                     if(length(w) == 1L) function(i)
                     c(length(i), sum(y[i]), sum(y[i]^2))
                     else function(i)
                     c(sum(w[i]), sum(w[i]*y[i]),sum(w[i]*y[i]^2))),
                             use.names = FALSE),
              ncol = 3, byrow = TRUE)
    }
    wbar <- tmp[, 1L]
    ybar <- tmp[, 2L]/ifelse(wbar > 0, wbar, 1)
    yssw <- sum(tmp[, 3L] - wbar*ybar^2) # will be added to RSS for GCV
    rm(iOx, xx, xxs, nd, tmp)
    ## cv in {NA,FALSE,TRUE} :
    if(is.na(cv <- as.logical(cv)) && !missing(df))
    stop("'cv' must not be NA when 'df' is specified")
    CV <- !is.na(cv) && cv
    if(CV && nx < n)
        warning("cross-validation with non-unique 'x' values seems doubtful")
    r.ux <- ux[nx] - ux[1L]
    xbar <- (ux - ux[1L])/r.ux           # scaled to [0,1]
    if(is.numeric(all.knots)) {
    if(is.unsorted(all.knots, strictly = TRUE))
        stop("Numeric 'all.knots' must be strictly increasing")
    if(!missing(nknots) && !is.null(nknots))
        warning("'all.knots' is vector of knots; 'nknots' specification is disregarded")
    nknots <- length(all.knots)
    if(0 < all.knots[1] || all.knots[nknots] < 1)
        stop("numeric 'all.knots' must cover [0,1] (= the transformed data-range)")
        ## otherwise, it seg.faults when .Fortran() is returning (why ?)
        knot <- c(rep(all.knots[1 ], 3),
                  all.knots,
                  rep(all.knots[nknots], 3))
    } else {
        if(all.knots) {
            if(!missing(nknots) && !is.null(nknots))
                warning("'all.knots' is TRUE; 'nknots' specification is disregarded")
            nknots <- nx
        } else if(is.null(nknots))# <- for back compatibility
            nknots <- .nknots.smspl(nx)
        else { ## all.knots is false; nknots not NULL
            if(is.function(nknots))
                nknots <- nknots(nx)
            else if(!is.numeric(nknots))
                stop("'nknots' must be numeric (in {1,..,n})")
            if(nknots < 1)
                stop("'nknots' must be at least 1")
            else if(nknots > nx)
                stop("cannot use more inner knots than unique 'x' values")
        }
        knot <- c(rep(xbar[1 ], 3),
                  if(all.knots) xbar else xbar[seq.int(1, nx, length.out = nknots)],
                  rep(xbar[nx], 3))
    }
    nk <- nknots + 2L ## == length(knot) - 4

    spar.is.lambda <- !missing(lambda)
    if (spar.is.lambda <- !missing(lambda)) {
        if(!missing(spar)) stop("must not specify both 'spar' and 'lambda'")
        ispar <- 1L
    } else
        ## ispar != 1 : compute spar (later)
        ispar <-
            if(is.null(spar) || missing(spar)) { ## || spar == 0
                if(contr.sp$trace) -1L else 0L
            } else 1L
    spar <- if(spar.is.lambda) as.double(lambda)
            else if(ispar == 1L) as.double(spar) else double(1)
    ## was <- if(missing(spar)) 0 else if(spar < 1.01e-15) 0 else  1
    ## but package forecast passed a length-0 vector.
    if(length(spar) != 1) stop("'spar' must be of length 1")

    ## icrit {../src/sslvrg.f}:
    ##      (0 = no crit,  1 = GCV ,  2 = ord.CV , 3 = df-matching)
    icrit <- if(is.na(cv)) 0L else if(cv) 2L else 1L
    if(!is.numeric(df.offset) || df.offset < 0)
        stop("df.offset must be numeric and >= 0")
    if(!is.numeric(penalty) || penalty <= 0)
        stop("penalty must be numeric and > 0")
    dofoff <- df.offset
    if(!missing(df)) { # not when cv was NA
    if(df > 1 && df <= nx) {
        icrit <- 3L
        dofoff <- df
    } else warning("not using invalid df; must have 1 < df <= n := #{unique x} = ", nx)
    }
    iparms <- c(icrit=icrit, ispar=ispar, iter = as.integer(contr.sp$maxit),
                spar.is.lambda)
    ans.names <- c("coef","ty","lev","spar","parms","crit","iparms","ier",
           if(keep.stuff) "scratch")
    fit <- .Fortran(C_rbart,        # code in ../src/qsbart.f
            as.double(penalty),
            as.double(dofoff),
            x = as.double(xbar),
            y = as.double(ybar),
            w = as.double(wbar), # changed in the Fortran code
            ssw = as.double(yssw),
            as.integer(nx),
            as.double(knot),
            as.integer(nk),
            coef = double(nk),
            ty = double(nx),
            lev = double(if(is.na(cv))1L else nx),
            crit = double(1),
            iparms = iparms,
            spar = spar,
            parms = c(unlist(ctrl.Num), ratio = -1.), # no NA here
            scratch = double((17L+1L) * nk + 1L),#
            ld4  = 4L,
            ldnk = 1L,
            ier = integer(1L)
            )[ans.names]

    if(is.na(cv)) lev <- df <- NA
    else { # now when dpfa() with 'tol', signals error earlier, happens less often:
    lev <- fit$lev
    df <- sum(lev)
    if(is.na(df))
        stop("NA lev[]; probably smoothing parameter 'spar' way too large!")
    }
    if(fit$ier > 0L ) {
    offKind <- if(spar.is.lambda) "extreme" # not easy to know if small | large
           else if(sml <- fit$spar < 0.5) "small" else "large"
    wtxt <- paste("smoothing parameter value too", offKind)
        if(spar.is.lambda || sml) {
            ## used to give warning too and mean() as below, but that's rubbish
            stop(wtxt)
        } else {
            fit$ty <- rep(mean(y), nx) ## would be df = 1
            df <- 1
            warning(wtxt,"\nsetting df = 1  __use with care!__")
        }
    }
    cv.crit <-
    if(is.na(cv)) NA
    else {
        r <- y - fit$ty[ox] # true residuals; if(nx < n) *not* those used in fitting
        if(cv) {
        ww <- wbar
        ww[ww == 0] <- 1
        r <- r / (1 - (lev[ox] * w)/ww[ox])
        if(no.wgts) mean(r^2) else weighted.mean(r^2, w)
        } else # GCV
        (if(no.wgts) mean(r^2) else weighted.mean(r^2, w)) /
            (1 - (df.offset + penalty * df)/n)^2
        }

    ## return :
    structure(
    ## parms :  c(low = , high = , tol = , eps = )
    list(x = ux, y = fit$ty, w = wbar, yin = ybar, tol = tol,
         data = if(keep.data) list(x = x, y = y, w = w), no.weights = no.wgts,
         n = n, # to reliably know (when keep.data is false) if (nx < n)
         lev = lev,
         cv = cv,
         cv.crit = cv.crit, pen.crit = sum(wbar * (ybar - fit$ty)^2),
         crit = fit$crit,
         df = df,
         spar = if(spar.is.lambda) NA else fit$spar,
         ratio= if(spar.is.lambda) NA else fit$parms[["ratio"]],
         lambda = fit$parms[["low"]],
         iparms = c(fit$iparms, errorI = if(fit$ier) fit$ier else NA),#c(icrit= ,ispar= ,iter= )
         auxM = if(keep.stuff)
            list(XWy  = fit$scratch[      seq_len(nk)],
                 XWX  = fit$scratch[nk  + seq_len(4*nk)],
                 Sigma= fit$scratch[5*nk+ seq_len(4*nk)],
                 R    = fit$scratch[9*nk+ seq_len(4*nk)] ),
         fit = structure(list(knot = knot, nk = nk, min = ux[1L], range = r.ux,
                  coef = fit$coef),
                 class = "smooth.spline.fit"),
         call = match.call()),
    class = "smooth.spline")
}

fitted.smooth.spline <- function(object, ...) {
    if(!is.list(dat <- object$data))
        stop("need result of smooth.spline(keep.data = TRUE)")
    ## note that object$x == unique(sort(object$data$x))
    object$y[match(dat$x, object$x)]
}

residuals.smooth.spline <-
    function (object, type = c("working", "response", "deviance",
                      "pearson", "partial"), ...)
{
    type <- match.arg(type)
    if(!is.list(dat <- object$data))
        stop("need result of smooth.spline(keep.data = TRUE)")
    r <- dat$y - object$y[match(dat$x, object$x)]
    ## this rest is `as' residuals.lm() :
    res <- switch(type,
                  working = ,
                  response = r,
                  deviance = ,
                  pearson = if (is.null(dat$w)) r else r * sqrt(dat$w),
                  partial = r)
    res <- naresid(object$na.action, res)
    if (type == "partial")
        stop('type = "partial" is not yet implemented')
        ## res <- res + predict(object, type = "terms")
    res
}

hatvalues.smooth.spline <- function (model, ...) {
    if(!is.list(dat <- model$data))
        stop("need result of smooth.spline(keep.data = TRUE)")
    ## "expand" leverages:
    hat <- model$lev
    hat[hat > 1 - 10 * .Machine$double.eps] <- 1 # as in hatvalues.lm
    hat[match(dat$x, model$x)]
}


print.smooth.spline <- function(x, digits = getOption("digits"), ...)
{
    if(!is.null(cl <- x$call)) {
    cat("Call:\n")
    dput(cl, control=NULL)
    }
    ip <- x$iparms
    cat("\nSmoothing Parameter  spar=", format(x$spar, digits=digits),
        " lambda=", format(x$lambda, digits=digits),
        if(ip["ispar"] != 1L) paste0("(", ip["iter"], " iterations)"))
    cat("\n")
    cat("Equivalent Degrees of Freedom (Df):", format(x$df,digits=digits))
    cat("\n")
    cat(sprintf("Penalized Criterion (%sRSS): %s\n",
        if(x$no.weights) "" else "weighted ",
        format(x$pen.crit, digits=digits)))
    if(!is.na(cv <- x$cv))
    cat(if(cv) "PRESS(l.o.o. CV): " else "GCV: ",
            format(x$cv.crit, digits = digits), "\n", sep = "")
    invisible(x)
}

predict.smooth.spline <- function(object, x, deriv = 0, ...)
{
    if(missing(x)) {
        if(deriv == 0)
            return(object[c("x", "y")])
        else x <- object$x
    }
    fit <- object$fit
    if(is.null(fit)) stop("not a valid \"smooth.spline\" object")
    else predict(fit, x, deriv, ...)
}

predict.smooth.spline.fit <- function(object, x, deriv = 0, ...)
{
    if(missing(x))
    x <- seq.int(from = object$min, to = object$min + object$range,
                     length.out = length(object$coef) - 4L)
    xs <- (x - object$min)/object$range # x scaled to [0,1]
    extrap.left <- xs < 0
    extrap.right <- xs > 1
    interp <- !(extrap <- extrap.left | extrap.right)
    n <- sum(interp) # number of xs in [0,1]
    y <- xs
    if(any(interp))
    y[interp] <- .Fortran(C_bvalus,
                  n   = as.integer(n),
                  knot = as.double(object$knot),
                  coef = as.double(object$coef),
                  nk = as.integer(object$nk),
                  x = as.double(xs[interp]),
                  s = double(n),
                  order = as.integer(deriv))$s
    if(any(extrap)) {
    xrange <- c(object$min, object$min + object$range)
    if(deriv == 0) {
        end.object <- Recall(object, xrange)$y
        end.slopes <- Recall(object, xrange, 1)$y * object$range
        if(any(extrap.left))
        y[extrap.left] <- end.object[1L] +
            end.slopes[1L] * (xs[extrap.left] - 0)
        if(any(extrap.right))
        y[extrap.right] <- end.object[2L] +
            end.slopes[2L] * (xs[extrap.right] - 1)
    } else if(deriv == 1) {
        end.slopes <- Recall(object, xrange, 1)$y * object$range
        y[extrap.left] <- end.slopes[1L]
        y[extrap.right] <- end.slopes[2L]
    }
    else y[extrap] <- 0
    }
    if(deriv > 0)
    y <- y/(object$range^deriv)
    list(x = x, y = y)
}

supsmu <-
  function(x, y, wt = rep(1, n), span = "cv", periodic = FALSE, bass = 0, trace = FALSE)
{
    if(span == "cv") span <- 0
    else if(span < 0 || span > 1) stop("'span' must be between 0 and 1.")
    n <- length(y)
    if(!n || !is.numeric(y)) stop("'y' must be numeric vector")
    if(length(x) != n) stop("number of observations in 'x' and 'y' must match.")
    if(length(wt) != n)
    stop("number of weights must match number of observations.")
    if(periodic) {
    iper <- 2L
    xrange <- range(x)
    if(xrange[1L] < 0 || xrange[2L] > 1)
        stop("'x' must be between 0 and 1 for periodic smooth")
    } else iper <- 1L
    okay <- is.finite(x + y + wt)
    ord <- order(x[okay], y[okay])
    ord <- cumsum(!okay)[okay][ord] + ord
    xo <- x[ord]
    leno <- length(ord)
    if(leno == 0L)
        stop("no finite observations")
    if(diff <- n - leno)
        warning(sprintf(ngettext(diff,
                                 "%d observation with NA, NaN or Inf deleted",
                                 "%d observations with NAs, NaNs and/or Infs deleted"),
                        diff), domain = NA)
    .Fortran(C_setsmu, as.integer(trace))
    smo <- .Fortran(C_supsmu,
            as.integer(leno),
            as.double(xo),
            as.double(y[ord]),
            as.double(wt[ord]),
            as.integer(iper),
            as.double(span),
            as.double(bass),
            smo=double(leno),
            double(n*7L), double(1L))$smo
    ## eliminate duplicate xsort values and corresponding smoothed values
    dupx <- duplicated(xo)
    list(x = xo[!dupx], y = smo[!dupx])
}