---
title: "Survival Analysis with brms and shrinkr"
author: "Jacob M. Maronge"
date: "`r Sys.Date()`"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Survival Analysis with brms and shrinkr}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include=FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  fig.width = 10,
  fig.height = 7,
  warning = FALSE,
  message = FALSE
)

run_expensive <- identical(Sys.getenv("SHRINKR_RUN_VIGNETTES"), "true")
```

## Overview

This vignette demonstrates hierarchical shrinkage for survival analysis using the classic `veteran` lung cancer dataset. We explore a key clinical question: **Does the treatment effect vary by lung cancer cell type?**

Rather than treating cell type-specific treatment effects as fixed interaction terms, we model them as **random effects drawn from a common distribution**. This hierarchical structure allows us to:

- Borrow strength across cell types, especially for small subgroups
- Estimate the overall mean treatment effect (`mu`)
- Quantify heterogeneity in treatment effects (`tau`)
- Shrink extreme subgroup estimates toward the group mean

We compare **three modeling approaches**:

1. **Two-stage (brms + shrinkr)**: fit a Cox model in `brms`, then apply hierarchical shrinkage in `shrinkr`
2. **Full hierarchical (brms)**: fit the hierarchical Cox model in one step
3. **Two-stage (frequentist + shrinkr)**: use Cox model estimates from `survival::coxph()`, then apply shrinkage

The two-stage `brms` workflow produces nearly identical results to the full hierarchical model, while making it easy to explore alternative hierarchical priors without repeatedly refitting the Stage 1 model.

Some model-fitting steps are computationally intensive and are not evaluated during routine package checks. All code needed to reproduce the analysis is shown below.

## Setup

```{r packages}
library(shrinkr)
library(brms)
library(tidybayes)
library(distributional)
library(tidyverse)
library(survival)
library(posterior)
library(patchwork)

theme_set(theme_minimal(base_size = 12))

cell_types <- c("squamous", "smallcell", "adeno", "large")

prior_specs <- list(
  very_strong = list(name = "Very Strong", scale = 0.1),
  strong = list(name = "Strong", scale = 0.25),
  moderate = list(name = "Moderate", scale = 0.5),
  weak = list(name = "Weak", scale = 1.0),
  very_weak = list(name = "Very Weak", scale = 2.0)
)
```

```{r load_cached_results, include=FALSE}
if (!run_expensive) {
  veteran_analysis <- get("veteran_analysis", envir = asNamespace("shrinkr"))
}
```

## The Veteran Dataset

```{r explore_data}
data(veteran, package = "survival")

head(veteran)
table(veteran$celltype, veteran$trt)

veteran %>%
  group_by(celltype, trt) %>%
  summarise(
    n = n(),
    deaths = sum(status),
    median_time = median(time),
    .groups = "drop"
  )
```

**Variables:**

- `time`: survival time (days)
- `status`: death indicator (`1 = died`)
- `trt`: treatment (`1 = standard`, `2 = test`)
- `celltype`: cancer type (`squamous`, `smallcell`, `adeno`, `large`)
- `karno`: Karnofsky score (performance status)
- `age`: age in years

The dataset contains 137 patients across four cell types, with varying sample sizes.

## Approach 1: Two-Stage (brms + shrinkr)

### Stage 1: Fit Cox Model

We begin by fitting a Cox proportional hazards model that allows the treatment effect to vary by cell type. At this stage we estimate subgroup-specific treatment effects without adding hierarchical shrinkage across cell types. That hierarchical regularization is introduced in Stage 2.

```{r fit_brms_uninformative, eval=run_expensive}
brms_uninformative <- brm(
  time | cens(1 - status) ~ trt:celltype + karno + age,
  data = veteran,
  family = cox(),
  chains = 4,
  iter = 4000,
  warmup = 1000,
  seed = 123
)

