frmtmb.spline held royston_parmar(),
rp_floored() and the curve functions
frm_curve(), frm_curve_deriv() and
frm_curve_feature() until it was retired. The
Royston-Parmar family is in this package now, and the curve functions
are in frmtmb. Its NEWS follows as it stood at frmtmb.spline 0.10.2,
with every heading two levels down, so that the record of the family
stays with it.
frmtmb.spline 0.10.2
Runs on frmtmb 0.68.1 and later. On frmtmb 0.71.0 the standard errors of a curve on an ill-conditioned design come from a factor of the covariance. frmtmb.spline 0.10.1 refuses such a fit on frmtmb 0.71.0, so update the two packages together.
Bug fixes
- The grid covariance, the derivative’s and the feature’s standard
errors are formed through the factor where
frm_lp_basis()returns one. On a raw polynomial of degree 8,C V C'from the whole matrix loses every digit of a band. Through the factor, a curve’s standard errors are within 1e-4 oflm()’s. A first derivative’s are within 1e-3, the accuracy of the central difference.
frmtmb.spline 0.10.1
Needs frmtmb 0.68.1, whose joint covariance is positive semi-definite on a fit that lost a standard error; its tests fail against 0.68.0.
Bug fixes
frm_curve()no longer refuses a fit whose smoothing sds ran to zero with “disagrees with frm_linpred(se.fit = TRUE) by 1 relative”. A standard error is nowsqrt(max(q, 0) + max(e, 0))from the coefficient varianceqand the varianceethat is not coefficient uncertainty, the order core uses. Clampingq + eas a whole gave 0 where core gavesqrt(e), wheneverqcame out below-e. The same arithmetic now servesfrm_curve_deriv()and the height and slope offrm_curve_feature().The rows without a standard error are read once from
frm_lp_basis():nonest(a rank-deficient design, NA) as well asse_nonest(a lost direction, NaN). The check againstfrm_linpred(se.fit = TRUE)also compares WHICH rows have none, and refuses with its own message when the two routes disagree.A difference curve evaluates a row that both grids share once, so a grid differenced with itself is exactly zero on every BLAS. With OpenBLAS one estimate came out -2.2e-16.
frmtmb.spline 0.10.0
Needs frmtmb 0.68.0, for frm_lp_basis(extra_cov = TRUE)
and frm_extra_cov_deriv().
Breaking changes
The
"Sigma"attribute offrm_curve(),frm_curve_deriv()and a difference curve is the grid’s whole covariance,A V A'plus the covariance of the part that is not coefficient uncertainty (an exactgp()‘s kriging residual, an unseen grouping level’s draw). It wasA V A'alone. Code that read it as the coefficients’ covariance gets a larger matrix wherever such a part exists, and the same one elsewhere.frm_curve_deriv()leaves out thegp()kernel’s nugget, which is white noise at each position and has no derivative, and takes the rest of that part’s derivative from frmtmb’sfrm_extra_cov_deriv()rather than by differencing it. Its.seat an exactgp()’s unseen positions therefore differs from earlier builds.
Bug fixes and changes
frm_curve(),frm_curve_deriv()andfrm_curve_feature()give aNaNstandard error and band to a grid row that moves along a direction the fit’s Hessian lost, and warn once per call with the number of such rows (classfrmtmb_se_lost_prediction, as core’s predictions). Those rows stay out of the covariance cross-check and out of the simultaneous critical value. Before, a curve ofaalone ony ~ a + bwithaandbboth~ 1 + xstopped with “the assembled covariance of this grid disagrees with frm_linpred(se.fit = TRUE) by NaN relative … this package is reading the seam wrongly”, which blamed this package for a property of the fit. The curve of the mean, which the data determine, keeps its band. A difference curve takes a row as lost when either grid’s row is, which is conservative. frmtmb 0.68.0 marks those rows (frm_lp_basis()$se_nonest).frm_curve_deriv(order = 2)past an exactgp()’s data, and at an unseen grouping level, has the right standard error. The design stencil was applied to the extra covariance as well, which divided its rounding by the step to the fourth power. On the review’s construction (y ~ gp(x), 50 points on [0, 4], grid 3.0, 4.2, 4.8, 5.5).sewas 0, 0, 0, 0.875 and is now 0.285, 0.692, 0.830, 0.959, which a 24 times larger step reproduces to 2e-5 relative; the simultaneous critical value over [3.5, 5.5] was 19.34 and is 2.50 (dev/gpby-rev-deriv2b.R,dev/gpby-p1-deriv2b-lane.txt). At an unseen level ofy ~ t + (1 + t | g), linear int, the order-2.sewas 0 to 1.495 and is 0, the true value (dev/gpby-p1-deriv2-lane.txt).A simultaneous band over an exact
gp()past its observed positions is no longer too narrow. The band drew its deviation process fromA V A'and divided by a standard error that carried the kriging variance, so it was narrow exactly where it should widen. It now draws from the grid’s whole covariance,A V A'plus the kriging covariance core returns, and its"Sigma"attribute is that whole covariance. Ony ~ fac + gp(x), 60 points on [0, 6], a grid on [7, 12]: the critical value was 2.022, the reviewer’s lower bound 2.330, and it is now 2.431 (dev/gpby-crit.R). Over 400 seeds of a GP drawn from the model itself on a grid running past the data, the whole-curve coverage moves from 0.7775 to 0.8100 (binomial mcse 0.0208 and 0.0196), 13 seeds covered by the new band only and none by the old one only (dev/gpby-cov-band.R); the rest of the shortfall from 0.95 is the plug-in hyperparameters’, which the pointwise band shares at 0.887 per point.A difference curve whose two grids sit at different exact
gp()positions is computed, not refused. Onefrm_lp_basis()call on the stacked grid returns the kriging covariance between the two grids.sp_same_latent()is gone, and with it the refusal ofy ~ fac + s(x, by = fac) + gp(x)contrasted acrossfac.An unseen grouping level carries its draw into every route. The simultaneous band,
frm_curve_deriv()andfrm_curve_feature()refused underallow_new_levels = TRUEat a level of a termre_formulakeeps, because the seam returned the level’s variance one row at a time; they now read its covariance. A difference between two different unseen levels carries both draws, and one at the same unseen level cancels it.A peak’s standard error leaves out the
gp()kernel’s nugget too, for the same reason asfrm_curve_deriv().A grid with no part outside the coefficients (a smooth, a Hilbert-space
gp(k = ), seen levels) allocates no extran x nmatrix:extra_covarrives as an empty sparse matrix and is skipped. On a 2000-rows(x)gridfrm_curve_deriv(nsim = 1000)peaks at the previous release’s 467 MB, andfrm_curve()on a 2000-row exactgp(x)grid past its data at 255 MB against 192 MB, the difference being the kriging covariance the band now carries (dev/gpby-p1-mem.R,dev/gpby-p1-mem2.R). ### frmtmb.spline 0.9.0Needs frmtmb 0.65.0.
frm_curve()and its relatives takere_formulafrom core, which from 0.65.0 keeps every smooth underre_formula = NA, one indexed by a grouping factor included. The help pages say so, andtest-curve.Rasserts the new rule, so it fails against an older frmtmb.frm_curve(),frm_curve_deriv()andfrm_curve_feature()takeallow_new_levels = FALSEand pass it to everyfrm_lp_basis()andfrm_linpred()call they make, so the design, the estimate and the covariance check read the same rows. WithTRUEat a subject the fit did not see, a model withs(t, subject, bs = "fs")gives its POPULATION curve: the factor smooth contributes a row of zeros and no variance, so the band is the uncertainty of the population smooth alone. The standard errors areidentical()to the seam restricted to the intercept ands(t)columns, and agree with mgcv’spredict.gam(exclude = )to a ratio between 1.000 and 1.004.The
curve-inferencevignette failedR CMD buildagainst frmtmb 0.65.0: it drew the population curve on a grid with nosubjectcolumn atre_formula = NA, which now keeps the factor smooth. It now names an unseen subject withallow_new_levels = TRUE, and its prose no longer says thatre_formula = NAdrops the per-subject curves. Every number it prints is unchanged from 0.8.0.frm_curve(contrast = , allow_new_levels = TRUE)refuses when either grid carries variance that is not coefficient uncertainty. An unseen level of a(1 | g)term loads no design column, so two different unseen levels look like one. Without the refusal their independent draws cancel and the difference standard error is exactly 0.With
allow_new_levels = TRUEand a grid row at an unseen level of a termre_formulakeeps, such as(1 | g)atNULL,frm_curve(simultaneous = TRUE),frm_curve_deriv()andfrm_curve_feature()refuse and name the grouping factor. One unseen level is one draw shared by every row,frm_lp_basis()returns its variance one number per row, and all three build their answer fromA V A'without it. Measured before the refusal: a simultaneous critical value of 0.646 where the full covariance gives 2.146, derivative standard errors 0.40 to 0.50 of the right ones, feature standard errors about a quarter. The pointwisefrm_curve()band adds that variance and is answered.frm_curve_deriv()andfrm_curve_feature()on a stored curve reuse itsallow_new_levelsonly when the call does not supply the argument. A supplied value wins,FALSEincluded.
frmtmb.spline 0.8.0
-
rp_floored()sees a group with no events. It tested only the EVENT rows for a non-positived(eta)/d(log t), so a random effect or a covariate ongamma1could put a group whose rows are all censored on a falling slope, a fitted survival function that RISES with time, and the check returned zero. Ondev/frailty/frailty-floor3.R’s design that happened on 4 of 6 seeds, withnlminbcode 0 and nothing warning. It now also tests every censored row at its own time (an interval row at both ends) and returns the count asn_nonmonotone_censored, with the rows in the"rows"attribute asnonmonotone_censored.n_nonmonotonekeeps its meaning, the event rows whose density is floored. -
rp_floored(),frm_curve(),frm_curve_deriv()andfrm_curve_feature()WARN on a fit with such a censored row and still answer; the fit warns as it is returned too. The warning gives the largest rise in the fitted survival, reported as the new fieldmax_survival_rise, because the size is what a user judges. An EVENT row on a non-positive slope still refuses, as before. The line is the one flexsurv draws:dsurvspline()makes an event row there-Inf, so its fit avoids it, and never checks a censored row, so a rise there passes silently. brms’scox()cannot rise at all (an M-spline times non-negative weights). -
BREAKING: the report has two new fields,
n_nonmonotone_censoredandmax_survival_rise, betweenn_nonmonotoneandscale. - On ordinary right-censored designs without a cure fraction the new
count never fired: 0 of 1600 simulated fits and 0 of 270 fits on nine
designs from eight real datasets, at
df1 to 5, proportional hazards andgamma1 ~ x(dev/phase3a-findings.md). - The widened check DOES fire on one ordinary design: a cure fraction
with a time-varying effect (
gamma1 ~ arm). Past an arm’s last event its spline is fitted to censored rows only and can turn over there. Measured on two arms of 250 with 40 and 55 percent cured,df3 to 6: 27 of 80 fits fire, every flagged row lies past its own arm’s last event, and the fitted survival rises across them by 5.2e-04 to 0.13. Those fits warn and answer;?rp_flooredsays why the check fires.
frmtmb.spline 0.7.0
-
frm_curve()’s transform check,sp_predict_eta(), the covariance cross-check and the Royston-Parmar monotonicity check readfrmtmb::frm_linpred()rather thanpredict(), which is brms’s predictive summary in frmtmb 0.61.0 and has notypeorse.fit. Every refusal and every printed check line namesfrm_linpred(se.fit = TRUE)for the same reason, includingfrm_curve_feature()’s. Requires frmtmb 0.61.0.
frmtmb.spline 0.6.0
Requires frmtmb 0.60.0, for
frm_stop()and the other condition helpers.Requires the frmtmb release that exports
frm_stop(); thefrmtmb (>= 0.59.0)floor must move to it.BREAKING: every error, warning and message that frmtmb.spline raises is classed. An error has the class
c("frmtmb_spline_error", "frmtmb_error", "error", "condition"), and warnings and messages follow the same pattern, sotryCatch(frmtmb_error = )catches any refusal. The class vector no longer containssimpleError,simpleWarningorsimpleMessage. See?frmtmb::frmtmb-conditions.BREAKING: a value that matches none of the choices of
frm_curve_feature(type =),royston_parmar(scale =)andrp_floored(action =)is refused with afrmtmb_spline_errorthat names the argument, the value and the choices. The old text was'arg' should be one of .... A partial value still matches.A refusal that frmtmb raises about
royston_parmar(), such asresiduals(type = "deviance"), is afrmtmb_spline_error.The
frmtmb_ps_span_warningthatfrm_curve()andfrm_curve_deriv()raise now also has the classesfrmtmb_spline_warningandfrmtmb_warning.
frmtmb.spline 0.5.2
- Requires frmtmb 0.59.0.
- The vignette and tests follow frmtmb 0.59.0’s brms names and
varcorr_matrices().
frmtmb.spline 0.5.1
The Royston-Parmar frailty is a MEASUREMENT rather than a smoke test.
Item 2.5 of dev/extension-gaps-plan.md; the numbers, the
seeds and the scripts are in dev/frailty-findings.md. No
density, estimate or fitted value changed; what did change is the
compatibility table, in the last bullet below.
(1 | centre)onmuis a shared log-normal frailty, and it is the modelrstpm2::stpm2(cluster =, RandDist = "LogN")fits. The two write the same natural cubic spline in different bases, and the change of basis between them is exact to 3.0e-14 of the linear predictor’s own scale. Over 200 replicates at 2000 subjects in 40 centres, 40 percent censored: the treatment coefficient agrees to 3.2e-04 of the run’s own standard error on average and 8.8e-04 at worst, the frailty standard deviation to 1.4e-03 relative, and the two standard errors on that coefficient to 2.6e-06 relative. Recovery on the same run:beta0.5984 against 0.6 at 93.0 percent coverage andsd0.4905 against 0.5 at 92.0 percent, against a binomial Monte Carlo error of 0.0154.The two do NOT report the same log likelihood, and the whole difference is the integration rule. frmtmb takes the Laplace approximation and rstpm2 takes 9-node adaptive Gauss-Hermite quadrature. Against an exact per-cluster integral written in plain R at the same parameters, the Laplace value is 0.058 units low on this design and rstpm2’s is 4.9e-06 low. It costs the ESTIMATE 5.5e-05 units of exact log likelihood, 0.1 percent of the offset itself, though frmtmb’s point was the worse one under the exact criterion on 200 of 200 replicates. The offset grows as the cluster shrinks: 0.0066 units at 200 subjects per centre, 0.058 at 50, and 1.38 at 4, where it also changes sign. Compare
logLik()across frmtmb fits, not across packages.rstpm2’s
thetais the frailty’s VARIANCE.sqrt(exp(logtheta))is whatVarCorr()’s standard deviation matches, and the test asserts it as a ratio, so readingthetaas a standard deviation fails.A random effect on
gamma1is NOT a frailty and it works.gamma1multiplieslog t, so a centre deviation gives that centre a cumulative hazardt^(gamma1 + u)and a hazard ratio against another centre oft^(u - u'), which moves with time; atdf = 1it is a per-centre Weibull shape. Over 60 replicates at 2000 subjects in 40 centres with a truesdof 0.2 it recovers 0.1994 at 90 percent coverage (Monte Carlo error 0.028), and the per-centre shape error is 0.089 against 0.163 for the same fit with its own centre deviations dropped, better on 60 of 60.A block on
gamma1ALONE is ANCHORED att = 1, andt = 1is a unit rather than a fact. Rescaling time bycmoveslog tbylog c, so the same model in the new units carries a random intercept of-u log cas well. Measured on one dataset read in years, months and days, with then_event log ca density owes a rescale: the slope-only fit’s log likelihood moves by 8.19 units and itssd(gamma1 | centre)falls from 0.2201 to 0.0370, while the paired block(1 | c | centre)onmuandgamma1together moves by 4.3e-08 units and holdssd(gamma1 | centre)at 0.2259 in all three.?royston_parmarsays to pair the block unless the anchor is meant.FOUND, NOT FIXED, and present in every earlier version: a group with NO EVENTS escapes
rp_floored(). Its monotonicity check readscens == 0 & detadx <= 0, so it tests observed EVENT rows only. With a random effect ongamma1an all-censored group has no barrier holding its slope positive, since the barrier is in the density, and the score in its deviation is negative for rows pastt = 1, so the fit drives that slope down. Measured on 40 centres of 10 with five followed to a common administrative time and no deaths: on 4 of 6 seeds those five come back at slopes of -0.21 to -0.31, the fit converges with no warning and a positive definite Hessian,rp_floored()reports nothing and does not refuse,frm_curve()passes it through, and one centre’s fitted survival RISES from 4.2e-50 to 0.774.?royston_parmarnow says which rows the check tests and gives the two-line check to run instead. Widening the check changes a shipped refusal and needs a measured false-alarm rate first, so it is filed rather than done.royston_parmar x usandroyston_parmar x |ID|join the compatibility table, andrstpm2joins Suggests. The comparison tests skip without it. This is the one user-visible behaviour change in the release:frm_compat("royston_parmar", "|ID|")$statusmoves fromuntestedtoworks, and theusrow carries the frailty measurement instead of the blanket covariance-structure note.
frmtmb.spline 0.5.0
frm_curve(object, newdata, contrast = newdata2) returns
the difference of two curves with pointwise and simultaneous bands, and
frm_curve_feature() on it finds where they meet.
Checked against
gratia::difference_smooths(). The estimate gap is provably the fit rather than the arithmetic, at 5.55e-16, and simultaneous coverage is 0.970 over 200 seeds against a binomial Monte Carlo error of 0.0154.An exact
gp()under a difference is answered where the kriging residual is the same random variable and refused where it is not, and sameness is decided on the DESIGN rather than on the numbers. A tolerant test is not merely laxer: on a mirrored grid the two variances agree to 1.1e-16 while being different draws, and differencing them anyway drops a term worth 1.3e-03 of standard error.FOUND, NOT FIXED, and present in every earlier version:
sp_sim_crit()draws fromA V A'and standardizes bysqrt(diag(A V A') + extra_var), so a SINGLE grid’s simultaneous band over an exactgp()off the observed positions is at least 17 percent too narrow, exactly where it should widen.simultaneous = FALSEand every pointwise column are correct. The fix waits on a core seam.-
frm_curve(object, newdata, contrast = )returns a DIFFERENCE curve. With a second grid of the same height the reported curve is the difference of the two linear predictors and its covariance is(A1 - A2) V (A1 - A2)', so the pointwise and simultaneous bands are bands on the difference and the simultaneous one answers “is this difference anywhere other than zero” over the whole grid at once.frm_curve_deriv()andfrm_curve_feature()carry the second grid through, sofrm_curve_feature(type = "crossing", at = 0)on a difference curve locates where the two curves meet, with the implicit-function standard error. Measured againstgratia::difference_smooths(group_means = TRUE)on the same mgcv ML fit: the difference agrees to 1.95e-08, which is 1.5 times the gap between the two packages’ own fitted curves and is provably that gap rather than the difference arithmetic, since the difference’s disagreement equals the difference of the two curves’ own to 5.6e-16. The standard errors agree to 1.6 percent, which sits between the two curves’ own 1.5 and 3.1 percent: mgcv’s Vp is conditional on the smoothing parameter and the joint precision this package inverts is not, and nothing measurable cancels. Simultaneous coverage of the true difference over the whole grid is 0.970 over 200 seeds against a pointwise band’s 0.685.A difference is not two curves compared afterwards, and the argument exists because doing it by hand gets the covariance wrong: the two curves share coefficients, so adding their standard errors in quadrature ignores exactly the term the difference is made of. A grid differenced with itself returns zero with a standard error of
identical()zero, where quadrature returnssqrt(2)times a positive number.Refused by name:
transform = TRUE, because a difference of linear predictors is not a difference of responses; two grids that load on different coefficients; a difference whose two grids sit at different exactgp()positions, since the kriging residual arrives per row and the seam returns no covariance between the grids; a newnewdatapassed tofrm_curve_deriv()orfrm_curve_feature()on a difference curve without a newcontrastbeside it, which would otherwise return the FIRST curve’s answer without a word; and, for a derivative or a feature, acontrastwhose search variable disagrees withnewdata’s, since both are moved together.A
gp()difference at ONE position is computed rather than refused. Both grids then load the same kriging residual of the same field, so it cancels exactly. Sameness is decided on the design rather than on the numbers: the two grids must agree bit for bit on EVERY column ofAoutside the fixed effects. Equality of the variances would not be enough, since two levels of one grouping block have identical marginal variances by construction and are different draws. Measured ony ~ fac + gp(x)at n = 90 with the grid off the observed positions: the difference acrossfaccomes back flat at 0.0618114535, which issqrt(vcov(fit)["facB", "facB"])to 12 digits, and moving the second grid’sxby 1e-10 refuses.That test is STRICTER than the mathematics needs, and the refusal says so rather than blaming the
gp()positions. Only the block carrying the residual has to match for it to cancel, so a contrast acrossfacony ~ fac + s(x, by = fac, k = 6) + gp(x)is refused even though itsgp()columns are bit-identical, because the by-factor smooth’s own columns differ. It fails closed, so that is an answer you do not get rather than one you should not trust, and the core seam below removes the need for the test altogether.The built-in covariance check means less on this path and
print()says so.predict(se.fit = TRUE)returns a marginal standard error per row and never the covariance between two grids, so the check runs on each half andcov_rel_erroris the worse of the two. -
KNOWN DEFECT, not introduced here and not fixed here: a SIMULTANEOUS band over an exact
gp()evaluated off the observed positions is too narrow.frm_curve(simultaneous = TRUE)draws its max-deviation process fromA V A'and standardizes it bysqrt(diag(A V A') + extra_var), and off the observed positions those are not the same object: an exactgp()’s kriging variance is in the divisor and not in the draw. The standardized maxima are then too small and thelevelquantile of them is too small with them.This is a ONE-GRID defect. It has nothing to do with a difference and it affects any fit with an exact
gp()term whose grid leaves the observed positions.simultaneous = FALSEis unaffected, and so is every pointwise column, including.se.Bounded with public quantities, since
extra_varcomes fromfrm_lp_basis()and the divisor frompredict(se.fit = TRUE). Ony ~ fac + gp(x), 60 observations on [0, 6], noise 0.2,nsim = 20000: the critical value is 1.00009 of the correctly scaled one on a grid inside the data, 1.00007 at the edge, and 1.17485 on a grid that extrapolates past the observed positions. So the band is at least 17 percent too narrow exactly where it should be widening. That ratio is a LOWER bound: rescaling fixes the marginal scale and keepsA V A'’s correlation, and a kriging residual that decorrelates faster than the mean function pushes the critical value higher still.The fix needs the conditional cross-covariance of the
gp(), which no public seam returns; it is filed as a core seam indev/diffcurve-findings.mdand as a backlog entry indev/feature-gaps.md. Until then, read a band over an extrapolatinggp()grid as a lower bound on its own width. frm_curve_feature()’s second span gate asks its question of each grid of a difference. The gate is unchanged for one grid: it fires when a column other than the search variable varies DOWN a grid. It is not asked whether the two grids of a difference differ from one another, which they always do. An out-of-span row in thecontrastgrid refuses exactly as one innewdatadoes, and every span message says which grid raised it.
frmtmb.spline 0.4.0
- FIX:
frm_curve_feature()’s second span check reads the grid you passed rather than the five-point stencil at the located roots. The stencil reachesepspast each root, so the old gate was true when a root happened to land within that distance of a knot and false otherwise, whatever the grid held. On a fit with twops()terms, one row whosezleft the second term’s span was refused when the crossing landed at the end of the bracket and returned in silence, with a bit-identical estimate, when it landed in the middle; and the refusal it did raise quotedz’s span while the gate that fired wast’s stencil fringe. The check now runs when, and only when, a column other than the search variable varies down the grid, which is the one thing the grid scan cannot see, and it refuses on what that question answers. A grid whose other columns are pinned to row 1, which is every grid in this package’s own examples, is unaffected and costs no extra prediction.
frmtmb.spline 0.3.0
A curve past the fitted knot span warns once under the function that
drew it, the feature search refuses, and frm_curve_deriv()
no longer dies on its default simultaneous band. Requires frmtmb 0.53.0
for the classed span warning.
The three curve functions say when the grid leaves a
ps()knot span. They are the doors a user actually draws a curve through, and they were the ones that said nothing useful:frm_curve()andfrm_curve_deriv()read the curve throughfrm_lp_basis(), which did not raise it at all, whilefrm_curve_feature()reached it throughpredict(), which did, ELEVEN times on one call. The first two now warn once, with the span in the message and under the name of the function that was called. The third refuses instead, below.The span surfacing needs the frmtmb this round ships, not the 0.52.0 the
Depends:floor names today. It is driven by thefrmtmb_ps_span_warningcondition class, which is new in core this round; against a released frmtmb 0.52.0 the class is never raised, sofrm_curve()andfrm_curve_deriv()fall silent again andfrm_curve_feature()stops refusing, with nothing to say so. The floor rises with core’s version at consolidation.FIX:
frm_curve_deriv()past aps()knot span works on its own DEFAULT arguments. It warned and then died inquantile.default()with “missing values and NaN’s not allowed”, naming neither the span nor the function. Past the outer knot the derivative design is exactly zero, so those rows carry a standard error of exactly zero, and the max-deviation simulation standardized their (exactly zero) deviation by it. Such a point is covered with probability one and cannot be the argmax, so it now leaves the maximization; the band is computed over the rows that carry uncertainty and the zero rows get a zero-width one. A grid on which EVERY row is past the outer knot refuses by name instead. Dropping nothing is bit-identical to the old arithmetic, so an ordinary grid is unaffected.FIX: a grid laid exactly on the knot span is inside it. The span was checked on the widened difference stencil, which reaches a millionth of the grid’s range past both ends, so a grid ending on a knot drew a warning from
frm_curve_deriv()and a REFUSAL fromfrm_curve_feature(), and narrowing the grid to the span, which is what the refusal tells the user to do, reproduced the refusal. Both now re-ask on the grid that was passed, and only when the widened call reported something, so an ordinary call pays nothing. Thefrm_curve_deriv()warning counts grid rows rather than stencil rows as a result.BEHAVIOR CHANGE:
frm_curve_feature()REFUSES a search whose bracket leaves the span, where it used to warn after the fact. A band drawn past the span is visible on the page; a peak or a crossing located past it leaves the function as a number with a standard error beside it and nothing to say which curve it came off, because the decaying partial sum has peaks and crossings of its own. The bracket is checked at the grid scan, before any root is refined, and again on the difference stencil at the located roots. Narrownewdatato the span, or refit with a largerpad =.BEHAVIOR CHANGE:
rp_floored()returnsn_censored_deepwhere it returnedn_censored_floored. Nothing about the count changed; the name did. Since 0.2.0 the censored term is scored exactly throughlccdfand no row past the threshold is floored, so a field that says “floored” reports the opposite of what the slot bought. On the review’s 600-subject design the count is 1, at-log Sof 55.73, and that row’s contribution is -55.7302136 exactly where the old probability-scale form gave-Infand the floor gave -35.127363.The package documentation no longer describes seams core has since supplied.
?frmtmb.spline-packagesaidfit$cache$Vjointwas an internal this package reaches into, which has not been true since 0.2.0, and named “anlccdfslot, or a post-fit family hook” as fixes core owed, both of which landed and both of which this package uses. What is still missing is named for what it is: a per-row log-likelihood slot, whichloo()andwaic()need, and a per-group one, whichfrm(importance =)corrects with. Thevignette("royston-parmar")paragraphs that predated the same seams are corrected with them.
frmtmb.spline 0.2.0
The three things this package had to work around are seams in frmtmb 0.52.0 now, so it stops working around them. Requires frmtmb (>= 0.52.0).
- The one internal this package reached into is gone.
frm_curve()and its two companions readfit$cache$Vjointdirectly, which was written by an@noRdfunction and documented nowhere; they now call the exportedfrmtmb::frm_lp_basis().?frm_curve’s section on it changes from “The one internal this reaches into” to “The route to the covariance”. - The design rebuild is gone with it.
frm_curve()used to rebuild the grid design by unit perturbation, onepredict()call per contributing coefficient plus one probe per block of 24 that contributed nothing;frm_lp_basis()returns the design core already had. Thepredict()call count no longer depends on the number of coefficients at all, and the linearity probe that guarded the perturbation is no longer needed. -
frm_curve()now works on a NONLINEAR (nl = TRUE) linear predictor, which it used to refuse.Ais a Jacobian there rather than a design, andpredict(se.fit = TRUE)is refused for a nonlinear predictor, so there is no second route to check against:cov_rel_errorisNAandprint()says the check did not run rather than reporting a passing one that never did. -
frm_curve()now works on anrr()block atre.form = NULL, which it used to refuse through the covariance check. A reduced-rank block’s loadings live intheta; the perturbation could not see the derivative with respect to them and the assembled standard errors came out 27 percent away frompredict(se.fit = TRUE)’s.frm_lp_basis()carries the loading columns, and the two now agree exactly. - An exact
gp()term works too. Its kriging variance is not coefficient uncertainty, andfrm_lp_basis()returns it separately asextra_varrather than folding it intoA V A'. -
royston_parmar()declares frmtmb’s newlccdfslot, so a right-censored row is scored fromlog Sdirectly, in closed form on all three scales:-exp(eta)on"hazard",-log1p(exp(eta))on"odds"andpnorm(eta, lower.tail = FALSE, log.p = TRUE)on"normal". The floor at -35.127363 is gone, and with it the region past-log S = 30where the term was flat and its gradient exactly zero.cens()moves fromconditionaltoworksin the compatibility table. -
rp_floored()’s censored count is a DIAGNOSTIC rather than a refusal. It still reports censored rows whose fitted-log Spasses 19.2, because such a row is one the data barely constrain, but it no longer stops: the arithmetic is exact there now. The MONOTONICITY floor is unchanged and still refuses, because a non-positived(eta)/d(log t)means no hazard exists and the reported likelihood is a pseudo-likelihood rather than a density. -
royston_parmar()declarespost$fit_check, frmtmb’s new fit-end hook, so a fit with a non-monotone row warns as it is returned rather than only when someone callsrp_floored().
frmtmb.spline 0.1.0
First release. Two things a spline needs that a fit does not give you: inference about the CURVE rather than about its coefficients, and a survival family whose parameter is a spline.
Curve inference
-
frm_curve()evaluates a fitted linear predictor on a grid and returns it with two intervals. The pointwise one is the usual delta-method interval. The simultaneous one covers the whole curve at once, by the max-deviation simulation of Ruppert, Wand and Carroll (2003, ch. 6): draw the curve’s own deviation process from its joint covariance, standardize by the pointwise standard error, take the largest absolute value over the grid, and use that distribution’s quantile in place of 1.96. This is the constructiongratia::confint(type = "simultaneous")uses, and this package’s simulation reproduces gratia’s critical value on the same mgcv fit to inside two Monte Carlo standard errors at 10000, 100000 and 500000 draws. - The critical value is reported WITH its Monte Carlo standard error,
which is the quantile standard error
sqrt(p(1-p)/n) / f(q). A critical value without one invites a comparison that simulation noise alone would fail: two packages agreeing to 0.007 at nsim = 10000 have agreed to half a standard error, and that is the honest way to say it. - The comparison itself needed care. gratia standardizes a SMOOTH-ONLY
deviation by
smooth_estimates()’s.se, which is the FULL linear predictor’s standard error, intercept column included; the two differ by up to 13.5 percent on the test model and the critical value that comes back is 8 percent smaller than the self-standardized one. Both bands are exact, because the same divisor calibrates and scales, but the critical VALUES are not comparable unless the divisor is. The package’s own bands standardize by their own standard error. -
frm_curve_deriv()gives the first or second derivative of the same curve with delta-method standard errors and the same two intervals. The derivative is taken of the DESIGN, not of the fitted values, so the estimate and its standard error describe one function. - The step size is not gratia’s. Swept against the exact derivative of
a known function, a central difference bottoms out near
eps = 1e-6at 4.0e-10 for the first derivative, and neareps = 1e-4at 2.2e-07 for the second, because a second difference divides byeps^2and turns cancellation into the dominant error. gratia’s fixedeps = 1e-7is 14 times off the optimum at order 1, which is negligible, and eight orders of magnitude off at order 2, which is not: on the test fit gratia’s second derivative differs from mgcv’s own same-stencil answer by 1.76, 52 percent at the grid edge, and this package’s agrees with it to 8.6e-05. Soepsis scaled by the covariate’s range and set per order. -
frm_curve_feature()locates a peak, a trough or a level crossing and gives its POSITION a standard error, by the implicit-function delta method:var(t*) = var(f'(t*)) / f''(t*)^2for a stationary point andvar(f(t*)) / f'(t*)^2for a crossing. These are the numbers a movement paper reports, the time of peak velocity and movement onset, and they are usually reported without an interval because nothing hands one over. - At a stationary point the curve’s own value has a simplification
worth knowing and the function reports it: the derivative of
f(t*)with respect to the coefficients loses itsf'(t*) dt*/dcterm becausef'(t*)is zero there, so the standard error of the PEAK HEIGHT is the ordinary pointwise standard error att*, not inflated by the uncertainty in where the peak is. - Every root the grid brackets is returned, so a curve with two peaks gives two rows and a curve with none gives a zero-row answer rather than an error. “This curve does not peak in this window” is an answer.
The covariance these three needed, and the core seam
- All three need the same object, the covariance of the whole grid
prediction, and frmtmb exports no route to it. A penalized smooth’s
wiggly part is a random-effect block even when the smooth is a
population term, so the curve covariance needs the joint covariance of
the fixed AND random coefficients.
vcov(full = TRUE)excludesbunder both of its branches, by construction: its documented invariant is that its row names areconfint()’s.predict(se.fit = TRUE)forms the grid covariance internally and returns its diagonal. - So the package rebuilds it, out of two facts. The linear predictor is LINEAR in the coefficients, so the difference between a prediction and the same prediction with one coefficient raised by one is that coefficient’s design column, exactly and with nothing to tune. And the joint covariance is the inverse of the fit’s own joint precision.
- Neither piece came from an exported function, so neither is trusted.
Every call recomputes
sqrt(diag(Sigma))and compares it withpredict(se.fit = TRUE), and refuses when they disagree by more thantol. Measured agreement on the package’s own models is 3e-15 to 9e-15 relative, which is machine precision, and it is reported byprint()rather than hidden. -
The covariance is core’s own, read from its cache rather
than recomputed.
predict(se.fit = TRUE), which this package calls anyway for the check, memoizes the inverted joint precision on the fit; the curve then subsets it. Recomputing it instead, which the first draft did, paid for a secondsdreport()per call, took a Schur complement over the coefficients the curve does not touch on a densified copy of a sparse matrix, and bypassedautoscale_sdreport(). Measured at 8006 random coefficients: 114 s and 2.1 GB before, 8.2 s and 1.2 GB after, with the covariance subset itself at 0.00 s. An autoscaled fit now works rather than being refused, at 3.7e-16 againstpredict(se.fit = TRUE). -
The cost is the joint-precision solve, not the call
count. Measured at
re.form = NAon a 20-point grid: design rebuild 0.01 s againstpredict(se.fit = TRUE)0.29 s at 8 random coefficients, 0.07 s against 0.98 s at 2006, and 0.28 s against 6.87 s at 8006. The term the call count measures is a tenth of the cost at every size. Size a job from the number of coefficients in the FIT, not from the grid. - The design rebuild is one
predict()call per contributing coefficient plus one probe per block of 24 that contributes nothing. Under the defaultre.form = NAa per-subject grouping block contributes nothing and is skipped in blocks rather than one coefficient at a time: measured, the vignette’s 20-subject factor-smooth model costs 32 calls against 110 random coefficients, and the count does not grow with the number of LEVELS. It is not the ideal 13 either, for two stated reasons: a chunk that straddles the boundary between a live block and a dead one is expanded whole, and a component short enough to skip the probe is expanded whether or not it contributes. -
dev/spline-seam-proposal.mdsays what core would have to export to make the rebuild unnecessary, and what a penalized coefficient block handed to a NONLINEAR body would need beyond that.
The Royston-Parmar family
-
royston_parmar()writes the log cumulative hazard as a natural cubic spline in log time, which is the flexible parametric survival model of Royston and Parmar (2002).scale = "hazard"is the proportional hazards version,"odds"the proportional odds version and"normal"the probit one; all three are here, because the second and third are oneswitcharm each once the first is written. - It is parameterized exactly as
flexsurv::flexsurvspline()parameterizes it, and that is testable rather than claimed. Taking flexsurv’s fitted coefficients on its ownbcdata (686 rows, 299 events) and evaluating frmtmb’s objective at them reproduces flexsurv’s log likelihood to between 1.4e-16 and 1.6e-15 relative across all three scales and 0, 1 and 3 interior knots. That is an identity, not an agreement. The two optima then match as well, and frmtmb’s is never the worse of the two. -
muisgamma0and the rest aregamma1,gamma2, and so on, all with identity links. A formula onmuis proportional hazards; a formula on any other coefficient is a TIME-VARYING effect, which is what flexsurv spellsanc =. Each is an ordinary distributional parameter, sos(), a random effect and a prior all reach them. - Knots go at equally spaced quantiles of the log UNCENSORED times, as
Royston and Parmar place them and as flexsurv defaults. The quantiles
need the response, so they are taken at frame assembly through
family_finalize()and the family the fit carries has them baked in;knots =andbknots =pin them at construction instead.dfcounts interior knots plus one, so flexsurv’skisdf - 1. - The family declares an
lcdfas well as anlpdf, socens()andtrunc()both reach the likelihood, and right, left and interval censoring all work. Both are registered CONDITIONAL rather than working, for the reason below. Note the coding: frmtmb’scens()reads 0 as an observed event and 1 as right censored, which is the OPPOSITE of aSurv()status column. -
rp_floored()is the check this family cannot do without, and it refuses by default. Two things inroyston_parmar()are floors rather than answers and both are silent in the fitted object. This function recomputes them at the fitted parameters and stops, naming the row count, the maximum, the threshold, the reason and the remedy.frm_curve()and its two companions call it, so the documented way to inspect this family will not draw a curve off a fit whose likelihood is a floor artifact. - The first floor is the one that made the family unshippable without
this check. frmtmb forms a right-censored contribution as
log(1 - F(y))on the PROBABILITY scale, and core offers a family no complementary log-CDF slot to hand backlog Sdirectly, so the scoredlog Scarries absolute error about.Machine$double.eps / Swhatever the family does internally: exact to 1.3e-13 at-log Sof 10, wrong by 1.7e-04 at 30, floored at -35.127363 past 36. Past 30 the term is FLAT, its gradient exactly zero, so the optimizer prices such a row at a constant and fits the rest as if it were free. Measured: a 600-subject fit with one subject censored far beyond every event time converges without a warning and puts its treatment coefficient tens of percent out, on data flexsurv declines to fit at all. How far the reported log likelihood is from the model’s depends on the data, not on the family: a floored row contributes -35.127363 instead of its own-log S, so the shortfall is about-log S - 35per row, and two runs of that design differing only in seed give 2.4e+03 and 2.166e+04. The threshold is one quantity for all three scales, because the error depends only onS:-log SisHon"hazard",log(1 + exp(eta))on"odds"and-log(Phi(-eta))on"normal", and 19.2 is whereeps / Spasses 1e-8. - The refusal is POST-FIT and cannot be otherwise.
logLik()readsobject$opt$objectivedirectly and the family protocol has no hook that runs when a fit finishes, so nothing in this package can makelogLik()orAIC()refuse on their own, and the optimizer may have walked through or stopped inside the flat region before the check is called. The real fix is a core one, anlccdfslot, and it is five sites rather than one: seedev/spline-seam-proposal.md. - Monotonicity of the cumulative hazard is NOT enforced, and flexsurv
does not enforce it either. What this family does instead is refuse to
answer
NaNfor it: where the spline’s derivative in log time goes non-positive, the log density is a large finite negative number, sinceNaNstops the optimizer and oneNaNcomponent poisons a mixture’s log-sum-exp. That floor is NOT inert when it is used: it turnslogLik()andAIC()into a pseudo-likelihood, measured at 6 floored rows and 3952 units on a cure-fraction dataset.rp_floored()counts those rows too and refuses on them. The floor is a smooth positive part rather than a branch, because RTMB refuses a comparison on an AD type outright and has noCondExp. It changes the log density by 2.4e-15 at a derivative of 1. - That floor had to be written twice, and the measurement is what
caught it. The textbook spelling,
0.5 * (u + sqrt(u^2 + eps2)), is right on paper and wrong in double precision on the side it exists for: atu = -35.75the square root rounds to|u|, the sum cancels to exactly zero andlog()returns-Infafter all. Measured, 647 of 686 rows reached-Infthat way. Recovering the small branch through(u + s)(s - u) = eps2and ADDING it back does not help either, because adding 1.4e-16 to 71.5 loses it. The form that works SELECTS between the two cancellation-free branches withsign(), which is available on an advector and whose zero derivative is the correct one because the branches agree atu = 0. - The spline basis is written branch-free, with the truncated power
spelled
0.5 * (e + abs(e)), so that it tapes if the response is ever promoted to a parameter. It is a function of the response, which is data, so nothing normally needs that; it costs one line to have it anyway.
Deliberate omissions
- No
post$mean_fn, sofitted()andpredict(type = "response")are refused. The mean of a Royston-Parmar survival time has no closed form, and core’scox()refuses for the same reason. Read the fitted log cumulative hazard withfrm_curve()instead. - No exact basis derivative in
frm_curve_deriv(). The design is rebuilt throughpredict(), which evaluates a basis and never differentiates one, so an exact derivative would mean reading mgcv smooth objects out of the fitted frame. That is a deeper reach into core than anything else here makes, for an error already at the tenth significant figure. - Derivatives of order 3 and higher are refused rather than offered: a
third central difference divides by
eps^3and there is no step size at which it is accurate. - No accurate log survival past
-log Sof about 19.2, and none at all past 36. See therp_floored()entries above: the squeeze that keeps the term finite rather than-Infcannot also keep it accurate, because the number core asks a family for isFand the complement of a double near 1 is not representable. flexsurv computeslog Sdirectly and has no such region. The package’s answer is refusal by name rather than a silent floor, and the real fix is a corelccdfslot.