Spaces:
Sleeping
Sleeping
File size: 4,922 Bytes
bf27edd | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | # 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 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.

**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.

## 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.*
|