Skip to content

Merge develop into master - #790

Merged
derrynknife merged 234 commits into
masterfrom
develop
Oct 9, 2026
Merged

derrynknife merged 234 commits into
masterfrom
develop

Conversation

@derrynknife

Copy link
Copy Markdown
Owner

Brings master up to develop: 115 merged pull requests since #639.

The work covers issues #597, #608, #611, #613, #624, #626, #627, #630–#634, #642, #645–#667, #671, #710, #711, #714, #715, #728, #730, #731, #733, #746, #760, #769 and #777. The details of each are in docs/changelog.rst under Unreleased.

The full test suite runs on this merge, as master's CI does on every push: pytest with --run-ml and the doctests on Python 3.11, 3.12 and 3.13, plus the conformance tests, the docs build (sphinx -W) and the wheel check.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TRhKL2fJBiAAfNo9rihwts


Generated by Claude Code

claude and others added 30 commits October 4, 2026 04:12
Every fit builds a SurpyvalData first. Two costs dominated it at 100,000
rows:

- np.unique(..., axis=0) for the distinct truncation windows, and again
  in _check_identifiable for the distinct non-right-censored values. It
  sorts rows as structured records. unique_pairs ranks each column and
  then the pairs of ranks as integers, with the same sorted pairs and
  inverse.
- A list input was converted to an array once per check (durations,
  missing values, floats). xcnt_handler now converts a flat list of
  numbers once; pairs, ragged lists and object lists reach the checks
  as given.

The data are identical, dtypes included, as are the error messages. On
100,000 units with mixed censoring and truncation: from arrays
58 -> 41 ms, from lists 89 -> 62 ms, the identifiability check
12 -> 2.5 ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FMWxwbs7MLH1PcRoyS8Zxa
Pull requests, into develop or master, now run lint only. The
conformance suite, the test matrix and the documentation build run once,
on the push to master that a release merge makes. The nightly workflow
no longer runs on a schedule; it can still be started by hand.

Each change is tested locally by the tests for what it changed. The
Contributing page lists what that covers: the tests and conformance
cases for the touched code, its doctests, lint and mypy, the docs pages
that call it, and other interpreters for numerical changes. A failure
the merge run finds is fixed in a point release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRhKL2fJBiAAfNo9rihwts
…uatqe

