Skip to content

survFit: support models fitted with case weights#122

Open
fabian-s wants to merge 5 commits into
boost-R:masterfrom
fabian-s:claude/survfit-case-weights-gitfk8
Open

survFit: support models fitted with case weights#122
fabian-s wants to merge 5 commits into
boost-R:masterfrom
fabian-s:claude/survfit-case-weights-gitfk8

Conversation

@fabian-s

Copy link
Copy Markdown

Summary

survFit() previously stopped with "survFit cannot (yet) deal with weights"
for any model fitted with non-unit case weights, even though blackboost() /
glmboost() / gamboost() happily accept weights with family = CoxPH()
(see #121, motivated by the tidymodels integration of mboost).

This PR implements the weighted Breslow estimator in survFit.mboost:

  • the number of events at each time point is the weighted event count
    Σ wᵢ·eventᵢ,
  • the risk-set sums are weighted: Σ_{tⱼ ≥ t} wⱼ·exp(ηⱼ),
  • the linear predictor is centered at its weighted mean, so the baseline
    curve (newdata = NULL) corresponds to the weighted "average" observation —
    matching survfit.coxph()'s convention of centering at the weighted
    covariate means.

For unit weights, weighted.mean() reduces to mean() and the computation is
identical to the previous implementation (verified below). Two further
properties fall out of the estimator:

  • mboost's internal rescaling of non-integer weights
    (rescale_weights(), a constant factor) cancels in the Breslow estimator,
  • zero-weight observations (e.g. from cvrisk()-style subsampling folds)
    correctly drop out of both events and risk sets.

Also added drop = FALSE when subsetting the survival matrix, so a fit with a
single distinct event time no longer collapses it to a vector.

Changes

  • R/survival.R: weighted Breslow estimator instead of the error.
  • man/survFit.Rd: documents the weighted behaviour, incl. that n.event is
    the weighted number of events for weighted fits.
  • inst/NEWS.Rd: entry referencing Feature request: survFit() with weights #121.
  • tests/regtest-glmboost.R + .Rout.save: equivalence and coxph comparison
    tests (see below).
  • tests/regtest-blackboost.R + .Rout.save: smoke test of the scenario from
    Feature request: survFit() with weights #121 (blackboost + CoxPH() + non-integer weights, survFit on newdata).

The tests use deterministic weights on purpose: there is no set.seed()
between the survFit section and the subsequent sections in the test files, so
consuming RNG draws there would silently change all downstream expected
output in the .Rout.save files.

Verification (R 4.3.3, survival 3.5-8)

Issue example now works. The exact reprex from #121 (blackboost on
lung with runif weights, survFit on newdata) runs and returns valid,
monotone survival curves.

Backward compatibility. Re-running regtest-glmboost.R and
regtest-gamboost.R against the stored .Rout.save files with R CMD Rdiff
shows zero differences outside the newly added section — e.g. the existing
unweighted comparison still prints max(A1$surv-A2$surv) = 4.99e-05
exactly as before.

Integer case weights ≡ expanded data (exact). Fitting glmboost(..., family = CoxPH(), weights = w) with integer w and fitting on the
row-expanded data ovarian[rep(1:n, w), ] give all.equal()-identical
survFit results for surv, time, n.event, and newdata predictions.

glmboost vs. weighted coxph (non-integer weights). With
wts <- seq(0.5, 1.5, length.out = nrow(ovarian)):

fitw <- coxph(Surv(futime, fustat) ~ age + resid.ds + rx + ecog.ps,
              data = ovarian, weights = wts)
fit6 <- glmboost(Surv(futime, fustat) ~ age + resid.ds + rx + ecog.ps - 1,
                 data = ovarian, family = CoxPH(), weights = wts,
                 control = boost_control(mstop = 1000), center = TRUE)

max(abs(survfit(fitw, censor = FALSE)$surv - survFit(fit6)$surv))
#> 6.805215e-05

newdata <- ovarian[c(1, 3, 12), ]
max(abs(survfit(fitw, newdata = newdata, censor = FALSE)$surv -
        survFit(fit6, newdata = newdata)$surv))
#> 0.0002357012

Both are within boosting-convergence tolerance of the partial-likelihood MLE
(the same order of magnitude as the existing unweighted comparisons in the
test file, 4.99e-05 / 8.24e-05), and the agreement of the baseline
curve confirms the weighted-mean centering matches survfit.coxph. The
regression tests assert both differences < 0.001.

Package checks. R CMD INSTALL builds cleanly and
tools::checkRd("man/survFit.Rd") passes.

Closes #121

https://claude.ai/code/session_01HqB9XX7JjdQJ1vah4eyWv9

Use the weighted Breslow estimator of the cumulative baseline hazard
instead of stopping with an error for models fitted with non-unit
weights: events and risk sets are weighted by the case weights, and the
baseline survivor function is computed at the weighted mean of the
linear predictor (identical to the previous behaviour for unit
weights).

Results agree with survival::survfit for weighted coxph fits, and fits
with integer case weights give results identical to fitting on the
correspondingly expanded data.

Closes boost-R#121

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HqB9XX7JjdQJ1vah4eyWv9
Copilot AI review requested due to automatic review settings July 23, 2026 11:26

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends survFit() for mboost Cox models to support non-unit case weights by implementing the weighted Breslow estimator, aligning behavior with survfit.coxph() conventions (weighted risk sets/events and weighted centering).

Changes:

  • Implement weighted Breslow baseline hazard estimation and weighted centering in survFit.mboost().
  • Document the new weighted behavior in survFit’s Rd docs and add a NEWS entry referencing #121.
  • Add regression tests covering integer-weight equivalence to expanded data and non-integer-weight agreement with coxph, plus a blackboost smoke test.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
R/survival.R Replaces the prior “cannot deal with weights” error with a weighted Breslow estimator and weighted centering; adds drop = FALSE to preserve matrix shape.
man/survFit.Rd Documents weighted-case behavior, including that n.event becomes a weighted event count.
inst/NEWS.Rd Adds a user-visible NEWS entry announcing survFit support for case-weighted fits.
tests/regtest-glmboost.R Adds regression coverage for integer/non-integer weights vs expanded data / weighted coxph.
tests/regtest-blackboost.R Adds a smoke test verifying survFit() works on a weighted blackboost Cox model.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread man/survFit.Rd
Comment on lines 30 to +34
of \code{object}, along with the Breslow estimator, is used for computing the
predicted survivor function for each row in \code{newdata}.

If the model was fitted with case weights, the weighted Breslow
estimator is used for computing the baseline survivor function, i.e.,
@fabian-s
fabian-s requested a review from thothorn July 23, 2026 12:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: survFit() with weights

4 participants