---
title: "Complete Clinical Analysis Walkthrough"
subtitle: "From Kaplan-Meier baseline to validated multivariable model"
author: "John Ehrlinger"
date: last-modified
format:
  html:
    code-fold: false
    toc: true
    toc-depth: 3
    number-sections: true
vignette: >
  %\VignetteIndexEntry{Complete Clinical Analysis Walkthrough}
  %\VignetteEngine{quarto::html}
  %\VignetteEncoding{UTF-8}
---

```{r}
#| include: false
has_pkg <- requireNamespace("TemporalHazard", quietly = TRUE)
has_ggplot2 <- requireNamespace("ggplot2", quietly = TRUE)
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  fig.width = 7,
  fig.height = 4.5,
  eval = has_pkg
)
```

The previous vignettes covered each piece of the analytical workflow
in isolation — how to fit a model, how to predict from it, how to
validate it. This vignette runs all of those pieces together on a
single dataset, in the order you'd actually run them in a real
clinical analysis. The point isn't to introduce new functions; it's
to show how the pieces chain into a disciplined sequence that turns
raw covariates into a fitted, validated, defensible hazard model.

The sequence mirrors the one in the original SAS HAZARD system, which
codified what cardiothoracic-surgery biostatisticians had been doing
informally for decades:

1. **Nonparametric baseline** — Kaplan-Meier life table to see what
   the data is saying before any model intervenes.
2. **Shape fitting** — start with a single-distribution Weibull,
   build up to a multiphase decomposition when the KM curve
   demands it.
3. **Variable screening** — univariable association tests and
   calibration plots to decide which covariates enter the model and
   in what functional form.
4. **Multivariable model** — covariates layered onto a hazard shape
   you already trust.
5. **Prediction** — patient-specific risk profiles for clinical
   reporting and decision support.
6. **Validation** — decile-of-risk calibration to verify the model
   is honest across the risk spectrum, not just on average.

This corresponds to the SAS programs `ac.*` → `hz.*` → `lg.*` →
`hm.*` → `hp.*` → `hs.*`. If you're familiar with the SAS workflow,
the section structure should feel familiar; if not, treat this as
the canonical analytical sequence to follow on any new dataset.

We use the **AVC** dataset (310 patients, atrioventricular canal
repair) which has rich covariates and two identifiable hazard
phases — fewer phases than the 3-phase CABG example you've seen in
other vignettes, but with the covariate complexity needed to
exercise the screening, multivariable-fit, and validation steps.

## Data preparation

Load the package and the AVC dataset, drop incomplete rows so the
design matrix is rectangular for the multivariable fits to come, and
inspect the resulting column types and ranges. The `na.omit()` step
is conservative — losing rows is a real cost — but for a walkthrough
we want every later fit to use the same patient set so the comparisons
are apples-to-apples.

```{r}
#| label: setup
library(TemporalHazard)
if (has_ggplot2) library(ggplot2)

data(avc)
avc <- na.omit(avc)
str(avc)
```

The AVC dataset contains `r if (exists("avc", inherits = TRUE)) nrow(avc) else 310` patients after removing incomplete
cases.  Key covariates include age at repair (`age`, in months), NYHA
functional class (`status`), presence of malalignment (`mal`), and
interventricular communication (`com_iv`).

## Step 1: Nonparametric baseline

Before fitting any parametric model, establish the empirical survival curve
using the Kaplan-Meier estimator.  This is the benchmark against which all
parametric fits will be compared.

`hzr_kaplan()` wraps `survival::survfit()` and adds logit-transformed exact
confidence limits that respect the `[0, 1]` boundary --- more accurate in
the tails than the default Greenwood intervals.  This matches the SAS
`kaplan.sas` macro output structure.

```{r}
#| label: km-life-table
km <- hzr_kaplan(time = avc$int_dead, status = avc$dead)
head(km)
```