brms_uninformative_summary <- capture.output(print(summary(brms_uninformative)))
```

```{r fit_brms_uninformative_fallback, include=FALSE}
if (!run_expensive) {
  brms_uninformative_summary <- veteran_analysis$brms_uninformative_summary
}
```

**What this model does:**

- Estimates a separate log hazard ratio for the test treatment in each cell type
- Adjusts for baseline performance status (`karno`) and age
- Leaves pooling across cell types to the second-stage hierarchical model

Results:

```{r show_brms_uninformative}
cat(brms_uninformative_summary, sep = "\n")
```

### Stage 2: Apply Hierarchical Shrinkage

Now we extract the cell type-specific treatment effect posteriors and apply hierarchical shrinkage.

#### Step 1: Extract posterior samples

```{r extract_posteriors, eval=run_expensive}
brms_posteriors <- brms_uninformative %>%
  spread_draws(`b_trt:celltypesquamous`, `b_trt:celltypesmallcell`,
               `b_trt:celltypeadeno`, `b_trt:celltypelarge`) %>%
  select(-c(.chain, .iteration, .draw)) %>%
  pivot_longer(everything(), names_to = "celltype", values_to = "value") %>%
  mutate(celltype = gsub("b_trt:celltype", "", celltype)) %>%
  group_by(celltype) %>%
  summarise(draws = list(matrix(value, ncol = 1)), .groups = "drop") %>%
  deframe()
```

```{r extract_posteriors_fallback, include=FALSE}
if (!run_expensive) {
  brms_posteriors <- veteran_analysis$brms_posteriors
}
```

`brms_posteriors` is a named list containing posterior draws for each cell type.

#### Step 2: Fit a Gaussian mixture approximation

The `fit_mixture()` function approximates each subgroup posterior with a mixture of Gaussian components. This creates a flexible representation of the Stage 1 posterior that can be passed to `shrink()`.

```{r fit_mixture_explain, eval=run_expensive}
mix_brms <- fit_mixture(brms_posteriors, K_max = 3, verbose = TRUE)
```

```{r fit_mixture_explain_fallback, include=FALSE}
if (!run_expensive) {
  mix_brms <- veteran_analysis$mix_brms
}
```

```{r show_mixture}
print(mix_brms)
plot(mix_brms, draws = brms_posteriors)
```

**Understanding the mixture approximation:**

- Each posterior is approximated as a weighted sum of Gaussian components
- The number of components is chosen separately for each cell type
- This allows the approximation to capture skewness or heavier tails when needed

#### Step 3: Apply a hierarchical prior

```{r define_moderate_prior}
priors_moderate <- list(
  mu = dist_normal(0, 1),
  tau = dist_truncated(dist_normal(0, 0.5), lower = 0)
)
```

```{r shrink_explain, eval=run_expensive}
fit_twostage_brms <- shrink(
  mixture = mix_brms,
  hierarchical_priors = priors_moderate,
  chains = 4,
  iter = 4000,
  warmup = 1000,
  seed = 456
)

moderate_brms_output <- capture.output(print(fit_twostage_brms))
```

```{r shrink_explain_fallback, include=FALSE}
if (!run_expensive) {
  moderate_brms_output <- veteran_analysis$sensitivity_summaries$moderate_brms$print_output
}
```

Results:

```{r show_twostage_brms}
cat(moderate_brms_output, sep = "\n")
```

**Interpreting the shrinkage:**

- Cell type-specific estimates are pulled toward the overall mean (`mu`)
- The amount of shrinkage depends on `tau`, the between-cell-type heterogeneity
- Smaller `tau` implies stronger pooling
- Larger `tau` implies weaker pooling

## Approach 2: Full Hierarchical (brms)

For comparison, we fit the corresponding one-stage hierarchical Cox model directly in `brms`.

```{r fit_brms_hierarchical, eval=run_expensive}
brms_hierarchical <- brm(
  time | cens(1 - status) ~ trt + (0 + trt | celltype) + karno + age,
  data = veteran,
  family = cox(),
  prior = c(
    prior(normal(0, 1), class = b, coef = "trt"),
    prior(normal(0, 0.5), class = sd, group = "celltype", lb = 0)
  ),
  chains = 4,
  iter = 4000,
  warmup = 1000,
  seed = 123
)

brms_hierarchical_summary <- capture.output(print(summary(brms_hierarchical)))

brms_hier_effects <- brms_hierarchical %>%
  spread_draws(r_celltype[celltype, term], b_trt) %>%
  filter(term == "trt") %>%
  mutate(theta = b_trt + r_celltype) %>%
  group_by(celltype) %>%
  summarise(
    hr_mean = exp(mean(theta)),
    hr_lower = exp(quantile(theta, 0.025)),
    hr_upper = exp(quantile(theta, 0.975)),
    .groups = "drop"
  )
