igor-millis~/work/diabetes-readmission
EN/PT
← all work · machine learning · logistic regression · model evaluation

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?

Logistic regressionscikit-learnLightGBMstatsmodelsNumPyHugging Facepytest
99,340
encounters · 69,987 patients
11.4%
readmitted within 30 days
94%
of boosting's lift over chance kept by the LR
40+
metrics per model · CIs by patient
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
00 · summary

In a nutshell

01 · problem

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.

irregularityevidencewhat was done
rare, weak target11.4% readmitted < 30 days; best AUC ≈ 0.68proper scores, PR AUC, calibration; thresholds chosen out-of-fold
deaths and hospice2,423 encounters cannot be readmittedremoved — otherwise dying looks “protective”
repeated patients69,987 patients; 46% of encounters from repeaterssplit, tune and bootstrap by patient
massive missingnessweight 97% · glucose 95% · HbA1c 83% · specialty 49% · payer 40%weight dropped; “not measured” kept as a level
high-cardinality codes716 ICD-9 primary diagnoses · 72 specialties9 ICD-9 chapters; top-12 specialties; rare levels pooled per fold
heavy tailsprior stays 0–21 · ER visits 0–76log1p + cubic B-splines inside the pipeline
IDs stored as numbersadmission / discharge / source are nominal codesmapped to clinically meaningful groups

Audit of the raw file (101,766 encounters). Cohort after exclusions: 99,340 encounters, 69,987 patients.

02 · validation

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:

leakage check · 5-fold CV on the training patientsROC AUC
CV, random encounter splitCV, split by patientheld-out test patients
0.600.620.640.660.680.70LR · log + splines (main) · CV, random encounter split: 0.6610.661LR · log + splines (main) · CV, split by patient: 0.6610.661LR · log + splines (main) · held-out test patients: 0.6700.670LR · log + splines (main)LightGBM · CV, random encounter split: 0.6730.673LightGBM · CV, split by patient: 0.6740.674LightGBM · held-out test patients: 0.6810.681LightGBMRandom forest · CV, random encounter split: 0.6710.671Random forest · CV, split by patient: 0.6700.670Random forest · held-out test patients: 0.6760.676Random forestRandom forest, leaf size 1 · CV, random encounter split: 0.6520.652Random forest, leaf size 1 · CV, split by patient: 0.6510.651Random forest, leaf size 1 · held-out test patients: 0.6610.661Random forest, leaf size 1
Mean ROC AUC over 5 folds on the 79,567 training encounters; third bar: held-out test set. Source: results/results.json (leakage).

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.

03 · models

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.

modelwhat it isselected hyperparametersCV log loss
LR · log + splines (main)log1p(counts) → cubic B-splines, one-hot categoricals, L2C=0.0100.3380
LR · splines + L1/L2same features, elastic-net path (saga)C=0.100, l1_ratio=1.0000.3378
LR · linear termsstandardised raw counts, one-hot, L2C=0.0100.3383
LR · splines + class_weightmain LR with class_weight=“balanced”C=0.0100.6490
LightGBMgradient boosting, 30-config random searchsubsample_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.8000.3337
Random forest400 trees, 6-config gridmax_features=0.300, min_samples_leaf=200.3345
LR · prior stays onlyone 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.

04 · comparison

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.

ROC AUC · held-out patients · 95% patient-level bootstrap CIn = 19,773
0.580.600.620.640.660.680.70LightGBMLightGBM: 0.681 [0.668, 0.693]0.681Random forestRandom forest: 0.676 [0.663, 0.690]0.676LR · splines + L1/L2LR · splines + L1/L2: 0.671 [0.658, 0.684]0.671LR · splines + class_weightLR · splines + class_weight: 0.670 [0.656, 0.683]0.670LR · log + splines (main)LR · log + splines (main): 0.670 [0.656, 0.683]0.670LR · linear termsLR · linear terms: 0.670 [0.656, 0.683]0.670LR · prior stays onlyLR · prior stays only: 0.611 [0.597, 0.627]0.611
B = 1,000 resamples of patients (not encounters). Source: results/results.json (test, ci).

Ranking metrics (threshold-free)

