{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Who Is at Risk of Heart Disease?\n",
    "\n",
    "heart_risk.py\n",
    "\n",
    "Capstone project: screening patients for heart disease from routine\n",
    "check-up measurements, so that those at risk are referred to a\n",
    "cardiologist early.\n",
    "\n",
    "CBSE Class XII, Artificial Intelligence (843), Part C - Capstone Project.\n",
    "SDG 3 (Good Health and Well-being).\n",
    "\n",
    "Data Science Methodology, 10 steps. A CLASSIFICATION problem: two models\n",
    "- Logistic Regression and a Decision Tree - compared with a train-test\n",
    "split and stratified 5-fold cross validation, using the confusion matrix,\n",
    "accuracy, precision, recall and F1. The decision threshold is then tuned,\n",
    "because in screening a missed patient costs far more than an extra test.\n",
    "\n",
    "Libraries: pandas, numpy, matplotlib, scikit-learn, joblib."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 1 - Business Understanding"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"=\" * 64)\n",
    "print(\"WHO IS AT RISK OF HEART DISEASE?\")\n",
    "print(\"=\" * 64)\n",
    "print(\"Problem : heart disease is often found late; cardiologists are few\")\n",
    "print(\"Goal    : flag the patients at a check-up who should see a specialist\")\n",
    "print()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 2 - Analytic Approach"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"Analytic approach : predictive - classification (disease: yes or no)\")\n",
    "print()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 3 - Data Requirements"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "from sklearn.model_selection import train_test_split, StratifiedKFold, cross_validate\n",
    "from sklearn.pipeline import make_pipeline\n",
    "from sklearn.preprocessing import StandardScaler\n",
    "from sklearn.linear_model import LogisticRegression\n",
    "from sklearn.tree import DecisionTreeClassifier\n",
    "from sklearn.metrics import confusion_matrix, accuracy_score, precision_score, recall_score, f1_score\n",
    "import joblib\n",
    "\n",
    "pd.set_option(\"display.width\", 140)\n",
    "pd.set_option(\"display.max_columns\", 20)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 4 - Data Collection"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data = pd.read_csv(\"patients.csv\")\n",
    "print(\"Rows collected :\", len(data))\n",
    "print(data.head(6))\n",
    "print()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 5 - Data Understanding"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"--- Step 5: Data Understanding ---\")\n",
    "print(\"Missing values:\")\n",
    "print(data.isnull().sum().to_string())\n",
    "print(\"Cholesterol recorded as 0 :\", (data[\"cholesterol\"] == 0).sum())\n",
    "print()\n",
    "print(\"Patients with and without heart disease:\")\n",
    "print(data[\"heart_disease\"].value_counts().to_string())\n",
    "print(\"Share with heart disease  :\", round(data[\"heart_disease\"].mean() * 100, 1), \"%\")\n",
    "print()\n",
    "\n",
    "sick = data[data[\"heart_disease\"] == 1]\n",
    "well = data[data[\"heart_disease\"] == 0]\n",
    "\n",
    "# Chart 1: age, with and without disease.\n",
    "plt.figure(figsize=(7, 4.3))\n",
    "plt.hist(well[\"age\"], bins=20, alpha=0.6, color=\"#4e79a7\", label=\"No heart disease\")\n",
    "plt.hist(sick[\"age\"], bins=20, alpha=0.6, color=\"#e15759\", label=\"Heart disease\")\n",
    "plt.title(\"Age of the patients\")\n",
    "plt.xlabel(\"Age (years)\")\n",
    "plt.ylabel(\"Patients\")\n",
    "plt.legend()\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"chart1_age.png\")\n",
    "plt.close()\n",
    "\n",
    "# Chart 2: maximum heart rate, as box plots.\n",
    "plt.figure(figsize=(6, 4.3))\n",
    "plt.boxplot([well[\"max_heart_rate\"], sick[\"max_heart_rate\"]], labels=[\"No heart disease\", \"Heart disease\"])\n",
    "plt.title(\"Maximum heart rate reached in an exercise test\")\n",
    "plt.ylabel(\"Beats per minute\")\n",
    "plt.grid(True, axis=\"y\", linestyle=\"--\", alpha=0.4)\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"chart2_max_heart_rate.png\")\n",
    "plt.close()\n",
    "\n",
    "# Chart 3: share with disease for each kind of chest pain, by sex.\n",
    "share = data.groupby([\"chest_pain\", \"sex\"])[\"heart_disease\"].mean().unstack() * 100\n",
    "share = share.reindex([\"typical\", \"atypical\", \"non-anginal\", \"none\"])\n",
    "print(\"Share with heart disease, by chest pain and sex (%):\")\n",
    "print(share.round(1))\n",
    "print()\n",
    "x = np.arange(4)\n",
    "plt.figure(figsize=(8, 4.3))\n",
    "plt.bar(x - 0.2, share[\"M\"], width=0.4, label=\"Men\", color=\"#4e79a7\")\n",
    "plt.bar(x + 0.2, share[\"F\"], width=0.4, label=\"Women\", color=\"#b07aa1\")\n",
    "plt.xticks(x, share.index)\n",
    "plt.ylabel(\"% with heart disease\")\n",
    "plt.title(\"Heart disease by type of chest pain\")\n",
    "plt.legend()\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"chart3_chest_pain.png\")\n",
    "plt.close()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 6 - Data Preparation"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"--- Step 6: Data Preparation ---\")\n",
    "before = len(data)\n",
    "data = data.drop_duplicates()\n",
    "print(\"Duplicate rows removed               :\", before - len(data))\n",
    "\n",
    "# Cholesterol of 0 means \"not measured\", as in the UCI data it is modelled\n",
    "# on. It is turned into a real missing value and then filled with the\n",
    "# median - and a new column records that it was missing, in case being\n",
    "# untested is itself informative.\n",
    "data[\"cholesterol_missing\"] = (data[\"cholesterol\"] == 0).astype(int)\n",
    "data[\"cholesterol\"] = data[\"cholesterol\"].replace(0, np.nan)\n",
    "data[\"cholesterol\"] = data[\"cholesterol\"].fillna(data[\"cholesterol\"].median())\n",
    "data[\"resting_bp\"] = pd.to_numeric(data[\"resting_bp\"])\n",
    "blank_bp = data[\"resting_bp\"].isnull().sum()\n",
    "data[\"resting_bp\"] = data[\"resting_bp\"].fillna(data[\"resting_bp\"].median())\n",
    "print(\"Cholesterol 0s replaced by the median:\", data[\"cholesterol_missing\"].sum())\n",
    "print(\"Blank blood pressures filled         :\", blank_bp)\n",
    "\n",
    "data[\"male\"] = (data[\"sex\"] == \"M\").astype(int)\n",
    "data[\"exercise_angina\"] = (data[\"exercise_angina\"] == \"Yes\").astype(int)\n",
    "data = pd.get_dummies(data, columns=[\"chest_pain\"], dtype=int)\n",
    "features = [\"age\", \"male\", \"resting_bp\", \"cholesterol\", \"cholesterol_missing\", \"fasting_sugar_high\",\n",
    "            \"max_heart_rate\", \"exercise_angina\", \"st_depression\", \"chest_pain_atypical\",\n",
    "            \"chest_pain_non-anginal\", \"chest_pain_none\", \"chest_pain_typical\"]\n",
    "print(\"Features used                        :\", len(features))\n",
    "print()\n",
    "\n",
    "# Chart 4: correlations between the number features, as a heat map.\n",
    "cols = [\"age\", \"resting_bp\", \"cholesterol\", \"max_heart_rate\", \"st_depression\", \"exercise_angina\", \"heart_disease\"]\n",
    "corr = data[cols].corr()\n",
    "plt.figure(figsize=(7.5, 6))\n",
    "plt.imshow(corr, cmap=\"RdBu_r\", vmin=-1, vmax=1)\n",
    "plt.colorbar(label=\"Correlation\")\n",
    "plt.xticks(range(len(cols)), cols, rotation=45, ha=\"right\")\n",
    "plt.yticks(range(len(cols)), cols)\n",
    "for i in range(len(cols)):\n",
    "    for j in range(len(cols)):\n",
    "        plt.text(j, i, round(corr.iloc[i, j], 2), ha=\"center\", va=\"center\", fontsize=8)\n",
    "plt.title(\"Correlation between the measurements\")\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"chart4_heatmap.png\")\n",
    "plt.close()\n",
    "print(\"Correlation with heart disease:\")\n",
    "print(corr[\"heart_disease\"].drop(\"heart_disease\").round(2).sort_values().to_string())\n",
    "print()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 7 - Modelling"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"--- Step 7: Modelling ---\")\n",
    "X = data[features]\n",
    "y = data[\"heart_disease\"]\n",
    "# stratify=y keeps the same share of disease in the training and test sets.\n",
    "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, stratify=y, random_state=11)\n",
    "print(\"Training rows :\", len(X_train), \" Testing rows :\", len(X_test))\n",
    "\n",
    "# Logistic Regression needs features on similar scales, so a scaler is\n",
    "# placed in front of it in a pipeline: the scaler learns only from the\n",
    "# training data, and the test data is scaled with what it learned.\n",
    "logistic = make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000))\n",
    "tree = DecisionTreeClassifier(max_depth=4, min_samples_leaf=10, random_state=0)\n",
    "logistic.fit(X_train, y_train)\n",
    "tree.fit(X_train, y_train)\n",
    "\n",
    "coef = pd.Series(logistic[-1].coef_[0], index=features).sort_values()\n",
    "print(\"Logistic Regression coefficients (scaled features):\")\n",
    "print(coef.round(2).to_string())\n",
    "print()\n",
    "\n",
    "# Chart 5: what Logistic Regression learned.\n",
    "plt.figure(figsize=(7.5, 5))\n",
    "plt.barh(coef.index, coef.values, color=[\"#59a14f\" if v < 0 else \"#e15759\" for v in coef.values])\n",
    "plt.axvline(0, color=\"#999999\")\n",
    "plt.title(\"Logistic Regression: red raises the risk, green lowers it\")\n",
    "plt.xlabel(\"Coefficient (on scaled features)\")\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"chart5_coefficients.png\")\n",
    "plt.close()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 8 - Evaluation"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"--- Step 8: Evaluation ---\")\n",
    "for name, model in [[\"Logistic Regression\", logistic], [\"Decision Tree\", tree]]:\n",
    "    pred = model.predict(X_test)\n",
    "    tn, fp, fn, tp = confusion_matrix(y_test, pred).ravel()\n",
    "    print(name)\n",
    "    print(\"  \" + name + \" TP, FN, FP, TN :\", tp, fn, fp, tn)\n",
    "    print(\"  \" + name + \" accuracy  :\", round(accuracy_score(y_test, pred), 3))\n",
    "    print(\"  \" + name + \" precision :\", round(precision_score(y_test, pred), 3))\n",
    "    print(\"  \" + name + \" recall    :\", round(recall_score(y_test, pred), 3))\n",
    "    print(\"  \" + name + \" F1        :\", round(f1_score(y_test, pred), 3))\n",
    "print()\n",
    "\n",
    "# Stratified 5-fold cross validation: every fold keeps the same share of\n",
    "# patients with disease as the whole dataset.\n",
    "folds = StratifiedKFold(n_splits=5, shuffle=True, random_state=1)\n",
    "cv = {}\n",
    "for name, model in [[\"Logistic Regression\", make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000))],\n",
    "                    [\"Decision Tree\", DecisionTreeClassifier(max_depth=4, min_samples_leaf=10, random_state=0)]]:\n",
    "    scores = cross_validate(model, X, y, cv=folds, scoring=[\"accuracy\", \"recall\"])\n",
    "    cv[name] = scores\n",
    "    print(name, \"- recall in each fold   :\", np.round(scores[\"test_recall\"], 3))\n",
    "    print(name, \"- mean CV accuracy      :\", round(scores[\"test_accuracy\"].mean(), 3))\n",
    "    print(name, \"- mean CV recall        :\", round(scores[\"test_recall\"].mean(), 3))\n",
    "print()\n",
    "\n",
    "# Chart 6: both confusion matrices.\n",
    "plt.figure(figsize=(10, 4.3))\n",
    "for k, (name, model) in enumerate([[\"Logistic Regression\", logistic], [\"Decision Tree\", tree]]):\n",
    "    m = confusion_matrix(y_test, model.predict(X_test), labels=[1, 0])\n",
    "    plt.subplot(1, 2, k + 1)\n",
    "    plt.imshow(m, cmap=\"Reds\")\n",
    "    labels = [[\"TP\", \"FN\"], [\"FP\", \"TN\"]]\n",
    "    for r in range(2):\n",
    "        for c in range(2):\n",
    "            plt.text(c, r, labels[r][c] + \"\\n\" + str(m[r][c]), ha=\"center\", va=\"center\", fontsize=12,\n",
    "                     color=\"white\" if m[r][c] > m.max() / 2 else \"black\")\n",
    "    plt.xticks([0, 1], [\"Predicted disease\", \"Predicted none\"])\n",
    "    plt.yticks([0, 1], [\"Disease\", \"No disease\"])\n",
    "    plt.title(name)\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"chart6_confusion.png\")\n",
    "plt.close()\n",
    "\n",
    "# Chart 7: recall in each fold.\n",
    "x = np.arange(5)\n",
    "plt.figure(figsize=(7.5, 4))\n",
    "plt.bar(x - 0.2, cv[\"Logistic Regression\"][\"test_recall\"], width=0.4, label=\"Logistic Regression\", color=\"#e15759\")\n",
    "plt.bar(x + 0.2, cv[\"Decision Tree\"][\"test_recall\"], width=0.4, label=\"Decision Tree\", color=\"#9c9c9c\")\n",
    "plt.xticks(x, [\"Fold 1\", \"Fold 2\", \"Fold 3\", \"Fold 4\", \"Fold 5\"])\n",
    "plt.ylim(0, 1)\n",
    "plt.ylabel(\"Recall\")\n",
    "plt.title(\"Stratified 5-fold cross validation: recall\")\n",
    "plt.legend()\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"chart7_kfold_recall.png\")\n",
    "plt.close()\n",
    "\n",
    "# The threshold. Logistic Regression gives each patient a probability;\n",
    "# by default a patient is flagged at 0.5. A lower threshold catches more\n",
    "# patients with disease (recall up) at the cost of more referrals that\n",
    "# turn out healthy (precision down).\n",
    "prob = logistic.predict_proba(X_test)[:, 1]\n",
    "thresholds = np.round(np.arange(0.2, 0.81, 0.05), 2)\n",
    "prec_list = []\n",
    "rec_list = []\n",
    "for t in thresholds:\n",
    "    flag = (prob >= t).astype(int)\n",
    "    prec_list.append(precision_score(y_test, flag, zero_division=0))\n",
    "    rec_list.append(recall_score(y_test, flag))\n",
    "chosen = 0.35\n",
    "flag = (prob >= chosen).astype(int)\n",
    "print(\"Threshold chosen for screening   :\", chosen)\n",
    "print(\"Recall at threshold 0.35         :\", round(recall_score(y_test, flag), 3))\n",
    "print(\"Precision at threshold 0.35      :\", round(precision_score(y_test, flag), 3))\n",
    "print(\"Patients referred at 0.35        :\", flag.sum(), \"of\", len(flag))\n",
    "print()\n",
    "\n",
    "# Chart 8: precision and recall at every threshold.\n",
    "plt.figure(figsize=(7.5, 4.3))\n",
    "plt.plot(thresholds, rec_list, marker=\"o\", color=\"#e15759\", label=\"Recall\")\n",
    "plt.plot(thresholds, prec_list, marker=\"s\", color=\"#4e79a7\", label=\"Precision\")\n",
    "plt.axvline(chosen, color=\"#999999\", linestyle=\"--\", label=\"Chosen threshold\")\n",
    "plt.title(\"Choosing the decision threshold\")\n",
    "plt.xlabel(\"Probability needed to refer a patient\")\n",
    "plt.ylim(0, 1.05)\n",
    "plt.legend()\n",
    "plt.grid(True, linestyle=\"--\", alpha=0.4)\n",
    "plt.tight_layout()\n",
    "plt.savefig(\"chart8_threshold.png\")\n",
    "plt.close()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 9 - Deployment"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"--- Step 9: Deployment ---\")\n",
    "joblib.dump(logistic, \"heart_model.pkl\")\n",
    "model = joblib.load(\"heart_model.pkl\")\n",
    "new_patients = pd.DataFrame({\n",
    "    \"age\": [62, 45, 51], \"male\": [1, 0, 1], \"resting_bp\": [150, 120, 135], \"cholesterol\": [290, 210, 245],\n",
    "    \"cholesterol_missing\": [0, 0, 0], \"fasting_sugar_high\": [1, 0, 0], \"max_heart_rate\": [118, 172, 150],\n",
    "    \"exercise_angina\": [1, 0, 0], \"st_depression\": [2.4, 0.2, 1.0], \"chest_pain_atypical\": [0, 1, 0],\n",
    "    \"chest_pain_non-anginal\": [0, 0, 1], \"chest_pain_none\": [0, 0, 0], \"chest_pain_typical\": [1, 0, 0],\n",
    "})\n",
    "p_new = model.predict_proba(new_patients)[:, 1]\n",
    "for i in range(len(new_patients)):\n",
    "    if p_new[i] >= chosen:\n",
    "        action = \"REFER to a cardiologist\"\n",
    "    else:\n",
    "        action = \"routine follow-up\"\n",
    "    print(\"Patient\", i + 1, \"- probability\", round(p_new[i], 3), \"->\", action)\n",
    "print()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 10 - Feedback"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"--- Step 10: Feedback ---\")\n",
    "# Months later, the diagnoses of 200 more patients are confirmed. Checking\n",
    "# the model against them - for men and women separately - shows whether it\n",
    "# works equally well for everyone.\n",
    "fu = pd.read_csv(\"follow_up.csv\")\n",
    "fu[\"cholesterol_missing\"] = 0\n",
    "fu[\"male\"] = (fu[\"sex\"] == \"M\").astype(int)\n",
    "fu[\"exercise_angina\"] = (fu[\"exercise_angina\"] == \"Yes\").astype(int)\n",
    "fu = pd.get_dummies(fu, columns=[\"chest_pain\"], dtype=int)\n",
    "fu_flag = (model.predict_proba(fu[features])[:, 1] >= chosen).astype(int)\n",
    "print(\"Follow-up patients                :\", len(fu))\n",
    "print(\"Recall on follow-up, everyone     :\", round(recall_score(fu[\"heart_disease\"], fu_flag), 3))\n",
    "men = fu[\"male\"] == 1\n",
    "print(\"Recall on follow-up, men          :\", round(recall_score(fu[\"heart_disease\"][men], fu_flag[men]), 3))\n",
    "print(\"Recall on follow-up, women        :\", round(recall_score(fu[\"heart_disease\"][~men], fu_flag[~men]), 3))\n",
    "gap = recall_score(fu[\"heart_disease\"][men], fu_flag[men]) - recall_score(fu[\"heart_disease\"][~men], fu_flag[~men])\n",
    "print(\"Recall gap, men minus women       :\", round(gap * 100, 1), \"points\")\n",
    "if gap > 0.05:\n",
    "    print(\"Feedback: the model misses more women than men - collect more records\")\n",
    "    print(\"          from women and review the features before wider use.\")\n",
    "else:\n",
    "    print(\"Feedback: recall is similar for men and women.\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
