A single-page clinical machine-learning web app for tabular registry data (TQIP, NSQIP, and any CSV cohort). It is the nexus for an ML study: upload a CSV, profile it, train and compare models, test them with full diagnostic plots, fit splines, and run standard clinical-epidemiology calculators — all from one page, all processed in Python.
One HTML page talks to one Flask backend. JavaScript only handles the UI and
fetch calls. PapaParse previews the CSV instantly in the browser; the raw
file still goes to Flask, which does all real parsing, statistics and ML with
pandas / scikit-learn. Every chart is rendered server-side with matplotlib
and returned as a PNG (with PDF / CSV export) — no SVG, no canvas, no Chart.js.
Clinical researchers working with registry data (TQIP, NSQIP, single-center cohorts) typically stitch together an ML study from several disconnected tools: a notebook for data profiling, hand-written scikit-learn code for model comparison, a separate online calculator for odds ratios and NNT, and another package entirely for Kaplan-Meier or Cox survival analysis. Each hand-off is a place for the leakage-free validation and correction conventions clinical work depends on (e.g. scoring hyperparameter search on a held-out validation set rather than the training fold, or applying a Haldane-Anscombe correction to a 2x2 table with a zero cell) to quietly get skipped. Several of those disconnected tools (online calculators, cloud AutoML platforms) also mean patient-derived data leaving the researcher's machine and landing on a third party's server, which is a real problem for registry data that is rarely fully de-identified.
ClinTAB-ML exists to remove those hand-offs. It is a single local tool that takes a research team from a raw CSV through column profiling, model training and tuning across 15 algorithms, full diagnostic evaluation (ROC/PR/calibration/Hosmer-Lemeshow), restricted cubic splines, and the standard clinical-epidemiology calculators, all with the conventions clinical prediction-model work expects built in by default rather than left to whoever wrote that day's notebook, and all running on the researcher's own machine so the data never has to leave it (see "Runs entirely on your machine" below). It targets researchers who want to move from cohort to trained, evaluated model without switching tools, sending data off-machine, or re-deriving the same statistical corrections each time, and who would otherwise be choosing between general-purpose, cloud-hosted AutoML tools (which don't know what a Hosmer-Lemeshow test is, and don't keep the data local) and writing the whole pipeline by hand.
cd ClinTAB-ML
./run.sh # creates .venv, installs deps, serves http://127.0.0.1:5000Then open http://127.0.0.1:5000 in a browser. A ready-made synthetic cohort
is at Data/sample_clinical.csv to try the whole flow.
Manual / server options:
pip install -r requirements.txt
python app.py # local dev (PORT, HOST, DEBUG env vars)
./run.sh prod # gunicorn on 0.0.0.0:5000 (server deploy)
# or directly:
gunicorn -w 1 -k gthread --threads 8 --timeout 600 -b 0.0.0.0:5000 "app:create_app()"Use one worker with threads. The training Server-Sent-Events stream and the Flask session must stay on the same process; threads handle concurrency fine for a research tool.
This is a local tool — python app.py / ./run.sh binds 127.0.0.1, so it
is only reachable from your own computer. All processing, model files and plots
stay in local folders; nothing is uploaded anywhere. You do not need to host
it on a server. The gunicorn / 0.0.0.0 instructions above are optional, for the
case where you later want others on your network to reach it. (One online
dependency: the in-browser CSV preview loads PapaParse from a CDN the first
time; the actual data processing is fully offline.)
clintab_cli.py gives the full feature set from the terminal, built on the same
pure modules as the web app:
python clintab_cli.py summarize --csv Data/sample_clinical.csv
python clintab_cli.py models # list the 15 models
python clintab_cli.py train --csv Data/sample_clinical.csv --outcome mortality_30d \
--exclude surv_time death_event rare_lab \
--models LogisticRegression RandomForest --scoring roc --stratify --smote --test
python clintab_cli.py test --models <saved_name> --csv newdata.csv --out roc.png
python clintab_cli.py spline --csv Data/sample_clinical.csv \
--predictor age --outcome mortality_30d --knots 4 --out spline.png
python clintab_cli.py epi or --a 20 --b 80 --c 10 --d 90
python clintab_cli.py epi km --csv Data/sample_clinical.csv --time surv_time --event death_event --group sex
python clintab_cli.py epi cox --csv Data/sample_clinical.csv --time surv_time --event death_event --cov age diabetes
python clintab_cli.py epi hl --csv Data/sample_clinical.csv --model <saved_name>Models trained from the CLI save to the same models/ folder and are visible in
the web app (and vice-versa). Run any subcommand with -h for its options.
You could start with just app.py + index.html to prove the upload works.
As it grows the HTTP layer gets long, so the ML is pulled out (it has nothing to
do with HTTP); models start saving everywhere, so a models/ folder is created;
static files get their own place. The result:
| File | Responsibility | Knows about Flask? |
|---|---|---|
app.py |
Creates the Flask app and starts the server. Nothing else. | yes |
routes.py |
All HTTP endpoints + the SSE stream. Parses requests, calls the modules below, returns JSON / streams / downloads. No stats or sklearn logic. | yes |
ml.py |
All sklearn: the 15 models, preprocessing, validation-set grid search, SMOTE, metrics, feature importance, single prediction. | no |
stats.py |
Column-type detection + descriptive summaries. | no |
spline.py |
Restricted cubic splines (patsy + statsmodels). | no |
epi.py |
Clinical epi: OR, RR, NNT/NNH, Kaplan–Meier, Cox, Hosmer–Lemeshow (lifelines/scipy). | no |
plots.py |
matplotlib → base64 PNG / PDF rendering. (Pulled out of routes so HTTP and drawing stay separate — a small addition to the original file list.) | no |
store.py |
Filesystem layout + session metadata (paths, JSON). (Infra helper, also an addition.) | no |
static/index.html + static/app.js |
The single-page UI. | — |
ml.py, stats.py, spline.py, epi.py, plots.py, store.py are all pure
and importable on their own — easy to test without a server.
runtime/
sessions/<session_id>/
raw.csv raw upload full.csv cleaned full dataset
train.csv val.csv test.csv meta.json column types + split config
exports/ feature-importance CSVs, etc.
models/
<ModelType>_<outcome>_<timestamp>.pkl trained model
<ModelType>_<outcome>_<timestamp>.meta.json metrics, params, features
The active session id is stored in the Flask session cookie ("locked into the Flask session"); everything heavy lives on disk so it survives restarts.
-
Data Upload — drop a CSV (PapaParse preview). Flask auto-types each column: binary = 2 unique values, categorical = strings or <10 unique integers, continuous = numeric >10 unique values, date = parses as a date. Columns >50 % missing are flagged → choose include / fill 0 / remove. Per-row radio buttons override any type. Set the split (default 70/15/15, random or stratified), pick a stratify/SMOTE column; if its minority class <30 % a SMOTE toggle appears. Confirm cleans, splits, and saves the partitions, locking the config into the session.
-
Data Summary — auto-populates after confirm. Continuous vars: N, % missing, mean, median, SD, IQR, min, max. Binary/categorical: N, % missing, count & % per category. Top card: total N, % complete cases, date range. Tables export to CSV.
-
Model Training — pick outcome + exclusion/confounding variables; pick from the 15 models (10 classifiers for binary/categorical outcomes, 5 regressors for continuous — the task is auto-detected from the outcome). Optionally edit the hyperparameter grid per model. Grid search is scored on the validation set only (then refit on train alone, so the reported validation metrics are leakage-free). Choose the tuning metric (ROC default, AUPRC, F1, F2, recall, …). SSE streams live progress. Binary metrics: AUROC, AUPRC, sensitivity, specificity, PPV, NPV, F1, Brier. Continuous: MAE, RMSE, R². Feature importance is shown as a PNG and exported as CSV. Models save as pickles to
models/. -
Model Testing — pick saved pickles (or upload one) and a held-out test set or new CSV. Get a metrics table plus matplotlib ROC, PR, confusion, calibration (binary), one-vs-rest ROC / PR per class (multiclass), or residuals, predicted-vs-actual (regression). Multiple binary models overlay on one ROC / PR chart. Download PNG / CSV. Single prediction: pick a model, fill the auto-built input form (or upload a model
.pkl+ a.jsonrow) → predicted outcome with probability and a plain-language explanation. -
Spline — pick a continuous predictor + binary outcome. Flask fits a restricted cubic spline (patsy
cr()+ statsmodels logistic) and plots log-odds (Y) vs predictor (X) with knots as vertical lines and a 95 % CI. -
Clinical Epi — OR / RR / NNT / NNH from a 2×2 table (with CIs, χ², and a Haldane–Anscombe correction for zero cells); Kaplan–Meier (with log-rank) and Cox proportional hazards via lifelines; Hosmer–Lemeshow goodness-of-fit with a calibration-by-decile chart from a saved binary model.
Classifiers (binary/categorical): Logistic, Ridge-Logistic, Lasso-Logistic, ElasticNet-Logistic, KNN, Decision Tree, Random Forest, Gradient Boosting, SVM, Naïve Bayes. Regressors (continuous): Linear, Ridge, Lasso, ElasticNet, Random-Forest Regressor.
| Method | Endpoint | Purpose |
|---|---|---|
| POST | /upload |
upload CSV → detected column types + preview |
| POST | /confirm |
apply types/missing handling, split, save, lock session |
| GET | /summary |
descriptive-statistics payload |
| GET | /session |
current session metadata |
| GET | /train/columns |
outcome candidates, columns, default grids |
| POST | /train |
store training config |
| GET | /train/stream |
SSE training progress + per-model results |
| GET | /models · POST /model/upload · GET /model/features |
model registry |
| POST | /test |
evaluate model(s) → metrics + plot PNGs (overlay ROC/PR) |
| POST | /predict-single |
single-row prediction (form or model+json upload) |
| POST | /spline |
fit restricted cubic spline → coords + PNG |
| POST | /epi/or · /epi/nnt · /epi/km · /epi/cox · /epi/hl |
clinical epi |
| POST | /render/pdf |
render any chart spec as a downloadable PDF |
- Project structure exactly as specified (+
plots.py,store.pyhelpers). - Upload → auto column typing (binary/categorical/continuous/date) → >50 %-missing flagging with include/zero/remove → overrides.
- Confirm: random and stratified 70/15/15 split, SMOTE hint, saved partitions, session lock.
- Data summary card + continuous & categorical tables, CSV export.
- Training: all 15 models, validation-set grid search (leakage-free refit), selectable tuning metric incl. F2, optional SMOTE on train only, SSE progress, binary + regression metric tables, feature-importance PNG + CSV, pickles saved with metadata sidecars.
- Testing: multi-model evaluation, ROC/PR overlay, confusion/calibration/ residual/predicted-vs-actual PNGs, single prediction (form + upload).
- Spline: restricted cubic spline with knots + CI.
- Clinical epi: OR/RR/NNT/NNH, Kaplan–Meier + log-rank, Cox PH, Hosmer– Lemeshow + calibration deciles.
- PNG / PDF / CSV export. Terminal launch via
run.sh; gunicorn for server.
- Confounding variables are currently kept as ordinary features and tagged; they are not yet given special statistical treatment (e.g. forced-in adjusted models). The UI captures them and they ride along in training.
- SMOTE timing: the toggle at Confirm records a preference, but SMOTE is applied at training time to the training fold only (it needs the chosen outcome, which is picked later). Both the Confirm and Training screens expose the toggle. This is intentional and noted so it is not mistaken for a bug.
- Preprocessing one-hot-encodes
objectcolumns and median-imputes/scales numerics inside each model pipeline. Integer-coded categoricals are left numeric; if you want them one-hot encoded, cast them to string first or extendml.build_preprocessorto readmeta['coltypes']. - Grid search defaults to a single train/val split (
PredefinedSplit), per the "validation set only" requirement. On small datasets that can make hyperparameter selection noisy; passcv_folds(--cv-foldson the CLI) to use k-fold CV on the training set instead. Either way, the validation set is never touched by the search itself, only by the metrics reported afterward. - Spline CI is the GLM linear-predictor (log-odds) confidence band.
- Authentication / multi-user isolation is not implemented — this is a
local / trusted-server research tool. Add a reverse proxy + auth before
exposing it publicly, and set
CLINTAB_SECRETin the environment.
The original src/ pipeline (class_tqip.py / select_features.py) only
covered the old classification path and has been replaced by
clintab_cli.py, which exposes the full feature set on top of the same shared
modules as the web app. Recover the old files from git history if ever needed.
Bug reports and feature requests are welcome via GitHub
Issues —
use the bug report or feature request template, whichever fits. See
CONTRIBUTING.md for dev setup, how to run the test suite, and branch/PR
conventions before opening a pull request. This project follows the
CODE_OF_CONDUCT.md in this repo; please report any violations as described
there.
For questions about using the app rather than a bug or a code change, open a GitHub Discussion or Issue rather than emailing maintainers directly, so the answer is visible to future users with the same question.
Python 3.9+. See requirements.txt (Flask, pandas, numpy, scikit-learn,
imbalanced-learn, matplotlib, scipy, statsmodels, patsy, lifelines, joblib,
gunicorn).