Evaluates a fitted linear predictor on a grid of covariate values and returns it with two intervals: the usual pointwise interval, and a SIMULTANEOUS band that covers the whole curve at once.
Usage
frm_curve(
object,
newdata,
contrast = NULL,
dpar = NULL,
resp = NULL,
re_formula = NA,
allow_new_levels = FALSE,
level = 0.95,
simultaneous = TRUE,
nsim = 10000L,
transform = FALSE,
seed = NULL,
tol = 1e-06
)Arguments
- object
A
frmtmb_fitfromfrm().- newdata
The grid, as a data frame. Every variable the linear predictor reads must be a column, held at the value the curve is wanted at.
- contrast
A second grid with the same number of rows, or
NULLfor an ordinary curve. With it the curve isnewdataminuscontrast, row by row.- dpar
Distributional parameter to read the curve off.
NULL, the default, is the location parametermu.- resp
Response name, for a multivariate fit.
- re_formula
NA(the default) drops the(x | g)group-level terms, the conventionmgcvandgratiaplot. It KEEPS every smooth, as brms does, a smooth indexed by a grouping factor included, so on a model withs(t, g, bs = "fs")the grid has to carrygatNAtoo and the curve is that level's.NULLkeeps every random effect, so the grid must carry every grouping column.?frm_linpredhas the full rule.- allow_new_levels
Allow a grouping level the fit did not see.
FALSE, the default, refuses such a row by name.TRUEpredicts it at the population level, and it is the way to read the POPULATION curve off a model withs(t, g, bs = "fs"): putgin the grid at a level the fit did not see, and the factor smooth contributes nothing. Passed to everyfrm_lp_basis()andfrm_linpred()call, so the design, the estimate and the covariance check read the same rows. See the section "The population curve of a factor-smooth model" offrm_curve(). At an unseen level of a termre_formulakeeps, such as(1 | g)atNULL, every route carries that level's draw; see the section "An unseen level of a(1 | g)term" offrm_curve().- level
Coverage of both intervals.
- simultaneous
Compute the simultaneous band.
FALSEreturns the pointwise interval alone and skips the simulation.- nsim
Draws in the max-deviation simulation. The default 10000 puts the Monte Carlo error of the critical value near 0.013; 200000 puts it near 0.003.
- transform
Return the curve and both bands through the link inverse. The bands are transformed end to end rather than rebuilt, which keeps their coverage under any monotone link.
- seed
Seed for the simulation, for a reproducible band.
- tol
Largest relative disagreement with
frm_linpred(se.fit = TRUE)the assembled covariance may show before the call refuses.
Value
A data frame of class frmtmb_curve: the columns of
newdata, .eta where there are several curves, then .estimate,
.se, .crit, .lower_ci,
.upper_ci, and when simultaneous = TRUE also .crit_sim,
.lower_sim and .upper_sim. The grid covariance, A V A' plus
the variance that is not coefficient uncertainty, is the "Sigma"
attribute, the fit is the "fit" attribute, and "check" carries
the covariance agreement and the predict() call count.
print() shows the critical values, the covariance check and the
first six rows. Its digits = is passed to print.data.frame()
for those rows: significant digits, getOption("digits") when not
given. The header lines keep their own precision.
Details
A pointwise interval is the wrong tool for the question a curve
usually raises. "Is the velocity above zero at 300 ms" is pointwise;
"does this curve have the shape I claim" is a statement about every
point at once, and a 95 percent pointwise band covers the whole curve
far less than 95 percent of the time. The simultaneous band is the
max-deviation simulation of Ruppert, Wand and Carroll (2003, ch. 6):
draw the curve's own deviation process from its joint covariance,
standardize each draw by the pointwise standard error, take the
largest absolute value over the grid, and use the level quantile of
those maxima in place of qnorm(0.975). It is the construction
gratia::confint(type = "simultaneous") uses on an mgcv fit, and
this simulation reproduces gratia's critical value inside its
Monte Carlo error.
The route to the covariance
A penalized smooth's wiggly part is a random-effect block in the fitted objective even when the smooth is a population term, so the covariance of a curve needs the joint covariance of the fixed AND random coefficients.
All of it comes from ONE call to frmtmb's exported seam,
frm_lp_basis(), which returns the design A over the
coefficient vector, the joint covariance V at exactly the rows A's
columns sit at, and the variance that is NOT coefficient uncertainty
(a new grouping level's draw, an exact gp()'s kriging residual) as
a separate covariance over the grid rows, extra_cov. Sigma is
A V A' + extra_cov, and the pointwise standard error is
sqrt(diag(Sigma)). The simultaneous band draws from the same
Sigma it standardizes by.
Up to frmtmb 0.51.0 there was no such seam. frmtmb.spline, where
this function lived until it moved into frmtmb, rebuilt A by unit
perturbation, one predict() call per contributing coefficient, and
read V out of fit$cache$Vjoint, which was an internal with no
precedent to point at. One call replaced the reconstruction and the
reach, and what the covariance check verifies has changed from "the
reconstruction reproduced core's number" to "the seam is being read
correctly".
The check itself stays. Every call compares that pointwise standard
error with frm_linpred(se.fit = TRUE), a second route through core,
and refuses when the two disagree by more than tol. The measured
agreement is in the "check" attribute and is reported by print();
it is at machine precision, 2.2e-16 relative on the curve-inference
vignette's model.
The one case with nothing to check against is a nonlinear (nl = TRUE) body: frm_linpred(se.fit = TRUE) is refused there, so
frm_lp_basis() is the only route to the number and cov_rel_error
is NA. print() says so rather than reporting a check that never
ran.
Cost
What this call costs is dominated by ONE thing: the single
frm_linpred(se.fit = TRUE) check call, inside which core inverts the
fit's joint precision matrix over EVERY coefficient, including the
ones this curve does not touch. Measured at re_formula = NA on a
20-point grid, one process each:
s(x, k = 10), 8 random coefficients: 0.29 s.s(t, k = 8) + (1 + t | subject), 1000 subjects and 2006 random coefficients: 0.98 s.the same over 4000 subjects, 8006 random coefficients: 6.87 s.
The design rebuild that used to sit beside those figures, and that
was a tenth of them at every size, is gone: frm_lp_basis() returns
the design core already had, so the predict() call count no longer
depends on the number of coefficients at all. The joint-precision
solve is now the whole cost, it is paid once because core memoizes
it, and it grows with the total number of coefficients in the fit
rather than with the grid.
A difference curve
contrast is a second grid of the same height. The curve returned is
then the DIFFERENCE of the two linear predictors, row by row, and its
covariance is (A1 - A2) V (A1 - A2)', so both bands describe the
difference and the simultaneous one answers "is this difference
anywhere other than zero" over the whole grid at once. It is the
quantity gratia::difference_smooths(group_means = TRUE) reports for
a factor-by smooth, and on the same mgcv fit the two agree. Name that
argument when you compare: gratia's default, group_means = FALSE,
zeroes the intercept and the parametric group columns and reports the
smooth-only difference, which is a different quantity. On the fixture
test-difference.R uses it is 0.52 away.
A difference is NOT the difference of two calls to this function. The two curves share coefficients, so their covariance is what the difference is made of, and adding two standard errors in quadrature would ignore it.
What the difference path cannot do, and refuses by name:
transform = TRUE. A difference of linear predictors is not a difference of responses under any link but the identity, so there is nothing to map it through.Two grids that load on different coefficients.
Two grids that load DIFFERENT draws of a latent field whose variance is not coefficient uncertainty. See the next section.
allow_new_levels = TRUEwhen either grid carries such a variance. An unseen level loads no column of the design, so two different unseen levels look the same as one, and treating them as one would cancel two independent draws. Measured on(1 | subject): the difference standard error came back as exactly 0 where it is at least 0.0515. An unseen level of anfssmooth carries no such variance and is not refused.
The covariance check also means less here, and print() says so.
frm_linpred(se.fit = TRUE) returns a marginal standard error per row
and never the covariance between the grids, so the check runs on each
half and cov_rel_error is the worse of the two: what it licenses is
that both designs were read correctly.
An exact gp() under a difference
An exact gp() evaluated off the observed positions carries
a kriging residual that is not coefficient uncertainty.
frm_lp_basis() returns its variance one number per ROW and
returns no covariance BETWEEN two grids, so var(g1 - g2) has no
public route.
It needs none in the ordinary case. A contrast taken across a factor
at ONE gp() position leaves both grids loading the same residual,
so it cancels exactly and the difference is (A1 - A2) V (A1 - A2)'
with nothing left over. That case is computed rather than refused.
Sameness is decided on the design and not on the numbers: the two
grids must agree bit for bit on EVERY column of A outside the fixed
effects. Equality of the variances would not be enough, because two
levels of one grouping block have identical marginal variances by
construction and are different draws.
That test is stricter than the mathematics needs, and it is worth
knowing where the extra strictness bites. Only the block carrying the
kriging residual has to match for the residual to cancel, but the
test asks it of every latent column, so a contrast across fac on
y ~ fac + s(x, by = fac) + gp(x) is REFUSED even though the gp()
columns are identical: the by-factor smooth's own columns differ,
which is what a by-factor smooth is for. It fails closed, so the cost
is an answer you do not get rather than one you should not trust.
The rest is refused because the SEAM cannot supply it, not because
the mathematics is missing. The conditional cross-covariance is
k(x1, x2) - Xr1 K Xr2', and core forms every piece of it while
predicting, but reduces the result to one variance per row before the
seam returns. Hold every latent term equal between the grids and
contrast a fixed effect, or read the two curves separately.
The population curve of a factor-smooth model
re_formula = NA keeps a factor smooth such as
s(t, subject, bs = "fs"), as brms does, so on
v ~ s(t) + s(t, subject, bs = "fs") it gives one subject's curve
and the grid must name a subject in every row. To read the
POPULATION curve, name a subject the fit did not see and pass
allow_new_levels = TRUE. mgcv's fs basis gives an unknown level a
row of zeros, so the factor smooth contributes nothing:
pop <- factor("new", levels = c(levels(d$subject), "new"))
frm_curve(fit, newdata = data.frame(t = tt, subject = pop),
allow_new_levels = TRUE)The band is then the uncertainty of the intercept and the s(t)
coefficients alone. frm_lp_basis() adds no variance for an
unseen fs level: its extra_var is exactly zero and the factor
smooth's columns of A are exactly zero. Measured on the
curve-inference vignette's model, 7200 rows over 20 subjects: the
standard errors are identical() to the same seam restricted by
hand to the intercept and s(t) columns, and agree with
mgcv::predict.gam(exclude = "s(t,subject)", se.fit = TRUE) on an
mgcv ML fit of the same model to a ratio between 1.000 and 1.004.
Two unseen fs levels are the population curve twice, so a
contrast between them is exactly 0 with a standard error of 0. That
is NOT the difference between two new subjects, which this function
cannot report.
An unseen level of a (1 | g) term
An unseen level of a bar term is different from an unseen fs
level. Where re_formula keeps the term, the unseen level is ONE
draw of its block's effect shared by every row at that level, so the
covariance of the grid is A V A' plus that draw's covariance, which
frm_lp_basis(extra_cov = TRUE) returns. Every route reads it: the
pointwise and simultaneous bands, frm_curve_deriv(), where a random
intercept moves the curve and not its slope and a random slope adds
its own variance to the slope's, and frm_curve_feature(). A
contrast at the SAME unseen level cancels the draw; one between two
different unseen levels carries both. Before frmtmb returned the
covariance, the bands drew from A V A' alone, and the simultaneous
critical value was 0.646 on (1 | subject) and 0.615 on
(1 + t | subject) on the curve-inference vignette's data, against
2.146 and 2.472 from the full covariance, so those routes refused.
re_formula = NA drops the term and gives the population curve on
every route.
An exact gp() past its positions
An exact gp() evaluated at a position the fit did not see carries
the field's kriging residual, one draw of the field shared by every
unseen row, and its covariance enters Sigma the same way. The band
therefore widens where the grid leaves the observed positions. The
1e-12 nugget frmtmb puts on the kernel's correlation is white noise
at each position, which has no derivative: frm_curve_deriv() and a
peak's standard error leave it out.
Several predictors in one call
With no dpar, an ordinal family with a cs() term has one curve per
threshold, eta + cs_k, and a family with several location predictors has
one per predictor: the predictors frm_linpred() returns as columns, which
is what brms's posterior_linpred() returns as layers. The result holds
every row of the grid once per curve, the column .eta naming it, in the
order as.numeric() reads that matrix. The simultaneous band covers all the
curves at once, and "Sigma" is over all the rows, so the covariance
between two thresholds is there. frm_curve_deriv() has the same layout, and
its derivative in a cs() variable differs by threshold.
frm_curve_feature() searches each curve on its own and adds .eta to its
table. dpar = "mu" reads the shared part, one curve, as in brms.
A me() term
newdata must hold the measurement SD. The curve reads the observed value,
and the band carries the variance of a new row's latent value, as
frm_linpred() does. So .se is not coefficient uncertainty alone. That
variance is white noise between rows, so a derivative and the location of a
feature leave it out. Under me(gr = ) a level holds one value, so its noisy
variable has no derivative and no feature within a level, and both refuse.
With contrast, two rows at one x and one sdx, to all.equal()'s
tolerance, read one draw, and it cancels, as a kriging residual at one
position does: a contrast in z has the standard error of z's coefficient,
0.051 at a coefficient standard error of 0.051. brms has no such curve. A
difference of its draws of the two grids reads a draw per row and has a
standard deviation of 0.96 on that fit. Under me(gr = ) one level is one
draw in one grid in both packages.
Two readers answer two questions, and each is right for its own:
frm_curve(contrast = )answers for one unit with one unknownx*, reported asx, compared at two values ofz.x*is not in that difference.emmeans()contrasts answer for two new rows, each with its own measurement error, as brms's draws do. They carry both draws: 0.97 on that fit.
For the effect of z, read frm_curve(contrast = ) or the coefficient. For
the effect of x, read frm_curve_deriv(), the slope's 0.053 on that fit: a
contrast in x itself has two values of x, so it reads two draws, 0.998.
Past a ps() knot span
A ps() basis is a partition of unity only between its
frozen outer knots. Past them it is a partial sum that decays to
zero, so a curve drawn there bends smoothly to whatever the rest of
the body gives, which is exactly the shape a reader does not
question. predict(newdata = ) says so, and so does
frm_lp_basis(), the seam this function reads. It is
surfaced again here, ONCE per ps() term per call and carrying the
span, so that the sentence names the function you called: this one
reads the seam on the grid, but frm_curve_deriv() reads it on a
three-point difference stencil and frm_curve_feature() on a
five-point one, and core counts the rows it was handed.
frm_curve_feature() REFUSES instead of warning. A band past the
span is visibly wrong on the page; a peak located past it is a
number with a standard error beside it and nothing to give it away.
References
Ruppert, D., Wand, M. P. and Carroll, R. J. (2003) Semiparametric Regression. Cambridge University Press, ch. 6.
See also
frm_curve_deriv() for the derivative of the same curve,
frm_curve_feature() for the location of a peak or a crossing.
Examples
set.seed(1)
dd <- data.frame(x = sort(runif(200)))
dd$y <- 2 * sin(pi * dd$x) + rnorm(200, 0, 0.4)
fit <- frm(bf(y ~ s(x, k = 8)),
family = stats::gaussian(), data = dd)
cv <- frm_curve(fit, newdata = data.frame(x = seq(0, 1, length.out = 25)),
nsim = 2000)
head(cv[, c("x", ".estimate", ".se", ".lower_ci", ".lower_sim")])
#> <frmtmb curve> , 6 grid points, level
#> critical value: pointwise NULL
#> covariance NOT checked: frm_linpred(se.fit = TRUE) is refused for a nonlinear predictor, so there is no second route to compare against
#> x .estimate .se .lower_ci .lower_sim
#> 1 0.00000000 0.1320144 0.15875289 -0.1791356 -0.32995857
#> 2 0.04166667 0.3413065 0.11074390 0.1242525 0.01904038
#> 3 0.08333333 0.5478148 0.07848158 0.3939937 0.31943241
#> 4 0.12500000 0.7472178 0.06874628 0.6124776 0.54716530
#> 5 0.16666667 0.9386515 0.06827916 0.8048268 0.73995831
#> 6 0.20833333 1.1261967 0.06628100 0.9962883 0.93331811