---
title: "Cox, AFT, and Piecewise-Exponential Objectives"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Cox, AFT, and Piecewise-Exponential Objectives}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
```

```{r}
library(fastgbm)
library(survival)
```

## Cox objective (`objective = "cox"`, the default)

Minimizes the negative Cox partial log-likelihood (Breslow handling of tied event
times). `predict(..., type = "link")` returns the log relative-risk score; higher
values mean higher risk (shorter expected survival). `predict(..., type =
"survival")` combines the fitted risk score with a Breslow baseline cumulative
hazard estimated after training to return survival probabilities at specific
horizons: $\hat S(t \mid x) = \hat S_0(t)^{\exp(f(x))}$.

```{r}
lung_dat <- na.omit(lung[, c("time", "status", "age", "sex", "ph.ecog")])
x <- as.matrix(lung_dat[, c("age", "sex", "ph.ecog")])

cox_fit <- fastgbm(x, time = lung_dat$time, status = lung_dat$status,
                   objective = "cox", ntrees = 100L, max_depth = 3L, verbose = FALSE)

risk <- predict(cox_fit, x, type = "link")
surv <- predict(cox_fit, x[1:5, ], type = "survival", times = c(90, 180, 365))
surv
```

## AFT objective (`objective = "aft"`)

Fits a normal location-scale accelerated failure time model: $\log T = f(x) + \sigma Z$,
$Z \sim N(0, 1)$. Right-censored observations contribute via the survival function of
the normal distribution. Unlike Cox, the linear predictor estimates $\log(\text{time})$
directly (higher = *longer* predicted survival), the opposite direction from a Cox risk
score -- `metrics()` accounts for this automatically when computing concordance, but if
you use the raw linear predictor for anything else, remember to negate it for a
risk-like ordering.

```{r}
set.seed(1)
n <- 200
x_aft <- matrix(rnorm(n * 2), ncol = 2, dimnames = list(NULL, c("x1", "x2")))
time <- exp(1 + 0.6 * x_aft[, 1] - 0.3 * x_aft[, 2] + rnorm(n, sd = 0.3))
status <- rbinom(n, 1, 0.75)

aft_fit <- fastgbm(x_aft, time = time, status = status, objective = "aft",
                   ntrees = 100L, max_depth = 3L, verbose = FALSE)
predict(aft_fit, x_aft[1:5, ], type = "survival", times = c(1, 3, 5))
```

## Piecewise-exponential objective (`objective = "pexp"`)

Unlike Cox and AFT, `pexp` doesn't fit a single scalar score per subject; it models
the hazard jointly over covariates *and* time. Training data is expanded into
person-time rows via the standard "Poisson trick" (one row per subject per time
interval they were at risk in), and the ensemble predicts the log hazard rate for
each row -- an ordinary Poisson-with-offset objective, no baseline assumption
needed. This means `predict(..., type = "link")` isn't a single "risk score" the way
Cox's is; it defaults to the cumulative hazard at the model's full fitted time
horizon, a fixed and well-defined (if coarser) risk score for ranking.
`predict(..., type = "survival")` evaluates the fitted hazard-over-time surface
directly at the requested times.

```{r}
pexp_fit <- fastgbm(x, time = lung_dat$time, status = lung_dat$status,
                    objective = "pexp", ntrees = 100L, max_depth = 3L, verbose = FALSE)
predict(pexp_fit, x[1:5, ], type = "survival", times = c(90, 180, 365))
metrics(pexp_fit, y = Surv(lung_dat$time, lung_dat$status))
```

## Concordance

```{r}
metrics(cox_fit, y = Surv(lung_dat$time, lung_dat$status))
metrics(aft_fit, y = Surv(time, status))
```

## Missing values

Missing predictor values are routed natively: at every candidate split, both
"missing goes left" and "missing goes right" are evaluated, and the direction that
reduces loss more is stored on the node. No imputation is performed unless you do it
yourself.

```{r}
x_missing <- x
x_missing[1, "ph.ecog"] <- NA
predict(cox_fit, x_missing[1:3, ], type = "link")
```