The returned data frame is the life table: one row per event time with
`n_risk`, `n_event`, `survival`, logit-transformed 95% CLs
(`cl_lower` / `cl_upper`), and a KM-based cumulative hazard
(`-log(survival)` --- use `hzr_nelson()` if you need the Nelson-Aalen
estimator instead).  Plotting just requires the survival column:

```{r}
#| label: fig-km-baseline
#| fig-cap: "Kaplan-Meier survival estimate for AVC patients (n = 310)"
#| eval: !expr requireNamespace("TemporalHazard", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE)
km_df <- data.frame(
  time     = km$time,
  survival = km$survival * 100,
  lower    = km$cl_lower * 100,
  upper    = km$cl_upper * 100
)

ggplot(km_df, aes(time, survival)) +
  geom_step(colour = "#D55E00", linewidth = 0.8) +
  geom_ribbon(aes(ymin = lower, ymax = upper),
              stat = "identity", alpha = 0.15, fill = "#D55E00") +
  labs(x = "Months after AVC repair", y = "Survival (%)") +
  coord_cartesian(ylim = c(0, 100)) +
  theme_minimal()
```

The KM curve shows a sharp early drop (operative mortality) followed by a
roughly constant attrition rate.  This two-phase pattern suggests an early
CDF phase plus a constant phase --- no obvious late rising hazard.

## Step 2: Shape fitting --- simple to complex

### 2a. Single-phase Weibull

The discipline here is to start with the simplest parametric model
and earn any added complexity. A single-distribution Weibull
intercept-only fit gives us a smooth two-parameter curve to compare
against the KM baseline. If the Weibull tracks the KM step function
reasonably well, we may be done; if it can't, the gap tells us
exactly where a richer model needs to go.

```{r}
#| label: weibull-fit
fit_weib <- hazard(
  survival::Surv(int_dead, dead) ~ 1,
  data  = avc,
  dist  = "weibull",
  theta = c(mu = 0.05, nu = 0.5),
  fit   = TRUE
)
summary(fit_weib)
```

The Weibull forces a monotone hazard shape.  Let's overlay it on the
Kaplan-Meier to see where it fits well and where it doesn't.

```{r}
#| label: fig-weibull-overlay
#| fig-cap: "Single Weibull vs. Kaplan-Meier"
#| eval: !expr requireNamespace("TemporalHazard", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE)
t_grid <- seq(0.01, max(avc$int_dead) * 0.9, length.out = 200)
surv_weib <- predict(fit_weib,
                     newdata = data.frame(time = t_grid),
                     type = "survival") * 100

ggplot() +
  geom_step(data = km_df, aes(time, survival),
            colour = "#D55E00", linewidth = 0.6) +
  geom_line(data = data.frame(time = t_grid, survival = surv_weib),
            aes(time, survival), colour = "#0072B2", linewidth = 0.8) +
  labs(x = "Months after AVC repair", y = "Survival (%)") +
  coord_cartesian(ylim = c(0, 100)) +
  theme_minimal()
```

The single Weibull typically misses the sharp early drop --- it compromises
between the early and late time frames.

### 2b. Two-phase model (early CDF + constant)

The KM curve drops steeply in the first few months — operative
mortality — and then settles into a roughly linear decline. The
Weibull tried to capture both with one shape and ended up
compromising. Splitting the hazard into two phases lets each
mechanism have its own parameterization: an `"cdf"` early phase that
saturates as operative risk resolves, plus a `"constant"` phase that
carries the steady background attrition. Two phases is what AVC
actually needs; CABG with longer follow-up would need a third
late-rising phase to handle graft deterioration, but the AVC
follow-up window doesn't extend far enough to identify one.

