NUMERICAL AUDIT · 1 NOVEMBER 2026

statsmodels: PHReg silently ignored unknown keywords such as weights=; warning merged

PHReg dropped unsupported keywords without a warning, so PHReg(..., weights=w) returned the unweighted Cox fit; in a simulated IPTW analysis with no true effect it reported HR 2.01 (p = 1.7e-7). The keyword check was merged on 30 October 2026.

Xamit Kadirbekov, Daniyal Kadirbekov · AI-assisted investigation and writing

Checked 1 November 2026: PR #10650 merged on 30 October 2026 (UTC); no release containing the change has been independently tested.

Upstream report #10650

Authors: Xamit Kadirbekov and Daniyal Kadirbekov (GERO). Published 1 November 2026; finding dated 30 October 2026.

Status: Checked 1 November 2026: PR #10650 merged on 30 October 2026 (UTC); no release containing the change has been independently tested.

PHReg dropped unsupported keywords without a warning, so PHReg(..., weights=w) returned the unweighted Cox fit; in a simulated IPTW analysis with no true effect it reported HR 2.01 (p = 1.7e-7). The keyword check was merged on 30 October 2026.

Published proposal

What the function does

PHReg (statsmodels/duration/hazard_regression.py) fits the Cox proportional-hazards model, the standard tool for estimating how a treatment changes the risk of an event, reported as a hazard ratio (HR). In observational drug studies, sicker patients are often more likely to receive the treatment; a common correction is inverse-probability-of-treatment weighting (IPTW), where weights are passed to the Cox model. R's coxph(weights=) and lifelines' weights_col support this. PHReg does not support case weights. We checked main e82bcf02f2cb7fd915264f6af79a51ca394965e8.

The discrepancy

PHReg.__init__ accepts **kwargs but never checks them, so unsupported keywords are dropped without any warning. Other statsmodels models (OLS, GLM, Logit and others) have warned with ValueWarning: unknown kwargs since GH 7741 / PR 7751; PHReg did not. Two common mistakes follow: weights= silently returns the unweighted fit, and event= (the lifelines name for status=) silently treats every censored observation as an event. tie= instead of ties= and groups= in the constructor were dropped the same way.

import numpy as np
from statsmodels.duration.hazard_regression import PHReg

rng = np.random.default_rng(5); n = 300
x = rng.normal(size=n); t = rng.exponential(np.exp(-0.5 * x))
st = (rng.uniform(size=n) < 0.7).astype(float); w = rng.uniform(0.2, 5, n)

PHReg(t, x[:, None], status=st).fit().params             # [0.47262404]
PHReg(t, x[:, None], status=st, weights=w).fit().params  # [0.47262404]  same, no warning
PHReg(t, x[:, None], event=st).fit().params              # [0.46537011]  censoring ignored, no warning

Simulated IPTW analysis: 400 patients; severe patients receive the drug with probability 0.8, mild ones with 0.2; severity raises the risk; the drug has no effect (true HR = 1). The IPTW weights are known exactly.

CalculationHR95% CIp
statsmodels PHReg(time, A, status=E, weights=w)2.009(1.547, 2.609)1.7·10⁻⁷
statsmodels without weights2.009 (identical: the weights were dropped)
Reference: weighted Breslow partial likelihood (R coxph formula), own Newton solver0.865
lifelines weights_col="w", robust=True0.865(0.637, 1.176)0.36
statsmodels PHReg(time, A, event=E)1.543 (164 of 400 censored patients counted as events)

The conclusion flips from "no effect" to "the drug doubles the risk, p < 10⁻⁶", without a warning.

Reference and why this is not a convention

The reference is an independent Cox implementation (Breslow/Efron, strata, R-style weights). Without weights it agrees with statsmodels to 3·10⁻¹⁵ and with scikit-survival to 1·10⁻¹²; with weights it agrees with lifelines to 1·10⁻⁴, the convergence tolerance of lifelines. The point is not a formula: statsmodels' own policy since GH 7741 is that models must not swallow unknown keywords silently, and PHReg lacked that check. weights is not documented for PHReg, so this is a missing input guard rather than a wrong formula.

Fix and tests

Upstream status

PR #10650 was merged on 30 October 2026 at 21:17 UTC, with a short thank-you comment from the maintainer. On 1 November we confirmed the merged state with the GitHub CLI; we have not installed or tested a statsmodels release containing the change, and we do not know which release will ship it.

Limits

The fix adds a warning; it does not add case-weight support to PHReg. The size of the flipped conclusion is specific to the simulated design, chosen with strong confounding; it shows what dropped weights can do, not how often analysts pass weights= to PHReg. The general class of silently swallowed keywords is known (GH 7741, PR 7751, GH 8779, PR 10427); our contribution is the missing check for PHReg, where a bounded GitHub search found no earlier report.

The weights= and event= example 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. Thank you to the statsmodels maintainers for the quick review and merge. Report: CC BY 4.0. Upstream patches retain their upstream licenses.