CI: full suite and docs build only on the merge into master
…low imputed points (#631, #632, #633)

- #633: the offset's upper bound was the smallest value of any row. It
  is now the smallest exact failure, left-censoring time (not with zero
  inflation, whose mass at 0 meets it) or interval upper end. A
  right-censored time, an interval start or a truncation time below
  gamma meets S = 1 / F = 0 there. The right- and left-censored terms
  hold the shifted time at the support's edge, so a custom cumulative
  hazard is not evaluated below it. 500 Weibull units inspected every 3:
  gamma 3.0 (the first interval start, 'verified', -904.69) -> 5.16
  (-896.85).
- #632: a Rayleigh offset+lfp+zi fit on interval data ran onto that bound
  and warned 'No finite maximum'. It now finds the interior maximum,
  gamma 4.2765 and -746.4526, as 0.22 did.
- #631: with zi, a left-censored row is imputed half way down to the
  exact zero, below the start taken from the nonzero data, and the
  shifted seed data went negative (Weibull OutsideSupportError, LogNormal
  NaN). The start is now below the imputed points too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRhKL2fJBiAAfNo9rihwts
…he corrected bound

The start below imputed points (#631) took its step from the data and
imputed points together, which changed every offset start. It now keeps
the data's own step and only lowers the start when an imputed point is
below it.

Two tests expected the old bound. A right-censored -1 no longer caps
gamma. Dataset B's offset Weibull, whose profile likelihood rises to the
first failure, was stopped by the cap at a censored time and reported
verified (#633); it now warns 'No finite maximum'.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRhKL2fJBiAAfNo9rihwts
…maximum (#633)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRhKL2fJBiAAfNo9rihwts
…uatqe

Offset fits: bound gamma only by values that constrain it; start below imputed points (#631, #632, #633)
A model with no offset, limited-failure or zero-inflation part (gamma 0,
lfp_p 1, f0 0) now gives its distribution's own sf, ff and df. The
distribution's support guard already holds them at their edge values
below the support, and the transforms it skips were identities, but
five passes over the query. The support and missing-value checks
wrapped around every distribution function now use plain numpy instead
of autograd's wrappers, since x is not traced there.

The results are bit-identical: 1,176 outputs over 14 families, with and
without lfp/zi/offset, for scalar, list, 2-D, NaN, inf and
below-support queries. A -0.0 below the support is still 0.0.

Weibull sf on 16 points: 58 -> 28 us. Exponential sf on 20,000 points:
193 -> 79 us. LogNormal sf on 16 points: 60 -> 34 us.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRhKL2fJBiAAfNo9rihwts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRhKL2fJBiAAfNo9rihwts
Build SurpyvalData about a third faster, with identical results
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRhKL2fJBiAAfNo9rihwts
The names 0.23 deprecated (removal in REMOVED_IN = 0.24) are gone:

- the limited-failure proportion's old name p (lfp_p) in fixed,
  param_cb, from_params and the attribute of the univariate and
  regression models; model.p is still the fitted parameter on Bernoulli,
  Binomial, Geometric, NegativeBinomial and FixedEventProbability, and
  elsewhere raises an AttributeError naming lfp_p;
- beta_j for a regression coefficient in fixed and param_cb (the
  column's name, else coef_j); dicts saved with them still load;
- se on Cox, Lin-Ying, proportional odds and Fine-Gray
  (standard_errors()); Parametric.cov_matrix, cov on Fine-Gray,
  proportional odds and additive hazards, and the covariance attribute
  of Royston-Parmar and the frailty models (covariance(), now a plain
  method);
- the recurrent models' aic and bic properties (aic(), bic());
- MixtureModel.log_likelihood(params) (log_likelihood is a float),
  MixtureModel.loglike (neg_ll()) and the public names of its EM steps;
- a Cox model's neg_ll(beta) (neg_ll_of(beta));
- CoxFrailty's loglik and loglik_no_frailty (log_likelihood,
  log_likelihood_no_frailty);
- success_run's confidence= and alpha= (alpha_ci=, keyword only, so an
  old positional confidence raises rather than being read as alpha_ci);
- surpyval.NUM, TINIEST and EPS, and their copies in
  surpyval.utils.numeric.

The deprecation helpers stay. REMOVED_IN is now "0.25" and
REMOVED_IN_NEXT "0.26" (nothing used REMOVED_IN_NEXT); the module and
helper docstrings speak of the v0.23 renames in the past. The removal
test's scan check now asserts it sees every helper in
surpyval.utils.deprecation.__all__ rather than shims in the package,
which has none left; it passes with __version__ at 0.24. The warns-tests
become old-name-is-gone tests, and a few representative v0.23 names join
test_removed_arguments. Tests that used an old spelling without failing
(hpp.bic, getattr(model, "aic") expecting a raise, from_params(p=) as a
structural argument) use the new one. Two mixture tests that counted EM
steps by patching MixtureModel.EM and Q counted nothing once those
became aliases; they now patch _em_iteration and _Q. The docs, the
bundled skill's example and the changelog's Unreleased section follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRhKL2fJBiAAfNo9rihwts
…uatqe

Faster sf, ff and df on fitted models (#642)
…uatqe

Remove the names deprecated in 0.23 (next release is 0.24)
- The reference results the tests read (tests/reference/data/*.json)
  ship as package data.
- The doctest comparison's helpers and the opt-in table move from the
  root conftest into surpyval/tests/_suite.py. test_doctest_checker
  imported them from the root conftest, which is not installed. The root
  conftest keeps the flags and the doctest patch, which must be in an
  initial conftest.
- The marks and the opt-in skipping move to surpyval/tests/conftest.py,
  which ships. The installed suite, with no flags registered, skips the
  ml / invariants / calibration groups as the repository does by
  default.
- Modules that need hypothesis, scikit-survival or bson are left out
  when those are not installed. The two checker tests that need the
  root conftest's doctest patch skip without it.
- The merge into master builds the wheel, installs it in a clean venv
  with pytest, and collects the suite.

From a clean install of the built wheel: collection went from 7 errors
to 20,827 tests collected. Reference, tails and doctest-checker modules:
1,586 passed, 2 skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRhKL2fJBiAAfNo9rihwts
…' on cb, quantile_cb and param_cb (#645)

An offset fit's covariance leaves the offset out, since a threshold's
likelihood is not regular. So its Wald bounds hold gamma at its estimate
and are far too narrow near it: a 90% bound on a 3-parameter Weibull's
B1 life from 30 failures covered 40% of the time. They now warn. The
plot's band does not.

cb, quantile_cb and param_cb take method='bootstrap', a parametric
bootstrap that follows the regressions' (#617). Each of n_boot data sets
(200 by default) is simulated from the model, each unit censored as it
was (Davison-Hinkley conditional censoring, the regressions' _Design)
and within its truncation window. The model is refitted to each, with
its offset and fixed parameters as fitted. The bound is the
bias-corrected percentile interval of the refits. param_cb('gamma',
method='bootstrap') is the one bound on the offset. Refits are kept per
(n_boot, integer random_state) and not pickled. Left- or interval-
censored data, lfp/zi models, discrete families and models without
their data are refused with one message each.

Also: bca_bounds no longer leaks an 'invalid value' RuntimeWarning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRhKL2fJBiAAfNo9rihwts
The new __getstate__ (#645) replaced the likelihood-ratio mixin's, so the
_lr_ caches were pickled again. It now builds on the mixin's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRhKL2fJBiAAfNo9rihwts
Two tests used method='bootstrap' as an unknown method; it is known now
(#645). An offset model's hf/df Wald bound test expects no warning other
than the new one. test_extras still built lfp models with p=, removed in
#669.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRhKL2fJBiAAfNo9rihwts
…uatqe

Offset models: Wald bounds warn that gamma is held; bootstrap bounds (#645)
The alternative starts (a custom distribution's grid, the
limited-failure start from the failures alone) were skipped for any fit
with a fixed parameter. A Weibull with lfp=True and lfp_p fixed at 0.5
stopped on the infant-mortality mode (alpha 262.7, beta 0.96, -440.38)
and reported it verified. The wear-out mode (92.25, 2.345, -429.07) is
11.3 units higher. The starts now run with the fixed values in place.
From lfp_p 0.6 up the other mode is the global one, and the fit still
finds it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRhKL2fJBiAAfNo9rihwts
…uatqe

Fits with fixed parameters try the alternative starts too (#649)
#650)

A component whose failures all lie beyond the data explains none. It
only holds back a share of the units, as a limited-failure proportion
does, and the likelihood keeps rising as it moves further out. A
two-Weibull fit came back with a component of scale 33,561 (largest
observation 1,150) as a verified maximum, 2e-8 below the limited-failure
limit. After the fit, each component is compared with one that never
fails (survival 1 at every time). If the likelihood is at least as high
to the verification's tolerance, the fit warns 'No finite maximum',
names the component and points to lfp=True. Truncated data are not
checked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRhKL2fJBiAAfNo9rihwts
…mes (#646)

With some times at or below 0, every family whose support starts at 0
was outside its support, and fit_best returned the best of the rest in
silence. With three zero ages in 50, that was a Normal putting 3% of the
units before day 0. One warning now names the families passed over,
counts the times at or below 0, says what was chosen, and gives zi=True
for units that failed at time 0. A Beta passed over for data outside
(0, 1) stays quiet (#485).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRhKL2fJBiAAfNo9rihwts
…uatqe

Mixture: a component that runs off past the data has no finite maximum (#650)
…uatqe

fit_best warns when the lifetime families are passed over for zero times (#646)
…s at tl (#658)

- A scalar i / c / n applies to every row (as tl / tr do); before, it
  failed with a bare IndexError in _check_xicn_lengths.
- Recurrent fit_from_df refuses a column passed by fit's name (c="ev"
  for c_col="ev"; x, i, n, a column label for tl / tr, Z for the
  regressions) with the univariate fit_from_df's TypeError.
- An exact event (c=0) given in 2-D x as [l, r] with l != r is refused:
  the intensity fits read it as l and the MCF as the midpoint.
- c=2 with 1-D x is a ValueError in split_for_nhpp_likelihood (it was a
  bare assert, which also vanishes under python -O).
- n > 1 on an exact event says to repeat the row (the likelihoods ignore
  n there); n > 1 on a c=1 row has its own message.
- Messages give the user's item label (no np.str_ / np.int64 reprs, the
  default item as 1), and without i say every row is read as one item.
- One rule for an event at tl: the window is (tl, T], so an event at tl
  is outside it. handle_xicn refuses it, naming the item and times, and
  the trend tests use the same message; the MCF counts an item at risk
  only after its tl (it counted it at tl), and gapped windows are
  (start, end] likewise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRhKL2fJBiAAfNo9rihwts
…uatqe

Recurrent input handling: scalars, column names, [l, r] events, events at tl (#658)
The univariate bootstrap (#645) imported _Design and _REFIT_ERRORS from
the regressions' module. test_private_imports allows no new private
imports across packages, so they are public now: ResampleDesign and
REFIT_ERRORS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRhKL2fJBiAAfNo9rihwts
claude and others added 27 commits October 9, 2026 06:31
cif was written with plain numpy's where, exp and asarray, so autograd
could not trace it: the NHPP likelihood on a Cox-Lewis intensity had no
autograd gradient, nor an ARI likelihood on a Cox-Lewis baseline (its
search fell back to nan where the hand-written terms did not apply).

(e**(beta x) - 1) / beta is now an autograd primitive: its value the
same plain-numpy expression as before (so untraced calls cost what they
did), its derivative in beta (x e**u - expm1(u) / beta) / beta, which
cancels as u = beta x -> 0 and is 0 / 0 at beta = 0, taken from its
series below |u| of 1/2 (x**2 / 2 at 0), and in x e**u; both written in
autograd.numpy with each branch fed only points it is finite at, so the
Hessian is exact too. Against mpmath: value, Jacobian and Hessian within
2e-15 from beta = 0 and 1e-12 to |beta x| of 50. iif uses autograd's
exp as well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRhKL2fJBiAAfNo9rihwts
…uatqe

CoxLewis.cif is differentiable by autograd (#760, CoxLewis.cif autograd)
…ox-Lewis units)

#746 made the ARI fits search and check a Cox-Lewis beta, a rate per
unit time, in units of one over the longest time (search_floor); the
plain Cox-Lewis NHPP fit did not, and its verification is affected. Its
Nelder-Mead search and verify_or_polish ran on beta itself with a floor
of 1: the check's central differences stepped a beta of 1e-5 by 1e-7
(1%) for the gradient and 1e-5 (100%) for the Hessian, and the simplex,
with absolute tolerances of 1e-4, hardly moved beta at all. On 30
simulated fleets each in units of 1e4, 1e5 and 1e6 hours, 10, 28 and 30
fits were called unverified, with a warning; in the fleets' own units
(times of order 1) every fit was verified.

The likelihood's search and its check now run with each coordinate in
its parameter's unit (NHPPFitter._search_units; CoxLewis.search_units:
1 for alpha, min(1, 1 / longest time) for beta, as
CoxLewisDerivatives.search_floor, which now calls it). The least-squares
search, and so every MSE fit, is unchanged, as is every other NHPP
model's (units of 1). The fitted likelihood carries the units as its
search_floor, as an ARI fit's does, and the conformance check of the
maximum reads them.

All 180 fits are verified now; on the larger units in about half the
time (median 0.52x at 1e6 hours, 0.58x at 1e5, 0.84x at 1e4; 1.0x on
data of order 1 to 10, MSE 1.0x everywhere). Log-likelihoods within
-1.3e-7 to +6.8e-7 of before: the check is now as strict for beta as
for alpha, where in units of 1 it had been 1 / max(|beta|, 1 / T)
(T the longest time) times stricter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRhKL2fJBiAAfNo9rihwts
…uatqe

Cox-Lewis NHPP fit searched and checked in the units of beta (#760, Cox-Lewis units)
With BC modes, CrowAMSAA.projection took the demonstrated intensity
lambda_CA from the MLE of the Crow-AMSAA shape. The handbook's
test-fix-find-test example (MIL-HDBK-00189A section 7.5, also ReliaWiki's
Crow Extended examples and Weibull++) uses (N - 1) / N of it: 56 failures
to T = 400 give beta 0.91026 (the MLE is 0.9268), lambda_CA 0.12744, and a
projected MTBF of 11.29418; the code gave 7.707 and 11.006. lambda_BD =
N_BD / T and the (K - 1) / K shape in h(T) already matched.

The example's data are now a test, reproducing the handbook's numbers,
and Weibull++'s 11.3182 (the same data with the factors to one decimal).
With BC modes and a single failure the projection raises, as the
bias-corrected shape is 0 there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRhKL2fJBiAAfNo9rihwts
…uatqe

Crow-AMSAA BC-mode projection uses the bias-corrected shape (#730)
…omething (#769)

Parametric.qf now checks the probabilities once (two reductions,
all_in_unit_interval) and, when they are all in [0, 1], calls the
distribution's formula directly, taking the zero-inflation, cure and
offset steps only where they change something: a plain model gives its
distribution's qf (a -0.0 as 0.0, as the offset always made it). NaN or
out-of-range probabilities, autograd boxes and a distribution qf that
is not the shared wrapper go through the old path, unchanged.

The distribution qf wrapper skips its NaN and [0, 1] checks after the
same two reductions; Parametric.random's plain path calls the formula on
its uniforms directly; uniform_draws draws as scipy's uniform.rvs does
without its argument handling. Values, shapes, types, warnings and the
random streams are identical (checked bit for bit against develop over
13,000 cases). Weibull qf on 1e6 values 30 -> 15 ns each, on 2,000
101 -> 31 us.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRhKL2fJBiAAfNo9rihwts
…uatqe

A fitted model's qf takes the identity steps only where they change something (#769)
…xFrailty group_hazard)

CoxFrailty's E-step formed each group's A_g as w exp(beta'Z) times the
Breslow (or Efron) cumulative hazard directly: at a run-off beta, exp
overflowed and the increments underflowed to 0, so a row's term was nan,
with numpy's warnings. Where beta'Z or beta'Z + offset is beyond 300 (the
baseline's own threshold, now the public sums_directly), the risk sums
come from log_baseline_sums and each row's term is exp(log w + beta'Z +
log H), Efron's own-time fractions included. Elsewhere the direct code
runs unchanged, so ordinary fits are the same to the bit.

A CoxFrailty fit now reaches the log-scale CoxInformation path end to
end: a covariate that orders the first deaths runs off to beta'Z of
about 440, with an ordinary second covariate. Its standard error matches
the full information's. The run-off coefficient's variance there was the
inverse of an information at rounding (3e10, or negative a little
further out); it is nan, as CoxPH reports it (#648, #728).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRhKL2fJBiAAfNo9rihwts
…uatqe

CoxFrailty's group hazards summed in logs at a run-off beta (#777, CoxFrailty group_hazard)
Far along a run-off, CoxPH's r (each time's risk-set weight, the sum of
n exp(beta'(Z - center)) over the units at risk) is beyond floating
point: inf, or 0 at the other end, where the increments h0, computed
from log r since #766, are right. Nothing in the package reads r but
serialisation, which keeps inf and 0 through to_dict, JSON and
from_dict. So r stays as it is, IEEE's representation of a value out of
range, symmetric with h0 underflowing to 0 there. Any finite stand-in
would be wrong, and a log-scale attribute would be new API with no
reader. The model's docstring now documents r and d and what r is at a
run-off; CoxPH.fit's Returns says so too. A test checks r against the
log sums and the saved model.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRhKL2fJBiAAfNo9rihwts
…uatqe

Cox risk-set weight r documented as inf at a run-off (#777, CoxPH.r)
Centring.finish's refusal quoted the baseline's parameters at Z = 0,
which it could not compute ("parameters [inf, 1.5]"), and said every
failure was an over- or underflow. #714's case is a likelihood that
precision does not reproduce, at finite values. It also named a moved
location or scale "running on" by the run-off check made at the
covariate means, as if at Z = 0. It now quotes the parameters by name
where the fit ran, at the means. It then gives one reason: the
parameters that would be out of range at Z = 0, named without values;
or the finite values there that do not reproduce the likelihood; or a
derivative of the move that is not finite. A moved parameter the search
found running on is said to run on "at the covariate means, where the
fit runs". Numbers are formatted plainly.

The semi-parametric refusal (Cox, proportional odds, Fine-Gray) said the
move by exp(beta'center) over- or underflowed even where beta'center was
0 and only the rows' exp(beta'Z) did: it now says so where the move is
not at fault. What is refused is unchanged. CoxFrailty's refusal, CoxPH's
from its first Cox fit, said to fit with center=True, which CoxFrailty
does not take. Its Cox fit now keeps its baseline at the means: only its
coefficients are used, and they are the same either way. Its own refusal
ends with advice it can follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRhKL2fJBiAAfNo9rihwts
…uatqe

Refusal at Z = 0 names what it can compute (#777, refusal at Z = 0)
, canonical order)

canonical_order (the row order of every parametric regression, #728)
sorted by time, censoring, count, truncation and covariates. canonical_rows
(the semi-parametric fits', #772) puts the count last, so that counted
data sort as their rows expanded one per unit do. The orders now agree:
the count is canonical_order's last key, and grouped_data (the frailty
fits', which follows canonical_order) puts it after the group.

Only rows tied in time, censoring and truncation, with different counts,
change places, so only the order of the likelihood's sums moves. On 84
fits of data with such ties (WeibullPH/AFT/PO, ExponentialPH,
LogNormalAFT, WeibullFrailty, CoxFrailty, 12 seeds) every verdict is the
same and the answers move at rounding: coefficients by up to 1e-13, a
CoxFrailty theta by 2e-7 (its search tolerance), a theta at 0 from 5e-21
to 1e-16. The row-order tests (#744, #757, #772), the bootstrap tests,
the doctests of the regression package and the executed cells of the two
regression guides are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRhKL2fJBiAAfNo9rihwts
…uatqe

Parametric and frailty fits sort the count last, as canonical_rows (#777, canonical order)
…start)

ARA failed with "Could not find a good solution" on a single item's
ExpoWeibull data: the lifetime start, fitted to the gaps as from age 0,
ran off to a power law ending at the longest gap, so a gap from any
later virtual age had zero likelihood at every default start.

Where no default start is finite, each restoration start now takes the
lifetime fitted to the gaps from the ages it leaves (end ages left
truncated at the start ages), the best lifetime for that restoration.
The ARA and Kijima likelihoods expose those ages (`virtual_ages`). A
single item's perfect-repair start is no longer fitted twice (it is the
same fit as the default start's).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRhKL2fJBiAAfNo9rihwts
…uatqe

Renewal fits start where the likelihood is finite (#777, ARA run-off start)
…ar.hf(inf))

hf was df / sf: 0/0 at infinity, and wherever both underflow far in
the tail (a hazard-scale fit's hazard was nan from x = 1e6), with a
RuntimeWarning. It is now one exp of the logs of its terms (on the
hazard scale log(-dS/deta / S) is eta itself), and at infinity the
limit along the spline's last piece, linear in log x with slope s:
on the hazard scale a Weibull's of shape s (inf, the constant, or 0),
and 0 on the odds and normal scales, as a LogLogistic's and a
LogNormal's hazard are.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRhKL2fJBiAAfNo9rihwts
…uatqe

RoystonParmar.hf at infinity is its limit, quietly (#777, RoystonParmar.hf(inf))
…derivatives)

Below y = beta x = 1000 the Gamma hazard was f / S, exp(log f - log S),
whose derivatives in x and alpha are differences of terms of size 1
that cancel to a size 1 / y: alpha's was 2e-7 off mpmath's at y = 100
and 4e-5 at 1000, x's 8e-8 at 1000, and the second derivative in alpha
off by more than itself.

The continued fraction of the upper incomplete gamma now takes over
from y = 30 (and a + 2 sqrt(a) + 1), as an autograd primitive rather
than a traced loop (traced from y = 10 it made a Gamma PH fit 1.7 times
slower). Its derivative in y is r (a - 1) g / y, with no cancelling
terms; in alpha the fraction's own derivative, a complex step through
the same Lentz loop (about the cost of the value); the second
derivatives are carried along the loop (forward mode), so a Hessian is
exact. Values, first and second derivatives are within 1e-14 of mpmath
from y = 30 up. Six GammaPH/AFT fits take 1.01 times as long in all
(1.08 at most, on data far into the tail; best of runs alternated with
develop's).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRhKL2fJBiAAfNo9rihwts
…uatqe

Gamma.hf's derivatives keep their digits from y = 30 (#777, Gamma.hf derivatives)
…ewal run-off verdict)

An ExpoWeibull life running to its power-law limit (beta to 1e11, mu to
1e-11) left the ARA, Kijima and G1 fits stopped somewhere on the ridge:
called "unverified", with the start's own lifetime fit warning of its
run-off besides (twice), in terms of the start.

A fit that is not a verified maximum now has its life fitted with the
restoration held at its value, to the times whose likelihood is then
the fit's (each gap's end age left truncated at its virtual age; G1's
gaps rescaled). Where that fit has no finite maximum, by the family's
own check, the renewal fit's maximum is "no finite maximum" and it
warns in the package's form, with the life's reason and advice (the
values its own), and it takes the life's parameters where they are
further up the ridge. The start fits are quiet. Other fits are
unchanged: on the 210 renewal benchmarks of #774 only the run-off data
set's verdicts change.

The fit is not walked further along the ridge: its supremum is not
attained and the parameters are meaningless there, as for the
univariate ExpoWeibull fit, which also stops where its check finds the
run-off (one Kijima-II case stays at -43.93, ARA-inf on the same model
reaches -43.81). Builds on the virtual_ages of the ARA run-off start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRhKL2fJBiAAfNo9rihwts
…uatqe

A renewal fit whose life runs off says "No finite maximum" (#777, renewal run-off verdict)
A letter straight after the closing backticks (``qf``s) left the inline
literal open, which the docs build (sphinx -W) reports as an error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRhKL2fJBiAAfNo9rihwts
…uatqe

Changelog: close the qf literal in #769's entry
@derrynknife
derrynknife merged commit 5179c73 into master Oct 9, 2026
5 checks passed
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.

2 participants