```{r}
#| label: multiphase-fit
fit_mp <- hazard(
  survival::Surv(int_dead, dead) ~ 1,
  data   = avc,
  dist   = "multiphase",
  phases = list(
    early    = hzr_phase("cdf", t_half = 0.5, nu = 1, m = 1,
                          fixed = "shapes"),
    constant = hzr_phase("constant")
  ),
  fit     = TRUE,
  control = list(n_starts = 5, maxit = 1000)
)
summary(fit_mp)
```

Note the use of `fixed = "shapes"` --- we fix the temporal shape parameters
and only estimate the scale (log_mu) for each phase.  This matches the
standard HAZARD workflow: shapes are set from clinical knowledge or
preliminary exploration, then scales and covariates are estimated.

```{r}
#| label: fig-multiphase-overlay
#| fig-cap: "Two-phase parametric model vs. Kaplan-Meier"
#| eval: !expr requireNamespace("TemporalHazard", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE)
surv_mp <- predict(fit_mp,
                   newdata = data.frame(time = t_grid),
                   type = "survival") * 100

ggplot() +
  geom_step(data = km_df, aes(time, survival),
            colour = "#D55E00", linewidth = 0.6) +
  geom_line(data = data.frame(time = t_grid, survival = surv_mp),
            aes(time, survival), colour = "#0072B2", linewidth = 0.8) +
  labs(x = "Months after AVC repair", y = "Survival (%)") +
  coord_cartesian(ylim = c(0, 100)) +
  theme_minimal()
```

### 2c. Decomposed hazard

The overlay plot shows the *total* fit tracks KM well, but it
doesn't show whether each phase is doing the job we asked of it.
Decomposing the cumulative hazard into per-phase contributions is
the diagnostic that answers that question: the early phase should
account for most of the steep early drop, the constant phase should
carry the rest, and neither phase should be doing implausible work
in the wrong time window.

```{r}
#| label: fig-decomposed
#| fig-cap: "Phase decomposition of cumulative hazard"
#| eval: !expr requireNamespace("TemporalHazard", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE)
decomp <- predict(fit_mp,
                  newdata = data.frame(time = t_grid),
                  type = "cumulative_hazard",
                  decompose = TRUE)

decomp_df <- data.frame(
  time  = t_grid,
  total = decomp[, "total"],
  early = decomp[, "early"],
  const = decomp[, "constant"]
)

ggplot(decomp_df, aes(x = time)) +
  geom_line(aes(y = total), linewidth = 0.9, colour = "black") +
  geom_line(aes(y = early), linewidth = 0.7, colour = "#E69F00",
            linetype = "dashed") +
  geom_line(aes(y = const), linewidth = 0.7, colour = "#56B4E9",
            linetype = "dashed") +
  labs(x = "Months after AVC repair", y = "Cumulative hazard") +
  theme_minimal()
```

The early phase contributes most of its risk in the first few months, then
levels off.  The constant phase accumulates linearly over time.

## Step 3: Variable screening

Before entering covariates into the hazard model, screen them for
association with the outcome.  This step corresponds to the `lg.*` programs
in the SAS workflow.

### 3a. Univariable logistic screening

The cheapest screening tool we have is a univariable logistic
regression of the event indicator on each covariate. It throws away
the time-to-event structure but it answers a binary question
quickly: does this covariate have *any* association with mortality
at all? Covariates whose univariable p-value is huge will not
suddenly become significant in the multivariable hazard model
either — those can be deprioritized. Covariates with small
univariable p-values deserve closer functional-form inspection
before they enter the formula.

```{r}
#| label: screening
covariates <- c("age", "status", "mal", "com_iv", "orifice",
                "inc_surg", "opmos")

screen_results <- data.frame(
  variable = covariates,
  coef     = numeric(length(covariates)),
  p_value  = numeric(length(covariates))
)

for (i in seq_along(covariates)) {
  v <- covariates[i]
  fml <- as.formula(paste("dead ~", v))
  lg <- glm(fml, data = avc, family = binomial)
  s <- summary(lg)$coefficients
  if (nrow(s) >= 2) {
    screen_results$coef[i] <- s[2, 1]
    screen_results$p_value[i] <- s[2, 4]
  }
}

screen_results <- screen_results[order(screen_results$p_value), ]
screen_results$significant <- ifelse(screen_results$p_value < 0.05,
                                      "*", "")
screen_results
```

