---
title: "Model checking and residuals"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Model checking and residuals}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  fig.width = 7,
  fig.height = 4.5,
  dpi = 96,
  message = FALSE,
  warning = FALSE
)
```

Residual diagnostics help you decide whether to **trust** a fitted model, **add covariates**,
**change the regression family** (PH vs PO), or **simplify the Bernstein degree**.

## Before you model

```{r setup}
library(spsurv)
library(survival)
library(ggplot2)
data(veteran)
veteran$celltype <- factor(veteran$celltype)
```

```{r eda-summary}
eda <- data.frame(
  n = nrow(veteran),
  events = sum(veteran$status),
  censored = sum(1 - veteran$status),
  pct_censored = round(100 * mean(1 - veteran$status), 1)
)
eda
```

```{r eda-km, fig.cap = "Kaplan-Meier survival for the veteran data (overall)."}
km_all <- survfit(Surv(time, status) ~ 1, data = veteran)
km_df <- data.frame(
  time = km_all$time,
  surv = km_all$surv
)
ggplot(km_df, aes(x = time, y = surv)) +
  geom_step(linewidth = 0.6) +
  labs(x = "Time (days)", y = "Survival probability", title = "Overall KM") +
  theme_bw()
```

### Why this is insightful

The overall KM and event rate tell you how much information is available for
baseline estimation. Heavy censoring or a short follow-up window limits how far
you can extrapolate smooth Bernstein baselines.

### What to decide

- Low event count relative to Bernstein degree → start with smaller `m` (see the
  *Bernstein polynomial degree* vignette).
- Steep early drop in KM → expect a flexible baseline; check diagnostics after fit.

## Fit and summarize

```{r fit}
fit <- bpph(
  Surv(time, status) ~ karno + celltype,
  degree = 5,
  data = veteran,
  approach = "mle",
  init = 0
)
summary(fit)
```

Use **MLE plug-in fits** for residual plots (Bayesian fits average over draws;
see note at the end).

## Visual check — martingale residuals

Martingale residuals should scatter around zero without a strong trend against
fitted cumulative hazard.

```{r martingale-data}
mart <- residuals(fit, type = "martingale")
csnell <- residuals(fit, type = "cox-snell")
fitted_ch <- csnell
```

```{r martingale-plot, fig.cap = "Martingale residuals vs fitted cumulative hazard (Cox-Snell)."}
mart_df <- data.frame(
  fitted = fitted_ch,
  martingale = mart,
  status = factor(veteran$status),
  celltype = veteran$celltype
)
ggplot(mart_df, aes(x = fitted, y = martingale, color = status)) +
  geom_point(alpha = 0.6, size = 1.8) +
  geom_hline(yintercept = 0, linetype = "dashed", color = "grey40") +
  geom_smooth(aes(group = 1), method = "loess", se = FALSE, color = "black", linewidth = 0.5) +
  coord_cartesian(ylim = c(-4, 2)) +
  labs(
    x = "Fitted cumulative hazard (Cox-Snell)",
    y = "Martingale residual",
    color = "Event"
  ) +
  theme_bw() +
  theme(legend.position = "bottom")
```

```{r martingale-facet, fig.cap = "Martingale residuals by cell type."}
ggplot(mart_df, aes(x = fitted, y = martingale)) +
  geom_point(alpha = 0.6, size = 1.5) +
  geom_hline(yintercept = 0, linetype = "dashed", color = "grey40") +
  geom_smooth(method = "loess", se = FALSE, color = "black", linewidth = 0.5) +
  facet_wrap(~ celltype, scales = "free_x") +
  coord_cartesian(ylim = c(-4, 2)) +
  labs(x = "Fitted cumulative hazard", y = "Martingale residual") +
  theme_bw()
