Diabetes Readmission
A famous, irregular clinical dataset: rare target, weak signal, 97% missing in one column, 716 diagnosis codes and repeated patients. How far does a well-specified logistic regression go against gradient boosting — measured with every metric that matters?
- dataset
- Diabetes 130-US Hospitals 1999–2008 (UCI · Strack et al., 2014)
- task
- binary classification · readmission < 30 days (11.4%)
- main model
- logistic regression · log-counts + cubic B-splines · L2
- compared
- LR variants (6) · random forest · LightGBM · + 3 richer LRs
- validation
- split, CV and bootstrap by patient (StratifiedGroupKFold)
- model on the hub
- huggingface.co/igorlimamillis/diabetes-readmission-logistic
In a nutshell
A famous dataset that fights back
Hospitals in the US are penalised when patients come back within 30 days. Flagging high-risk diabetic patients at discharge would let a team call them, review their medication or schedule an early visit. The UCI Diabetes 130-US Hospitals dataset (101,766 encounters, 1999–2008) is the standard public benchmark for this — and it is a hard one.
The question here is not “which algorithm wins a leaderboard” but how far a well-specified logistic regression goes, what gradient boosting adds on top, and what each metric actually reveals when the target is rare and the signal is weak.
| irregularity | evidence | what was done |
|---|---|---|
| rare, weak target | 11.4% readmitted < 30 days; best AUC ≈ 0.68 | proper scores, PR AUC, calibration; thresholds chosen out-of-fold |
| deaths and hospice | 2,423 encounters cannot be readmitted | removed — otherwise dying looks “protective” |
| repeated patients | 69,987 patients; 46% of encounters from repeaters | split, tune and bootstrap by patient |
| massive missingness | weight 97% · glucose 95% · HbA1c 83% · specialty 49% · payer 40% | weight dropped; “not measured” kept as a level |
| high-cardinality codes | 716 ICD-9 primary diagnoses · 72 specialties | 9 ICD-9 chapters; top-12 specialties; rare levels pooled per fold |
| heavy tails | prior stays 0–21 · ER visits 0–76 | log1p + cubic B-splines inside the pipeline |
| IDs stored as numbers | admission / discharge / source are nominal codes | mapped to clinically meaningful groups |
Audit of the raw file (101,766 encounters). Cohort after exclusions: 99,340 encounters, 69,987 patients.
Split by patient — and check whether it mattered
Almost half of the encounters belong to patients seen more than once. A random split puts the same person in training and test, which can let a flexible model recognise people instead of learning risk. So the held-out test set is 13,998 patients never seen in training, hyperparameters are tuned with 5-fold StratifiedGroupKFold, and confidence intervals come from a patient-level bootstrap.
Then I measured what the shortcut would have cost:
An honest null result. Splitting encounters instead of patients inflated CV AUC by at most 0.0016 — even for a forest with leaves of size 1, built to memorise. No feature identifies the patient, so there is little to memorise. The grouped split is still the right one (it is the only one that answers “how does this work on new patients?”), but here the cost of getting it wrong was small. Test AUC sits 0.006–0.009 above CV AUC for every model alike: a property of this particular split, not of any model.
One feature pipeline, eight models
Feature engineering is a row-wise function (age bands → years, ICD-9 codes → chapters, discharge IDs → destinations, medication counts) that sits at the start of every scikit-learn Pipeline. Everything learned from data — scaling, spline bases, rare-level pooling — is fitted inside each CV fold. Every model is tuned on the same grouped folds by log loss, a proper score, not accuracy.
| model | what it is | selected hyperparameters | CV log loss |
|---|---|---|---|
| LR · log + splines (main) | log1p(counts) → cubic B-splines, one-hot categoricals, L2 | C=0.010 | 0.3380 |
| LR · splines + L1/L2 | same features, elastic-net path (saga) | C=0.100, l1_ratio=1.000 | 0.3378 |
| LR · linear terms | standardised raw counts, one-hot, L2 | C=0.010 | 0.3383 |
| LR · splines + class_weight | main LR with class_weight=“balanced” | C=0.010 | 0.6490 |
| LightGBM | gradient boosting, 30-config random search | subsample_freq=1, subsample=1.000, reg_lambda=1.000, num_leaves=31, n_estimators=800, min_child_samples=100, learning_rate=0.010, colsample_bytree=0.800 | 0.3337 |
| Random forest | 400 trees, 6-config grid | max_features=0.300, min_samples_leaf=20 | 0.3345 |
| LR · prior stays only | one feature: inpatient stays in the prior year | — | — |
Grouped 5-fold CV on the training patients. Lower log loss is better; the prevalence-only model scores 0.3537 on test. Source: results/results.json (cv).
The selected elastic net is pure L1 and keeps 142 of 201 coefficients — sparsity without loss. Note the class-weighted LR: same features, same ranking, twice the log loss. Section 06 explains why.
Boosting wins by 0.01 AUC — real, and small
On held-out patients, LightGBM reaches ROC AUC 0.681 and the main logistic regression 0.670. Measured from chance (0.5), the logistic model keeps 94% of the lift that boosting achieves — with about 200 coefficients that can be read, audited and shipped as a formula. One feature alone (prior inpatient stays) already gets 0.611.
Ranking metrics (threshold-free)
| model | ROC AUC | 95% CI | Gini | KS | PR AUC | PR lift | recall @ top 10% | recall @ top 20% | lift @ top 10% |
|---|---|---|---|---|---|---|---|---|---|
| LR · log + splines (main) | 0.670 | [0.656, 0.683] | 0.340 | 0.249 | 0.218 | 1.93× | 23.0% | 38.8% | 2.30× |
| LR · splines + L1/L2 | 0.671 | [0.658, 0.684] | 0.343 | 0.253 | 0.219 | 1.93× | 22.8% | 39.0% | 2.28× |
| LR · linear terms | 0.670 | [0.656, 0.683] | 0.340 | 0.246 | 0.222 | 1.96× | 22.7% | 38.9% | 2.27× |
| LR · splines + class_weight | 0.670 | [0.656, 0.683] | 0.340 | 0.248 | 0.217 | 1.91× | 22.8% | 38.9% | 2.28× |
| LightGBM | 0.681 | [0.668, 0.693] | 0.361 | 0.259 | 0.242 | 2.13× | 25.5% | 40.3% | 2.55× |
| Random forest | 0.676 | [0.663, 0.690] | 0.353 | 0.259 | 0.239 | 2.10× | 24.0% | 40.6% | 2.40× |
| LR · prior stays only | 0.611 | [0.597, 0.627] | 0.223 | 0.191 | 0.184 | 1.63× | 21.3% | 35.1% | 2.13× |
| Prevalence only | 0.500 | — | 0.000 | 0.000 | 0.113 | 1.00× | 10.0% | 20.0% | 1.00× |
PR AUC = average precision; PR lift = PR AUC / prevalence (0.1135). Recall @ top k% = share of all readmissions found among the k% highest-risk encounters.
Probabilistic metrics (proper scores)
| model | Brier | 95% CI | Brier skill | log loss | log-loss skill | discrimination slope |
|---|---|---|---|---|---|---|
| LR · log + splines (main) | 0.0962 | [0.0925, 0.0997] | 4.4% | 0.3347 | 5.4% | 0.043 |
| LR · splines + L1/L2 | 0.0962 | [0.0925, 0.0997] | 4.4% | 0.3345 | 5.4% | 0.044 |
| LR · linear terms | 0.0961 | [0.0924, 0.0996] | 4.4% | 0.3348 | 5.4% | 0.043 |
| LR · splines + class_weight | 0.2254 | [0.2232, 0.2276] | -124.1% | 0.6453 | -82.4% | 0.086 |
| LightGBM | 0.0950 | [0.0914, 0.0985] | 5.6% | 0.3305 | 6.6% | 0.051 |
| Random forest | 0.0953 | [0.0917, 0.0988] | 5.2% | 0.3316 | 6.2% | 0.047 |
| LR · prior stays only | 0.0973 | [0.0935, 0.1010] | 3.3% | 0.3416 | 3.4% | 0.031 |
| Prevalence only | 0.1006 | — | 0.0% | 0.3537 | 0.0% | 0.000 |
Skill = improvement over the prevalence-only model (negative = worse than predicting 11.4% for everyone). Discrimination slope = mean p̂ among readmitted − mean p̂ among not readmitted (Tjur's R²).
Paired differences vs. the main LR (same bootstrap resamples)
| model − LR · splines | Δ ROC AUC | Δ PR AUC | Δ Brier | Δ log loss | AUC verdict |
|---|---|---|---|---|---|
| LR · splines + L1/L2 | +0.002 [0.000, 0.002] | +0.000 [-0.001, 0.002] | −0.00003 [-0.00008, 0.00003] | −0.0002 [-0.0004, 0.0000] | better |
| LR · linear terms | −0.000 [-0.003, 0.003] | +0.004 [-0.001, 0.008] | −0.00006 [-0.00035, 0.00018] | +0.0001 [-0.0007, 0.0009] | tie |
| LR · splines + class_weight | +0.000 [-0.001, 0.001] | −0.001 [-0.003, 0.000] | +0.12922 [0.12599, 0.13273] | +0.3106 [0.3023, 0.3197] | tie |
| LightGBM | +0.011 [0.006, 0.016] | +0.024 [0.016, 0.034] | −0.00119 [-0.00162, -0.00083] | −0.0041 [-0.0056, -0.0029] | better |
| Random forest | +0.007 [0.000, 0.013] | +0.020 [0.012, 0.031] | −0.00086 [-0.00130, -0.00049] | −0.0030 [-0.0045, -0.0017] | better |
| LR · prior stays only | −0.059 [-0.069, -0.049] | −0.034 [-0.042, -0.026] | +0.00113 [0.00062, 0.00170] | +0.0069 [0.0050, 0.0088] | worse |
Point difference and 95% patient-level bootstrap interval. For Brier and log loss, negative = better than the main LR.
Can the logistic model close the gap? Boosting gets two things for free: high-cardinality codes and interactions. I gave the logistic regression both — the 3-digit ICD-9 codes (~700 levels, rare ones pooled) and interactions suggested by the trees (pairs of log-counts, prior utilisation × discharge destination):
Closing the gap: richer logistic models
| model | coefficients | CV log loss | test ROC AUC | Δ AUC vs LR · splines | Δ AUC vs LightGBM | PR AUC | Brier | cal. slope |
|---|---|---|---|---|---|---|---|---|
| LR · log + splines (main) | 201 | 0.3380 | 0.670 | — | −0.011 | 0.218 | 0.0962 | 1.04 |
| LR · splines + 3-digit ICD-9 | 517 | 0.3373 | 0.673 | +0.003 [0.000, 0.006] | −0.008 [-0.014, -0.002] | 0.220 | 0.0960 | 1.04 |
| LR · splines + interactions | 251 | 0.3374 | 0.671 | +0.001 [-0.001, 0.004] | −0.010 [-0.014, -0.005] | 0.226 | 0.0959 | 1.03 |
| LR · splines + ICD-9 + interactions | 567 | 0.3367 | 0.674 | +0.004 [0.001, 0.007] | −0.007 [-0.012, -0.002] | 0.228 | 0.0957 | 1.03 |
| LightGBM | — | 0.3337 | 0.681 | +0.011 | — | 0.242 | 0.0950 | 1.08 |
Same split, folds and bootstrap resamples as above. Source: results/gap.json (scripts/close_the_gap.py).
The richest logistic model closes about a third of the gap (+0.004 of 0.011 AUC) and gains most in PR AUC and log loss, but LightGBM stays significantly ahead. The rest of the signal lives in higher-order interactions that a hand-specified linear predictor does not reach: on this dataset, interpretability costs about 0.007 AUC. Whether that is worth paying is a deployment decision, not a modelling one.
Every confusion-matrix metric, at four policies
Threshold metrics depend on a decision, so the decision is made explicit. Three thresholds are chosen on out-of-fold training predictions — never on the test set — and the fourth is the default 0.5 that most tutorials use. Pick a policy:
| model | τ | flagged | accuracy | bal. acc. | precision | recall | specificity | NPV | F1 | F2 | MCC | κ | LR+ | LR− | DOR |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| LR · log + splines (main) | 0.131 | 26.1% | 0.733 | 0.620 | 0.206 | 0.475 | 0.766 | 0.919 | 0.287 | 0.376 | 0.174 | 0.153 | 2.03 | 0.69 | 2.95 |
| LR · splines + L1/L2 | 0.132 | 25.6% | 0.737 | 0.619 | 0.207 | 0.467 | 0.771 | 0.919 | 0.287 | 0.374 | 0.174 | 0.154 | 2.04 | 0.69 | 2.96 |
| LR · linear terms | 0.130 | 25.2% | 0.736 | 0.611 | 0.202 | 0.450 | 0.773 | 0.917 | 0.279 | 0.362 | 0.163 | 0.145 | 1.98 | 0.71 | 2.78 |
| LR · splines + class_weight | 0.538 | 26.8% | 0.726 | 0.617 | 0.202 | 0.476 | 0.759 | 0.919 | 0.283 | 0.374 | 0.168 | 0.147 | 1.97 | 0.69 | 2.85 |
| LightGBM | 0.140 | 24.1% | 0.751 | 0.626 | 0.219 | 0.464 | 0.788 | 0.920 | 0.297 | 0.379 | 0.187 | 0.169 | 2.19 | 0.68 | 3.21 |
| Random forest | 0.138 | 27.0% | 0.727 | 0.621 | 0.204 | 0.485 | 0.758 | 0.920 | 0.287 | 0.380 | 0.174 | 0.152 | 2.00 | 0.68 | 2.95 |
| LR · prior stays only | 0.119 | 32.8% | 0.672 | 0.596 | 0.172 | 0.497 | 0.694 | 0.915 | 0.256 | 0.361 | 0.129 | 0.105 | 1.62 | 0.72 | 2.24 |
τ maximises F1 on out-of-fold training predictions of each model. Test set, n = 19,773.
| model | τ | flagged | accuracy | bal. acc. | precision | recall | specificity | NPV | F1 | F2 | MCC | κ | LR+ | LR− | DOR |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| LR · log + splines (main) | 0.110 | 37.6% | 0.645 | 0.623 | 0.179 | 0.594 | 0.651 | 0.926 | 0.275 | 0.406 | 0.160 | 0.122 | 1.70 | 0.62 | 2.73 |
| LR · splines + L1/L2 | 0.110 | 37.1% | 0.649 | 0.623 | 0.180 | 0.590 | 0.657 | 0.926 | 0.276 | 0.406 | 0.162 | 0.124 | 1.72 | 0.62 | 2.76 |
| LR · linear terms | 0.106 | 41.2% | 0.617 | 0.621 | 0.173 | 0.627 | 0.615 | 0.928 | 0.271 | 0.411 | 0.156 | 0.113 | 1.63 | 0.61 | 2.69 |
| LR · splines + class_weight | 0.492 | 36.8% | 0.651 | 0.622 | 0.180 | 0.586 | 0.659 | 0.926 | 0.276 | 0.404 | 0.161 | 0.124 | 1.72 | 0.63 | 2.73 |
| LightGBM | 0.115 | 36.2% | 0.657 | 0.624 | 0.183 | 0.583 | 0.666 | 0.926 | 0.278 | 0.405 | 0.164 | 0.127 | 1.75 | 0.63 | 2.79 |
| Random forest | 0.116 | 38.8% | 0.638 | 0.629 | 0.180 | 0.616 | 0.641 | 0.929 | 0.279 | 0.415 | 0.167 | 0.125 | 1.72 | 0.60 | 2.86 |
| LR · prior stays only | 0.119 | 32.8% | 0.672 | 0.596 | 0.172 | 0.497 | 0.694 | 0.915 | 0.256 | 0.361 | 0.129 | 0.105 | 1.62 | 0.72 | 2.24 |
τ maximises sensitivity + specificity − 1 out-of-fold: more flags, higher recall, lower precision.
| model | τ | flagged | accuracy | bal. acc. | precision | recall | specificity | NPV | F1 | F2 | MCC | κ | LR+ | LR− | DOR |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| LR · log + splines (main) | 0.147 | 19.9% | 0.775 | 0.605 | 0.220 | 0.385 | 0.825 | 0.913 | 0.280 | 0.335 | 0.167 | 0.158 | 2.20 | 0.75 | 2.95 |
| LR · splines + L1/L2 | 0.148 | 20.1% | 0.775 | 0.607 | 0.221 | 0.391 | 0.824 | 0.914 | 0.282 | 0.339 | 0.170 | 0.161 | 2.22 | 0.74 | 3.00 |
| LR · linear terms | 0.141 | 19.7% | 0.776 | 0.605 | 0.221 | 0.384 | 0.826 | 0.913 | 0.280 | 0.334 | 0.167 | 0.159 | 2.21 | 0.75 | 2.96 |
| LR · splines + class_weight | 0.578 | 19.9% | 0.776 | 0.607 | 0.221 | 0.389 | 0.825 | 0.913 | 0.282 | 0.338 | 0.170 | 0.161 | 2.22 | 0.74 | 3.00 |
| LightGBM | 0.152 | 20.1% | 0.777 | 0.615 | 0.228 | 0.404 | 0.825 | 0.915 | 0.292 | 0.350 | 0.181 | 0.171 | 2.31 | 0.72 | 3.20 |
| Random forest | 0.157 | 19.8% | 0.780 | 0.616 | 0.231 | 0.404 | 0.828 | 0.916 | 0.294 | 0.352 | 0.185 | 0.175 | 2.35 | 0.72 | 3.27 |
| LR · prior stays only | 0.120 | 13.8% | 0.811 | 0.578 | 0.228 | 0.277 | 0.880 | 0.905 | 0.250 | 0.265 | 0.144 | 0.143 | 2.30 | 0.82 | 2.80 |
τ = 80th percentile of the out-of-fold scores: a fixed budget of follow-ups for 20% of discharges.
| model | τ | flagged | accuracy | bal. acc. | precision | recall | specificity | NPV | F1 | F2 | MCC | κ | LR+ | LR− | DOR |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| LR · log + splines (main) | 0.500 | 0.2% | 0.887 | 0.504 | 0.541 | 0.009 | 0.999 | 0.887 | 0.018 | 0.011 | 0.058 | 0.014 | 9.19 | 0.99 | 9.26 |
| LR · splines + L1/L2 | 0.500 | 0.2% | 0.887 | 0.504 | 0.512 | 0.009 | 0.999 | 0.887 | 0.018 | 0.012 | 0.057 | 0.014 | 8.20 | 0.99 | 8.27 |
| LR · linear terms | 0.500 | 0.4% | 0.887 | 0.509 | 0.629 | 0.020 | 0.999 | 0.888 | 0.038 | 0.024 | 0.097 | 0.031 | 13.22 | 0.98 | 13.46 |
| LR · splines + class_weight | 0.500 | 34.9% | 0.665 | 0.621 | 0.183 | 0.565 | 0.678 | 0.924 | 0.277 | 0.399 | 0.162 | 0.127 | 1.76 | 0.64 | 2.73 |
| LightGBM | 0.500 | 0.2% | 0.887 | 0.505 | 0.667 | 0.011 | 0.999 | 0.888 | 0.021 | 0.013 | 0.074 | 0.018 | 15.62 | 0.99 | 15.78 |
| Random forest | 0.500 | 0.0% | 0.887 | 0.500 | — | 0.000 | 1.000 | 0.887 | 0.000 | 0.000 | 0.000 | 0.000 | — | 1.00 | — |
| LR · prior stays only | 0.500 | 0.3% | 0.888 | 0.509 | 0.689 | 0.019 | 0.999 | 0.888 | 0.036 | 0.023 | 0.101 | 0.031 | 17.27 | 0.98 | 17.58 |
With 11.4% prevalence, calibrated models almost never reach 0.5: accuracy ≈ 0.887, the accuracy of predicting “no” for everyone. Only the class-weighted LR behaves sensibly here — because its probabilities are inflated.
Two lessons. First, at a sensible threshold every model except the one-feature baseline lands in a narrow band — F1 0.279–0.297, MCC 0.163–0.187: threshold metrics barely separate models, while proper scores and paired bootstrap differences do. Second, the accuracy paradox in plain sight: the best “accuracy” in the 0.5 tab belongs to models that flag almost nobody.
class_weight is a calibration bug, not an imbalance fix
A risk score is only useful for decisions if “20%” means 20%. The main LR and LightGBM both track the diagonal (ECE 0.007). The class-weighted LR has the same AUC but predicts a mean risk of 47% for an outcome that happens 11% of the time:
| model | mean p̂ | O/E | intercept | slope | ECE | MCE | Spiegelhalter z | HL χ²(8) | HL p |
|---|---|---|---|---|---|---|---|---|---|
| LR · log + splines (main) | 0.114 | 1.00 | -0.001 | 1.043 | 0.0070 | 0.0133 | -0.14 | 14.20 | 0.077 |
| LR · splines + L1/L2 | 0.114 | 1.00 | -0.004 | 1.039 | 0.0084 | 0.0171 | -0.24 | 18.80 | 0.016 |
| LR · linear terms | 0.114 | 1.00 | -0.002 | 1.083 | 0.0090 | 0.0268 | -0.25 | 35.92 | < 0.001 |
| LR · splines + class_weight | 0.466 | 0.24 | -2.053 | 0.974 | 0.3522 | 0.4769 | -6.05 | 11,113.66 | < 0.001 |
| LightGBM | 0.113 | 1.00 | 0.003 | 1.082 | 0.0067 | 0.0205 | -0.43 | 12.52 | 0.129 |
| Random forest | 0.114 | 1.00 | -0.004 | 1.086 | 0.0068 | 0.0145 | -0.81 | 13.00 | 0.112 |
| LR · prior stays only | 0.113 | 1.00 | 0.002 | 1.101 | 0.0111 | 0.0268 | -0.11 | 31.84 | < 0.001 |
Observed prevalence 0.113. O/E = observed / expected events. Intercept and slope from a logistic recalibration (ideal 0 and 1). ECE / MCE = mean / max absolute gap over deciles. HL = Hosmer–Lemeshow; with n ≈ 20,000 it rejects small deviations, so read it with the slope and ECE.
Why it happens: weighting the positives by w shifts the logit intercept by about log w and leaves the slopes nearly untouched. Ranking is preserved, probabilities are not. Moving the threshold achieves the same trade-off without destroying the probabilities. A smaller lesson: the splines did not improve discrimination (0.6698 vs 0.6698) but they did improve calibration — slope 1.04 vs 1.08, maximum decile error 1.3 vs 2.7 points, Hosmer–Lemeshow p 0.077 vs < 0.001.
What drives readmission — with honest standard errors
For interpretation I fitted a separate logistic GLM with readable terms (counts per unit, most frequent level as reference, 68 parameters) using IRLS written from scratch in NumPy — Newton–Raphson on the log-likelihood, with model-based, sandwich and cluster-robust (by patient) covariance. Coefficients and clustered standard errors match statsmodels to about 10−14. Terms with |z| > 3.5:
- Prior inpatient stays dominate: OR 1.30 per stay in the previous year (z = 35.6).
- Where the patient goes next is the second signal: rehab OR 3.7, another inpatient facility 2.0, SNF/ICF 1.5 (vs. home).
- Measured HbA1c > 8 is associated with lower risk (OR 0.86) — in line with Strack et al.: measuring HbA1c goes with closer follow-up.
- Clustering matters unevenly: the median clustered/model SE ratio is 1.02, but for prior ER visits it is 1.54 — the patients with many ER visits are exactly the ones who come back.
Associations, not causal effects. The predictive model (section 03) and this inferential model share the data, not the purpose.
When is the model worth using?
A decision curve converts the threshold into a cost ratio: acting at τ means a missed readmission costs (1 − τ)/τ times a needless follow-up. Net benefit beats “treat everyone” from τ ≈ 0.05 upward but shrinks toward zero above ~0.25. The model pays for itself for cheap interventions — a phone call, a pharmacist review — not expensive ones.
And the data-size question: refitting both models on random subsets of patients shows where the extra flexibility starts to pay.
Subgroups (main LR, held-out patients)
| variable | group | n | observed rate | mean predicted | ROC AUC |
|---|---|---|---|---|---|
| age | 70+ | 8,752 | 11.7% | 12.3% | 0.631 |
| age | 50–69 | 7,823 | 11.3% | 10.7% | 0.692 |
| age | <50 | 3,198 | 10.6% | 10.3% | 0.708 |
| race | Caucasian | 14,813 | 11.7% | 11.5% | 0.667 |
| race | African American | 3,697 | 10.8% | 11.5% | 0.669 |
| race | missing | 447 | 7.2% | 9.6% | 0.638 |
| race | Hispanic | 398 | 9.0% | 10.4% | 0.748 |
| race | other | 302 | 10.3% | 9.7% | 0.681 |
| sex | female | 10,726 | 11.4% | 11.5% | 0.674 |
| sex | male | 9,047 | 11.2% | 11.2% | 0.665 |
Groups with n ≥ 300. Discrimination is weaker for the oldest patients; the “race missing” group is over-predicted.
Reproducible, tested, published
- Code and every number: GitHub repository ↗ — four scripts (download, experiment, close-the-gap, figures) write
results/results.json; nothing on this page is typed by hand. - Model: Hugging Face Hub ↗ — the main pipeline serialised with
skops(no pickle), with a model card, metrics and a usage example. - Tests: 23 pytest cases — ICD-9 grouping, cohort rules, IRLS vs scikit-learn and statsmodels (coefficients, model and clustered SEs), calibration metrics on simulated data, decision-curve formula.
- Limits: 1999–2008 US data with ICD-9 codes; readmissions to other hospitals are invisible, so the label under-counts; encounters ending in death or hospice were excluded and must not be scored. Not for clinical use.
Data: Strack et al. (2014), UCI Machine Learning Repository, doi:10.24432/C5230J, CC BY 4.0 — downloaded at runtime, not redistributed.