Repository navigation
TemporalHazard 1.2.9
Bug fixes
-
The phase-identifiability warning no longer names a cause that did not
occur.variationis the relative range of a phase's contribution across
the observed times, so it collapses for every phase when those times carry
nothing that separates them. The saturated scan fired anyway: with
time = rep(2, 20)it told aconstantphase that its shape parameters were
unidentified, when aconstantphase has none, and blamed a short half-life.
A single-row fit produced the same text.That case is now reported in its own words, under a condition that takes two
things together: the observed times' own relative range is below the
tolerance, and no phase's contribution varies across them. Neither half is
sufficient. The range alone would condemn times bunched far from the origin,
where a steep phase can be fully identified; the phase measures alone would
condemn well-spread times whenever a single phase saturates. Because the
range is relative, near-ties reach the condition as exact ties do --
c(100, 100.000001)produced the old wording verbatim. Per-phase saturation
and absence over well-spread times are reported exactly as before, and the
condition is now caught even when every phase carries covariates, where the
per-phase measures are withheld and previously nothing was said at all.Two related corrections. A phase that has not started is reported as absent
even in a left-truncated fit, since that test rests on the share and not on
how the times are spread. And when the counting-process entry times or
interval bounds supply enough evaluation points, the degenerate-times verdict
is withheld -- and "enough" is counted rather than assumed. With the event
times tied, each added point supplies one more functional of the parameters,
so one more than the number of added points must reach the number of free
parameters. The package's default two-phase model has five, and pinning a
phase's shapes lowers the bar because those parameters are no longer free.
Points that add nothing do not count: a zero entry time, a bound equal to an
event time -- both of whichSurv(type = "interval")synthesises for every
row -- or a bound the objective never reads, sincetime_upperis live only
for left-censored and interval rows. Counting any of those silenced this
warning on data that needed it while changing no likelihood value.The message also no longer names shape parameters for a model that has none.
A singleconstantphase is now silent, since one functional determines its
muexactly; twoconstantphases still warn, since only their sum is
determined, but the wording says the phases cannot be told apart rather than
naming shapes they do not have.Where the times are NOT degenerate, the per-phase saturation message is
reported regardless of the bounds, so adding astartcolumn no longer
removes a correct diagnostic (#211).Known limitation, unchanged by this work and stated rather than implicit: the
per-phase measures are taken over the event times alone, and the count of
evaluation points is deliberately conservative -- it does not credit the
extra functional a zero entry time makes separately observable, so a fit can
be warned about while being identified. That cuts both ways.
A phase can be flat across the event times while its shape is identified
through the bounds, so the saturated message can overstate what is lost; and
the counting rule above is a local identification argument for tied event
times, so a fit can clear it and still have a flat direction the guard does
not see, in which case nothing is said at all. Tracked in #228. -
The SAS nomogram parser no longer deletes a whole-year time column. The
PROC PRINTobservation counter is identified as a leading column running
exactly1..n, which rested on no measurement column ever being a gapless
1-based integer run -- true of the corpus in hand, not of SAS listings. A
counter-suppressed nomogram on a whole-year grid printsYEARSas1.0000, 2.0000, 3.0000, and the parser deleted the time key. Because the remaining
columns then matched the header count, the shape check that would have caught
it passed: the listing parsed, and the comparison ran against a table with no
time column.The header now decides: a leading column the header names as a counter is
dropped whatever values it carries, which also fixes the reverse case -- page
two of a paginated listing starts at 41, and requiring a1..nrun left that
counter in the data. The labels are matched case-insensitively, after the
leading underscore the parser strips, so SAS's_N_is recognised. A1..n
run under a name the parser does not know is now kept and warned about rather
than silently dropped: the listing cannot distinguish a counter spelled a new
way from a measurement that happens to run1..n, and an extra column is
visible where a deleted one is not (#212). -
A phase named
totalis now rejected rather than silently losing its
contribution.totalis the key the multiphase cumulative-hazard
accumulator is stored under, sophases = list(total = hzr_phase("cdf"), ...)
overwrote that phase's own contribution vector with the summed hazard. The fit
returned normally; the phase then reported a contribution share of exactly 1
and the identifiability diagnostic read it as dominating the model. The name is
reserved across the decomposition and prediction output as well, where it
labels the total column.hazard()andhzr_theta_names()-- which applies
the same validation -- now stop with an explanatory message, and the
reservation is documented on thephasesargument (#214). -
objective = "sas"data defects are reported as data defects. The two
conditions the SAS objective imposes -- no left-censored rows, and a positive
width on every interval-censored row -- are pure functions of the data, but
were checked inside the objective. The optimizer's per-start handler caught
them and reported "produced no usable fit from N starts", which reads as a
convergence problem and invites raisingn_starts: a remedy that cannot work,
because the condition is identical at every start.hazard()now checks both
before any optimization, and the interval-width message reports the offending
row in the data rather than its position among the interval rows. The guards
inside the objective and gradient are unchanged, since the gradient is
reachable withouthazard()(#213). -
Behaviour change: because those preconditions are properties of the data
and not of the fit, they are now checked wheneverobjective = "sas"is
specified, includingfit = FALSE.hazard(..., fit = FALSE, objective = "sas")on left-censored or zero-width-interval data previously returned a
hazardobject carrying an objective it could never be fitted with; it now
stops. AnNAinstatusunderobjective = "sas"is also reported by
argument and row, rather than as a baremissing value where TRUE/FALSE needed.Note this reaches only the codes
hazard()is given. On the vector
interface asurvival::Surv()object is unclassed without translating its
codes, so they mean something else on arrival, and what goes wrong depends on
type: undertype = "left"a left-censored row is coded0and is fitted
as right-censored; undertype = "interval"it is coded2and is read as
an interval of zero width, which this guard then rejects; and a genuine
interval row, coded3, matches no branch of the likelihood and contributes
nothing. The formula interface translates and is guarded. That asymmetry is a
pre-existing defect of the vector path, tracked in #226.