```

```{r fit_brms_hierarchical_fallback, include=FALSE}
if (!run_expensive) {
  brms_hierarchical_summary <- veteran_analysis$brms_hierarchical_summary
  brms_hier_effects <- veteran_analysis$brms_hier_effects
}
```

Results:

```{r show_brms_hierarchical}
cat(brms_hierarchical_summary, sep = "\n")
```

## Approach 3: Two-Stage (Frequentist + shrinkr)

We can also apply the second-stage shrinkage model to standard Cox model estimates and their covariance matrix.

```{r fit_cox, eval=run_expensive}
cox_model <- coxph(
  Surv(time, status) ~ trt:celltype + karno + age,
  data = veteran
)

cox_summary <- summary(cox_model)

trt_idx <- grep("^trt:celltype", names(coef(cox_model)))

trt_effects <- coef(cox_model)[trt_idx]
trt_vcov <- vcov(cox_model)[trt_idx, trt_idx, drop = FALSE]

names(trt_effects) <- gsub("^trt:celltype", "", names(trt_effects))
rownames(trt_vcov) <- colnames(trt_vcov) <- names(trt_effects)
```

```{r fit_cox_fallback, include=FALSE}
if (!run_expensive) {
  cox_summary <- veteran_analysis$cox_summary
  trt_effects <- veteran_analysis$trt_effects
  trt_vcov <- veteran_analysis$trt_vcov
}
```

```{r show_cox}
print(cox_summary)
```

```{r show_cox_effects}
print("Treatment effects (log HR):")
print(trt_effects)

print("\nStandard errors:")
print(sqrt(diag(trt_vcov)))
```

```{r shrink_freq, eval=run_expensive}
fit_twostage_freq <- shrink(
  mle = trt_effects,
  var_matrix = trt_vcov,
  hierarchical_priors = priors_moderate,
  chains = 4,
  iter = 4000,
  warmup = 1000,
  seed = 456
)

moderate_freq_output <- capture.output(print(fit_twostage_freq))
```

```{r shrink_freq_fallback, include=FALSE}
if (!run_expensive) {
  moderate_freq_output <- veteran_analysis$sensitivity_summaries$moderate_freq$print_output
}
```

Results:

```{r show_twostage_freq}
cat(moderate_freq_output, sep = "\n")
```

## Compare Three Approaches

### Numerical comparison

```{r comparison_table, eval=run_expensive}
theta_brms <- summary(fit_twostage_brms)$theta %>%
  transmute(
    celltype = group,
    twostage_brms = mean
  )

theta_freq <- summary(fit_twostage_freq)$theta %>%
  transmute(
    celltype = group,
    twostage_freq = mean
  )

comparison <- brms_hier_effects %>%
  transmute(
    celltype,
    full_hier_brms = log(hr_mean)
  ) %>%
  left_join(theta_brms, by = "celltype") %>%
  left_join(theta_freq, by = "celltype") %>%
  mutate(
    diff_two_stage_vs_full = twostage_brms - full_hier_brms
  )
```

```{r comparison_table_fallback, include=FALSE}
if (!run_expensive) {
  comparison <- veteran_analysis$comparison
}
```

```{r comparison_table_show}
knitr::kable(
  comparison[, 1:4],
  digits = 3,
  caption = "Comparison of treatment effects (log HR scale)"
)
```

**Key observations:**

- The two-stage `brms + shrinkr` and full hierarchical `brms` fits are nearly identical
- This supports the equivalence of the two formulations in this example
- The frequentist Stage 1 approach differs modestly because the first-stage estimates differ
- All approaches show shrinkage toward a common mean

### Visual comparison

```{r compare_approaches, fig.width=12, fig.height=8, eval=run_expensive}
theta_brms_plot <- summary(fit_twostage_brms)$theta %>%
  mutate(
    approach = "Two-Stage (brms + shrinkr)",
    hr_mean = exp(mean),
    hr_lower = exp(q2.5),
    hr_upper = exp(q97.5),
    celltype = group
  ) %>%
  select(celltype, approach, hr_mean, hr_lower, hr_upper)

