# calibration_incidence()


Aalen-Johansen calibration error for cause-specific cumulative incidence predictions.


Usage

``` python
calibration_incidence(
    surv,
    incidence_prob,
    times,
    *,
    data=None,
    cause,
)
```


At each evaluation time, compares the mean predicted CIF across all subjects against the marginal Aalen-Johansen CIF estimate (the observed reference). Returns the absolute calibration error at each time. A well-calibrated model has errors close to zero.

This implements a marginal calibration check: for a given cause, the average predicted CIF should match the nonparametric Aalen-Johansen estimate at every time point.

**Interpretation**:

- 0.0: Perfect marginal calibration (mean predictions match the AJ reference exactly).
- Positive values indicate miscalibration. Large errors at early times suggest the model over- or under-predicts the initial incidence. Large errors at late times suggest drift in the tail.


## Parameters


`surv: Surv | Outcome | str`  
A multi-state [Surv](Surv.md#greenwood.Surv) response (from `gw.Surv()` with a categorical event). An [Outcome](Outcome.md#greenwood.Outcome) or a formula response such as `'Surv(time, status == 2)'` is also accepted, with its columns read from `data`.

`incidence_prob: Any`  
Predicted cumulative incidence probabilities for the cause of interest, shape `(n_subjects, n_times)`. Each entry is a predicted probability that cause `cause` has occurred by the corresponding time.

`times: Any`  
Evaluation times where calibration is assessed. 1-D array-like. Must have length equal to the second dimension of `incidence_prob`.

`cause: int`  
The cause of interest, as an integer event code from the [Surv](Surv.md#greenwood.Surv) response.

`data: Any = None`  
A data frame (pandas, Polars, PyArrow, DuckDB, a lazy frame, …) holding the columns named by the response and `incidence_prob`. When `surv` is an [Outcome](Outcome.md#greenwood.Outcome) or a formula, rows with a missing value in any column used are dropped first, along with the matching rows of any arrays passed alongside.


## Returns


`ndarray`  
Absolute calibration error at each time, shape `(len(times),)`. Lower is better.


## Details

The calibration error at time t for cause k is:

 \mathrm{CalErr}\_k(t) = \left\|\bar{\hat{F}}\_k(t) - \hat{F}\_k^{\mathrm{AJ}}(t)\right\| 

where \bar{\hat{F}}\_k(t) = \frac{1}{n}\sum\_{i=1}^n \hat{F}\_k(t \mid \mathbf{x}\_i) is the mean predicted CIF and \hat{F}\_k^{\mathrm{AJ}}(t) is the marginal Aalen-Johansen CIF. The AJ estimator is fit internally from the supplied `surv` response.


## Examples

Name the competing endpoints in the `mgus2` dataset and check calibration of a marginal baseline model (which should be perfectly calibrated by construction):


``` python
import greenwood as gw
import numpy as np

# The first of progression (PCM) or death, each with its own time and status columns
mgus2 = gw.load_dataset("mgus2", backend="polars")
first = gw.Outcome.first_event(
    endpoints={"pcm": ("ptime", "pstat"), "death": ("futime", "death")}
)

# Fit Aalen-Johansen for the marginal CIF
aj = gw.AalenJohansen().fit(first, data=mgus2)
cif_df = aj.to_frame(format="polars")
pcm_cif = cif_df.filter(cif_df["cause"] == "pcm")

# Naive model: same marginal CIF for every subject
times = np.array([60, 120, 240])
marginal = np.array(
    [float(np.interp(t, pcm_cif["time"].to_numpy(), pcm_cif["estimate"].to_numpy()))
     for t in times]
)
probs = np.tile(marginal, (mgus2.height, 1))

# Calibration error should be near zero for the marginal model
cal_err = gw.calibration_incidence(
    first, incidence_prob=probs, times=times, cause=1, data=mgus2
)
cal_err
```


    array([6.93889390e-18, 1.38777878e-17, 1.38777878e-17])
