Mathematical Foundations of TemporalHazard
Generalized decomposition, multiphase additive hazard, censoring, and time-varying covariates
Source:vignettes/mf-mathematical-foundations.qmd
This vignette provides the mathematical foundation for the models implemented in TemporalHazard. It covers the generalized temporal decomposition family (Blackstone, Naftel, and Turner 1986), the additive multiphase hazard model, maximum likelihood estimation under mixed censoring, and extensions for time-varying covariates. The decomposition framework extends naturally to longitudinal mixed-effects settings (Rajeswaran et al. 2018).
For users migrating from the legacy SAS/C HAZARD program, each section notes how the R parameterization relates to the original parameter names. For the full translation table, see vignette("sas-to-r-migration") or call hzr_argument_mapping().
1 Generalized Temporal Decomposition
1.1 The parametric family
Every temporal phase in TemporalHazard is built from a single parametric family, decompos(t; t_half, nu, m), introduced by Blackstone, Naftel, and Turner (1986) that produces three linked quantities from just three parameters:
- — cumulative distribution function (CDF), bounded
- — probability density function
- — hazard function
The three parameters control the shape of the distribution:
| Parameter | Meaning | Constraint |
|---|---|---|
t_half |
Half-life: time at which | |
nu |
Time exponent — controls rate dynamics | any finite value |
m |
Shape exponent — controls distributional form | any finite value |
SAS/C parameter bridge
The original C code used different parameter names per phase:
- Early phase (G1):
DELTA,RHO/THALF,NU,Mt_half,nu,m- Late phase (G3):
TAU,GAMMA,ALPHA,ETAt_half,nu,mBoth collapse onto the same 3-parameter decomposition family. The C
DELTAparameter controlled a time transformation that is absorbed into the shape.
In R, hzr_decompos() computes all three quantities:
t_grid <- seq(0.01, 10, length.out = 200)
# Standard sigmoidal (Case 1: m > 0, nu > 0)
d <- hzr_decompos(t_grid, t_half = 3, nu = 2, m = 1)
# Verify the half-life property: G(t_half) = 0.5
d_half <- hzr_decompos(3, t_half = 3, nu = 2, m = 1)
cat("G(t_half) =", round(d_half$G, 6), "\n")
#> G(t_half) = 0.5
# Verify the identity: h(t) = g(t) / (1 - G(t))
h_check <- d$g / (1 - d$G)
cat("Max |h - g/(1-G)| =", max(abs(d$h - h_check)), "\n")
#> Max |h - g/(1-G)| = 01.2 The six valid cases
The signs of nu and m determine six distinct distributional shapes. The combination and is undefined and raises an error.
| Case | m | nu | Behavior |
|---|---|---|---|
| 1 | > 0 | > 0 | Standard sigmoidal |
| 1L | = 0 | > 0 | Weibull-like (exponential limit as m -> 0) |
| 2 | < 0 | > 0 | Heavy-tailed |
| 2L | < 0 | = 0 | Exponential decay |
| 3 | > 0 | < 0 | Bounded cumulative |
| 3L | = 0 | < 0 | Bounded exponential |
The following plot shows the CDF , density , and hazard for each case, all with t_half = 3:
if (has_ggplot) {
library(ggplot2)
params <- list(
"Case 1: m=1, nu=2" = list(nu = 2, m = 1),
"Case 1L: m=0, nu=2" = list(nu = 2, m = 0),
"Case 2: m=-0.5, nu=2" = list(nu = 2, m = -0.5),
"Case 2L: m=-0.5, nu=0" = list(nu = 0, m = -0.5),
"Case 3: m=1, nu=-0.5" = list(nu = -0.5, m = 1),
"Case 3L: m=0, nu=-0.5" = list(nu = -0.5, m = 0)
)
t_grid <- seq(0.01, 10, length.out = 200)
rows <- list()
for (nm in names(params)) {
p <- params[[nm]]
d <- hzr_decompos(t_grid, t_half = 3, nu = p$nu, m = p$m)
rows <- c(rows, list(data.frame(
time = rep(t_grid, 3),
value = c(d$G, d$g, d$h),
quantity = rep(c("G(t) — CDF", "g(t) — density", "h(t) — hazard"),
each = length(t_grid)),
case = nm
)))
}
df <- do.call(rbind, rows)
# Cap hazard display at reasonable values for readability
df$value[df$quantity == "h(t) — hazard" & df$value > 5] <- NA
ggplot(df, aes(x = time, y = value)) +
geom_line(linewidth = 0.6) +
facet_grid(quantity ~ case, scales = "free_y") +
labs(x = "Time", y = NULL) +
theme_minimal(base_size = 10) +
theme(strip.text = element_text(size = 7))
}
1.3 Derivation sketch
The construction begins with a rate function chosen so that exactly. For Case 1 ():
Then, with the dimensionless time :
The other five cases arise as limits (, ) or sign reflections of this base form. The implementation in hzr_decompos() dispatches to the appropriate branch after checking the signs of nu and m.
2 Additive Multiphase Hazard Model
2.1 Model specification
The total cumulative hazard decomposes additively across phases:
where:
- is the phase-specific log-linear scale with covariates
- is the temporal shape, determined by the phase type
The three phase types correspond to different uses of the decomposition output:
| Phase type | Domain | Interpretation | |
|---|---|---|---|
"cdf" |
Early risk that resolves over time | ||
"hazard" |
Late or aging risk that accumulates | ||
"constant" |
Flat background rate (no shape parameters) |
SAS/C bridge
The classic three-phase HAZARD model used:
- G1 (early)
hzr_phase("cdf", ...)- G2 (constant)
hzr_phase("constant")- G3 (late)
hzr_phase("hazard", ...)TemporalHazard generalizes this to phases of any type.
2.2 Derived quantities
From the cumulative hazard, the instantaneous hazard and survival are:
The derivative depends on phase type:
-
"cdf": (the density) -
"hazard": -
"constant":
2.3 Constructing phases in R
Each phase is specified with hzr_phase() and passed to hazard():
# Classic three-phase pattern
early <- hzr_phase("cdf", t_half = 0.5, nu = 2, m = 0)
const <- hzr_phase("constant")
late <- hzr_phase("hazard", t_half = 5, nu = 1, m = 0)
print(early)
#> <hzr_phase> cdf (early risk)
#> t_half = 0.5 nu = 2 m = 0
print(const)
#> <hzr_phase> constant (flat rate)
print(late)
#> <hzr_phase> hazard (late risk)
#> t_half = 5 nu = 1 m = 0The cumulative hazard contribution and instantaneous hazard for each phase can be computed directly:
if (has_ggplot) {
t_grid <- seq(0.01, 12, length.out = 200)
phi_early <- hzr_phase_cumhaz(t_grid, t_half = 0.5, nu = 2, m = 0,
type = "cdf")
phi_late <- hzr_phase_cumhaz(t_grid, t_half = 5, nu = 1, m = 0,
type = "hazard")
phi_const <- hzr_phase_cumhaz(t_grid, type = "constant")
dphi_early <- hzr_phase_hazard(t_grid, t_half = 0.5, nu = 2, m = 0,
type = "cdf")
dphi_late <- hzr_phase_hazard(t_grid, t_half = 5, nu = 1, m = 0,
type = "hazard")
dphi_const <- hzr_phase_hazard(t_grid, type = "constant")
df <- data.frame(
time = rep(t_grid, 6),
value = c(phi_early, phi_late, phi_const,
dphi_early, dphi_late, dphi_const),
phase = rep(rep(c("cdf (early)", "hazard (late)", "constant"),
each = length(t_grid)), 2),
quantity = rep(c("Phi(t) — cumulative", "phi(t) — instantaneous"),
each = 3 * length(t_grid))
)
ggplot(df, aes(x = time, y = value, colour = phase)) +
geom_line(linewidth = 0.8) +
facet_wrap(~ quantity, scales = "free_y", ncol = 1) +
scale_colour_manual(values = c(
"cdf (early)" = "#0072B2",
"hazard (late)" = "#D55E00",
"constant" = "#009E73"
)) +
labs(x = "Time", y = NULL, colour = "Phase type") +
theme_minimal(base_size = 11) +
theme(legend.position = "bottom")
}
2.4 Additive composition example
To build intuition, here is how three phases combine into a total hazard. The key insight is that phases contribute additively to the cumulative hazard , not to the survival directly.
if (has_ggplot) {
t_grid <- seq(0.01, 15, length.out = 300)
mu_early <- 0.3
mu_const <- 0.05
mu_late <- 0.2
H_early <- mu_early * hzr_phase_cumhaz(t_grid, t_half = 0.5, nu = 2,
m = 0, type = "cdf")
H_const <- mu_const * hzr_phase_cumhaz(t_grid, type = "constant")
H_late <- mu_late * hzr_phase_cumhaz(t_grid, t_half = 5, nu = 1,
m = 0, type = "hazard")
H_total <- H_early + H_const + H_late
df <- data.frame(
time = rep(t_grid, 4),
cumhaz = c(H_early, H_const, H_late, H_total),
component = rep(c("Early (cdf)", "Constant", "Late (hazard)", "Total"),
each = length(t_grid))
)
df$component <- factor(df$component,
levels = c("Total", "Early (cdf)", "Constant", "Late (hazard)"))
ggplot(df, aes(x = time, y = cumhaz, colour = component,
linewidth = component)) +
geom_line() +
scale_colour_manual(values = c(
"Total" = "black", "Early (cdf)" = "#0072B2",
"Constant" = "#009E73", "Late (hazard)" = "#D55E00"
)) +
scale_linewidth_manual(values = c(
"Total" = 1.2, "Early (cdf)" = 0.6,
"Constant" = 0.6, "Late (hazard)" = 0.6
)) +
labs(x = "Time", y = "Cumulative hazard H(t)",
colour = NULL, linewidth = NULL) +
theme_minimal(base_size = 11) +
theme(legend.position = "bottom")
}
3 Maximum Likelihood Estimation
3.1 Log-likelihood under mixed censoring
TemporalHazard supports four censoring types in a single dataset, coded by the status indicator:
| Code | Type | Contribution to log-likelihood |
|---|---|---|
| Exact event | ||
| Right-censored | ||
| Left-censored | ||
| Interval-censored |
where and are the lower and upper bounds of the censoring interval.
The full log-likelihood is:
The terms are computed using the numerically stable primitive hzr_log1mexp(), which avoids catastrophic cancellation when is near zero.
# Naive log(1 - exp(-x)) fails near zero
x_small <- 1e-15
cat("Naive: ", log(1 - exp(-x_small)), "\n")
#> Naive: -34.53958
cat("Stable: ", hzr_log1mexp(x_small), "\n")
#> Stable: -34.538783.2 Internal parameterization
During optimization, shape parameters are reparameterized for unconstrained search. For each phase , the internal parameter sub-vector is:
| Parameter | Internal (estimation) | User-facing (reported) |
|---|---|---|
| Scale | ||
| Half-life | ||
| Time exponent | (unconstrained) | |
| Shape | (unconstrained) | |
| Covariates | same |
Constant phases omit log_t_half, nu, and m, contributing only one shape-free parameter (log_mu). The full is the concatenation of all phase sub-vectors.
3.3 Optimization strategy
The optimizer uses stats::optim() with BFGS on the unconstrained internal scale, wrapped by .hzr_optim_generic(). Key features:
-
Multi-start: the optimizer runs from random perturbations around the user-supplied starting values (default , controlled by
control$n_starts). The run with the highest log-likelihood is kept. - Feasibility guard: any parameter vector where and for the same phase returns immediately.
- Post-fit Hessian: the numerical Hessian at the solution is inverted to produce the variance-covariance matrix. Standard errors are .
4 Covariates and Phase-Specific Formulas
4.1 Global covariates
By default, every phase shares the same covariate vector from the model formula. Each phase gets its own coefficient vector :
This means the same predictors can have different effects on early vs. late risk — a core feature of multiphase models.
4.2 Phase-specific covariates
When different phases are driven by different risk factors, each phase can specify its own one-sided formula:
# Early risk depends on surgical factors; late risk on chronic conditions
hazard(
survival::Surv(time, status) ~ age + nyha + shock,
data = dat,
dist = "multiphase",
phases = list(
early = hzr_phase("cdf", t_half = 0.5, nu = 2, m = 0,
formula = ~ age + shock),
late = hzr_phase("hazard", t_half = 5, nu = 1, m = 0,
formula = ~ age + nyha)
),
fit = TRUE
)When a phase has a formula, it gets its own design matrix built from the data, and only those covariates appear in its . The parameter vector is shorter for phases with fewer covariates, which can improve identifiability and convergence.
4.3 Time-varying coefficients
For single-distribution models, TemporalHazard supports piecewise time-varying coefficients via the time_windows argument. This expands the design matrix into window-specific blocks:
where are the time windows defined by the cut points. Each predictor gets a separate coefficient in each window, allowing effects to change over follow-up time.
Multiphase vs. time-varying coefficients
For multiphase models, the phase structure itself captures time-varying effects: early phases dominate at small and late phases at large . The
time_windowsmechanism is a complementary approach for single-distribution models.
5 Identifiability and Practical Considerations
5.1 Parameter identifiability
Multiphase models with many free parameters can have flat or multi-modal likelihood surfaces. Practical guidelines:
Fix shape parameters when possible. If clinical knowledge suggests a specific temporal pattern (e.g. early mortality follows a Weibull shape with ), fix
min thehzr_phase()starting values and inspect whether the optimizer moves it.Start from the SAS/C estimates. If legacy results are available, translate them using
hzr_argument_mapping()and supply as starting values.Use multi-start optimization. The default
control$n_starts = 5helps escape local optima, but complex models may benefit from more starts.Phase-specific covariates improve identifiability. Restricting each phase to clinically relevant predictors reduces the parameter count and sharpens the likelihood surface.
5.2 Numerical stability
The decomposition engine applies several guards:
- Time is clamped above
.Machine$double.xminto avoid -
is clamped above
.Machine$double.xminbefore computing the hazard - Left- and interval-censored contributions use
hzr_log1mexp()to avoid when is very small
These guards ensure finite gradients throughout the optimization, even in regions of parameter space far from the optimum.
6 Summary of Key Functions
| Function | Purpose |
|---|---|
hzr_decompos(t, t_half, nu, m) |
Core decomposition: returns , , |
hzr_phase_cumhaz(t, ..., type) |
Phase cumulative hazard |
hzr_phase_hazard(t, ..., type) |
Phase instantaneous hazard |
hzr_phase(type, t_half, nu, m, formula) |
Construct a phase specification |
hazard(..., dist = "multiphase", phases = ...) |
Fit a multiphase model |
predict(fit, type, decompose = TRUE) |
Per-phase decomposed predictions |
hzr_argument_mapping() |
SAS/C R parameter translation table |
hzr_log1mexp(x) |
Stable |
For a worked clinical example, see vignette("getting-started"). For migration from SAS HAZARD, see vignette("sas-to-r-migration").
References
Blackstone EH, Naftel DC, Turner ME Jr. The decomposition of time-varying hazard into phases, each incorporating a separate stream of concomitant information. J Am Stat Assoc. 1986;81(395):615–624. doi: 10.1080/01621459.1986.10478314
Rajeswaran J, Blackstone EH, Ehrlinger J, Li L, Ishwaran H, Parides MK. Probability of atrial fibrillation after ablation: Using a parametric nonlinear temporal decomposition mixed effects model. Stat Methods Med Res. 2018;27(1):126–141. doi: 10.1177/0962280215623583