theta_freq_plot <- summary(fit_twostage_freq)$theta %>%
  mutate(
    approach = "Two-Stage (Frequentist + shrinkr)",
    hr_mean = exp(mean),
    hr_lower = exp(q2.5),
    hr_upper = exp(q97.5),
    celltype = group
  ) %>%
  select(celltype, approach, hr_mean, hr_lower, hr_upper)

all_approaches <- bind_rows(
  theta_brms_plot,
  brms_hier_effects %>% mutate(approach = "Full Hierarchical (brms)"),
  theta_freq_plot
) %>%
  mutate(
    approach = factor(approach, levels = c(
      "Two-Stage (brms + shrinkr)",
      "Full Hierarchical (brms)",
      "Two-Stage (Frequentist + shrinkr)"
    ))
  )
```

```{r compare_approaches_fallback, include=FALSE}
if (!run_expensive) {
  theta_brms_plot <- veteran_analysis$sensitivity_summaries$moderate_brms$theta_summary %>%
    mutate(
      approach = "Two-Stage (brms + shrinkr)",
      hr_mean = exp(mean),
      hr_lower = exp(q2.5),
      hr_upper = exp(q97.5),
      celltype = group
    ) %>%
    select(celltype, approach, hr_mean, hr_lower, hr_upper)

  theta_freq_plot <- veteran_analysis$sensitivity_summaries$moderate_freq$theta_summary %>%
    mutate(
      approach = "Two-Stage (Frequentist + shrinkr)",
      hr_mean = exp(mean),
      hr_lower = exp(q2.5),
      hr_upper = exp(q97.5),
      celltype = group
    ) %>%
    select(celltype, approach, hr_mean, hr_lower, hr_upper)

  all_approaches <- bind_rows(
    theta_brms_plot,
    veteran_analysis$brms_hier_effects %>% mutate(approach = "Full Hierarchical (brms)"),
    theta_freq_plot
  ) %>%
    mutate(
      approach = factor(approach, levels = c(
        "Two-Stage (brms + shrinkr)",
        "Full Hierarchical (brms)",
        "Two-Stage (Frequentist + shrinkr)"
      ))
    )
}
```

```{r compare_approaches_show, fig.width=12, fig.height=8}
ggplot(all_approaches, aes(x = celltype, y = hr_mean, color = approach)) +
  geom_hline(yintercept = 1, linetype = "dashed", alpha = 0.5) +
  geom_pointrange(
    aes(ymin = hr_lower, ymax = hr_upper),
    position = position_dodge(width = 0.5),
    size = 0.8
  ) +
  scale_y_log10() +
  scale_color_brewer(palette = "Set1") +
  labs(
    title = "Comparison of Three Modeling Approaches",
    subtitle = "Treatment effects by cell type (hazard ratios)",
    x = "Cell Type",
    y = "Hazard Ratio (log scale)",
    color = "Approach"
  ) +
  theme(
    legend.position = "bottom",
    panel.grid.minor = element_blank()
  )
```

## Sensitivity Analysis: Exploring Different Priors

A main advantage of the two-stage framework is that we can explore many hierarchical priors in Stage 2 without refitting the Stage 1 survival model.

```{r show_priors}
prior_summary <- tibble(
  Strength = c("Very Strong", "Strong", "Moderate", "Weak", "Very Weak"),
  Prior = c(
    "Half-Normal(0, 0.1)",
    "Half-Normal(0, 0.25)",
    "Half-Normal(0, 0.5)",
    "Half-Normal(0, 1.0)",
    "Half-Normal(0, 2.0)"
  ),
  Scale = c(0.1, 0.25, 0.5, 1.0, 2.0),
  Interpretation = c(
    "Very similar effects expected",
    "Similar effects expected",
    "Moderate heterogeneity allowed",
    "Substantial differences allowed",
    "Large differences allowed"
  )
)

