Nothing
# lib.R: miscellaneous functions for plotmo and related packages
# functions in this file are in alphabetical order
any1 <- function(x)
{
any(x != 0) # like any but no warning if x not logical
}
cat0 <- function(...) # cat with no added spaces
{
cat(..., sep="")
}
check <- function(object, object.name, check.name, check.func, na.ok=FALSE)
{
any <- check.func(object)
if(na.ok)
any <- any[!is.na(any)]
else {
which.na <- which(is.na(any))
if(length(which.na)) {
stopf("NA in %s\n %s[%d] is %g",
object.name, object.name,
which.na[1], object[which.na[1]])
}
}
if(any(any)) {
which <- which(check.func(object))
stopifnot(length(which) > 0)
stopf("%s in %s\n %s[%d] is %g",
check.name, object.name, object.name,
which[1], object[which[1]])
}
}
# TODO commented out the following because it is too slow for big data
# (the as.character is very slow)
#
# # The args argument is assumed to be a list of arguments for do.call.
# # An argument in args will be an unforced promise if it couldn't be
# # evaluated earlier e.g. if call.plot was invoked with arg=nonesuch.
# # If an argument is such an unforced promise, issue an error message now
# # to prevent very confusing error messages later. To do this, we have to
# # determine if the arg is a promise, which we do with the if statement
# # below.
# # This makes me nervous, because the R language manual says "There is
# # generally no way in R code to check whether an object is a promise or not".
#
# check.do.call.args <- function(func, args, fname)
# {
# stopifnot(is.list(args))
# for(i in seq_along(args)) {
# if(length(args[i]) == 1 && !is.na(args[i]) &&
# substr(as.character(args[i]), 1, 2) == "..") {
# printf("\n")
# s <- paste0(strwrap(list.as.char(args),
# width=getOption("width"), exdent=7), collapse="\n")
# stop0("cannot evaluate ", quotify(names(args)[i], "'"),
# " in\n ", fname, "(", s, ")")
# }
# }
# }
# mostly for checking user arguments (so error wording is for that)
# but also occasionally used for other sanity checking
check.boolean <- function(b) # b==0 or b==1 is also ok
{
if(length(b) != 1)
stop0("the ", short.deparse(substitute(b), "given"),
" argument is not FALSE, TRUE, 0, or 1")
if(!(is.logical(b) || is.numeric(b)) || is.na(b) || !(b == 0 || b == 1))
stop0(short.deparse(substitute(b), "the argument"), "=", as.char(b),
" but it should be FALSE, TRUE, 0, or 1")
b != 0 # convert to logical
}
is.boolean <- function(b) # b==NA or b==0 or b==1
{
length(b) == 1 &&
(is.logical(b) || is.numeric(b)) &&
(is.na(b) || b == 0 || b == 1)
}
check.classname <- function(object, substituted.object, allowed.classnames)
{
expected.classname <- quotify(allowed.classnames)
if(length(allowed.classnames) > 1)
expected.classname <- sprint("one of\n%s", expected.classname)
if(is.null(object))
stopf("object is NULL but expected an object of class of %s",
expected.classname)
if(!inherits(object, allowed.classnames)) {
stopf("the class of %s is \"%s\" but expected the class to be %s",
quotify(paste.trunc(substituted.object, maxlen=30)),
class(object)[1], expected.classname)
}
}
# adjust name so e.g. error message is "argument is NULL" not "NULL is NULL"
tweak.name <- function(name, quote=TRUE)
{
quoted.name <- quotify(name, quote="'")
if(name %in% c("NULL", "NA") ||
(substr(name[1], 1, 1) %in% c("+", "-")) ||
grepl("[0-9]", substr(name[1], 1, 1))) {
quoted.name <- name <- "argument"
}
if(quote) quoted.name else name
}
check.integer.scalar <- function(object, min=NA, max=NA, null.ok=FALSE,
na.ok=FALSE, logical.ok=TRUE,
char.ok=FALSE,
object.name=short.deparse(substitute(object)))
{
stop.msg <- function(s)
{
s.null <- if(null.ok) ", or NULL" else ""
s.na <- if(na.ok) ", or NA" else ""
s.logical <- if(logical.ok) ", or TRUE or FALSE" else ""
s.char <- if(char.ok) ", or a string" else ""
stop0(s,
" but it should be an integer",
s.null, s.na, s.logical, s.char)
}
if(is.character(object)) {
if(!char.ok || length(object) != 1)
stop.msg(paste0(tweak.name(object.name), " is a string"))
} else {
check.numeric.scalar(object, min, max, null.ok, na.ok, logical.ok,
char.ok.msg=char.ok, object.name=object.name)
if(!is.null(object) && !is.na(object) && object != floor(object))
stop.msg(paste0(tweak.name(object.name, quote=FALSE), "=", object[1]))
}
object
}
check.level.arg <- function(level, zero.ok)
{
if(anyNA(level) || is.null(level)) # treat NA and NULL as 0
level <- 0
check.numeric.scalar(level)
if(!((zero.ok && level == 0) || level >= .5 || level < 1)) {
stop0("level=", level, " but it should be ",
if(zero.ok) "zero or " else "", "between 0.5 and 1")
}
level
}
check.no.na.in.mat <- function(object)
{
if(anyNA(object)) { # quick initial check
# detailed check for detailed error message
for(icol in seq_along(ncol(object))) {
check.name <-
if(!is.null(colnames(object)))
colnames(object)[icol]
else
sprint("%s[,%d]",
short.deparse(substitute(object), "matrix"), icol)
check(object[,icol], check.name, "NA", is.na, na.ok=FALSE)
}
}
}
# x can be a data.frame or matrix
check.df.numeric.or.logical <- function(x, xname=trunc.deparse(substitute(x)))
{
stopifnot(!is.null(x), length(dim(x)) == 2)
for(icol in seq_len(NCOL(x))) {
if(!is.numeric(x[,icol]) && !is.logical(x[,icol]))
stopf("the class of %s is \"%s\" (expected numeric or logical)",
colname(x, icol, xname), class(x[,icol]))
is.na <- is.na(x[,icol])
if(any(is.na))
stopf("%s[%g] is NA", colname(x, icol, xname), which(is.na)[1])
is.infinite <- !is.finite(x[,icol])
if(any(is.infinite))
stopf("%s[%g] is Inf", colname(x, icol, xname), which(is.infinite)[1])
}
}
check.numeric.scalar <- function(object, min=NA, max=NA, null.ok=FALSE,
na.ok=FALSE, logical.ok=FALSE,
char.ok.msg=FALSE, # only affects error msg
object.name=short.deparse(substitute(object)))
{
s.logical <- if(logical.ok) ", or TRUE or FALSE" else ""
if(na.ok)
logical.ok <- TRUE # needed because NA is a logical
any.na <- !is.null(object) &&
# following needed because anyNA gives error on some objects
(is.numeric(object) || is.logical(object) ||
is.list(object) || is.character(object)) &&
anyNA(object)
if(is.null(object)) {
if(!null.ok)
stop0(tweak.name(object.name), " is NULL")
} else if(any.na && !na.ok)
stop0(tweak.name(object.name), " is NA")
else if(!is.numeric(object) && !(is.logical(object) && logical.ok)) {
s.na <- if(na.ok) ", or NA" else ""
s.null <- if(null.ok) ", or NULL" else ""
s.char <- if(char.ok.msg) ", or a string" else ""
stopf("%s must be numeric%s%s%s%s (whereas its current class is %s)",
tweak.name(object.name),
s.null, s.na, s.char, s.logical,
class.as.char(object, quotify=TRUE))
} else if(length(object) != 1)
stopf("the length of %s must be 1 (whereas its current length is %d)",
tweak.name(object.name), length(object))
if(!is.null(object) && !any.na) {
if(!is.na(min) && !is.na(max) && (object < min || object > max)) {
stop0(tweak.name(object.name, quote=FALSE), "=", object,
" but it should be between ", min, " and ", max)
}
if(!is.na(min) && object < min) {
stop0(tweak.name(object.name, quote=FALSE), "=", object,
" but it should be at least ", min)
}
if(!is.na(max) && object > max) {
stop0(tweak.name(object.name, quote=FALSE), "=", object,
" but it should not be greater than ", max)
}
}
object
}
# We allow 20% of x to be nonpositive, useful if the response is essentially
# positive, but the predicted response has a few nonpositive values at the extremes.
# Needed for example if we will later take log(x) or sqrt(x).
check.that.most.are.positive <- function(x, xname, user.arg, non.positive.msg, frac.allowed=.2)
{
check.numeric.scalar(frac.allowed)
stopifnot(frac.allowed >= 0, frac.allowed <= 1)
nonpos <- x <= 0
if(sum(nonpos, na.rm=TRUE) > frac.allowed * length(x)) { # more than frac.allowed nonpos?
ifirst <- which(nonpos)[1]
stop0(sprint(
"%s is not allowed because too many %ss are %s\n",
user.arg, unquote(xname), non.positive.msg),
sprint(
" %.2g%% are %s (%g%% is allowed)\n",
100 * sum(nonpos) / length(x), non.positive.msg, 100 * frac.allowed),
sprint(" e.g. %s[%d] is %g", unquote(xname), ifirst, x[ifirst]))
}
}
check.vec <- function(object, object.name, expected.len=NA, logical.ok=TRUE, na.ok=FALSE)
{
if(!(NROW(object) == 1 || NCOL(object) == 1))
stop0(tweak.name(object.name), " is not a vector\n ",
"It has dimensions ", NROW(object), " by ", NCOL(object))
if(!((logical.ok && is.logical(object)) || is.numeric(object)))
stop0(tweak.name(object.name), " is not numeric")
if(!is.na(expected.len) && length(object) != expected.len)
stop0(tweak.name(object.name), " has the wrong length ",
length(object), ", expected ", expected.len)
if(na.ok)
object[is.na(object)] <- 1 # prevent check is.finite from complaining
else
check(object, object.name, "NA", is.na)
check(object, object.name, "non-finite value", function(object) {!is.finite(object)})
}
cleantry <- function(err) # clean up a try.err (remove "Error: " etc.)
{
stopifnot(is.try.err(err))
attributes(err) <- NULL
err <- gsub("^[^:]*: *", "", err) # remove "Error: " (actually everything up to the first colon)
err <- gsub("\n", " ", err, fixed=TRUE) # remove newlines
err <- gsub(" +", " ", err) # multiple spaces to single spaces
gsub(" $", "", err) # remove trailing space
}
# returns the column name, if that is not possible then something like x[,1]
colname <- function(object, i, object.name=trunc.deparse(substitute(object)))
{
check.numeric.scalar(i)
check.index(i, object.name, object, is.col.index=TRUE, allow.negatives=FALSE)
colnames <- safe.colnames(object)
if(!is.null(colnames))
colnames[i]
else if(NCOL(object) > 1)
sprint("%s[,%g]", object.name, i)
else
sprint(object.name)
}
# if trace>0 or the func fails, then print the call to func
do.call.trace <- function(func, args,
fname=short.deparse(deparse(func), "FUNC"), trace=0)
{
stopifnot(is.logical(trace) || is.numeric(trace), length(trace) == 1)
# TODO commented out the following because it is too slow for big data
# check.do.call.args(func, args, fname)
trace <- as.numeric(trace)
if(trace > 0)
printf.wrap("%s(%s)\n", fname, list.as.char(args))
try <- try(do.call(what=func, args=args), silent=TRUE)
if(is.try.err(try)) {
if(trace == 0) # didn't print call above? then print it now
printf.wrap("\n%s(%s)\n\n", fname, list.as.char(args))
else if(trace >= 2) # TODO is this best?
printf("\n")
# Re-call func so user can do a traceback within the function. Note that
# if do.call.trace was called with try, this will be caught by that try.
# TODO is there a better way to achieve this, perhaps using tryCatch
# this could be confusing if func has side effects (unlikely)
do.call(what=func, args=args)
# should never get here
stop0("second do.call(", fname,
", ...) did not give the expected error: ", try[1])
}
invisible(try) # TODO is invisible necessary?
}
# identical to base::eval() but has trace and expr.name arguments
eval.trace <- function(
expr,
envir = parent.frame(),
enclos = if(is.list(envir) || is.pairlist(envir))
parent.frame()
else
baseenv(),
trace = 0,
expr.name = NULL)
{
stopifnot(is.environment(envir))
stopifnot(is.environment(enclos))
if(trace >= 2)
printf("eval(%s, %s)\n",
if(is.null(expr.name)) trunc.deparse(substitute(expr))
else expr.name,
environment.as.char(envir))
eval(expr, envir, enclos)
}
exp10 <- function(x) # e.g. exp10(-3) = 1e-3
{
exp(x * log(10))
}
# This function is used for checking both xlim and ylim.
# This checks that lim is is a 2 element numeric vector.
# Also, if xlim[1] == xlim[2], then plot() issues a confusing message.
# We don't want that, so use this function to make sure xlim[2]
# is different to xlim[1].
fix.lim <- function(lim)
{
if(!is.null(lim) && !inherits(lim, "Date")) {
stopifnot(is.numeric(lim), length(lim) == 2)
# constants below are arbitrary
small <- max(1e-6, .01 * abs(lim[1] - lim[2]))
if(abs(lim[2] - lim[1]) < small) # illegal lim?
lim <- c(lim[1] - small, lim[2] + small)
}
lim
}
# Ensure all columns of x have column names. Won't overwrite existing column names.
gen.colnames <- function(x, prefix="x", alt.prefix=prefix, trace=0)
{
if(NCOL(x) == 0)
return(NULL)
# If prefix is long and has characters like ( or [ then use the
# alternate prefix. This is sometimes necessary when prefix is
# generated using deparse and the arg is something like
# "cbind(trees$Volume,trees$Volume+100)"
if(any(nchar(prefix) > 30) && grepany("[([,]", prefix)) {
trace2(trace, "using alt.prefix \"%s\" instead of prefix \"%s\"\n",
alt.prefix, prefix)
prefix <- alt.prefix
}
stopifnot(length(prefix) <= NCOL(x))
prefix <- substr(prefix, 1, 60)
new.colnames <-
if(NCOL(x) == length(prefix))
prefix
else if(grepany("\\[", prefix))
new.colnames <- paste0(prefix, "[", seq_len(NCOL(x)), "]")
else
new.colnames <- paste0(prefix, seq_len(NCOL(x)))
colnames <- org.colnames <- colnames(x)
if(is.null(colnames))
colnames <- new.colnames
else {
missing <- !nzchar(colnames)
if(any(missing))
colnames[missing] <- new.colnames[missing]
}
if(length(unique(colnames)) != length(colnames))
stop0("Duplicate colname in ", paste.trunc(prefix),
" (colnames are ",
paste.with.quotes(colnames, maxlen=60), ")")
if(trace >= 2 && !identical(org.colnames, colnames))
trace2(trace, "colname%s %s now %s\n",
if(length(colnames) > 1) "s were" else " was",
if(is.null(org.colnames)) "NULL"
else paste.trunc(quotify(org.colnames)),
paste.trunc(quotify(colnames)))
colnames
}
get.mean.rsq <- function(rss, tss, wp)
{
if(is.null(wp))
wp <- repl(1, length(rss))
stopifnot(length(rss) == length(tss), length(wp) == length(tss))
total.rsq <- 0
for(iresp in seq_along(rss))
total.rsq <- total.rsq + wp[iresp] * get.rsq(rss[iresp], tss[iresp])
sum(total.rsq) / sum(wp)
}
# Get the environment for evaluating the model data:
# 1. Return the environment in which the model function
# was originally called.
# 2. Else if the model already has an attribute .Environment, use that.
# 3. Else return the environment in which the caller of this function
# was called (e.g. return the environment of plotmo's caller).
get.model.env <- function(object, object.name="object", trace=0, use.submodel=FALSE)
{
# check args, because this func is called very early in plotmo (and friends)
check.numeric.scalar(trace, logical.ok=TRUE)
if(trace >= 2) {
callers.name <- callers.name()
my.call <- call.as.char(n=2)
printf.wrap("%s trace %g: %s\n", callers.name, trace, my.call)
printf("--get.model.env for object with class %s\n", class.as.char(object))
}
stopifnot.string(object.name)
if(is.null(object))
stopf("argument %s is NULL", object.name)
if(!is.list(object))
stopf("%s is not an S3 model", object.name)
if(class(object)[1] == "list") # some packages build models without a specific class
stopf("%s is a plain list, not an S3 model", object.name)
obj <- object
# Special handling for parsnip models. Their class is like c("_earth", "model_fit").
# For these models, use the env if any saved with the submod (e.g. earth)
# (We don't do this for caret models because caret models have a terms field.)
#
# TODO this code is preliminary (works with parsnip 0.1.3)
# and only works if model saves the data (e.g. lm, earth(keepxy=TRUE), not rpart
if(use.submodel && inherits(object, "model_fit")) { # parsnip
trace2(trace, "plotmo parsnip model: will plot %s$fit, not %s itself\n",
gsub("'", "", object.name), object.name)
obj <- object[["fit"]]
if(!is.list(obj)) # sanity check
stopf("plotmo parsnip model: %s$fit is not an S3 model",
gsub("'", "", object.name))
# TODO following is temporary, hopefully
if(inherits(obj, "rpart") && is.null(obj$model))
stop0(
"Cannot plot parsnip rpart model: need model=TRUE in call to rpart\n",
" Do it like this: set_engine(\"rpart\", model=TRUE)")
}
if(trace >= 2) {
call <- getCall(obj)
if(is.null(call))
printf("object has no call field (it's class is %s)\n", class.as.char(object))
else
printf.wrap("object call is %s\n", strip.deparse(call), maxlen=120)
}
terms <- try(terms(obj), silent=trace < 3)
# Following will fail (correctly) for non-formula models because they have no terms.
#
# TODO Also, if use.submodel, don't use terms (because the term env was
# inside the parsnip func that created the submodel)
# But that also fails later when we eval the formula because
# eval will use GlobalEnv instead of the data passed to the model
if(!is.null(terms) && !is.try.err(terms)) {
model.env <- attr(terms, ".Environment")
if(is.null(model.env)) {
if(inherits(obj, "glmnet.formula") || # glmnetUtils package
inherits(obj, "cv.glmnet.formula"))
if(inherits(obj, "glmnet.formula"))
stop0(
"for this plot, glmnet.formula must be called with use.model.frame=TRUE")
if(inherits(obj, "cv.glmnet.formula"))
stop0(
"for this plot, cv.glmnet.formula must be called with use.model.frame=TRUE")
stop0("attr(terms, \".Environment\") is NULL")
}
if(!is.environment(model.env))
stop0("attr(terms, \".Environment\") is not an environment")
else {
trace2(trace, "using the environment saved in $terms of the %s model: %s\n",
class.as.char(obj), environment.as.char(model.env))
return(model.env)
}
}
model.env <- attr(obj, ".Environment")
if(is.environment(model.env)) {
trace2(trace, "using attr(obj,\".Environment\") saved with %s model: %s\n",
class.as.char(obj), environment.as.char(model.env))
return(model.env)
}
if(!is.null(model.env))
stop0("attr(obj, \".Environment\") is not an environment")
# n=2 is the caller of the function that called get.model.env
# for plotmo it will be the caller of plotmo, typically R_GlobalEnv
model.env <- parent.frame(n=2)
trace2(trace,
"assuming the environment of the %s model is that of %s's caller: %s\n",
class.as.char(obj), callers.name, environment.as.char(model.env))
model.env
}
get.rsq <- function(rss, tss)
{
rsq <- 1 - rss / tss
# following makes testing easier across machines in presence of numerical error
rsq[rsq > -1e-5 & rsq < 1e-5] <- 0
rsq
}
get.weighted.rsq <- function(y, yhat, w=NULL) # NAs will be dropped before calc
{
stopifnot(length(y) > 0, length(y) == length(yhat))
if(is.null(w)) {
is.na <- is.na(y) | is.na(yhat)
y <- y[!is.na]
yhat <- yhat[!is.na]
if(length(y) == 0)
stop0("length(y) == 0 after deleting NAs in y or yhat")
rss <- sos(y - yhat)
tss <- sos(y - mean(y))
} else {
stopifnot(length(w) == length(yhat))
is.na <- is.na(y) | is.na(yhat) | is.na(w)
y <- y[!is.na]
yhat <- yhat[!is.na]
w <- w[!is.na]
if(length(y) == 0)
stop0("length(y) == 0 after deleting NAs in y or yhat or w")
rss <- sos(y - yhat, w)
tss <- sos(y - weighted.mean(y, w), w)
}
get.rsq(rss, tss)
}
# TRUE if pattern is in any of the strings in x
grepany <- function(pattern, x, ignore.case=FALSE, ...)
{
any(grepl(pattern, x, ignore.case=ignore.case, ...))
}
# scalar form of ifelse, with short name :-)
# only evaluates the "no" argument if necessary
ife <- function(ife.test, ife.yes, ife.no)
{
ife.test <- check.boolean(ife.test)
stopifnot(!missing(ife.yes))
stopifnot(!missing(ife.no))
if(ife.test) ife.yes else ife.no
}
# returns an index, choices is a vector of strings
imatch.choices <- function(arg, choices,
argname=short.deparse(substitute(arg), "function"),
errmsg.has.index=FALSE, # TRUE if integer "arg" is legal elsewhere
errmsg="", # error message, "" for automatic
errmsg.ext="") # extension to error message
{
errmsg.ext <- paste0(
if(errmsg.has.index) " an integer index or" else "",
if(nchar(errmsg.ext)) paste0(" ", errmsg.ext, " or") else "")
if(nchar(errmsg) == 0)
errmsg <- sprint("Choose%s one of: %s", errmsg.ext, quotify(choices))
if(!is.character(arg) || length(arg) != 1 || !nzchar(arg))
stopf("illegal %s argument\n%s", quotify(argname, "'"), errmsg)
if(argname %in% c("NULL", "NA"))
argname <- "argument"
imatch <- pmatch(arg, choices)
if(anyNA(imatch)) {
imatch <- NULL
for(i in seq_along(choices))
if(pmatch(arg, choices[i], nomatch=0))
imatch <- c(i, imatch)
if(length(imatch) == 0) {
if(length(choices) == 1)
stopf("%s=\"%s\" is not allowed\n Only%s %s is allowed",
argname, paste(arg), errmsg.ext, quotify(choices))
else
stopf("%s=\"%s\" is not allowed\n%s",
argname, paste(arg), errmsg)
}
if(length(imatch) > 1)
stopf("%s=\"%s\" is ambiguous\n%s",
argname, paste(arg), errmsg)
}
imatch
}
# TRUE if all values in object are integers, ignoring NAs
# assumes object is numeric or logical (check this before call this function)
is.integral <- function(object)
{
object <- object[!is.na(object)]
length(object) > 0 &&
is.null(dim(object)) && # prevent error in floor for e.g. survival objects
all(floor(object) == object)
}
# is.specified's main purpose is to see if a plot component should be
# drawn, i.e., to see if the component "has a color"
is.specified <- function(object)
{
try <-
try(!is.null(object) && !anyNA(object) && !is.zero(object) &&
# following needed for e.g. col=c("red", 0) because 0 is converted to string
!identical(object, "0") && !identical(object, "0L") &&
!identical(object, "NA"), silent=FALSE)
if(is.try.err(try)) {
# this occurs if object is say a closure and anyNA fails
# anyNA was introduced in R 3.1.0
printf("\n") # separate from any message printed by try() above
stop0(deparse(substitute(object)), ": illegal value")
}
try
}
is.try.err <- function(object)
{
class(object)[1] == "try-error"
}
is.zero <- function(object) # needed because identical(object, 0) fails if object is 0L
{
identical(object, 0) || identical(object, 0L)
}
# Lighten color by amount 0 ... 1 where 1 is white.
# If amount is negative, then darken the color, -1 is black.
lighten <- function(col, lighten.amount, alpha=1)
{
# stopifnot.scalar(lighten.amount)
# stopifnot(lighten.amount >= -1 && lighten.amount <= 1)
rgb <- col2rgb(col) / 255
# empirically, sqrt makes visual effect of lighten.amount more linear
lighten.amount2 <- sqrt(abs(lighten.amount))
rgb <-
if(lighten.amount > 0)
rgb + lighten.amount2 * (c(1,1,1) - rgb) # move each r,g,b towards 1
else # darken
rgb - lighten.amount2 * rgb # move each r,g,b towards 0
rgb[rgb < 0] <- 0 # clamp
rgb[rgb > 1] <- 1
if(alpha == 1)
rgb(rgb[1,], rgb[2,], rgb[3,])
else
rgb(rgb[1,], rgb[2,], rgb[3,], alpha)
}
# returns the expanded arg (error msg if arg is not an allowed choice in calling func)
match.arg1 <- function(arg, argname=deparse(substitute(arg)))
{
formal.args <- formals(sys.function(sys.parent()))
formal.argnames <- eval(formal.args[[argname]])
formal.argnames[imatch.choices(arg[1], formal.argnames, argname)]
}
# returns a string, choices is a vector of strings
# error msg if arg is not an allowed choice
match.choices <- function(arg,
choices,
argname=deparse(substitute(arg)),
errmsg="", # error message ("" for automatic)
errmsg.ext="") # extension to error message
{
choices[imatch.choices(arg, choices, argname,
errmsg=errmsg, errmsg.ext=errmsg.ext)]
}
# This uses the object's .Environment attribute, which was
# pre-assigned to the object via get.model.env
# If this gives an error saying that class(model.env) is "NULL"
# then that pre-assignment wasn't done.
model.env <- function(object)
{
model.env <- attr(object, ".Environment")
if(!is.environment(model.env))
stopf("class(model.env) is \"%s\"", class(model.env)[1])
model.env
}
# Like as.data.frame() but retains the original colnames, if any, and can
# handle matrices from the Matrix etc. packages, if as.matrix() works for
# them. Also it has a stringsAsFactors argument which works even if x is
# already a data.frame.
my.data.frame <- function(x, trace, stringsAsFactors=TRUE)
{
if(is.data.frame(x)) {
if(stringsAsFactors) {
# Convert any character columns to factors. Note as.data.frame
# won't do this for us when x is already a data.frame.
# We don't have a levels argument to pass to factor()
# but I believe that this will not be a problem in the
# context in which we use my.data.frame (plotmo_x).
for(i in seq_len(length(x)))
if(is.character(x[[i]]))
x[[i]] <- factor(x[[i]])
}
return(x)
}
df <- try(as.data.frame(x, stringsAsFactors=stringsAsFactors), silent=TRUE)
if(is.try.err(df)) {
# come here for sparse matrices from the Matrix package
df <- try(as.matrix(x))
if(is.try.err(df))
stopf("Cannot convert %s object to a data.frame or matrix",
quotify(class(x)[1]))
df <- as.data.frame(df, stringsAsFactors=stringsAsFactors)
trace2(trace, "converted %s object to data.frame\n", class(x)[1])
}
colnames(df) <- safe.colnames(x) # restore original column names
df
}
# default min.nrow=3 to use fixed point only if more than intercept and one other term
my.fixed.point <- function(x, digits, min.nrow=3)
{
if(is.null(dim(x)))
x <- as.matrix(x)
if(NROW(x) >= min.nrow)
x <- apply(x, 2, zapsmall, digits+1)
x
}
# If s is a string vector s, return the number of lines in
# the element that has the most lines
# Examples: nlines(c(" ", " \n ") is 2
# nlines(c(" ", " \n") is 2
# nlines(" ") is 1
# nlines("") is 0 (special case)
nlines <- function(s)
{
if(!nzchar(s[1])) # special case, caption="" is not printed
0
else if(anyNA(s))
0
else
length(strsplit(s, "\n")[[1]])
}
paste.c <- function(object, maxlen=16) # return 'x1' or 'c(x1, x2)'
{
if(length(object) == 1)
paste.trunc(object)
else
paste0("c(", paste.trunc(object, collapse=",", maxlen=maxlen), ")")
}
paste.with.quotes <- function(object, maxlen=16) # return '"x1"' or '"x1", "x2"'
{
if(is.null(object[1]))
"NULL"
else if(length(object) == 0)
"EMPTY"
else
paste0(paste.trunc("\"", object, "\"", collapse=", ", sep="", maxlen=maxlen))
}
paste.collapse <- function(...)
{
paste(..., collapse=" ")
}
# collapse, and truncate if strings in ... are too long
paste.trunc <- function(..., sep=" ", collapse=" ", maxlen=60)
{
s <- paste(..., sep=sep, collapse=collapse)
if(nchar(s) > maxlen) {
stopifnot(maxlen > 3)
s <- paste0(substr(s, 1, maxlen-3),
if(substr(s, maxlen-3, maxlen-3) == ".") ".." # avoid 4 dots
else "...")
}
s
}
pastef <- function(s, fmt, ...) # paste the printf style args to s
{
paste0(s, sprint(fmt, ...))
}
print_first_few_elements_of_vector <- function(x, trace, name=NULL)
{
try(cat(" min", min(x), "max", max(x)), silent=TRUE)
spaces <- " "
if(!is.null(name))
spaces <- sprint("%*s", nchar(name), " ") # nchar spaces
cat0("\n", spaces, " value")
len <- if(trace >= 4) length(x)
else min(if(is.logical(x)) 20 else 10, length(x))
if(is.logical(x))
for(i in 1:len)
cat0(if(x[i]) " T" else " F")
else
for(i in 1:len)
cat0(" ", x[i])
if(length(x) > len)
cat(" ...")
cat("\n")
if(trace >= 4) {
cat("\n")
print(summary(x))
}
}
# A safe version of sprintf.
# Like sprintf except that %s on NULL prints "NULL" rather than
# preventing the entire string from being printed
#
# e.g. sprintf("abc %s def", NULL) returns an empty string -- a silent failure!
# but sprint("abc %s def", NULL) returns "abc NULL def"
#
# e.g. sprintf("abc %d def", NULL) returns an empty string!
# but sprint("abc %d def", NULL) causes an error msg (not a silent failure)
sprint <- function(fmt, ...)
{
dots <- list(...)
dots <- lapply(dots, function(e) if(is.null(e)) "NULL" else e)
do.call(sprintf, c(fmt, dots))
}
printf <- function(fmt, ...) # like c printf
{
cat(sprint(fmt, ...), sep="")
}
# like printf but wrap at terminal width
# exdent=NULL for automatic determination of xdent (line up to func opening paren)
# TODO maxlen seems to be ignored, strwrap truncates before that?
printf.wrap <- function(fmt, ..., exdent=NULL, maxlen=2000)
{
s <- paste.trunc(paste.collapse(sprint(fmt, ...)), maxlen=maxlen)
if(is.null(exdent)) {
# align to opening paren of func call e.g. "graphics::par(xxx)" or "foo$method("
# TODO this doesn't account for leading newlines if any
exdent <- 4
igrep <- gregexpr("[ ._$:[:alnum:]]+\\(", s)[[1]]
if(igrep[1] == 1) {
len <- attr(igrep, "match.length")[1]
exdent <- min(25, len)
}
}
# strwrap doesn't preserve newlines in the input string, so do it manually :(
for(i in seq_len(nchar(s))) # print leading newlines
if(substr(s, i, i) == "\n") cat0("\n") else break
cat(paste0(strwrap(s, width=getOption("width"), exdent=exdent),
collapse="\n"))
if(nchar(s) > i) for(j in nchar(s):i) # print trailing newlines
if(substr(s, j, j) == "\n") cat0("\n") else break
}
pt.cex <- function(ncases, npoints=ncases)
{
n <- if(npoints > 0) min(npoints, ncases) else ncases
if (n >= 20000) .2
else if(n >= 5000) .3
else if(n >= 3000) .4
else if(n >= 1000) .6
else if(n >= 300) .8
else if(n >= 30) 1
else 1.2
}
# like short.deparse but quotify the deparsed obj (unless the alternative is used)
quote.deparse <- function(object, alternative="object")
{
s <- strip.deparse(object)
if(nchar(s) > 60)
alternative
else
quotify(s, quote="'")
}
quote.with.c <- function(names) # return "x" or c("x1", "x2")
{
if(length(names) == 1)
sprint("\"%s\"", names)
else
sprint("c(%s)", paste0("\"", paste(names, collapse="\", \""), "\""))
}
quotify <- function(s, quote="\"") # add quotes and collapse to a single string
{ # called quotify because quote is taken
if(is.null(s))
"NULL"
else if(length(s) == 0)
paste0(quote, quote)
else if(substr(s[1], 1, 1) == "'" || substr(s[1], 1, 1) == "\"")
paste.collapse(s) # already has quotes
else
paste0(quote, paste(s, collapse=paste0(quote, " ", quote)), quote)
}
# like quotify, but use the alternative name if s is too long
quotify.short <- function(s, alternative="object", quote="\"")
{
stopifnot(is.character(s))
s <- paste0(s, collapse="")
if(nchar(s) > 60) # 60 is arb but seems ok for plot titles etc
alternative
else
quotify(s, quote)
}
quotify.trunc <- function(s, quote="\"", maxlen=60)
{
stopifnot(is.character(s))
s <- quotify(s, quote)
if(nchar(s) > maxlen) {
stopifnot(maxlen > 3)
paste0(substr(s, 1, maxlen-3), "...")
} else
s
}
range1 <- function(object, ...)
{
stopifnot(length(dim(object)) <= 2)
if(!is.null(dim(object)))
object <- object[,1]
if(is.factor(object))
c(1, nlevels(object))
else if(inherits(object, "Date")) # Sep 2020, R 4.0.2: range no longer works with Date objects
c(min(object), max(object))
else
range(object, finite=TRUE, ...)
}
recycle <- function(object, ref.object)
{
repl(object, length.out=length(ref.object))
}
repl <- function(object, length.out)
{
# following "if" added for R-2.15.3 otherwise
# get warning: 'x' is NULL so the result will be NULL
if(is.null(object))
return(NULL)
check.numeric.scalar(length.out)
stopifnot(floor(length.out) == length.out)
stopifnot(length.out > 0)
rep(object, length.out=length.out)
}
# the standard colnames() can crash for certain objects
# TODO figure out when and why
safe.colnames <- function(object)
{
colnames <- try(colnames(object), silent=TRUE)
if(is.try.err(colnames))
NULL
else
colnames
}
# if deparse(object) is too long, return the alternative
short.deparse <- function(object, alternative="object")
{
s <- strip.deparse(object)
if(nchar(s) > 60)
alternative
else
s
}
# Remove duplicates in x, then sort (smallest first).
# Also works for Dates.
sort_unique <- function(x)
{
sort(unique(x), na.last=NA) # na.last=NA drops NAs
}
sos <- function(x, weights=NULL) # sum of squares
{
if(is.null(weights))
sum(as.vector(x^2))
else {
stopifnot(length(weights) == length(x))
sum(weights * as.vector(x^2))
}
}
stop0 <- function(...)
{
stop(..., call.=FALSE)
}
stopf <- function(fmt, ...) # args like printf
{
stop(sprint(fmt, ...), call.=FALSE)
}
# stop if s is not a one element character vector
stopifnot.string <- function(s, name=short.deparse(substitute(s)),
null.ok=FALSE, allow.empty=FALSE)
{
if(name %in% c("NULL", "NA"))
name <- "argument"
if(is.null(s)) {
if(null.ok)
return()
else
stop0(quotify(name, "'"), " is NULL (it should be a string)")
}
if(!is.character(s))
stop0(quotify(name, "'"), " is not a character variable (class(",
name, ") is \"", class(s), "\")")
if(length(s) == 0)
stop0(quotify(name, "'"), " is empty (it has no elements)")
if(length(s) != 1)
stop0(quotify(name, "'"), " has more than one element\n ",
name, " = c(", paste.trunc("\"", s, "\"", sep=""), ")")
if(!allow.empty && !nzchar(s))
stop0(quotify(name, "'"), " is an empty string")
}
strip.deparse <- function(object) # deparse, collapse, remove most white space
{
s <- strip.space.collapse(deparse(object))
gsub(",", ", ", s) # put back space after commas
}
strip.space <- function(s)
{
gsub("[ \t\n]", "", s)
}
strip.space.collapse <- function(s) # returns a single string
{
gsub("[ \t\n]", "", paste(s, collapse="")) # paste converts vec to single
}
# like text, but with a white background
# TODO sign of adj is backwards?
text.on.white <- function(x, y, label,
cex=1, adj=.5, font=1, xmar=.3, srt=0, white="white", ...)
{
stopifnot(length(label) == 1)
if(length(adj) == 1)
adj <- c(adj, .5)
width <- strwidth(label, cex=cex, font=font)
char.width <- strwidth("X", cex=cex, font=font)
height <- strheight(label, cex=cex, font=font)
char.height <- strheight("X", cex=cex, font=font)
if(srt == 0) {
if(is.specified(label))
rect(x - adj[1] * width - xmar * char.width,
y - adj[2] * height - .3 * char.height, # .3 for extra space at bottom
x + (1-adj[1]) * width + xmar * char.width,
y + (1-adj[2]) * height + .1 * char.height,
col=white, border=NA)
text(x=x, y=y, labels=label, cex=cex, adj=adj, font=font, ...)
} else if(srt == 90 || srt == -90) {
# width and height are in usr coords, adjust these for flip of coords
usr <- par("usr") # xmin, xmax, ymin, ymax
xrange <- abs(usr[2] - usr[1])
yrange <- abs(usr[4] - usr[3])
height <- xrange / yrange * height
width <- yrange / xrange * width
char.height <- xrange / yrange * char.height
char.width <- yrange / xrange * char.width
if(is.specified(label))
rect(x + (1-adj[1]) * height, # left
y + (1-adj[2]) * width + xmar * char.width, # bottom
x - adj[1] * height, # right
y - adj[2] * width - xmar * char.width, # top
col=white, border=NA)
text(x=x, y=y, labels=label, cex=cex, adj=adj, font=font, srt=srt, ...)
}
else
stop0("srt=", srt, " is not allowed (only 0, 90, and -90 are supported)")
}
to.logical <- function(object, len) # object can be a boolean or numeric vector
{
xlogical <- repl(FALSE, len)
xlogical[object] <- TRUE
xlogical
}
trace0 <- function(trace, fmt, ...)
{
stopifnot(!(is.numeric(trace) && is.logical(trace)))
if(trace >= 0)
cat(sprint(fmt, ...), sep="")
}
trace1 <- function(trace, fmt, ...)
{
stopifnot(!(is.numeric(trace) && is.logical(trace)))
if(trace >= 1)
cat(sprint(fmt, ...), sep="")
}
trace2 <- function(trace, fmt, ...)
{
stopifnot(is.numeric(trace))
if(trace >= 2)
cat(sprint(fmt, ...), sep="")
}
# Truncate deparse(object) if it is too long.
# Necessary because deparse(substitute(x)) might return something very
# long, like c(1000, 1001, 1002, 1003, 1004, 1005, 1006, 1008, 1009, etc.)
# Return a one element character vector.
trunc.deparse <- function(object, maxlen=60)
{
s <- strip.deparse(object)
if(nchar(s) > maxlen) {
stopifnot(maxlen > 3)
paste0(substr(s, 1, maxlen-3), "...")
} else
s
}
# Return the number of lines in s (where lines are separated by \n).
try.eval <- function(
expr,
envir = parent.frame(),
trace = 0,
expr.name = NULL,
silent = trace < 2)
{
if(trace && is.null(expr.name))
expr.name <- trunc.deparse(substitute(expr))
try(eval.trace(expr, envir, trace=trace, expr.name=expr.name), silent=silent)
}
unquote <- function(s) # remove leading and trailing quotes, if any
{
if(is.character(s)) {
s <- gsub("^\"|^'", "", s) # leading quotes
s <- gsub("\"$|'$", "", s) # trailing quotes
}
s
}
# warn.if.not.all.finite helps preempt confusing message from code later.
# Return TRUE if warning issued.
warn.if.not.all.finite <- function(object, text="unknown")
{
is.factors <- sapply(object, is.factor)
if(any(is.factors)) {
if(NCOL(object) == 1 || all(is.factors)) # TODO suspect
return(FALSE)
object <- object[, !is.factors] # remove factor columns before is.finite check
}
if(any(sapply(object, is.na))) {
warning0("NA in ", text)
return(TRUE)
}
if(!all(sapply(object, is.finite))) {
warning0("non finite value in ", text)
return(TRUE)
}
FALSE
}
warnf <- function(fmt, ...) # args like printf
{
warning(sprint(fmt, ...), call.=FALSE)
}
warning0 <- function(...)
{
warning(..., call.=FALSE)
}
# Binomial pairs response: fraction true for each row.
#
# This function is used by both earth and plotmo.
# If you change it here, change it there too.
#
# The first column of y is considered to be "true", the second "false".
#
# Example y:
# survived died
# 1 1
# 0 0 # both values zero
# 3 4
#
# becomes:
# survived
# .5 # 1 / (1 + 1)
# 0 # special case (both survived and died equal to 0)
# .43 # 3 / (3 + 4)
bpairs.yfrac <- function(y, trace)
{
stopifnot(NCOL(y) == 2)
both.zero <- (y[,1] == 0) & (y[,2] == 0)
y[both.zero, 2] <- 1 # so zero rows will be translated to 0 in next line
yfrac <- y[, 1, drop=FALSE] / (y[,1] + y[,2]) # fraction true
trace.bpairs.yfrac(yfrac, trace)
yfrac
}
trace.bpairs.yfrac <- function(yfrac, trace)
{
# based on code in print.earth.fit.args
if(trace >= 4)
cat("\n")
if(trace >= 1 && trace < 7) { # don't print matrices when doing very detailed earth.c tracing
tracex <- if(trace >= 5) 4 else 2 # adjust trace for print_summary
details <- if(trace >= 4) 2 else if(trace >= 1) -1 else 0
print_summary(yfrac, "yfrac", tracex, details=details)
if(details > 1) printf("\n")
}
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.