modelROC AUC95% CIGiniKSPR AUCPR liftrecall @ top 10%recall @ top 20%lift @ top 10%
LR · log + splines (main)0.670[0.656, 0.683]0.3400.2490.2181.93×23.0%38.8%2.30×
LR · splines + L1/L20.671[0.658, 0.684]0.3430.2530.2191.93×22.8%39.0%2.28×
LR · linear terms0.670[0.656, 0.683]0.3400.2460.2221.96×22.7%38.9%2.27×
LR · splines + class_weight0.670[0.656, 0.683]0.3400.2480.2171.91×22.8%38.9%2.28×
LightGBM0.681[0.668, 0.693]0.3610.2590.2422.13×25.5%40.3%2.55×
Random forest0.676[0.663, 0.690]0.3530.2590.2392.10×24.0%40.6%2.40×
LR · prior stays only0.611[0.597, 0.627]0.2230.1910.1841.63×21.3%35.1%2.13×
Prevalence only0.500—0.0000.0000.1131.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)

modelBrier95% CIBrier skilllog losslog-loss skilldiscrimination slope
LR · log + splines (main)0.0962[0.0925, 0.0997]4.4%0.33475.4%0.043
LR · splines + L1/L20.0962[0.0925, 0.0997]4.4%0.33455.4%0.044
LR · linear terms0.0961[0.0924, 0.0996]4.4%0.33485.4%0.043
LR · splines + class_weight0.2254[0.2232, 0.2276]-124.1%0.6453-82.4%0.086
LightGBM0.0950[0.0914, 0.0985]5.6%0.33056.6%0.051
Random forest0.0953[0.0917, 0.0988]5.2%0.33166.2%0.047
LR · prior stays only0.0973[0.0935, 0.1010]3.3%0.34163.4%0.031
Prevalence only0.1006—0.0%0.35370.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 lossAUC 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

modelcoefficientsCV log losstest ROC AUCΔ AUC vs LR · splinesΔ AUC vs LightGBMPR AUCBriercal. slope
LR · log + splines (main)2010.33800.670—−0.0110.2180.09621.04
LR · splines + 3-digit ICD-95170.33730.673+0.003
[0.000, 0.006]
−0.008
[-0.014, -0.002]
0.2200.09601.04
LR · splines + interactions2510.33740.671+0.001
[-0.001, 0.004]
−0.010
[-0.014, -0.005]
0.2260.09591.03
LR · splines + ICD-9 + interactions5670.33670.674+0.004
[0.001, 0.007]
−0.007
[-0.012, -0.002]
0.2280.09571.03
LightGBM—0.33370.681+0.011—0.2420.09501.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.

05 · thresholds

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τflaggedaccuracybal. acc.precisionrecallspecificityNPVF1F2MCCκLR+LR−DOR
LR · log + splines (main)0.13126.1%0.7330.6200.2060.4750.7660.9190.2870.3760.1740.1532.030.692.95
LR · splines + L1/L20.13225.6%0.7370.6190.2070.4670.7710.9190.2870.3740.1740.1542.040.692.96
LR · linear terms0.13025.2%0.7360.6110.2020.4500.7730.9170.2790.3620.1630.1451.980.712.78
LR · splines + class_weight0.53826.8%0.7260.6170.2020.4760.7590.9190.2830.3740.1680.1471.970.692.85
LightGBM0.14024.1%0.7510.6260.2190.4640.7880.9200.2970.3790.1870.1692.190.683.21
Random forest0.13827.0%0.7270.6210.2040.4850.7580.9200.2870.3800.1740.1522.000.682.95
LR · prior stays only0.11932.8%0.6720.5960.1720.4970.6940.9150.2560.3610.1290.1051.620.722.24

τ maximises F1 on out-of-fold training predictions of each model. Test set, n = 19,773.

modelτflaggedaccuracybal. acc.precisionrecallspecificityNPVF1F2MCCκLR+LR−DOR
LR · log + splines (main)0.11037.6%0.6450.6230.1790.5940.6510.9260.2750.4060.1600.1221.700.622.73
LR · splines + L1/L20.11037.1%0.6490.6230.1800.5900.6570.9260.2760.4060.1620.1241.720.622.76
LR · linear terms0.10641.2%0.6170.6210.1730.6270.6150.9280.2710.4110.1560.1131.630.612.69
LR · splines + class_weight0.49236.8%0.6510.6220.1800.5860.6590.9260.2760.4040.1610.1241.720.632.73
LightGBM0.11536.2%0.6570.6240.1830.5830.6660.9260.2780.4050.1640.1271.750.632.79
Random forest0.11638.8%0.6380.6290.1800.6160.6410.9290.2790.4150.1670.1251.720.602.86
LR · prior stays only0.11932.8%0.6720.5960.1720.4970.6940.9150.2560.3610.1290.1051.620.722.24

