Perform K-fold cross-validation to select the kernel bandwidth for skmle().
Usage
skmle_cv(
formula,
data,
id,
obs_times,
s = 0,
K = 5,
h_grid = NULL,
n_h = 10,
nknots = 3,
lq_nodes = 64,
maxeval = 10000,
xtol_rel = 1e-06,
seed = NULL,
quiet = FALSE,
one_sided = TRUE,
weight = NULL
)
# S3 method for class 'cv.skmle'
print(x, ...)Arguments
- formula
A model formula. The left-hand side must be a
survival::Surv()response and the right-hand side must contain at least one covariate.- data
Data frame containing the variables used in
formula,id, andobs_times.- id
Subject identifier. Non-numeric identifiers are allowed and are internally converted to integer subject codes.
- obs_times
Longitudinal observation times aligned row-wise with
data. Times may be on any scale; the sieve basis and the cumulative-hazard quadrature are built on the observed follow-up, so there is no need to rescale to the unit interval first.hmust be on the same scale.- s
Box-Cox transformation parameter, defaulting to
0.s = 0is the proportional hazards model,s = 1is the additive hazards model, and values in between interpolate. If you do not have a reason to choose otherwise, the default is the familiar Cox model.- K
Number of folds.
- h_grid
Optional numeric vector of candidate bandwidth values. If
NULL, a grid is generated automatically from the observed time gaps.- n_h
Number of candidate bandwidths to generate when
h_gridisNULL.- nknots
Number of interior knots used in the sieve approximation of the baseline component. Knots are placed at
(1:nknots)/(nknots + 1). The basis is a natural cubic spline (splines::ns); its order is fixed, which is why there is nonorderargument.- lq_nodes
Number of Legendre-Gauss quadrature nodes used in numerical integration.
- maxeval
Maximum number of optimizer evaluations.
- xtol_rel
Relative convergence tolerance passed to the optimizer.
- seed
Optional integer seed for the random subject-to-fold assignment. If
NULL, the current RNG state is used and no explicit seed is set.- quiet
Logical; if
TRUE, suppress progress output.- one_sided
Logical.
TRUE(the default) uses a half kernel: only covariate observations strictly before the event or quadrature time inform that time, which is the risk-set restriction and the estimator as published.FALSEuses a full, two-sided kernel, smoothing the covariate path from both sides. The switch applies to the risk-set averages inside the C++ backend as well as to the row weights, so the two are always consistent.- weight
Weight function of the standardised lag \((t - r)/h\), or
NULL(the default) for the Epanechnikov kernel implied byone_sided. Any R function will do; see kernel-weights.- x
A
cv.skmleobject.- ...
Ignored.
Value
An object of class cv.skmle with components:
h_cv: selected bandwidth,fit:skmlefit refit on the full data,cv_results: data frame of candidate bandwidths, their CV losses, and the standard error of each loss across the folds,h_grid: bandwidth grid used in the search,fold_id: the subject-to-fold assignment vector (lengthn),seed: the value ofseedsupplied by the user, orNULL,call: the matched call.
Kernel choice
one_sided is used inside the fold loop as well as being passed through to
the refit, so the bandwidth is selected under the same kernel the final fit
uses.
How the folds are formed
skmle_cv() splits subjects across folds. Several rows belong to the same
subject in long format, so splitting by row would put one subject on both
sides of the split.
After choosing the bandwidth with the smallest average validation loss, the
function refits skmle() on the full data set using the selected value.
Because the fold assignment is random, pass seed (or set.seed() before
calling) to make the grid selection reproducible.
What the held-out loss is
Each training fold is fitted at the candidate bandwidth, and the fit is scored on the held-out subjects by an ordinary log-likelihood per subject,
$$-\frac{1}{n_{\mathrm{test}}} \sum_{i \in \mathrm{test}} \left[ \delta_i \log g\{\hat\alpha(X_i) + Z_i(X_i)^\top \hat\beta\} - \int_0^{X_i} g\{\hat\alpha(t) + Z_i(t)^\top \hat\beta\}\,dt \right],$$
where \(Z_i(t)\) is the covariate carried forward from the last observation at or before \(t\) (and the first observation carried back, before the first observation time). The integral is exact up to the Legendre rule applied between consecutive observation times, where the carried-forward path jumps.
No kernel and no bandwidth appear in that expression, and that is the point.
The kernel-weighted log-likelihood skmle() maximises cannot be compared
across bandwidths: its weights are \(W(u/h)/h\), so the weight each subject
contributes falls away as h grows, and the criterion decreases
monotonically in h whatever the fit is worth. Scored that way the largest
candidate wins every grid on every data set, and coefficients further from the
truth are preferred to coefficients nearer it. Dividing by the admitted
weight, or scoring at one bandwidth held fixed across the grid, does not
rescue it.
The carried-forward likelihood still favours some bandwidths over others. A carried-forward covariate is out of date by the time since the
last visit, so the held-out score is maximised by coefficients attenuated by
that lag. The fit produces such coefficients at a bandwidth of the same
order as the typical gap between visits. As \(n\) grows, the selected
bandwidth therefore converges to a constant determined by the visit
process rather than to zero, and the coefficients at h_cv remain biased
for \(\beta\) at any sample size. Use skmle_cv() to choose a bandwidth
for predicting the hazard, and skmle_bc() to estimate \(\beta\) with
confidence intervals.
The default grid
When h_grid is NULL the grid is log-spaced over
\([\max\{\min_i (X_i - T_{ij})_+,\ \tau n^{-0.6}\},\
\min\{\max_i \max_j (X_i - T_{ij})_+,\ \tau n^{-0.3}\}]\), with
\(\tau = \max_i X_i\), so it adapts to the scale of the times on its own.
Always look at cv_results. A minimum at an endpoint of the grid raises a
warning: the selected value is then the best of the values offered rather than
a minimum, and the grid should be widened. On the automatic grid that warning
is common, because \(n^{-0.3}\) is the rate the asymptotics assume while the
finite-sample minimum of the loss frequently lies above it. Widening h_grid
by hand shows where the curve turns.
How sharp the selection is
Not very, and the se column says so: it is the standard error of each loss
across the folds, and over a wide middle range of h the losses sit inside one
standard error of each other. Read the curve, not only h_cv.
The criterion scores prediction of the held-out hazard, in which the baseline
\(\hat\alpha\) can absorb attenuation in \(\hat\beta\), so it leans towards
more smoothing than the coefficients on their own would want. Over 10
replicates at \(n = 200\) on a grid spanning 0.05 to 0.9, the mean
squared error of \(\hat\beta\) at the selected bandwidth was 0.155, against
0.205 at the largest candidate and 0.065 at the bandwidth an oracle would
have picked. Resist the temptation to correct the lean by taking the smallest
bandwidth within one standard error of the minimum: that lands in the noisy
small-h end, and scored 0.205 over the same replicates – no better than
taking the largest candidate. The rise at the left of the curve is real.
Examples
# \donttest{
library(survival)
set.seed(123)
dat <- sim_skmle_data(
n = 60,
mu = function(tt) 8 * (0.75 + (0.5 - tt)^2),
mu_bar = 8,
alpha = function(tt) 0.5 * 0.75 + 0.75 * (tt * (1 - sin(2 * pi * (tt - 0.25)))),
beta = c(1, -0.5),
s = 0,
cen = 0.7
)
cv_fit <- skmle_cv(
Surv(X, delta) ~ covariates,
data = dat,
id = id,
obs_times = obs_times,
s = 0,
K = 3,
seed = 2026,
quiet = TRUE
)
cv_fit$h_cv
#> [1] 0.158428
# Read the whole table, not just the selection: a minimum on the edge of the
# grid is a boundary artefact and warns.
cv_fit$cv_results
#> # A tibble: 10 × 3
#> h cvloss se
#> <dbl> <dbl> <dbl>
#> 1 0.0969 0.295 0.0577
#> 2 0.110 0.294 0.0644
#> 3 0.124 0.258 0.0976
#> 4 0.140 0.235 0.115
#> 5 0.158 0.228 0.119
#> 6 0.179 0.233 0.116
#> 7 0.203 0.238 0.116
#> 8 0.229 0.238 0.109
#> 9 0.259 0.229 0.100
#> 10 0.293 0.233 0.0928
summary(cv_fit$fit)
#> Call:
#> skmle::skmle(formula = Surv(X, delta) ~ covariates, data = dat,
#> id = id, obs_times = obs_times, s = 0, h = 0.158427999136039)
#>
#> n= 60
#>
#> Estimate Std. Error z value Pr(>|z|)
#> covariates1 1.52299 0.40819 3.7310 0.0001907 ***
#> covariates2 -0.36860 0.42041 -0.8768 0.3806090
#> ---
#> Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1
#>
#> Log-likelihood: 0.4295
# }