NEWS
nbsurv 0.5.1 (2026-09-11)
CRAN resubmission
DESCRIPTION: added method references to the Description field -
Wolfson et al. (2015) doi:10.1002/sim.6526 for the censored naive
Bayes model, and Gerds and Schumacher (2006) doi:10.1002/bimj.200610301
for the IPCW Brier score used in evaluation.
man/nbsurv-class.Rd: added a \value section documenting the return
values of the print() and plot() methods for "nbsurv" objects.
nbsurv 0.5.0
New features: kernel-smoothed horizons (time_smooth)
- New
time_smooth = TRUE / bandwidth arguments to nbsurv()/
cv_nbsurv() (default FALSE, fully backward-compatible). The real
remaining inefficiency identified after cov_structure = "full" (0.4.0):
every horizon was fit completely independently, discarding the fact
that class-conditional statistics should vary smoothly with time -
costly right where one of the two horizon-defined classes (already
failed vs. known to survive) is smallest, typically near the earliest or
latest requested horizon. time_smooth = TRUE precomputes class-
conditional statistics once at every point of time_grid at fit time,
then combines them at predict() time via Nadaraya-Watson kernel
regression across horizons (Gaussian kernel, width bandwidth) -
borrowing strength from neighboring horizons while still allowing
genuinely time-varying effects, unlike a proportional-hazards model
that shares one coefficient across all of time. Because the combination
is a convex (weight-normalized) average of already-valid per-grid-point
statistics, smoothed covariance matrices stay positive semi-definite and
smoothed categorical probabilities stay valid automatically - no PSD
repair or renormalization needed. bandwidth is now a tunable column in
tune_nbsurv()'s param_grid.
- Bug fix found and fixed during implementation: the first working
version returned
NA for every smoothed prediction. 0 * NA is still
NA in R, so zero-weighting an invalid (all-NA) grid point before
summing did not neutralize it; colSums() propagated the NA. Fixed by
zeroing the invalid rows' values, not just their weights, before
summing. Regression test locks this in
(tests/testthat/test-time-smooth.R).
- Empirical validation against
coxph on lung (20 held-out 70/30
splits, age + sex + ph.ecog): a genuine, principled bias-variance
tradeoff, confirmed rather than assumed. Concordance improves as
bandwidth widens (0.60 at the default heuristic up to ~0.615-0.619 as
bandwidth grows toward pooling most of the time range), closing roughly
half to two-thirds of the gap to coxph's 0.629 - but IPCW Brier score
at the most extreme horizon tested (t=400) worsens monotonically as
bandwidth widens (0.240 to 0.254), the expected cost of over-smoothing
away genuine horizon-specific calibration. No single bandwidth wins on
both metrics at every horizon; the default heuristic (one quarter of the
fitted time range) is a reasonable balance, not a universally optimal
choice - use tune_nbsurv()'s bandwidth grid column to tune it for
a specific dataset, exactly as any other hyperparameter.
- Even at the best bandwidths tried, the
lung benchmark gap to coxph
narrows but does not fully close - reported honestly in the README
rather than oversold. age/ph.ecog's covariate effects are close to
log-linear and non-interacting on this dataset, which is exactly what a
correctly-specified Cox model is built to exploit; nbsurv's value
proposition is flexibility (genuinely time-varying, non-proportional
effects a fixed-coefficient Cox model cannot represent), not raw
accuracy dominance on every dataset.
- No C++ was added. Benchmarked at n=5000 (3270 distinct event times):
time_smooth = TRUE fit time is ~2.5s vs ~0.03s without it - a real but
tolerable one-time cost at realistic survival-analysis sample sizes,
with predict() itself remaining fast (~17ms for 500 predictions) since
it only does cheap kernel-weighted averaging over the precomputed grid.
The existing time_grid argument already lets users pass a coarser
custom grid to cut this cost further if needed. Rcpp would be
unjustified complexity for a bottleneck that doesn't clearly exist at
the scale this package targets.
nbsurv 0.4.0
New features
- New
cov_structure = c("diagonal", "full") argument to nbsurv()/
cv_nbsurv() (default "diagonal", fully backward-compatible). With
"full", continuous predictors are modeled jointly as a single
multivariate Gaussian per class (survivor/event), relaxing naive Bayes's
conditional-independence assumption between them, instead of the product
of independent univariate Gaussians. The covariance matrix is shrunk
toward its diagonal by the new shrinkage argument (default 0.2) for
numerical stability at small sample sizes. Categorical predictors are
unaffected and remain conditionally independent either way. With fewer
than 2 continuous predictors, "full" is numerically identical to
"diagonal" (verified by regression test).
- Empirical validation (see
tests/testthat/test-cov-structure.R): on
a synthetic case with two continuous predictors correlated at r=0.85,
cov_structure = "full" gives a consistent ~5-6% relative improvement in
held-out IPCW Brier score over "diagonal" at every horizon tested. Its
effect on concordance is small and inconsistent even under strong
correlation - concordance depends only on the ranking of risk scores,
which a monotonic transform of the naive product often preserves even
when the underlying probabilities are miscalibrated; Brier score, which
scores the actual probability values, is the metric this correction
reliably improves. On the real lung dataset (age/ph.ecog correlated
at r=0.31, n=167), the improvement was smaller and mixed (Brier improved
at 2 of 3 horizons tested, concordance improved at 2 of 3) - "full" is
a genuine, validated improvement when continuous predictors are
correlated, not a guaranteed win on every dataset, and did not close the
gap to coxph on lung specifically (see README benchmark).
nbsurv 0.3.2
Bug fixes (scientific validity)
evaluate_nbsurv()/cv_nbsurv()/tune_nbsurv() reported the
complement of concordance (1 - true concordance), not concordance
itself. concordance_at_horizon() calls
survival::concordance(Surv(time, status) ~ risk), whose formula
interface defaults to the opposite direction convention from a risk
score (it expects higher risk to mean longer survival unless told
otherwise). Since every internal caller passes a genuine risk-direction
score (higher = more likely to fail), the returned value was
systematically inverted - a model with a true C-index of 0.63 was
reported as 0.37, making a genuinely discriminating model look
worse than random. Fixed by adding reverse = TRUE to the
survival::concordance() call. Verified against survival::concordance()
on both a coxph linear predictor and an nbsurv fit (matches to full
precision either way), and a new regression test on a clearly-separable
synthetic case that would fail loudly if the direction ever flips back.
Brier scores, predictions, and every other metric were unaffected -
this only touched the concordance calculation.
nbsurv 0.3.1
Bug fixes
plot.nbsurv() silently double-scaled continuous predictors before
plotting, distorting every survival curve it drew. nbsurv() only ever
stored the scaled training data on the fit object; plot.nbsurv()
passed that already-scaled data into predict(), whose prepare_newdata()
step scales its input again using the same stored means/sds. The result
was visually "chunky" curves with implausible flat segments and near-zero
collapses for some rows, especially past t=400 in the README example -
not real model behavior, an artifact of predicting from
twice-standardized covariates. Fixed by storing the raw (unscaled)
training data separately (fit$training_data_raw) and having
plot.nbsurv() use that instead. predict()/evaluate_nbsurv()/
cv_nbsurv()/tune_nbsurv()/varimp_nbsurv() were never affected - they
operate on user-supplied newdata, which is only ever scaled once. New
regression test locks plot.nbsurv()'s curves to match predict()
called directly on the same raw rows.
nbsurv 0.3.0
New features
- New
varimp_nbsurv(): permutation-based variable importance. For each
predictor, its values are independently shuffled (n_repeats times),
evaluate_nbsurv() is recomputed on the permuted data, and importance is
the resulting degradation in the chosen metric ("brier" or
"concordance"), averaged over the requested horizons and repeats.
Returns a data.frame (class varimp_nbsurv) sorted by decreasing
importance, with its own print() method.
- New base-graphics
plot() methods: plot.varimp_nbsurv() (horizontal
bar chart of importance), plot.cv_nbsurv() (mean cross-validation
metric vs. horizon), and plot.tune_nbsurv() (mean tuning metric per
candidate, best candidate highlighted).
nbsurv 0.2.0
New features
evaluate_nbsurv() now computes the integrated Brier score (IBS) via the
trapezoidal rule in addition to horizon-specific Brier scores and concordance.
- New
calibration_plot_nbsurv() function for visual calibration assessment at
a single prediction horizon.
Bug fixes and improvements
- Continuous predictor standard deviations are now bounded away from zero by
min_sd in both the survivor and event classes, preventing numerical collapse
in degenerate training subsets.
predict.nbsurv() now enforces monotone survival curves via cumulative minimum
across increasing horizons.
- Categorical likelihoods now use Laplace smoothing controlled by the
laplace
argument, with IPCW-weighted event-class counts.
nbsurv 0.1.0
- Initial release (prototype; class
CNB, single-horizon prediction).