### 3b. Functional form assessment

For continuous covariates that appear significant, examine whether the
relationship with outcome is monotone on the logit scale.  `hzr_calibrate()`
implements the SAS `logit.sas` / `logitgr.sas` macros: bin the continuous
covariate into quantile groups, compute the observed event rate per bin,
and transform it to the logit scale.

```{r}
#| label: age-calibrate
cal_age <- hzr_calibrate(x = avc$age, event = avc$dead,
                          groups = 10, link = "logit")
cal_age
```

Plot the logit-transformed event rate against the bin centre (`mean`
column).  A straight line means the covariate can enter the model
untransformed; monotone-but-curved shapes suggest a log or square-root
transform; U-shapes call for a quadratic term.

```{r}
#| label: fig-calibrate-age
#| fig-cap: "Decile calibration: age vs. logit(P(death))"
#| eval: !expr requireNamespace("TemporalHazard", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE)
ggplot(cal_age, aes(mean, link_value)) +
  geom_point(size = 3, colour = "#0072B2") +
  geom_line(colour = "#0072B2", alpha = 0.4) +
  geom_smooth(method = "lm", formula = y ~ x, se = FALSE,
              colour = "#D55E00", linetype = "dashed") +
  labs(x = "Age at repair (months, decile midpoint)",
       y = "logit(P(death))") +
  theme_minimal()
```

The blue points are the observed decile logits; the dashed red line is a
linear reference.  Deviations from linearity flag where a transform is
warranted.

## Step 4: Multivariable model

### 4a. Manual specification

We've already established the two-phase shape and screened the
covariates; the multivariable model is the synthesis. Drop the
covariates that passed screening onto the right-hand side of the
formula and refit. The shape parameters stay fixed (we've earned
that decision from §2); the optimizer estimates the two phase
scales and one coefficient per covariate, jointly. This is the
working model you'd report from this analysis.

```{r}
#| label: multivariable-fit
fit_mv <- hazard(
  survival::Surv(int_dead, dead) ~ age + status + mal + com_iv,
  data   = avc,
  dist   = "multiphase",
  phases = list(
    early    = hzr_phase("cdf", t_half = 0.5, nu = 1, m = 1,
                          fixed = "shapes"),
    constant = hzr_phase("constant")
  ),
  fit     = TRUE,
  control = list(n_starts = 5, maxit = 1000)
)
summary(fit_mv)
```

The coefficient table shows phase-specific covariate effects.  A positive
coefficient means higher hazard (worse prognosis).  Interpretation:
covariates scale the phase-specific hazard multiplicatively via
exp(x * beta).

### 4b. Automated stepwise selection

The manual approach works when screening has already narrowed the
candidate pool, but with a larger pool it helps to let the model
choose.  `hzr_stepwise()` runs forward, backward, or two-way
selection against an existing `hazard` fit, scoring candidates with
Wald p-values or delta-AIC.  Defaults match SAS `PROC HAZARD`
(`SLENTRY = 0.30`, `SLSTAY = 0.20`).

We start from a baseline with no covariates, offer the same screened
candidate pool, and let the algorithm decide.  For multiphase models
the scope is a named list of one-sided formulas keyed by phase, so
each phase can advertise its own candidate set; stepping operates
per `(variable, phase)` pair, letting a covariate enter one phase
and not another.  Single-distribution models accept either a flat
one-sided formula or a character vector of names.

