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
Metadata
Release files for fast-cindex 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| fast_cindex-0.1.0.tar.gz | 16.2 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| fast_cindex-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 30.1 kB
Release files / fast_cindex-0.1.0.tar.gz
| Download URL | fast_cindex-0.1.0.tar.gz |
|---|---|
| Size | 16.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ce3804d4d32cca1af0862e5e9d92b1aadb5f4ac3d92c9804f1fcd7eae372f542
|
|
BLAKE2b-256 checksum How to use checksums |
95a0de574216ed351a8854c79bd13c7a04a6bc678f366518836d71d01627cde9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.11
|
Release files / fast_cindex-0.1.0-py3-none-any.whl
| Download URL | fast_cindex-0.1.0-py3-none-any.whl |
|---|---|
| Size | 13.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ef0bff35bfff5ead2edd2bfedc2b7113f12ad158f2290c0a74d6f3e46ea4ccc6
|
|
BLAKE2b-256 checksum How to use checksums |
d927162fc238b0f7067b97a8e7f4c37940be56379e05c5a3dabf141f362776f5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.11
|
Security for fast-cindex 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.