knitr::kable(prior_summary)
```

```{r sensitivity_fits, eval=run_expensive}
all_priors <- list(
  very_strong = list(
    mu = dist_normal(0, 1),
    tau = dist_truncated(dist_normal(0, 0.1), lower = 0)
  ),
  strong = list(
    mu = dist_normal(0, 1),
    tau = dist_truncated(dist_normal(0, 0.25), lower = 0)
  ),
  moderate = list(
    mu = dist_normal(0, 1),
    tau = dist_truncated(dist_normal(0, 0.5), lower = 0)
  ),
  weak = list(
    mu = dist_normal(0, 1),
    tau = dist_truncated(dist_normal(0, 1.0), lower = 0)
  ),
  very_weak = list(
    mu = dist_normal(0, 1),
    tau = dist_truncated(dist_normal(0, 2.0), lower = 0)
  )
)

# --- brms fits ---
sensitivity_fits_brms <- lapply(all_priors, function(prior) {
  shrink(
    mixture = mix_brms,
    hierarchical_priors = prior,
    chains = 4,
    iter = 4000,
    warmup = 1000
  )
})

# --- frequentist fits ---
sensitivity_fits_freq <- lapply(all_priors, function(prior) {
  shrink(
    mle = trt_effects,
    var_matrix = trt_vcov,
    hierarchical_priors = prior,
    chains = 4,
    iter = 4000,
    warmup = 1000
  )
})

# --- summaries ---
sensitivity_summaries <- c(
  purrr::imap(sensitivity_fits_brms, function(fit, nm) {
    summ <- summary(fit)
    list(
      theta_summary = summ$theta,
      mu_tau_summary = summ$mu_tau,
      print_output = capture.output(print(fit))
    )
  }),
  purrr::imap(sensitivity_fits_freq, function(fit, nm) {
    summ <- summary(fit)
    list(
      theta_summary = summ$theta,
      mu_tau_summary = summ$mu_tau,
      print_output = capture.output(print(fit))
    )
  })
)

# --- name them clearly ---
names(sensitivity_summaries) <- c(
  paste0(names(all_priors), "_brms"),
  paste0(names(all_priors), "_freq")
)
```

```{r sensitivity_fits_fallback, include=FALSE}
if (!run_expensive) {
  sensitivity_summaries <- veteran_analysis$sensitivity_summaries
  prior_specs <- veteran_analysis$prior_specs
}
```

### Prior densities

```{r prior_densities, fig.width=10, fig.height=5}
tau_seq <- seq(0, 3, length.out = 200)

prior_densities <- lapply(names(prior_specs), function(spec_name) {
  spec <- prior_specs[[spec_name]]
  tibble(
    tau = tau_seq,
    density = dnorm(tau_seq, 0, spec$scale) * 2,
    prior_strength = spec$name,
    scale = spec$scale
  )
}) %>%
  bind_rows() %>%
  mutate(
    prior_strength = factor(prior_strength, levels = c(
      "Very Strong", "Strong", "Moderate", "Weak", "Very Weak"
    ))
  )

ggplot(prior_densities, aes(x = tau, y = density, color = prior_strength)) +
  geom_line(linewidth = 1.2) +
  scale_color_brewer(palette = "RdYlBu", direction = -1) +
  labs(
    title = "Prior Densities for the Heterogeneity Parameter (tau)",
    subtitle = "Half-Normal(0, sigma) priors with increasing scale",
    x = "tau",
    y = "Density",
    color = "Prior Strength"
  ) +
  theme(legend.position = "right")
```

### Heterogeneity estimates

```{r tau_sensitivity}
tau_results <- lapply(names(sensitivity_summaries), function(fit_name) {
  summary_obj <- sensitivity_summaries[[fit_name]]
  prior_name <- sub("_(brms|freq)$", "", fit_name)
  approach <- if (grepl("_brms$", fit_name)) "brms + shrinkr" else "Frequentist + shrinkr"

  summary_obj$mu_tau_summary %>%
    filter(parameter == "tau") %>%
    mutate(
      prior_strength = prior_specs[[prior_name]]$name,
      prior_scale = prior_specs[[prior_name]]$scale,
      approach = approach
    )
}) %>%
  bind_rows() %>%
  mutate(
    prior_strength = factor(
      prior_strength,
      levels = c("Very Strong", "Strong", "Moderate", "Weak", "Very Weak")
    )
  )

