NUMERICAL AUDIT · 1 NOVEMBER 2026
statsmodels: the Hall–Wellner band uses the number at risk instead of the sample size
SurvfuncRight.simultaneous_cb puts the number at risk where the textbook formula has the total sample size, so the nominal 95% band is too wide early, too narrow in the tail and covered the true curve 86–91% of the time in simulations.
Xamit Kadirbekov, Daniyal Kadirbekov · AI-assisted investigation and writing
Checked 1 November 2026: PR #10649 is open, with no review yet; not merged or released. Local regression checks and limitations documented.
Authors: Xamit Kadirbekov and Daniyal Kadirbekov (GERO). Published 1 November 2026; finding dated 30 October 2026.
Status: Checked 1 November 2026: PR #10649 is open, with no review yet; not merged or released. Local regression checks and limitations documented.
SurvfuncRight.simultaneous_cb puts the number at risk where the textbook formula has the total sample size, so the nominal 95% band is too wide early, too narrow in the tail and covered the true curve 86–91% of the time in simulations.
Published proposal
- statsmodels PR #10649 — head commit
052bc2fb32486d5bb659452811c41993b0b5f6f0, OPEN at submission on 30 October 2026.
What the function does
A Hall–Wellner band is a corridor around the Kaplan–Meier curve that should contain the whole true survival curve at once with 95% probability, not each point separately; that is what "simultaneous" means. SurvfuncRight.simultaneous_cb in statsmodels/duration/survfunc.py computes it. We checked main e82bcf02f2cb7fd915264f6af79a51ca394965e8; the code is the same in release 0.15.0.
The discrepancy
The method sets nn = self.n_risk, the number still at risk at each time, as n in the band. In Hall and Wellner (1980) and Klein and Moeschberger (2003), eqs. 4.4.3–4.4.5, n is the total sample size, a single number for the whole curve; the number at risk has its own symbol. With the shrinking number at risk the band is too wide early and too narrow in the tail.
s2 = self.surv_prob_se**2 / self.surv_prob**2
nn = self.n_risk # number at risk at time t
theta = 1.3581 * (1 + nn * s2) / (np.sqrt(nn) * np.log(self.surv_prob))
The textbook example used in statsmodels' own tests (bmt data, group ALL, n = 38, log transform):
| Day | At risk | KM | statsmodels | Textbook, n = 38 |
|---|---|---|---|---|
| 110 | 31 | 0.789 | (0.4359, 0.9349) | (0.4632, 0.9300) |
| 172 | 27 | 0.684 | (0.3879, 0.8589) | (0.4121, 0.8501) |
| 466 | 18 | 0.467 | (0.2316, 0.6725) | (0.2404, 0.6655) |
From day 609 on, the current band is narrower than the textbook band. Simultaneous coverage of the nominal 95% band (exponential data, 1,000 replications each, log transform, checked over the whole step function, not only at event times):
| n | Censoring | Current | With fix |
|---|---|---|---|
| 50 | none | 0.910 | 0.948 |
| 100 | none | 0.870 | 0.946 |
| 200 | none | 0.863 | 0.945 |
| 100 | 30% | 0.872 | 0.945 |
The arcsine transform behaves the same way. Under-coverage grows with sample size. A separate re-run (500 replications, n = 100) gave 0.896 against 0.960 without censoring and 0.856 against 0.944 with censoring; coverage must be checked across each step, because the difference is not visible at event times alone.
Reference and why this is not a convention
Three independent sources use the total sample size: Klein and Moeschberger's formulas, SAS PROC LIFETEST's "Confidence Bands" documentation and the R package km.ci (n <- survi$n). The reference values were computed without statsmodels or NumPy: KM and the Greenwood sum in exact fractions, the band in mpmath. We know of no definition of the Hall–Wellner band with the number at risk, and with it the band does not deliver its stated 95%. The critical value 1.3581 is a legitimate simplification and was left unchanged; with the correct n it gives about 95%.
Fix and tests
- Use the total number of observations, or the sum of
freq_weightswhen given. - The reference values in
test_simultaneous_cbwere regression values of the current output ("close to page 103 of Klein and Moeschberger"), so the test could not catch the error. They are replaced by values computed independently from the textbook formulas with n = 38. Newtest_simultaneous_cb_uses_total_sample_sizechecks day 466 for both transforms. - With the old code 2 tests fail; with the fix
test_survfunc.pypasses (23 passed).
Upstream status
As of 1 November 2026, PR #10649 is open, with no review or comment. Nothing has been merged or released. The companion quantile PR #10648 from the same day has been merged; see the separate note.
Limits
With left truncation (entry) the effective sample size for this band is not defined in the references; the patch uses the number of records, which may deserve separate discussion with the maintainers. The log-rank test is not affected; statements read off the band, such as a lower bound on survival at two years, are. The band is used mostly for plots and secondary statements rather than primary trial endpoints. Novelty rests on a bounded GitHub search and the file's history since 2019; mailing lists were not searched.
The coverage simulation was re-run in a separate session on 30 October before submission. No reproduction archive is attached to this page yet.
Attribution and license
AI tools (Claude) helped find the cases, build the reference calculations and draft the patches and tests; the examples were re-run independently before submission. Xamit Kadirbekov and Daniyal Kadirbekov are the report authors and reviewed the submitted diff; upstream software remains credited to its developers. Report: CC BY 4.0. Upstream patches retain their upstream licenses.