```

### Why this is insightful

A systematic trend in the LOESS smooth (e.g. upward curve at high fitted values)
suggests covariate or baseline misspecification. Faceting reveals subgroups where
the model fits poorly.

### What to decide

| Pattern | Suggested action |
|---------|------------------|
| Clear trend vs fitted CH | Add terms, try PO (`bppo`), or lower/increase degree |
| One facet behaves differently | Consider interaction or separate analysis |
| Flat scatter around zero | Proceed; still check Cox-Snell calibration |

## Visual check — Cox-Snell calibration

Under a well-specified model, the Kaplan-Meier of `exp(-CS)` should track the
**Exponential(1)** reference (45-degree line on a cumulative-hazard scale).

```{r coxsnell-calibration, fig.cap = "Cox-Snell calibration: KM of exp(-CS) vs Exponential(1) reference."}
km_cs <- survfit(Surv(csnell, veteran$status) ~ 1)
cs_df <- data.frame(
  cs = km_cs$time,
  cumhaz = -log(pmax(km_cs$surv, .Machine$double.eps))
)
ggplot(cs_df, aes(x = cs, y = cumhaz)) +
  geom_step(linewidth = 0.6) +
  geom_abline(slope = 1, intercept = 0, color = "steelblue", linetype = "dashed") +
  labs(
    x = "Cox-Snell residual",
    y = "Nelson-Aalen cumulative hazard of exp(-CS)",
    title = "Cox-Snell calibration"
  ) +
  theme_bw()
```

### Why this is insightful

Curvature above the reference line indicates **overall miscalibration** of the
cumulative hazard (baseline too low or too high on average). This is a global
check complementary to martingale trends.

### What to decide

- Systematic departure from the line → revisit Bernstein degree or model family.
- Good calibration but bad martingale trend → local misspecification (covariates).

## Visual check — deviance residuals

Large deviance residuals flag observations poorly explained by the fitted model.

```{r deviance-plot, fig.cap = "Deviance residuals (largest magnitudes highlighted)."}
dev <- residuals(fit, type = "deviance")
dev_df <- data.frame(
  index = seq_along(dev),
  deviance = dev,
  highlight = abs(dev) >= quantile(abs(dev), 0.95)
)
ggplot(dev_df, aes(x = index, y = deviance, color = highlight)) +
  geom_point(size = 1.8, alpha = 0.7) +
  scale_color_manual(values = c("FALSE" = "grey40", "TRUE" = "firebrick"), guide = "none") +
  labs(x = "Observation index", y = "Deviance residual") +
  theme_bw()
```

### What to decide

- Few extreme points → investigate those subjects; consider robustness of conclusions.
- Many extremes → global lack of fit; revisit model structure.

## Compare with Cox (reference)

```{r cox-compare, fig.cap = "spsurv vs Cox martingale residuals."}
cox_fit <- coxph(Surv(time, status) ~ karno + celltype, data = veteran)
mart_cox <- residuals(cox_fit, type = "martingale")
ggplot(data.frame(bp = mart, cox = mart_cox), aes(x = bp, y = cox)) +
  geom_point(alpha = 0.5, size = 1.8) +
  geom_abline(slope = 1, intercept = 0, linetype = "dashed", color = "grey40") +
  labs(x = "spsurv martingale", y = "Cox martingale") +
  theme_bw()
```

Disagreement is expected in general (different likelihoods and baselines). Large
outliers in the comparison warrant case-level review.

## Gamma information stability

```{r vcov-stability}
v <- vcov(fit, bp.param = TRUE)
stab <- data.frame(
  gamma_information_stable = attr(v, "gamma_information_stable"),
  kappa_gamma = signif(attr(v, "gamma_information_kappa"), 4)
)
stab
```

### What to decide

- `gamma_information_stable == FALSE` → lower Bernstein degree; treat `survfit()`
  standard errors and bands as unreliable until stability improves.

## Bayesian fits

For `approach = "bayes"`, `residuals()` uses posterior-mean cumulative hazards.
For diagnostic plots comparable to classical methods, refit with MLE or use
posterior predictive checks outside this vignette.