```{r}
#| label: stepwise-forward
#| eval: !expr requireNamespace("TemporalHazard", quietly = TRUE)
base_mp <- hazard(
  survival::Surv(int_dead, dead) ~ 1,
  data   = avc,
  dist   = "multiphase",
  phases = list(
    early    = hzr_phase("cdf", t_half = 0.5, nu = 1, m = 1,
                          fixed = "shapes"),
    constant = hzr_phase("constant")
  ),
  fit     = TRUE,
  control = list(n_starts = 3, maxit = 500)
)

fit_step <- hzr_stepwise(
  base_mp,
  scope     = list(
    early    = ~ age + status + mal + com_iv,
    constant = ~ age + status + mal + com_iv
  ),
  data      = avc,
  direction = "both",
  criterion = "wald",
  trace     = FALSE,
  control   = list(n_starts = 2, maxit = 500)
)
fit_step
```

The `$steps` table records every accepted action with its Wald
statistic and p-value:

```{r}
#| eval: !expr requireNamespace("TemporalHazard", quietly = TRUE)
fit_step$steps[, c("step_num", "action", "variable", "phase",
                   "p_value", "aic")]
```

The final model is the fit at the top of the object, reachable
through the usual hazard accessors:

```{r}
#| eval: !expr requireNamespace("TemporalHazard", quietly = TRUE)
logLik_manual <- fit_mv$fit$objective
logLik_step   <- fit_step$fit$objective
c(manual = logLik_manual, stepwise = logLik_step,
  aic_manual = 2 * length(fit_mv$fit$theta) - 2 * logLik_manual,
  aic_step   = 2 * length(fit_step$fit$theta) - 2 * logLik_step)
```

When the screening and stepwise agree on the same covariate set the
log-likelihoods match exactly; when stepwise selects a leaner model
AIC drops.  For an AIC-driven run use `criterion = "aic"`; for a
forward-only sweep `direction = "forward"`.  See `?hzr_stepwise` for
the full argument surface including `force_in`, `force_out`, and the
`max_move` oscillation guard.

## Step 5: Prediction

### 5a. Baseline survival curve

Generate the survival curve for a reference patient (median age, NYHA
class 1, no malalignment, no interventricular communication).

`predict(..., se.fit = TRUE)` adds a delta-method standard error and 95%
confidence band around every prediction (log(-log) scale for survival so
the band is guaranteed to lie in `[0, 1]`).  This is the parametric
analogue of the KM confidence limits from Step 1 --- a patient-specific
uncertainty estimate the nonparametric curve cannot provide.

```{r}
#| label: fig-prediction
#| fig-cap: "Reference patient survival with 95% delta-method CI and KM overlay"
#| eval: !expr requireNamespace("TemporalHazard", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE)
ref_patient <- data.frame(
  time   = t_grid,
  age    = median(avc$age),
  status = 1,
  mal    = 0,
  com_iv = 0
)

surv_ref <- predict(fit_mv, newdata = ref_patient,
                    type = "survival", se.fit = TRUE)

ref_df <- data.frame(
  time     = t_grid,
  survival = surv_ref$fit * 100,
  lower    = surv_ref$lower * 100,
  upper    = surv_ref$upper * 100
)

ggplot() +
  geom_step(data = km_df, aes(time, survival),
            colour = "#D55E00", linewidth = 0.6, alpha = 0.7) +
  geom_ribbon(data = ref_df, aes(time, ymin = lower, ymax = upper),
              fill = "#0072B2", alpha = 0.15) +
  geom_line(data = ref_df, aes(time, survival),
            colour = "#0072B2", linewidth = 0.8) +
  labs(x = "Months after AVC repair", y = "Survival (%)") +
  coord_cartesian(ylim = c(0, 100)) +
  theme_minimal()
```

### 5b. Sensitivity analysis --- risk factor comparison

Coefficient tables tell you about effect *direction and magnitude*
on the log-hazard scale. They don't directly tell a clinician what
to expect at the bedside. Sensitivity analysis closes that gap by
scoring two clinically meaningful profiles — a low-risk patient
drawn from the favorable end of each covariate, and a high-risk
patient drawn from the unfavorable end — through the fitted model
and plotting the resulting survival curves with delta-method
confidence bands. The vertical and horizontal gaps between the two
curves are the model's clinical-impact statement, in the units
(survival probability and time) that the audience actually
recognizes.

