Harrell’s concordance index: discrimination of risk scores against observed survival.
concordance_index(
surv,
risk,
*,
data=None,
)
Computes Harrell’s C-statistic, a measure of how well a risk score discriminates between subjects who experience early events and those who survive longer. The concordance index compares all comparable pairs of subjects: those with an observed event are compared to those still under observation at the same time or later. Higher risk should correspond to earlier failure. If predictions match reality better than chance, the index exceeds 0.5.
Interpretation:
- 0.5: Random discrimination (no better than a coin flip)
- 0.6-0.7: Moderate discrimination (clinically useful)
- 0.7-0.8: Strong discrimination
- 0.8+: Excellent discrimination (rare in practice)
Practical use: Validates a model’s ability to rank subjects by risk. After fitting a Cox model or other survival model, compute the concordance index of its predictions to assess out-of-sample discrimination. A model with high concordance index generalizes well to ranking future subjects by risk.
Parameters
surv: Surv | Outcome | str
-
A right-censored Surv response (time-to-event data). An Outcome or a formula response such as 'Surv(time, status == 2)' is also accepted, with its columns read from data.
risk: Any
-
Risk score for each subject, one per observation. Can be a 1-D array, Pandas/Polars series, or Python sequence. Higher values indicate higher risk (earlier expected failure). Examples: Cox model linear predictor, predicted log-hazard, or predicted cumulative incidence.
data: Any = None
-
A data frame (pandas, Polars, PyArrow, DuckDB, a lazy frame, …) holding the columns named by the response and
risk. When surv is an 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
float
-
Concordance index between 0 and 1. Values above 0.5 indicate the model discriminates better than random. Below 0.5 indicates worse-than-random discrimination (possibly an inverted risk scale).
Details
Pair comparison rule:
- A subject with an observed event at time t is compared to all subjects still under observation at time t or later (including censored subjects at exactly t).
- Pairs are concordant if the subject with the event has higher risk than the subject without.
- Pairs with tied risk scores are counted as 0.5 (half-concordant).
- Pairs with the same event time are excluded.
Censoring handling: Censored subjects are handled through the comparable pairs definition. A censored subject at time t can only be compared to subjects with events at times strictly greater than t. This avoids artificial inflation of concordance from censored subjects.
Relationship to other metrics: The concordance index is related to the rank correlation between risk and observed survival. It’s invariant to monotonic transformations of risk (e.g., \exp(\text{lp}) vs. \text{lp} both give the same concordance).
Examples
Fit a Cox model on the lung dataset and evaluate its discrimination. Higher linear predictor values should correspond to earlier deaths.
import greenwood as gw
# Load data and name the response columns
lung = gw.load_dataset("lung", backend="polars")
death = gw.Outcome.surv(time="time", event="status")
cox = gw.CoxPH().fit(death, covariates=["age", "sex"], data=lung)
# Compute the concordance index from the Cox linear predictor
lp = cox.predict(type="lp")
c_index = gw.concordance_index(death, risk=lp, data=lung)
c_index
A concordance index of ~0.6 indicates moderate discrimination. Compare with baseline (naive model assuming all subjects are at equal risk):
import numpy as np
# Compare model discrimination against a naive baseline
baseline_c = gw.concordance_index(death, risk=np.zeros(lung.height), data=lung)
print(f"Baseline: {baseline_c:.3f}")
print(f"Cox model: {c_index:.3f}")
print(f"Improvement: {c_index - baseline_c:.3f}")
Baseline: 0.500
Cox model: 0.603
Improvement: 0.103
Evaluate on hold-out test data to assess generalization (use Cox model fit on training data, predict on test data):
# cox = CoxPH().fit(y_train, covariates=x_train)
# lp_test = cox.predict(x_test, type="lp")
# c_test = gw.concordance_index(y_test, lp_test)