τ maximises sensitivity + specificity − 1 out-of-fold: more flags, higher recall, lower precision.

modelτflaggedaccuracybal. acc.precisionrecallspecificityNPVF1F2MCCκLR+LR−DOR
LR · log + splines (main)0.14719.9%0.7750.6050.2200.3850.8250.9130.2800.3350.1670.1582.200.752.95
LR · splines + L1/L20.14820.1%0.7750.6070.2210.3910.8240.9140.2820.3390.1700.1612.220.743.00
LR · linear terms0.14119.7%0.7760.6050.2210.3840.8260.9130.2800.3340.1670.1592.210.752.96
LR · splines + class_weight0.57819.9%0.7760.6070.2210.3890.8250.9130.2820.3380.1700.1612.220.743.00
LightGBM0.15220.1%0.7770.6150.2280.4040.8250.9150.2920.3500.1810.1712.310.723.20
Random forest0.15719.8%0.7800.6160.2310.4040.8280.9160.2940.3520.1850.1752.350.723.27
LR · prior stays only0.12013.8%0.8110.5780.2280.2770.8800.9050.2500.2650.1440.1432.300.822.80

τ = 80th percentile of the out-of-fold scores: a fixed budget of follow-ups for 20% of discharges.

modelτflaggedaccuracybal. acc.precisionrecallspecificityNPVF1F2MCCκLR+LR−DOR
LR · log + splines (main)0.5000.2%0.8870.5040.5410.0090.9990.8870.0180.0110.0580.0149.190.999.26
LR · splines + L1/L20.5000.2%0.8870.5040.5120.0090.9990.8870.0180.0120.0570.0148.200.998.27
LR · linear terms0.5000.4%0.8870.5090.6290.0200.9990.8880.0380.0240.0970.03113.220.9813.46
LR · splines + class_weight0.50034.9%0.6650.6210.1830.5650.6780.9240.2770.3990.1620.1271.760.642.73
LightGBM0.5000.2%0.8870.5050.6670.0110.9990.8880.0210.0130.0740.01815.620.9915.78
Random forest0.5000.0%0.8870.500—0.0001.0000.8870.0000.0000.0000.000—1.00—
LR · prior stays only0.5000.3%0.8880.5090.6890.0190.9990.8880.0360.0230.1010.03117.270.9817.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.

06 · calibration

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:

reliability diagram · risk deciles · held-out patientsobserved vs predicted
LR · log + splines (main)LightGBMLR · splines + class_weightperfect calibration
0.00.10.20.30.00.10.20.30.40.50.60.70.8LR · splines + class_weight: x = 0.273 · y = 0.042LR · splines + class_weight: x = 0.333 · y = 0.052LR · splines + class_weight: x = 0.367 · y = 0.070LR · splines + class_weight: x = 0.397 · y = 0.082LR · splines + class_weight: x = 0.428 · y = 0.082LR · splines + class_weight: x = 0.461 · y = 0.104LR · splines + class_weight: x = 0.500 · y = 0.121LR · splines + class_weight: x = 0.548 · y = 0.140LR · splines + class_weight: x = 0.613 · y = 0.182LR · splines + class_weight: x = 0.736 · y = 0.259LightGBM: x = 0.042 · y = 0.037LightGBM: x = 0.056 · y = 0.056LightGBM: x = 0.067 · y = 0.064LightGBM: x = 0.079 · y = 0.073LightGBM: x = 0.090 · y = 0.096LightGBM: x = 0.103 · y = 0.109LightGBM: x = 0.117 · y = 0.103LightGBM: x = 0.138 · y = 0.141LightGBM: x = 0.171 · y = 0.168LightGBM: x = 0.269 · y = 0.290LR · log + splines (main): x = 0.050 · y = 0.043LR · log + splines (main): x = 0.064 · y = 0.054LR · log + splines (main): x = 0.072 · y = 0.066LR · log + splines (main): x = 0.081 · y = 0.083LR · log + splines (main): x = 0.090 · y = 0.084LR · log + splines (main): x = 0.100 · y = 0.107LR · log + splines (main): x = 0.114 · y = 0.111LR · log + splines (main): x = 0.134 · y = 0.147LR · log + splines (main): x = 0.166 · y = 0.180LR · log + splines (main): x = 0.264 · y = 0.261
Each point is one decile of predicted risk (≈ 1,977 encounters). Source: results/results.json (reliability).
modelmean p̂O/EinterceptslopeECEMCESpiegelhalter zHL χ²(8)HL p
LR · log + splines (main)0.1141.00-0.0011.0430.00700.0133-0.1414.200.077
LR · splines + L1/L20.1141.00-0.0041.0390.00840.0171-0.2418.800.016
LR · linear terms0.1141.00-0.0021.0830.00900.0268-0.2535.92< 0.001
LR · splines + class_weight0.4660.24-2.0530.9740.35220.4769-6.0511,113.66< 0.001
LightGBM0.1131.000.0031.0820.00670.0205-0.4312.520.129
Random forest0.1141.00-0.0041.0860.00680.0145-0.8113.000.112
LR · prior stays only0.1131.000.0021.1010.01110.0268-0.1131.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.