```{r}
#| label: fig-sensitivity
#| fig-cap: "Survival by risk profile: low-risk (blue) vs. high-risk (red)"
#| eval: !expr requireNamespace("TemporalHazard", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE)
low_risk <- data.frame(
  time   = t_grid,
  age    = quantile(avc$age, 0.25),
  status = 1,
  mal    = 0,
  com_iv = 0
)

high_risk <- data.frame(
  time   = t_grid,
  age    = quantile(avc$age, 0.90),
  status = 4,
  mal    = 1,
  com_iv = 1
)

surv_lo <- predict(fit_mv, newdata = low_risk,
                   type = "survival", se.fit = TRUE)
surv_hi <- predict(fit_mv, newdata = high_risk,
                   type = "survival", se.fit = TRUE)

sens_df <- data.frame(
  time     = rep(t_grid, 2),
  survival = c(surv_lo$fit, surv_hi$fit) * 100,
  lower    = c(surv_lo$lower, surv_hi$lower) * 100,
  upper    = c(surv_lo$upper, surv_hi$upper) * 100,
  profile  = rep(c("Low risk", "High risk"), each = length(t_grid))
)

ggplot(sens_df, aes(time, survival, colour = profile, fill = profile)) +
  geom_ribbon(aes(ymin = lower, ymax = upper),
              alpha = 0.15, colour = NA) +
  geom_line(linewidth = 0.8) +
  scale_colour_manual(values = c("Low risk" = "#0072B2",
                                  "High risk" = "#D55E00")) +
  scale_fill_manual(values = c("Low risk" = "#0072B2",
                                "High risk" = "#D55E00")) +
  labs(x = "Months after AVC repair", y = "Survival (%)",
       colour = NULL, fill = NULL) +
  coord_cartesian(ylim = c(0, 100)) +
  theme_minimal() +
  theme(legend.position = "bottom")
```

Non-overlapping confidence bands between the two profiles indicate the
risk-factor combination is well-identified; overlap around the bands'
centre-of-mass flags where the model can't distinguish the profiles with
the available sample size.

## Step 6: Validation --- decile-of-risk calibration

Partition patients into deciles by predicted risk and compare observed
vs. expected event rates.  Good calibration means the two track each
other.  This implements the workflow of the SAS `deciles.hazard.sas`
macro.

```{r}
#| label: fig-deciles
#| fig-cap: "Observed vs. expected event rates by risk decile"
#| eval: !expr requireNamespace("TemporalHazard", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE)
cal <- hzr_deciles(fit_mv, time = max(avc$int_dead))
print(cal)
```

```{r}
#| label: fig-decile-plot
#| fig-cap: "Decile calibration: observed (bars) vs. expected (points) event rates"
#| eval: !expr requireNamespace("TemporalHazard", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE)
ggplot(cal, aes(x = group)) +
  geom_col(aes(y = observed_rate), fill = "#56B4E9", alpha = 0.7) +
  geom_point(aes(y = expected_rate), colour = "#D55E00", size = 3) +
  geom_line(aes(y = expected_rate), colour = "#D55E00", linewidth = 0.5) +
  scale_x_continuous(breaks = seq_len(nrow(cal))) +
  labs(x = "Risk decile (1 = lowest)", y = "Event rate",
       caption = paste("Overall chi-sq =",
                        round(attr(cal, "overall")$chi_sq, 2),
                        ", p =",
                        format.pval(attr(cal, "overall")$p_value,
                                     digits = 3))) +
  theme_minimal()
```

A non-significant overall chi-square statistic (p > 0.05) indicates
adequate calibration --- the model's predictions are consistent with
observed event rates across the risk spectrum.

