Skip to content

TemporalHazard 1.2.9

Choose a tag to compare

@ehrlinger ehrlinger released this 05 Sep 18:25
· 1584 commits to main since this release
fe57e60

Bug fixes

  • The phase-identifiability warning no longer names a cause that did not
    occur.
    variation is 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 a constant phase that its shape parameters were
    unidentified, when a constant phase 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 which Surv(type = "interval") synthesises for every
    row -- or a bound the objective never reads, since time_upper is 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 single constant phase is now silent, since one functional determines its
    mu exactly; two constant phases 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 a start column 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 PRINT observation counter is identified as a leading column running
    exactly 1..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 prints YEARS as 1.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 a 1..n run 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. A 1..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 run 1..n, and an extra column is
    visible where a deleted one is not (#212).

  • A phase named total is now rejected rather than silently losing its
    contribution.
    total is the key the multiphase cumulative-hazard
    accumulator is stored under, so phases = 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() and hzr_theta_names() -- which applies
    the same validation -- now stop with an explanatory message, and the
    reservation is documented on the phases argument (#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 raising n_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 without hazard() (#213).

  • Behaviour change: because those preconditions are properties of the data
    and not of the fit, they are now checked whenever objective = "sas" is
    specified, including fit = FALSE. hazard(..., fit = FALSE, objective = "sas") on left-censored or zero-width-interval data previously returned a
    hazard object carrying an objective it could never be fitted with; it now
    stops. An NA in status under objective = "sas" is also reported by
    argument and row, rather than as a bare missing value where TRUE/FALSE needed.

    Note this reaches only the codes hazard() is given. On the vector
    interface a survival::Surv() object is unclassed without translating its
    codes, so they mean something else on arrival, and what goes wrong depends on
    type: under type = "left" a left-censored row is coded 0 and is fitted
    as right-censored; under type = "interval" it is coded 2 and is read as
    an interval of zero width, which this guard then rejects; and a genuine
    interval row, coded 3, 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.