07 · inference

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:

odds ratios · 95% CI with patient-clustered SEs · log scalen = 79,567 · p = 68
raises the oddslowers the odds
0.500.751.001.502.00discharged to rehabOR 3.68 [3.24, 4.18]3.68discharged to other inpatient careOR 2.01 [1.81, 2.22]2.01discharged to SNF/ICFOR 1.50 [1.40, 1.61]1.50inpatient stays, prior year (per 1)OR 1.30 [1.28, 1.32]1.30insulin dose loweredOR 1.27 [1.16, 1.40]1.27discharged home + home healthOR 1.18 [1.10, 1.27]1.18diagnoses (per 1)OR 1.04 [1.03, 1.06]1.04payer: MedicareOR 0.86 [0.80, 0.91]0.86no diabetes medicationOR 0.85 [0.79, 0.91]0.85primary diagnosis respiratoryOR 0.82 [0.75, 0.89]0.82specialty: cardiologyOR 0.77 [0.68, 0.87]0.77
Reference levels: discharged home, admitted through the ER, primary diagnosis circulatory, HbA1c not measured, no insulin, payer missing, specialty missing. Source: results/results.json (inference).
  • 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.

08 · decisions

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.

decision curve · held-out patientsnet benefit vs threshold
LR · log + splines (main)LightGBMtreat alltreat none
0.000.010.020.030.040.050.060.070.080.090.050.100.150.200.250.300.350.40
Net benefit = TP/n − FP/n · τ/(1 − τ) (Vickers & Elkin, 2006). Source: results/results.json (dca).

And the data-size question: refitting both models on random subsets of patients shows where the extra flexibility starts to pay.

learning curve · test AUC vs training size (patients subsampled)ROC AUC
LR · log + splines (main)LightGBM
0.6000.6250.6500.6750.7001,6103,9667,92019,85439,80979,567LightGBM: x = 1,610 · y = 0.618LightGBM: x = 3,966 · y = 0.618LightGBM: x = 7,920 · y = 0.643LightGBM: x = 19,854 · y = 0.661LightGBM: x = 39,809 · y = 0.678LightGBM: x = 79,567 · y = 0.681LR · log + splines (main): x = 1,610 · y = 0.624LR · log + splines (main): x = 3,966 · y = 0.633LR · log + splines (main): x = 7,920 · y = 0.648LR · log + splines (main): x = 19,854 · y = 0.658LR · log + splines (main): x = 39,809 · y = 0.667LR · log + splines (main): x = 79,567 · y = 0.670
Same test set throughout; hyperparameters fixed at the full-data choice. With ≤ 8k encounters the LR wins; boosting overtakes it around 20k. Source: results/results.json (learning_curve).

Subgroups (main LR, held-out patients)

variablegroupnobserved ratemean predictedROC AUC
age70+8,75211.7%12.3%0.631
age50–697,82311.3%10.7%0.692
age<503,19810.6%10.3%0.708
raceCaucasian14,81311.7%11.5%0.667
raceAfrican American3,69710.8%11.5%0.669
racemissing4477.2%9.6%0.638
raceHispanic3989.0%10.4%0.748
raceother30210.3%9.7%0.681
sexfemale10,72611.4%11.5%0.674
sexmale9,04711.2%11.2%0.665

Groups with n ≥ 300. Discrimination is weaker for the oldest patients; the “race missing” group is over-predicted.

09 · ship

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.