### Conservation of events check

The conservation-of-events principle states that a well-fitting model
should predict the same total number of events as actually observed.
`hzr_gof()` tracks this cumulatively over time --- the residual
(expected minus observed) should stay near zero.

```{r}
#| label: gof-summary
#| eval: !expr requireNamespace("TemporalHazard", quietly = TRUE)
gof <- hzr_gof(fit_mv)
print(gof)
```

```{r}
#| label: fig-gof-survival
#| fig-cap: "Parametric (blue) vs. Kaplan-Meier (orange) survival"
#| eval: !expr requireNamespace("TemporalHazard", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE)
ggplot(gof, aes(x = time)) +
  geom_step(aes(y = km_surv * 100), colour = "#D55E00", linewidth = 0.6) +
  geom_line(aes(y = par_surv * 100), colour = "#0072B2", linewidth = 0.8) +
  labs(x = "Months after AVC repair", y = "Survival (%)") +
  coord_cartesian(ylim = c(0, 100)) +
  theme_minimal()
```

```{r}
#| label: fig-gof-events
#| fig-cap: "Cumulative observed (orange) vs. expected (blue) events"
#| eval: !expr requireNamespace("TemporalHazard", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE)
ggplot(gof, aes(x = time)) +
  geom_line(aes(y = cum_observed), colour = "#D55E00", linewidth = 0.8) +
  geom_line(aes(y = cum_expected), colour = "#0072B2",
            linewidth = 0.8, linetype = "dashed") +
  labs(x = "Months after AVC repair", y = "Cumulative events") +
  theme_minimal()
```

```{r}
#| label: fig-gof-residual
#| fig-cap: "Conservation-of-events residual (expected minus observed) over time"
#| eval: !expr requireNamespace("TemporalHazard", quietly = TRUE) && requireNamespace("ggplot2", quietly = TRUE)
ggplot(gof, aes(x = time, y = residual)) +
  geom_line(linewidth = 0.7, colour = "grey30") +
  geom_hline(yintercept = 0, linetype = "dashed", colour = "grey60") +
  labs(x = "Months after AVC repair",
       y = "Residual (expected - observed)") +
  theme_minimal()
```

A residual that hovers near zero across the full time range indicates the
model conserves events well.  Persistent positive residual means the model
overpredicts risk; persistent negative means it underpredicts.

## Why not Cox regression?

For this kind of analysis the temporal parametric approach buys you
several things that Cox proportional hazards does not:

| Feature | Cox (`coxph`) | TemporalHazard |
|:---|:---:|:---:|
| Proportional hazards required | Yes | No |
| Baseline hazard specified | No | Yes (parametric) |
| Multiple hazard phases | No | Yes (additive) |
| Patient-specific survival prediction | Approximate | Exact |
| Smooth extrapolation beyond data | No | Yes |
| Conservation of events check | No | Yes (`hzr_gof()`) |
| Interval censoring | Not standard | Supported |

Many clinical outcomes show **non-proportional
hazards**: the risk factors that dominate early mortality (e.g., surgical
complexity) differ from those driving late attrition (e.g., age, comorbidity).
The multiphase model captures this through phase-specific
covariate effects.

## Summary

The complete analytical workflow follows a disciplined sequence:

1. **Kaplan-Meier baseline** --- establish the empirical survival pattern
2. **Shape fitting** --- match the temporal shape, simple to complex
3. **Variable screening** --- logistic screening, LOESS functional form
4. **Multivariable model** --- enter covariates with fixed shapes
5. **Prediction** --- reference curves, sensitivity analysis
6. **Validation** --- decile calibration, conservation-of-events check

This sequence ensures that the temporal shape is established before
covariates are introduced, and that the final model is validated against
the observed data.  See `vignette("getting-started")` for the minimal
API workflow and `vignette("mf-mathematical-foundations")` for the
mathematical details.
