##############################################################################
# Copyright (c) 2008-2010, 2012-2016 Russell V. Lenth #
# #
# This file is part of the rsm package for R (*rsm*) #
# #
# *rsm* 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. #
# #
# *rsm* 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/> #
##############################################################################
### Functions to facilitate response-surface analysis
# Nov 2012 mod: changed naming of effects...
# FO(x1,x3) --> FO(x1,x3)x1, FO(x1,x3)x3
# TWI(x1,x3) --> TWI(x1,x3)x1:x3
# PQ(x1,x3) --> PQ(x1,x3)x1^2, PQ(x1,x3)x3^2
# First-order model
FO = function(...) {
nm = as.character(substitute(list(...)))[-1]
fo = sapply(list(...), I)
if (is.null(nrow(fo))) fo = matrix(fo, nrow=1)
dimnames(fo) = list(NULL, nm)
fo
}
#### I tried developing a formula interface for FO, TWI, and SO.
#### Decided it is NOT a good idea
# # New version of FO that supports a formula
# FO = function(...) {
# env = parent.frame()
# .make.matrix = function(vars) {
# form = as.formula(paste("~", paste(vars, collapse="+"), "-1"))
# model.matrix(form, data=env)
# }
# if(inherits(form <- list(...)[[1]], "formula"))
# .make.matrix(all.vars(form))
# else
# .make.matrix(sapply(match.call()[-1], as.character))
# }
# Pure quadratic
PQ = function(...) {
X = FO(...)^2
nm = dimnames(X)[[2]]
if (is.null(nm)) nm = 1:ncol(X)
dimnames(X) = list(NULL, paste(nm,"2",sep="^"))
X
}
# # New version of PQ that supports a formula
# # Identical to FO except for squaring and renaming
# PQ = function(...) {
# env = parent.frame()
# .make.matrix = function(vars) {
# form = as.formula(paste("~", paste(vars, collapse="+"), "-1"))
# X = model.matrix(form, data=env)^2
# dimnames(X)[[2]] = paste(vars,"2", sep="^")
# X
# }
# if(inherits(form <- list(...)[[1]], "formula"))
# .make.matrix(all.vars(form))
# else
# .make.matrix(sapply(match.call()[-1], as.character))
# }
# Two-way interactions
# Nov 2012 -- aded formula argument
TWI = function(..., formula) {
if (missing(formula)) {
fo = FO(...)
k = ncol(fo)
fon = dimnames(fo)[[2]]
if (is.null(fon)) fon=1:k
X = matrix(0, nrow=nrow(fo), ncol=k*(k-1)/2)
nm = rep("", k*(k-1)/2)
col = 1
for (i in 1:(k-1)) {
for (j in (i+1):k) {
X[, col] = fo[ ,i] * fo[ ,j]
nm[col] = paste(fon[i],fon[j],sep=":")
col = col+1
}
}
dimnames(X) = list(NULL,nm)
X
}
else { # formula is provided
if (!inherits(formula, "formula"))
formula = as.formula(paste("~", formula))
trms = terms(formula)
attr(trms, "intercept") = 0
X = model.matrix(trms, data=parent.frame())[, attr(trms, "order")==2, drop=FALSE]
if(ncol(X) == 0)
stop("Formula yields no two-way interactions. Re-specify or omit 'TWI' term from model")
else if (ncol(X) == 1) {
new.expr = paste("TWI(", gsub(":", ",", dimnames(X)[[2]]), ")", sep="")
stop(paste("Result is just one column. Revise the model using '",
new.expr, "'", sep=""))
}
X
}
}
# Second-order model. But in rsm(), this will get replaced by FO()+TWI()+PQ()
SO = function(...)
cbind(FO(...), TWI(...), PQ(...))
# Pure-error model
PE = function(...)
factor(paste(...))
# Fit a response-surface model
rsm = function (formula, data, ...) {
CALL = match.call(stats::lm)
CALL[[1]] = as.name("lm")
oc = paste(as.character(deparse(formula)), collapse = " ")
nc = sub("SO\\(([a-zA-Z0-9, ._]+)\\)", "FO\\(\\1\\) + TWI\\(\\1\\) + PQ\\(\\1\\)", oc)
# no comma -> only 1 var -> no TWI ...
nc = sub("TWI\\([a-zA-Z0-9 ._]+\\)", "", nc)
CALL$formula = formula(nc)
LM = eval(CALL, parent.frame())
LM$call[[1]] = as.name("rsm")
LM$call$formula = formula(oc)
if (missing(data))
data = as.data.frame(sapply(all.vars(formula), get))
LM$data = data
newlabs = nm = names(LM$coef)
names(newlabs) = nm
i.fo = grep("FO\\(", nm)
if (length(i.fo) == 0) {
warning("No FO() terms in model; cannot use RSM methods\nAn 'lm' object has been returned.")
return(LM)
}
k = length(i.fo)
LM$b = LM$coef[i.fo]
LM$order = 1
foterm = as.list(LM$terms[LM$assign[min(i.fo)]][[3]])
fonm = names(LM$b) = sapply(foterm, as.character)[-1]
#-DEPR LM$labels = list(FO=list(idx=i.fo, lab=fonm))
newlabs[i.fo] = fonm
#-depr names(LM$coef)[i.fo] = LM$labels
LM$B = matrix(0, k, k)
dimnames(LM$B) = list(fonm, fonm)
i.twi = grep("TWI\\(", nm)
if ((k > 1) & (length(i.twi) > 0)) {
btwi = LM$coef[i.twi]
LM$order = 1.5
twi.lab = sapply(names(btwi), function(s) {
# Below, usually "TWI(arguments)colname" --> lb = c("TWI", arguments, colname)
# so that 3rd elt is colname. But if arguments has a formula with parens, could be longer
# Messy part: If there is only one column in TWI result, colname will be missing
# In TWI code, I force an error unless call is made w/o a formula so that
# we can be sure to be able to parse "TWI(x1, x2)" into "x1:x2"
lb = strsplit(s, "\\(|\\)")[[1]]
if (length(lb) >= 3) rev(lb)[1]
else {
tmp = gsub(" ","", lb[2])
gsub(",", ":", tmp)
}
})
names(twi.lab) = NULL
for (i in 1:length(twi.lab)) {
vn = strsplit(twi.lab[i], ":")[[1]]
idx = match(vn, fonm)
if (!is.na(btwi[i]))
LM$B[idx[1],idx[2]] = LM$B[idx[2],idx[1]] = btwi[i] / 2
else
twi.lab[i] = paste(twi.lab[i],"@", sep="")
}
#-DEPR LM$labels$TWI = list(idx=i.twi, lab=twi.lab)
newlabs[i.twi] = twi.lab
}
i.pq = grep("PQ\\(", nm)
if (length(i.pq) > 0) {
LM$order = 2
if(length(i.pq) > 1)
pq.lab = sapply(names(LM$coef[i.pq]), function(s) strsplit(s, "\\)")[[1]][2])
else
pq.lab = paste(strsplit(names(LM$coef[i.pq]), "\\(|\\)")[[1]][2], "^2", sep="")
names(pq.lab) = NULL
vn = sapply(pq.lab, function(s) substr(s, 1, nchar(s)-2))
for (i in 1:length(vn)) LM$B[vn[i],vn[i]] = LM$coef[i.pq[i]]
#-DEPR LM$labels$PQ = list(idx=i.pq, lab=pq.lab)
newlabs[i.pq] = pq.lab
}
LM$newlabs = newlabs
if (LM$order==1)
aliased = any(is.na(LM$b))
else
aliased = any(is.na(cbind(LM$B, LM$b)))
if (aliased)
warning("Some coefficients are aliased - cannot use 'rsm' methods.\n Returning an 'lm' object.")
else {
if (!is.null(data))
if (inherits(data, "coded.data"))
LM$coding = attr(data, "codings")
class(LM) = c("rsm", "lm")
}
LM
}
# do a lack-of-fit test
loftest = function (object) {
cl = match.call(lm, call = object$call)
cl[[1]] = as.name("lm")
pieces = as.character(object$call$formula)
pieces[3] = sub("(FO)|(SO)", "PE", pieces[3])
cl$formula = formula(paste(pieces[2], "~", pieces[3]))
cl$data = object$data
lof = anova(object, eval(cl))
df = c(lof[1,1], lof[2,3], lof[2,1])
ss = c(lof[1,2], lof[2,4], lof[2,2])
ans = data.frame (
df, ss, ss/df, c(NA, lof[2,5], NA), c(NA, lof[2,6], NA),
row.names = c("Model residual", "Lack of fit", "Pure error"))
names(ans) = c("Df","Sum Sq","Mean Sq", "F value","Pr(>F)")
class(ans) = class(lof)
ans
}
# Summary method
summary.rsm = function (object, adjust = rev(p.adjust.methods), ...) {
# figure out which dots to pass to summary.lm
dots = list(...)
adjust = match.arg(adjust)
tidx = pmatch(names(dots), "threshold")
if (!all(is.na(tidx))) {
threshold = dots[!is.na(tidx)][1]
dots[!is.na(tidx)] = NULL
}
else
threshold = 1e-4
dots$object = object
SUM = do.call("summary.lm", dots)
if (adjust != "none") {
SUM$coefficients[, 4] = p.adjust(SUM$coefficients[, 4], adjust)
attr(SUM$coefficients, "adjust") = adjust
}
if (object$order > 0) {
if (!is.null(object$labels)) ### compatibility with old objects
for (lst in object$labels)
row.names(SUM$coefficients)[lst$idx] = lst$lab
else {
idx = match(row.names(SUM$coefficients), names(object$newlabs))
row.names(SUM$coefficients)[1:length(idx)] = object$newlabs[idx]
}
}
if (object$order > 1)
SUM$canonical = canonical(object, threshold=threshold)
else SUM$sa = object$b/sqrt(sum(object$b^2))
SUM$lof = rbind(anova(object), loftest(object)[-1,])
SUM$coding = object$coding
class(SUM) = c("summary.rsm", "summary.lm")
SUM
}
# Print method for summary
print.summary.rsm = function(x, ...) {
### --- replace: getS3method("print", "summary.lm") (x, ...)
### Just show the call and coefs; skip the resid summary
cat("\nCall:\n", paste(deparse(x$call), sep = "\n", collapse = "\n"),
"\n\n", sep = "")
printCoefmat(x$coefficients, ...)
adj = attr(x$coefficients, "adjust")
if(!is.null(adj))
cat(paste0("P-value adjustment: \"", adj, "\"\n"))
cat("\n")
# This block is "borrowed" from print.summary.lm
digits = list(...)$digits
if (is.null(digits))
digits = max(3L, getOption("digits") - 3L)
if (!is.null(x$fstatistic)) {
cat("Multiple R-squared: ", formatC(x$r.squared, digits = digits))
cat(",\tAdjusted R-squared: ", formatC(x$adj.r.squared, digits = digits),
"\nF-statistic:", formatC(x$fstatistic[1L], digits = digits),
"on", x$fstatistic[2L], "and", x$fstatistic[3L],
"DF, p-value:", format.pval(pf(x$fstatistic[1L],
x$fstatistic[2L], x$fstatistic[3L], lower.tail = FALSE),
digits = digits))
cat("\n")
}
cat("\n")
print(x$lof, signif.stars=FALSE, ...)
cat("\n")
can = x$canonical
if (!is.null(can)) {
cat("Stationary point of response surface:\n")
print(can$xs)
if(!is.null(x$coding)) {
cat("\nStationary point in original units:\n")
print (code2val (can$xs, x$coding))
}
cat("\nEigenanalysis:\n")
print(can$eigen)
}
else {
cat("Direction of steepest ascent (at radius 1):\n")
print(x$sa)
cat("\nCorresponding increment in original units:\n")
temp = code2val (rbind(x$sa, 2*x$sa), x$coding)
print (temp[2,] - temp[1,])
}
cat("\n")
}
# Steepest ascent (and ridge analysis)
steepest = function (object, dist=seq(0,5,by=.5), descent=FALSE) {
goal = ifelse(descent, "descent", "ascent")
dist = abs (dist)
if (is.null(object$B)) {
d = object$b / sqrt (sum (object$b^2))
if (descent) d = -d
path = t(sapply(dist, function(x) d*x))
cat(paste("Linear path of steepest", goal, "\n"))
}
else {
iden = diag (rep (1, length(object$b)))
rng = range (eigen (object$B) $values)
soln = function (gam) {
-0.5 * solve (object$B - gam*iden, object$b)
}
deldist = function (gam, d) {
xx = soln (gam)
sqrt (sum (xx^2)) - d
}
find.pt = function(d, bd) {
if (abs(d) < .01) return (0 * object$b)
gamma = uniroot (deldist, bd, d)$root
soln (gamma)
}
incr.out = function(bd, inc, mind) {
while (deldist(bd, mind) > 0) {
bd = bd + inc
inc = 2*inc
}
bd
}
mind = min(dist[dist>.009])
if (descent)
bds = c(incr.out(rng[1]-5, -2, mind), rng[1]-.001)
else
bds = c(rng[2]+.001, incr.out(rng[2]+5, 2, mind))
path = t(sapply(dist, find.pt, bds))
cat(paste("Path of steepest", goal, "from ridge analysis:\n"))
}
path = newdata = as.data.frame (round (path, 3))
md = model.data(object)
for (vn in names(md))
if (is.null(newdata[[vn]])) {
v = md[[vn]]
if(is.factor(v))
newdata[[vn]] = factor(levels(v)[1], levels=levels(v))
else newdata[[vn]] = mean(v)
}
yhat = predict(object, newdata=newdata)
path[["|"]] = factor("|")
if (!is.null(object$coding)) {
orig = code2val(path, object$coding)
path = cbind(path, orig)
}
ans = cbind(data.frame(dist=dist), path, yhat=round(yhat,3))
ans
}
canonical.path = function(object,
which = ifelse(descent, length(object$b), 1),
dist = seq(-5, 5, by=.5),
descent = FALSE,
...)
{
if (!inherits(object, "rsm"))
stop(paste(as.character(substitute(object)),"is not an 'rsm' object"))
if (object$order == 1)
stop("Requires a seconnd-order response surface")
args = list(object = object, ...)
can = do.call(canonical, args)
dir = can$eigen$vectors[ , which]
path = t(sapply(dist, function(d) can$xs + d*dir))
path = newdata = as.data.frame(round(path, 3))
md = model.data(object)
for (vn in names(md)) if (is.null(newdata[[vn]])) {
v = md[[vn]]
if (is.factor(v))
newdata[[vn]] = factor(levels(v)[1], levels = levels(v))
else newdata[[vn]] = mean(v)
}
yhat = predict(object, newdata = newdata)
path[["|"]] = factor("|")
if (!is.null(object$coding)) {
orig = code2val(path, object$coding)
path = cbind(path, orig)
}
ans = cbind(data.frame(dist = dist), path, yhat = round(yhat, 3))
ans
}
# Canonical analysis -- allows singular B matrix and may set a
# higher threshold on e'vals considered to be zero
canonical = function(object, threshold = 0.1*max.eigen) {
if (!inherits(object, "rsm"))
stop ("Not an 'rsm' object")
if (object$order == 1)
stop("Canonical analysis is not possible for first-order models")
EA = eigen(object$B)
max.eigen = max(abs(EA$values))
active = which(abs(EA$values) >= threshold)
if (length(active) == 0)
stop("threshold is greater than the largest |eigenvalue|")
if ((nzero <- length(EA$values) - length(active)) > 0)
message("Near-stationary-ridge situation detected -- stationary point altered\n",
" Change 'threshold' if this is not what you intend")
U = EA$vectors[, active, drop=FALSE]
laminv = 1 / EA$values[active]
xs = as.vector(-0.5 * U %*% diag(laminv, ncol=ncol(U)) %*% t(U) %*% object$b)
names(xs) = names(object$b)
dimnames(EA$vectors) = list(names(object$b), NULL)
if (length(active) < nrow(U)) {
###EA$vectors[, -active] = 0
EA$values[-active] = 0
}
list(xs=xs, eigen=EA)
}
xs = function(object, ...) {
canonical(object, ...)$xs
}
# Unfortunately, it turns out that rsm's 'codings' member is named "coding".
# Too late now, as people may have old rsm objects laying around.
codings.rsm = function(object)
object$coding
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.