Fast O(N log N) concordance index (C-index) with Numba-accelerated bootstrapping
Project description
fast-cindex
Fast O(N log N) concordance index (C-index) for survival analysis, with parallel Numba-accelerated bootstrapping.
Installation
pip install fast-cindex
Usage
import numpy as np
from lifelines import CoxPHFitter
from lifelines.datasets import load_rossi
from sklearn.model_selection import train_test_split
from fast_cindex import cindex, bootstrap_cindex, compare_cindex
# Load dataset and fit a Cox model
rossi = load_rossi()
train, test = train_test_split(rossi, test_size=0.2, random_state=42)
cph = CoxPHFitter()
cph.fit(train, duration_col='week', event_col='arrest')
times = test['week'].values
events = test['arrest'].values
scores = -cph.predict_partial_hazard(test).values # higher = higher predicted risk
Single C-index
ci = cindex(times, scores, events)
print(f"C-index: {ci:.3f}")
Bootstrap confidence interval
boot = bootstrap_cindex(times, scores, events, n_bootstraps=1000, seed=42)
lo, hi = np.percentile(boot, [2.5, 97.5])
print(f"C-index: {boot.mean():.3f} (95% CI {lo:.3f}–{hi:.3f})")
Compare two models
Bootstrap samples are shared across both models, enabling a valid paired test.
boot1, boot2 = compare_cindex(times, events, scores1, scores2, n_bootstraps=1000, seed=42)
diff = boot1 - boot2
p_value = np.mean(diff <= 0) # one-sided: P(model1 ≤ model2)
print(f"Mean difference: {diff.mean():.4f}, p = {p_value:.4f}")
API
| Function | Returns | Description |
|---|---|---|
cindex(times, scores, observed) |
float |
Single C-index |
bootstrap_cindex(times, scores, observed, n_bootstraps, seed) |
ndarray (B,) |
Per-bootstrap C-indices |
compare_cindex(times, observed, scores1, scores2, n_bootstraps, seed) |
(ndarray, ndarray) |
Paired bootstrap for two models |
Score convention: higher score = higher predicted risk (e.g. a predicted hazard or negative predicted survival time).
Benchmarks
Tested on the Rossi recidivism dataset (replicated to the target size), 1000 bootstrap iterations, Apple M-series CPU.
Single C-index
| Size | lifelines | fast-cindex | Speedup |
|---|---|---|---|
| 100 | 0.0004s | 2.9865s | 0.0x |
| 1,000 | 0.0032s | 0.0001s | 26.9x |
| 5,000 | 0.0144s | 0.0004s | 39.6x |
| 10,000 | 0.0280s | 0.0007s | 40.2x |
Note: the 100-sample run is dominated by Numba JIT compilation on first call. Subsequent calls are fast.
Bootstrap (1000 iterations)
| Size | lifelines | fast-cindex | Speedup |
|---|---|---|---|
| 100 | 0.2651s | 1.3314s | 0.2x |
| 1,000 | 3.0063s | 0.0209s | 144.2x |
| 5,000 | 14.0425s | 0.1031s | 136.2x |
| 10,000 | 27.9935s | 0.1900s | 147.4x |
To reproduce:
pip install lifelines scikit-learn
python benchmark.py
import time
import numpy as np
import pandas as pd
from lifelines.utils import concordance_index
from lifelines.datasets import load_rossi
from lifelines import CoxPHFitter
from sklearn.model_selection import train_test_split
from fast_cindex import cindex, bootstrap_cindex
SIZES = [100, 1000, 5000, 10000]
N_BOOTSTRAP = 1000
# Build a large base dataset to sample from
rossi = load_rossi()
while len(rossi) < max(SIZES):
rossi = pd.concat([rossi, rossi], axis=0)
rossi = rossi.reset_index(drop=True).iloc[:max(SIZES)]
# Fit model once on the full dataset
cph = CoxPHFitter()
train, _ = train_test_split(rossi, test_size=0.2, random_state=42)
cph.fit(train, duration_col='week', event_col='arrest')
risk_scores = -cph.predict_partial_hazard(rossi).values
durations = rossi['week'].values
events = rossi['arrest'].values
print(f"{'Size':>8} {'lifelines cindex':>18} {'fast cindex':>14} {'speedup':>10}")
print(f"{'':->8} {'':->18} {'':->14} {'':->10}")
for n in SIZES:
t = durations[:n]
e = events[:n]
r = risk_scores[:n]
start = time.perf_counter()
concordance_index(t, r, e.astype(bool))
lifelines_time = time.perf_counter() - start
start = time.perf_counter()
cindex(t, r, e)
fast_time = time.perf_counter() - start
print(f"{n:>8} {lifelines_time:>16.4f}s {fast_time:>12.4f}s {lifelines_time/fast_time:>9.1f}x")
print()
print(f"{'Size':>8} {'lifelines bootstrap':>21} {'fast bootstrap':>17} {'speedup':>10}")
print(f"{'':->8} {'':->21} {'':->17} {'':->10}")
for n in SIZES:
t = durations[:n]
e = events[:n]
r = risk_scores[:n]
rng = np.random.RandomState(42)
start = time.perf_counter()
for _ in range(N_BOOTSTRAP):
idx = rng.choice(n, n, replace=True)
concordance_index(t[idx], r[idx], e[idx].astype(bool))
lifelines_time = time.perf_counter() - start
start = time.perf_counter()
bootstrap_cindex(t, r, e, n_bootstraps=N_BOOTSTRAP, seed=42)
fast_time = time.perf_counter() - start
print(f"{n:>8} {lifelines_time:>19.4f}s {fast_time:>15.4f}s {lifelines_time/fast_time:>9.1f}x")
License
Apache 2.0
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fast_cindex-0.1.0.tar.gz.
File metadata
- Download URL: fast_cindex-0.1.0.tar.gz
- Upload date:
- Size: 16.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ce3804d4d32cca1af0862e5e9d92b1aadb5f4ac3d92c9804f1fcd7eae372f542
|
|
| MD5 |
b0a12c5ee3d80dac29c6d58bc310bee6
|
|
| BLAKE2b-256 |
95a0de574216ed351a8854c79bd13c7a04a6bc678f366518836d71d01627cde9
|
File details
Details for the file fast_cindex-0.1.0-py3-none-any.whl.
File metadata
- Download URL: fast_cindex-0.1.0-py3-none-any.whl
- Upload date:
- Size: 13.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ef0bff35bfff5ead2edd2bfedc2b7113f12ad158f2290c0a74d6f3e46ea4ccc6
|
|
| MD5 |
ae6e2266af58e78803e620589a826bdb
|
|
| BLAKE2b-256 |
d927162fc238b0f7067b97a8e7f4c37940be56379e05c5a3dabf141f362776f5
|