if (all(c("q2.5", "q97.5") %in% names(tau_results))) {
  tau_results <- tau_results %>%
    mutate(lower = `q2.5`, upper = `q97.5`)
} else if (all(c("q5", "q95") %in% names(tau_results))) {
  tau_results <- tau_results %>%
    mutate(lower = q5, upper = q95)
} else {
  stop(
    "Could not find interval columns in sensitivity_summaries$mu_tau_summary. ",
    "Available columns are: ",
    paste(names(tau_results), collapse = ", ")
  )
}

ggplot(tau_results, aes(x = prior_scale, y = mean, color = approach)) +
  geom_point(size = 3, position = position_dodge(width = 0.1)) +
  geom_errorbar(
    aes(ymin = lower, ymax = upper),
    width = 0.1,
    linewidth = 1,
    position = position_dodge(width = 0.1)
  ) +
  geom_line(aes(group = approach), position = position_dodge(width = 0.1)) +
  scale_x_log10(breaks = c(0.1, 0.25, 0.5, 1.0, 2.0)) +
  scale_color_brewer(palette = "Set2") +
  labs(
    title = "Sensitivity of the Heterogeneity Parameter (tau)",
    subtitle = "How prior scale affects the estimated between-cell-type variation",
    x = "Prior Scale (log scale)",
    y = "Posterior tau",
    color = "Stage 1 Approach"
  ) +
  theme(legend.position = "bottom")
```

**Interpretation:**

- Stronger priors constrain `tau` toward smaller values and produce more shrinkage
- Weaker priors allow more between-cell-type variation
- The posterior for `tau` stabilizes as the prior becomes less restrictive

### Impact on cell type estimates

```{r theta_sensitivity_prep}
theta_sensitivity <- lapply(names(sensitivity_summaries), function(fit_name) {
  summary_obj <- sensitivity_summaries[[fit_name]]
  prior_name <- sub("_(brms|freq)$", "", fit_name)
  approach <- if (grepl("_brms$", fit_name)) "brms + shrinkr" else "Frequentist + shrinkr"

  summary_obj$theta_summary %>%
    mutate(
      prior_strength = prior_specs[[prior_name]]$name,
      prior_scale = prior_specs[[prior_name]]$scale,
      approach = approach,
      hr_mean = exp(mean),
      hr_lower = exp(q2.5),
      hr_upper = exp(q97.5)
    )
}) %>%
  bind_rows() %>%
  mutate(
    prior_strength = factor(prior_strength, levels = c(
      "Very Strong", "Strong", "Moderate", "Weak", "Very Weak"
    ))
  )
```

```{r theta_sensitivity_plot, fig.width=12, fig.height=10}
ggplot(theta_sensitivity, aes(x = prior_scale, y = hr_mean, color = approach)) +
  geom_hline(yintercept = 1, linetype = "dashed", alpha = 0.5) +
  geom_point(size = 2, position = position_dodge(width = 0.1)) +
  geom_errorbar(
    aes(ymin = hr_lower, ymax = hr_upper),
    width = 0.1,
    position = position_dodge(width = 0.1)
  ) +
  geom_line(aes(group = approach), position = position_dodge(width = 0.1)) +
  facet_wrap(~group, ncol = 2, scales = "free_y") +
  scale_x_log10(breaks = c(0.1, 0.25, 0.5, 1.0, 2.0)) +
  scale_y_log10() +
  scale_color_brewer(palette = "Set2") +
  labs(
    title = "Sensitivity Analysis: Cell Type-Specific Treatment Effects",
    subtitle = "How the prior scale affects hazard ratio estimates",
    x = "Prior Scale (log scale)",
    y = "Hazard Ratio (log scale)",
    color = "Stage 1 Approach"
  ) +
  theme(
    legend.position = "bottom",
    panel.grid.minor = element_blank()
  )
```

## Key Takeaways

1. The two-stage `brms + shrinkr` workflow closely matches the full hierarchical `brms` analysis in this example.
2. The two-stage approach is modular: fit the survival model once, then explore many hierarchical priors efficiently.
3. Sensitivity analysis becomes straightforward because Stage 2 can be rerun without refitting Stage 1.
4. `fit_mixture()` provides a flexible approximation to the subgroup posteriors, and `shrink()` adds hierarchical regularization on top of that approximation.

## Session Info

```{r session}
sessionInfo()
```