{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# ML for Healthcare & Predictive Analytics\n", "\n", "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Harshini-Suresha/ml-healthcare-app/blob/main/ML_Healthcare_Demo.ipynb)\n", "\n", "One notebook that walks through four supervised-learning projects end to end: **diabetes** (KNN), **breast cancer** (KNN vs Logistic Regression), **heart disease** (neural network) and **autism spectrum disorder** (three classifiers). Run the cells top to bottom. The diabetes and breast cancer sections run on bundled data. The heart section also runs on bundled data. The autism section asks for its CSV file.\n", "\n", "\n", "| Project | Method | Data |\n", "|---|---|---|\n", "| Diabetes | KNN, preprocessing comparison, tuning k | 768 records, 8 features (`data/diabetes.csv`) |\n", "| Breast cancer | KNN with CV-chosen K vs grid-searched Logistic Regression | Wisconsin diagnostic data from scikit-learn |\n", "| Heart disease | 13-8-14-1 neural network | 303 records, 13 attributes + `target` (`heart.csv`) |\n", "| Autism | Cleaning, oversampling, LR / boosted trees / SVM | Kaggle \"Autism Prediction\" `train.csv` |" ] }, { "cell_type": "code", "metadata": {}, "source": [ "import os, sys, warnings\n", "import numpy as np, pandas as pd\n", "import matplotlib.pyplot as plt\n", "from sklearn.model_selection import train_test_split, cross_val_score, GridSearchCV, RepeatedStratifiedKFold, StratifiedKFold\n", "from sklearn.preprocessing import StandardScaler, MinMaxScaler, LabelEncoder\n", "from sklearn.neighbors import KNeighborsClassifier\n", "from sklearn.linear_model import LogisticRegression\n", "from sklearn.neural_network import MLPClassifier\n", "from sklearn.svm import SVC\n", "from sklearn.impute import SimpleImputer\n", "from sklearn.pipeline import Pipeline\n", "from sklearn.utils import resample\n", "from sklearn.metrics import (accuracy_score, precision_score, recall_score, f1_score, confusion_matrix,\n", " classification_report, roc_auc_score, roc_curve, ConfusionMatrixDisplay)\n", "\n", "warnings.filterwarnings(\"ignore\")\n", "plt.rcParams.update({\"figure.dpi\": 110, \"axes.spines.top\": False, \"axes.spines.right\": False, \"axes.grid\": True, \"grid.alpha\": .25})\n", "TEAL, RED, GREY = \"#1d6b55\", \"#c8283b\", \"#8fa89a\"\n", "REPO_RAW = \"https://raw.githubusercontent.com/Harshini-Suresha/ml-healthcare-app/main/data/\"\n", "\n", "def get_file(name):\n", " \"\"\"Find a data file locally, download it from the repo, or (in Colab) ask for an upload.\"\"\"\n", " for p in (f\"data/{name}\", name):\n", " if os.path.exists(p): return p\n", " try:\n", " import urllib.request\n", " urllib.request.urlretrieve(REPO_RAW + name, name); return name\n", " except Exception:\n", " pass\n", " if \"google.colab\" in sys.modules:\n", " from google.colab import files\n", " print(f\"Please upload {name}\"); up = files.upload()\n", " if up: os.rename(next(iter(up)), name); return name\n", " return None\n", "\n", "def scores(y, pred, prob=None):\n", " out = {\"Accuracy\": accuracy_score(y, pred), \"Precision\": precision_score(y, pred, zero_division=0),\n", " \"Recall\": recall_score(y, pred, zero_division=0), \"F1\": f1_score(y, pred, zero_division=0)}\n", " if prob is not None: out[\"ROC-AUC\"] = roc_auc_score(y, prob)\n", " return pd.Series(out).round(3)" ], "outputs": [], "execution_count": null }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 1. Diabetes: K-Nearest Neighbors\n", "\n", "768 patient records, 8 diagnostic features and a binary `Outcome`. In five columns a **zero means the value is missing**, so how we treat those zeros is one of the things we compare." ] }, { "cell_type": "code", "metadata": {}, "source": [ "df = pd.read_csv(get_file(\"diabetes.csv\"))\n", "FEATURES = list(df.columns[:-1]); ZERO_COLS = [\"Glucose\", \"BloodPressure\", \"SkinThickness\", \"Insulin\", \"BMI\"]\n", "print(df.shape, \"| diabetic:\", int(df.Outcome.sum()), \"| non-diabetic:\", int((df.Outcome == 0).sum()))\n", "print(\"\\nZeros that mean missing:\"); print((df[ZERO_COLS] == 0).sum().sort_values(ascending=False))\n", "df.describe().T.round(2)" ], "outputs": [], "execution_count": null }, { "cell_type": "code", "metadata": {}, "source": [ "fig, axes = plt.subplots(2, 4, figsize=(14, 6))\n", "for ax, f in zip(axes.ravel(), FEATURES):\n", " d = df[df[f] != 0] if f in ZERO_COLS else df\n", " edges = np.histogram_bin_edges(d[f], bins=20)\n", " for o, c, l in [(0, TEAL, \"Non-diabetic\"), (1, RED, \"Diabetic\")]:\n", " ax.hist(d.loc[d.Outcome == o, f], bins=edges, alpha=.65, color=c, label=l)\n", " ax.set_title(f, fontsize=10)\n", "axes[0, 0].legend(frameon=False); plt.tight_layout(); plt.show()\n", "\n", "c = df.corr()\n", "plt.figure(figsize=(6.5, 5.5)); plt.imshow(c, cmap=\"RdYlGn\", vmin=-1, vmax=1); plt.colorbar(shrink=.8)\n", "plt.xticks(range(len(c)), c.columns, rotation=60, ha=\"right\"); plt.yticks(range(len(c)), c.columns)\n", "for i in range(len(c)):\n", " for j in range(len(c)): plt.text(j, i, f\"{c.iloc[i, j]:.2f}\", ha=\"center\", va=\"center\", fontsize=7)\n", "plt.title(\"Correlation matrix\"); plt.grid(False); plt.tight_layout(); plt.show()\n", "print(\"Correlation with Outcome:\"); print(c[\"Outcome\"].drop(\"Outcome\").sort_values(ascending=False).round(2))" ], "outputs": [], "execution_count": null }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Preprocessing study\n", "\n", "Same model (KNN, k = 5), nine preprocessing combinations: three ways to treat the zeros times three scalers. Imputation and scaling are fitted on the training split only." ] }, { "cell_type": "code", "metadata": {}, "source": [ "def prepare(df, zero_mode, scaler, test_size=0.3, seed=42):\n", " d = df[(df[ZERO_COLS] != 0).all(axis=1)] if zero_mode == \"Drop rows\" else df.copy()\n", " X, y = d[FEATURES].astype(float), d.Outcome\n", " Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=test_size, random_state=seed, stratify=y)\n", " Xtr, Xte = Xtr.copy(), Xte.copy()\n", " if zero_mode == \"Median impute\":\n", " med = Xtr[ZERO_COLS].replace(0, np.nan).median()\n", " for part in (Xtr, Xte): part[ZERO_COLS] = part[ZERO_COLS].replace(0, np.nan).fillna(med)\n", " sc = {\"None\": None, \"StandardScaler\": StandardScaler(), \"MinMaxScaler\": MinMaxScaler()}[scaler]\n", " if sc is not None: Xtr, Xte = sc.fit_transform(Xtr), sc.transform(Xte)\n", " return np.asarray(Xtr), np.asarray(Xte), ytr.values, yte.values\n", "\n", "rows = []\n", "for z in [\"Keep zeros\", \"Median impute\", \"Drop rows\"]:\n", " for s in [\"None\", \"StandardScaler\", \"MinMaxScaler\"]:\n", " Xtr, Xte, ytr, yte = prepare(df, z, s)\n", " m = KNeighborsClassifier(5).fit(Xtr, ytr)\n", " rows.append((z, s, m.score(Xtr, ytr), m.score(Xte, yte)))\n", "study = pd.DataFrame(rows, columns=[\"Zeros\", \"Scaling\", \"Train\", \"Test\"]).assign(Gap=lambda t: t.Train - t.Test)\n", "study.sort_values(\"Test\", ascending=False).round(3)" ], "outputs": [], "execution_count": null }, { "cell_type": "code", "metadata": {}, "source": [ "Xtr, Xte, ytr, yte = prepare(df, \"Median impute\", \"StandardScaler\")\n", "ks = range(1, 26); tr, te = [], []\n", "for k in ks:\n", " m = KNeighborsClassifier(k).fit(Xtr, ytr); tr.append(m.score(Xtr, ytr)); te.append(m.score(Xte, yte))\n", "plt.figure(figsize=(7, 3.6)); plt.plot(ks, tr, color=GREY, lw=2, label=\"Train\"); plt.plot(ks, te, color=TEAL, lw=2, label=\"Test\")\n", "plt.xlabel(\"k\"); plt.ylabel(\"Accuracy\"); plt.legend(frameon=False); plt.title(\"Accuracy by k (median-imputed, standardized)\"); plt.show()\n", "\n", "best_k = int(ks[int(np.argmax(te))]); m = KNeighborsClassifier(best_k).fit(Xtr, ytr); pred = m.predict(Xte)\n", "print(f\"best k on the test split: {best_k}, test accuracy {m.score(Xte, yte):.3f}\")\n", "ConfusionMatrixDisplay.from_predictions(yte, pred, cmap=\"Greens\", display_labels=[\"No\", \"Yes\"]); plt.grid(False); plt.show()\n", "print(classification_report(yte, pred, target_names=[\"Non-diabetic\", \"Diabetic\"]))" ], "outputs": [], "execution_count": null }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 2. Breast cancer: KNN vs tuned Logistic Regression\n", "\n", "Uses the Wisconsin diagnostic dataset that ships with scikit-learn (569 records, 30 features). **K** for KNN is chosen by cross-validation. Logistic Regression is tuned with `GridSearchCV` over a `RepeatedStratifiedKFold`. Missing values (none in this copy) would be mean-imputed; features are standardized." ] }, { "cell_type": "code", "metadata": {}, "source": [ "from sklearn.datasets import load_breast_cancer\n", "bc = load_breast_cancer(as_frame=True).frame.copy(); bc[\"malignant\"] = 1 - bc.pop(\"target\")\n", "X, y = bc.drop(columns=\"malignant\"), bc.malignant\n", "print(bc.shape, \"| malignant:\", int(y.sum()), \"| benign:\", int((y == 0).sum()))\n", "Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=.25, random_state=42, stratify=y)\n", "pipe = lambda model: Pipeline([(\"imp\", SimpleImputer(strategy=\"mean\")), (\"sc\", StandardScaler()), (\"m\", model)])\n", "\n", "cv = StratifiedKFold(5, shuffle=True, random_state=42); ks = list(range(1, 26))\n", "cvacc = [cross_val_score(pipe(KNeighborsClassifier(k)), Xtr, ytr, cv=cv).mean() for k in ks]\n", "best_k = ks[int(np.argmax(cvacc))]\n", "plt.figure(figsize=(7, 3.4)); plt.plot(ks, cvacc, color=TEAL, lw=2, marker=\"o\", ms=3); plt.axvline(best_k, color=RED, ls=\"--\")\n", "plt.xlabel(\"K\"); plt.ylabel(\"CV accuracy\"); plt.title(f\"Cross-validated accuracy by K (best K = {best_k})\"); plt.show()\n", "\n", "grid = GridSearchCV(pipe(LogisticRegression(max_iter=3000)), {\"m__C\": [.01, .1, 1, 10, 100], \"m__solver\": [\"lbfgs\", \"liblinear\"]},\n", " cv=RepeatedStratifiedKFold(n_splits=5, n_repeats=2, random_state=42), scoring=\"accuracy\").fit(Xtr, ytr)\n", "print(\"Logistic Regression best params:\", grid.best_params_, \"| CV accuracy:\", round(grid.best_score_, 3))" ], "outputs": [], "execution_count": null }, { "cell_type": "code", "metadata": {}, "source": [ "models = {f\"KNN (K={best_k})\": pipe(KNeighborsClassifier(best_k)).fit(Xtr, ytr), \"Logistic Regression\": grid.best_estimator_}\n", "table, plt_data = {}, []\n", "for name, mdl in models.items():\n", " pred, prob = mdl.predict(Xte), mdl.predict_proba(Xte)[:, 1]\n", " table[name] = scores(yte, pred, prob); plt_data.append((name, *roc_curve(yte, prob)[:2], table[name][\"ROC-AUC\"]))\n", " print(name); print(confusion_matrix(yte, pred))\n", "display(pd.DataFrame(table)) if \"display\" in globals() else print(pd.DataFrame(table))\n", "plt.figure(figsize=(4.8, 4))\n", "for (n, f, t, a), c in zip(plt_data, [TEAL, RED]): plt.plot(f, t, color=c, lw=2, label=f\"{n} (AUC {a:.3f})\")\n", "plt.plot([0, 1], [0, 1], \"--\", color=\"#b3cdb1\"); plt.legend(frameon=False, loc=\"lower right\"); plt.xlabel(\"False positive rate\"); plt.ylabel(\"True positive rate\"); plt.show()" ], "outputs": [], "execution_count": null }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 3. Heart disease: neural network\n", "\n", "A 13-8-14-1 feed-forward network (ReLU, ReLU, sigmoid output; Adam; batch size 8; 100 epochs), standardized inputs, 70/30 split. The original notebook uses Keras; this version uses scikit-learn's `MLPClassifier` with the same layout and Keras-style small uniform starting weights so it runs anywhere.\n", "\n", "**Data:** a CSV with the 13 attributes `age, sex, cp, trestbps, chol, fbs, restecg, thalach, exang, oldpeak, slope, ca, thal` and a `target` column. It is bundled as `data/heart.csv`." ] }, { "cell_type": "code", "metadata": {}, "source": [ "heart_path = get_file(\"heart.csv\")\n", "if heart_path is None:\n", " print(\"heart.csv not found: add it to the data/ folder to run this section.\")\n", "else:\n", " hd = pd.read_csv(heart_path); HF = [\"age\",\"sex\",\"cp\",\"trestbps\",\"chol\",\"fbs\",\"restecg\",\"thalach\",\"exang\",\"oldpeak\",\"slope\",\"ca\",\"thal\"]\n", " X, y = hd[HF].values.astype(float), hd[\"target\"].values.astype(int)\n", " Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=.3, random_state=0)\n", " sc = StandardScaler(); Xtr, Xte = sc.fit_transform(Xtr), sc.transform(Xte)\n", " net = MLPClassifier(hidden_layer_sizes=(8, 14), activation=\"relu\", solver=\"adam\", batch_size=8, random_state=0); hist = []\n", " net.partial_fit(Xtr[:8], ytr[:8], classes=[0, 1]); rng = np.random.RandomState(0) # Keras-style 'uniform' init\n", " net.coefs_ = [rng.uniform(-.05, .05, w.shape) for w in net.coefs_]; net.intercepts_ = [np.zeros_like(v) for v in net.intercepts_]\n", " for e in range(100):\n", " net.partial_fit(Xtr, ytr); hist.append((net.loss_, net.score(Xtr, ytr)))\n", " hist = np.array(hist); fig, ax = plt.subplots(1, 2, figsize=(11, 3.4))\n", " ax[0].plot(hist[:, 0], color=RED, lw=2); ax[0].set_title(\"Training loss\"); ax[1].plot(hist[:, 1], color=TEAL, lw=2); ax[1].set_title(\"Training accuracy\")\n", " for a in ax: a.set_xlabel(\"Epoch\")\n", " plt.tight_layout(); plt.show()\n", " pred = net.predict(Xte); print(scores(yte, pred, net.predict_proba(Xte)[:, 1]))\n", " ConfusionMatrixDisplay.from_predictions(yte, pred, cmap=\"Greens\"); plt.grid(False); plt.show()" ], "outputs": [], "execution_count": null }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 4. Autism spectrum disorder: screening-based classification\n", "\n", "Cleaning: `yes/no` to 1/0, unknown `?` and `others` merged into `Others`, two extreme negative `result` values removed. Features added: `sum_score` (total of the ten screening answers), `ageGroup`, `ind`; `age` is log-transformed. The training set is **randomly oversampled** to balance the classes, then standardized. Three models are compared with ROC-AUC computed from predicted labels.\n", "\n", "**Data:** the Kaggle \"Autism Prediction\" `train.csv`. Save it as `data/autism_train.csv` (or upload it when asked)." ] }, { "cell_type": "code", "metadata": {}, "source": [ "asd_path = get_file(\"autism_train.csv\")\n", "if asd_path is None:\n", " print(\"autism_train.csv not found: add it to the data/ folder to run this section.\")\n", "else:\n", " raw = pd.read_csv(asd_path)\n", " if \"Class/ASD\" not in raw.columns and \"ASD\" in raw.columns: raw = raw.rename(columns={\"ASD\": \"Class/ASD\"})\n", " A = [f\"A{i}_Score\" for i in range(1, 11)]\n", " d = raw.replace({\"yes\": 1, \"no\": 0, \"?\": \"Others\", \"others\": \"Others\"}); d = d[d[\"result\"] > -5].copy()\n", " d[\"ageGroup\"] = d[\"age\"].apply(lambda a: \"Toddler\" if a < 4 else \"Kid\" if a < 12 else \"Teenager\" if a < 18 else \"Young\" if a < 40 else \"Senior\")\n", " for c in [\"jaundice\", \"austim\", \"used_app_before\"]: d[c] = d[c].astype(int)\n", " d[\"sum_score\"] = d[A].sum(axis=1); d[\"ind\"] = d[\"austim\"] + d[\"used_app_before\"] + d[\"jaundice\"]; d[\"age\"] = np.log(d[\"age\"].astype(float))\n", " for c in d.columns:\n", " if not pd.api.types.is_numeric_dtype(d[c]): d[c] = LabelEncoder().fit_transform(d[c].astype(str))\n", " X = d.drop(columns=[\"ID\", \"age_desc\", \"used_app_before\", \"austim\", \"Class/ASD\"]); y = d[\"Class/ASD\"].astype(int)\n", " print(f\"{len(raw)} raw records -> {len(d)} after cleaning | ASD share {y.mean():.1%}\")\n", " pd.crosstab(d[\"sum_score\"], d[\"Class/ASD\"]).plot(kind=\"bar\", color=[TEAL, RED], figsize=(7, 3.2), title=\"Cases by screening sum score\"); plt.show()" ], "outputs": [], "execution_count": null }, { "cell_type": "code", "metadata": {}, "source": [ "if asd_path is not None:\n", " Xtr, Xv, ytr, yv = train_test_split(X, y, test_size=.2, random_state=10)\n", " minority = int(np.argmin(np.bincount(ytr))); imin, imaj = np.where(ytr.values == minority)[0], np.where(ytr.values != minority)[0]\n", " extra = resample(imin, replace=True, n_samples=len(imaj) - len(imin), random_state=0); idx = np.concatenate([np.arange(len(ytr)), extra])\n", " Xo, yo = Xtr.values[idx], ytr.values[idx]; print(\"training rows:\", len(ytr), \"-> oversampled:\", len(yo))\n", " sc = StandardScaler(); Xo, Xv_s = sc.fit_transform(Xo), sc.transform(Xv)\n", " try:\n", " from xgboost import XGBClassifier; boosted = (\"XGBoost\", XGBClassifier(eval_metric=\"logloss\", random_state=0))\n", " except Exception:\n", " from sklearn.ensemble import HistGradientBoostingClassifier; boosted = (\"Gradient Boosting\", HistGradientBoostingClassifier(random_state=0))\n", " mods = {\"Logistic Regression\": LogisticRegression(max_iter=2000), boosted[0]: boosted[1], \"SVM (RBF)\": SVC(probability=True, random_state=0)}\n", " res = []\n", " for n, m in mods.items():\n", " m.fit(Xo, yo); res.append((n, roc_auc_score(yo, m.predict(Xo)), roc_auc_score(yv, m.predict(Xv_s))))\n", " res = pd.DataFrame(res, columns=[\"Model\", \"Train ROC-AUC\", \"Validation ROC-AUC\"]).set_index(\"Model\").round(3); print(res)\n", " res.plot(kind=\"bar\", color=[GREY, TEAL], figsize=(7, 3.4), rot=0, ylim=(0, 1.08), title=\"Overfitting check: train vs validation\"); plt.show()\n", " first = list(mods.values())[0]; ConfusionMatrixDisplay.from_estimator(first, Xv_s, yv, cmap=\"Greens\"); plt.grid(False); plt.show()" ], "outputs": [], "execution_count": null }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Notes\n", "\n", "- Every number above is computed from the data when you run the notebook; nothing is hard-coded.\n", "- The same logic powers the interactive Streamlit app in this repository (`app.py` and `ml_core.py`).\n", "- Metrics are on a single train/test split unless a cross-validation is stated, so small differences between settings can be noise.\n", "- Contact: [harshinisuresha7@gmail.com](mailto:harshinisuresha7@gmail.com)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python" }, "colab": { "provenance": [] } }, "nbformat": 4, "nbformat_minor": 4 }