Nephroscreen / CASE_STUDY.md
sadiqM's picture
Solo framing + interpretability, maturity signals, case study
bf27edd
|
Raw
History Blame Contribute Delete
4.92 kB
# Case Study β€” NephroScreen
*An end-to-end applied machine-learning project: screening for chronic kidney
disease from routine lab values, built and analysed solo.*
---
## At a glance
| | |
|---|---|
| **Problem** | Binary classification β€” predict CKD vs. healthy from routine clinical/lab features |
| **Data** | UCI Chronic Kidney Disease dataset β€” 400 patients, 24 attributes, ~11% cells missing |
| **Best model** | Random Forest β€” 100% test accuracy, ROC-AUC 1.0000 (read the caveat below) |
| **Stack** | scikit-learn Β· FastAPI Β· vanilla JS frontend Β· Docker/Render |
| **What makes it mature** | Leakage-safe pipeline Β· SHAP interpretability Β· calibration Β· screening-tuned threshold Β· tests |
## 1. The problem
Chronic kidney disease often progresses silently until it is advanced, yet the
warning signs are already present in routine blood and urine tests. The task:
given a patient's lab panel, flag likely CKD early enough to matter. This is a
classic **screening** problem, which shapes every later decision β€” especially
how errors are weighted.
## 2. The data β€” and an honest catch
The UCI CKD dataset is small (400 rows) and the two classes are **highly
separable** on a few biomarkers. Every model I trained scored in the high 90s,
and Random Forest reached **100% on the held-out test set**.
**That number is a red flag, not a trophy.** With an 80-patient test set drawn
from one clean, single-source dataset, near-perfect accuracy reflects the
*separability of this data*, not real-world clinical performance. I treat it that
way throughout β€” the engineering value here is doing the workflow *correctly*,
not chasing a headline metric.
## 3. Approach
**Leakage-safe preprocessing.** All imputation, encoding, and feature
engineering live in a custom scikit-learn transformer (`CKDPreprocessor`) wrapped
in a `Pipeline`. Every statistic β€” medians, modes, the scaler, the `age_group`
fill value β€” is fit on the **training fold only**, so cross-validation and the
live API never see test-set information.
**Domain feature engineering.** Four clinically motivated features, e.g.
`kidney_stress_index = serum_creatinine Γ— blood_urea / hemoglobin` (the CKD triad
in one number). SHAP later confirmed it earns its place in the top five.
**Three models compared** with 10-fold stratified CV:
| Model | Accuracy | Recall | F1 | ROC-AUC | 10-Fold CV |
|---|---|---|---|---|---|
| Logistic Regression | 98.75% | 98% | 98.99% | 1.0000 | 99.38% Β± 1.25% |
| **Random Forest** | **100%** | **100%** | **100%** | **1.0000** | 98.75% Β± 2.86% |
| Decision Tree | 97.50% | 98% | 98.00% | 0.9893 | 96.56% Β± 2.19% |
## 4. Interpretability β€” what the model actually uses
![SHAP summary](assets/shap_summary.png)
SHAP attributions are clinically coherent and build trust in the model:
- **Low hemoglobin and PCV** push strongly toward CKD (anemia of kidney disease).
- **High serum creatinine** and the engineered **kidney_stress_index** are top drivers.
- **Hypertension and diabetes = "yes"** push toward CKD β€” the two leading real-world causes.
This is the difference between "the model is accurate" and "I understand *why*
the model is accurate" β€” the latter is what makes a result defensible.
## 5. Maturity signals
**Calibration.** Do the probabilities mean what they say? The reliability curve
checks whether an "0.8" prediction is CKD ~80% of the time β€” essential before
anyone acts on a probability.
![Calibration](assets/calibration.png)
**A screening-tuned threshold.** For screening, a *missed* CKD case (false
negative) is far costlier than a false alarm. Instead of the default 0.5, the API
serves a threshold tuned to keep **recall β‰₯ 0.99**, accepting more false
positives as the safer trade.
![Threshold trade-off](assets/threshold.png)
## 6. From model to product
The trained pipeline is persisted with `joblib` and served by a **FastAPI**
backend; a dependency-free web frontend collects a lab panel (missing fields are
imputed) and shows the probability, a risk band, and which entered values fall
outside normal clinical ranges. Containerised for free-tier deployment.
```
Browser β†’ FastAPI /api/predict β†’ joblib pipeline (CKDPreprocessor β†’ RandomForest)
```
## 7. Limitations & what I'd do next
- **Single small dataset** β†’ results will not transfer to real clinical
populations without external validation on an independent cohort.
- **No serving-time calibration** yet; probabilities should be calibrated
(e.g. isotonic) before being shown as clinical likelihoods.
- **Next experiment:** find a **minimal, low-cost feature subset** β€” how much
accuracy is lost if the expensive labs are dropped? That's the question with
real value for screening in resource-limited settings.
---
*Built solo by Sadiq Mansoor. Dataset: UCI ML Repository β€” Chronic Kidney
Disease (ID 336). Educational project, not a medical device.*