Spaces:
Sleeping
Sleeping
Deploy MediPredict AI Gradio app
Browse files- .gitignore +32 -0
- README.md +186 -6
- app.py +182 -0
- data/.gitkeep +1 -0
- model/.gitkeep +1 -0
- requirements.txt +6 -0
- train.py +185 -0
.gitignore
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
|
| 6 |
+
# Virtual environments
|
| 7 |
+
.venv/
|
| 8 |
+
venv/
|
| 9 |
+
env/
|
| 10 |
+
|
| 11 |
+
# Environment and credentials
|
| 12 |
+
.env
|
| 13 |
+
.env.*
|
| 14 |
+
kaggle.json
|
| 15 |
+
|
| 16 |
+
# Local notebooks and editor files
|
| 17 |
+
.ipynb_checkpoints/
|
| 18 |
+
.vscode/
|
| 19 |
+
.idea/
|
| 20 |
+
|
| 21 |
+
# OS files
|
| 22 |
+
.DS_Store
|
| 23 |
+
Thumbs.db
|
| 24 |
+
|
| 25 |
+
# Generated dataset and model artifacts
|
| 26 |
+
data/*
|
| 27 |
+
!data/.gitkeep
|
| 28 |
+
model/*
|
| 29 |
+
!model/.gitkeep
|
| 30 |
+
|
| 31 |
+
# Logs
|
| 32 |
+
*.log
|
README.md
CHANGED
|
@@ -1,12 +1,192 @@
|
|
| 1 |
---
|
| 2 |
title: MediPredict AI
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
colorTo: green
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
-
|
| 9 |
-
pinned: false
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
title: MediPredict AI
|
| 3 |
+
emoji: 🏥
|
| 4 |
+
colorFrom: blue
|
| 5 |
colorTo: green
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: "4.0.0"
|
| 8 |
+
python_version: "3.10"
|
|
|
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# MediPredict AI - Disease Prediction System
|
| 12 |
+
|
| 13 |
+
MediPredict AI is an end-to-end machine learning project that predicts diabetes risk using the Pima Indians Diabetes Database. It trains Logistic Regression, Random Forest, and XGBoost classifiers, evaluates them with standard classification metrics, saves the best model, and serves predictions through a Gradio web interface.
|
| 14 |
+
|
| 15 |
+
This project is built for Python 3.10, VS Code, and Hugging Face Spaces.
|
| 16 |
+
|
| 17 |
+
## Project Structure
|
| 18 |
+
|
| 19 |
+
```text
|
| 20 |
+
disease-prediction-app/
|
| 21 |
+
├── data/
|
| 22 |
+
│ └── diabetes.csv
|
| 23 |
+
├── model/
|
| 24 |
+
│ ├── model.pkl
|
| 25 |
+
│ └── scaler.pkl
|
| 26 |
+
├── app.py
|
| 27 |
+
├── train.py
|
| 28 |
+
├── requirements.txt
|
| 29 |
+
└── README.md
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
`diabetes.csv`, `model.pkl`, and `scaler.pkl` are generated after running `train.py`.
|
| 33 |
+
|
| 34 |
+
## Dataset
|
| 35 |
+
|
| 36 |
+
Dataset: Pima Indians Diabetes Database
|
| 37 |
+
Kaggle slug: `uciml/pima-indians-diabetes-database`
|
| 38 |
+
|
| 39 |
+
The training script downloads the dataset automatically with the Kaggle API.
|
| 40 |
+
|
| 41 |
+
## Kaggle API Setup
|
| 42 |
+
|
| 43 |
+
1. Install the Kaggle package:
|
| 44 |
+
|
| 45 |
+
```bash
|
| 46 |
+
pip install kaggle
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
2. Go to your Kaggle account settings:
|
| 50 |
+
|
| 51 |
+
```text
|
| 52 |
+
https://www.kaggle.com/settings
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
3. Create a new API token. Kaggle downloads a file named `kaggle.json`.
|
| 56 |
+
|
| 57 |
+
4. Place `kaggle.json` in the correct folder.
|
| 58 |
+
|
| 59 |
+
On Windows:
|
| 60 |
+
|
| 61 |
+
```text
|
| 62 |
+
C:\Users\<your-username>\.kaggle\kaggle.json
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
On macOS or Linux:
|
| 66 |
+
|
| 67 |
+
```text
|
| 68 |
+
~/.kaggle/kaggle.json
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
5. On macOS or Linux, set file permissions:
|
| 72 |
+
|
| 73 |
+
```bash
|
| 74 |
+
chmod 600 ~/.kaggle/kaggle.json
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
## Local Setup in VS Code
|
| 78 |
+
|
| 79 |
+
Open the project folder in VS Code:
|
| 80 |
+
|
| 81 |
+
```bash
|
| 82 |
+
cd disease-prediction-app
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
Create a virtual environment:
|
| 86 |
+
|
| 87 |
+
```bash
|
| 88 |
+
python -m venv .venv
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
Activate the virtual environment on Windows PowerShell:
|
| 92 |
+
|
| 93 |
+
```bash
|
| 94 |
+
.\.venv\Scripts\Activate.ps1
|
| 95 |
+
```
|
| 96 |
+
|
| 97 |
+
Activate the virtual environment on macOS or Linux:
|
| 98 |
+
|
| 99 |
+
```bash
|
| 100 |
+
source .venv/bin/activate
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
Install dependencies:
|
| 104 |
+
|
| 105 |
+
```bash
|
| 106 |
+
pip install -r requirements.txt
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
Train the model:
|
| 110 |
+
|
| 111 |
+
```bash
|
| 112 |
+
python train.py
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
Run the Gradio app:
|
| 116 |
+
|
| 117 |
+
```bash
|
| 118 |
+
python app.py
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
Open the local URL shown in the terminal, usually:
|
| 122 |
+
|
| 123 |
+
```text
|
| 124 |
+
http://127.0.0.1:7860
|
| 125 |
+
```
|
| 126 |
+
|
| 127 |
+
## Model Training
|
| 128 |
+
|
| 129 |
+
`train.py` performs the full ML workflow:
|
| 130 |
+
|
| 131 |
+
- Downloads `diabetes.csv` from Kaggle.
|
| 132 |
+
- Loads the data into a pandas DataFrame.
|
| 133 |
+
- Replaces zero values in `Glucose`, `BloodPressure`, `SkinThickness`, `Insulin`, and `BMI` with `NaN`.
|
| 134 |
+
- Fills missing values with column means.
|
| 135 |
+
- Scales features with `StandardScaler`.
|
| 136 |
+
- Trains Logistic Regression, Random Forest, and XGBoost.
|
| 137 |
+
- Evaluates models using accuracy, precision, recall, F1-score, and ROC-AUC.
|
| 138 |
+
- Saves the best model to `model/model.pkl`.
|
| 139 |
+
- Saves the scaler to `model/scaler.pkl`.
|
| 140 |
+
|
| 141 |
+
## Gradio App
|
| 142 |
+
|
| 143 |
+
`app.py` loads the saved model and scaler, accepts patient health inputs, scales the input, predicts diabetes risk, and displays:
|
| 144 |
+
|
| 145 |
+
- Prediction: `Low Risk` or `High Risk`
|
| 146 |
+
- Probability score
|
| 147 |
+
|
| 148 |
+
The app uses Gradio only. It does not use Streamlit.
|
| 149 |
+
|
| 150 |
+
## Hugging Face Spaces Deployment
|
| 151 |
+
|
| 152 |
+
1. Train the model locally first:
|
| 153 |
+
|
| 154 |
+
```bash
|
| 155 |
+
python train.py
|
| 156 |
+
```
|
| 157 |
+
|
| 158 |
+
2. Confirm these files exist:
|
| 159 |
+
|
| 160 |
+
```text
|
| 161 |
+
data/diabetes.csv
|
| 162 |
+
model/model.pkl
|
| 163 |
+
model/scaler.pkl
|
| 164 |
+
```
|
| 165 |
+
|
| 166 |
+
3. Create a new Hugging Face Space:
|
| 167 |
+
|
| 168 |
+
- Go to `https://huggingface.co/spaces`
|
| 169 |
+
- Click **Create new Space**
|
| 170 |
+
- Select **Gradio** as the SDK
|
| 171 |
+
- Use Python 3.10
|
| 172 |
+
|
| 173 |
+
4. Upload or push these files to the Space repository:
|
| 174 |
+
|
| 175 |
+
```text
|
| 176 |
+
app.py
|
| 177 |
+
train.py
|
| 178 |
+
requirements.txt
|
| 179 |
+
README.md
|
| 180 |
+
data/diabetes.csv
|
| 181 |
+
model/model.pkl
|
| 182 |
+
model/scaler.pkl
|
| 183 |
+
```
|
| 184 |
+
|
| 185 |
+
5. Hugging Face Spaces will install dependencies from `requirements.txt` and run `app.py` automatically.
|
| 186 |
+
|
| 187 |
+
## Important Notes
|
| 188 |
+
|
| 189 |
+
- This app is for educational use only.
|
| 190 |
+
- It is not a medical diagnosis tool.
|
| 191 |
+
- The Gradio app uses a default Diabetes Pedigree Function value of `0.47` because the requested UI inputs do not include that dataset feature.
|
| 192 |
+
- For production medical use, consult clinical experts, validate the model rigorously, and follow healthcare compliance requirements.
|
app.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pickle
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
import gradio as gr
|
| 5 |
+
import pandas as pd
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
BASE_DIR = Path(__file__).resolve().parent
|
| 9 |
+
MODEL_FILE = BASE_DIR / "model" / "model.pkl"
|
| 10 |
+
SCALER_FILE = BASE_DIR / "model" / "scaler.pkl"
|
| 11 |
+
|
| 12 |
+
DEFAULT_DPF = 0.47
|
| 13 |
+
FEATURE_COLUMNS = [
|
| 14 |
+
"Pregnancies",
|
| 15 |
+
"Glucose",
|
| 16 |
+
"BloodPressure",
|
| 17 |
+
"SkinThickness",
|
| 18 |
+
"Insulin",
|
| 19 |
+
"BMI",
|
| 20 |
+
"DiabetesPedigreeFunction",
|
| 21 |
+
"Age",
|
| 22 |
+
]
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def load_pickle(path: Path):
|
| 26 |
+
if not path.exists():
|
| 27 |
+
raise FileNotFoundError(
|
| 28 |
+
f"{path.name} was not found in {path.parent}. Run `python train.py` first."
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
with path.open("rb") as file:
|
| 32 |
+
return pickle.load(file)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
model = load_pickle(MODEL_FILE)
|
| 36 |
+
scaler = load_pickle(SCALER_FILE)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def validate_inputs(values: list[float]) -> str | None:
|
| 40 |
+
labels = [
|
| 41 |
+
"Pregnancies",
|
| 42 |
+
"Glucose",
|
| 43 |
+
"Blood Pressure",
|
| 44 |
+
"Skin Thickness",
|
| 45 |
+
"Insulin",
|
| 46 |
+
"BMI",
|
| 47 |
+
"Age",
|
| 48 |
+
]
|
| 49 |
+
|
| 50 |
+
for label, value in zip(labels, values):
|
| 51 |
+
if value is None:
|
| 52 |
+
return f"Please enter a value for {label}."
|
| 53 |
+
if value < 0:
|
| 54 |
+
return f"{label} cannot be negative."
|
| 55 |
+
|
| 56 |
+
if values[5] == 0:
|
| 57 |
+
return "BMI must be greater than 0."
|
| 58 |
+
|
| 59 |
+
return None
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def predict_diabetes(
|
| 63 |
+
pregnancies: float,
|
| 64 |
+
glucose: float,
|
| 65 |
+
blood_pressure: float,
|
| 66 |
+
skin_thickness: float,
|
| 67 |
+
insulin: float,
|
| 68 |
+
bmi: float,
|
| 69 |
+
age: float,
|
| 70 |
+
) -> tuple[str, str]:
|
| 71 |
+
values = [
|
| 72 |
+
pregnancies,
|
| 73 |
+
glucose,
|
| 74 |
+
blood_pressure,
|
| 75 |
+
skin_thickness,
|
| 76 |
+
insulin,
|
| 77 |
+
bmi,
|
| 78 |
+
age,
|
| 79 |
+
]
|
| 80 |
+
|
| 81 |
+
error = validate_inputs(values)
|
| 82 |
+
if error:
|
| 83 |
+
return "Invalid Input", error
|
| 84 |
+
|
| 85 |
+
input_data = pd.DataFrame(
|
| 86 |
+
[
|
| 87 |
+
{
|
| 88 |
+
"Pregnancies": pregnancies,
|
| 89 |
+
"Glucose": glucose,
|
| 90 |
+
"BloodPressure": blood_pressure,
|
| 91 |
+
"SkinThickness": skin_thickness,
|
| 92 |
+
"Insulin": insulin,
|
| 93 |
+
"BMI": bmi,
|
| 94 |
+
"DiabetesPedigreeFunction": DEFAULT_DPF,
|
| 95 |
+
"Age": age,
|
| 96 |
+
}
|
| 97 |
+
],
|
| 98 |
+
columns=FEATURE_COLUMNS,
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
scaled_input = scaler.transform(input_data)
|
| 102 |
+
prediction = int(model.predict(scaled_input)[0])
|
| 103 |
+
probability = float(model.predict_proba(scaled_input)[0][1])
|
| 104 |
+
|
| 105 |
+
risk_label = "High Risk" if prediction == 1 else "Low Risk"
|
| 106 |
+
risk_message = (
|
| 107 |
+
"The model predicts a higher likelihood of diabetes."
|
| 108 |
+
if prediction == 1
|
| 109 |
+
else "The model predicts a lower likelihood of diabetes."
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
return risk_label, f"{probability * 100:.2f}% probability. {risk_message}"
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
custom_css = """
|
| 116 |
+
.gradio-container {
|
| 117 |
+
max-width: 960px !important;
|
| 118 |
+
margin: auto !important;
|
| 119 |
+
}
|
| 120 |
+
.risk-box {
|
| 121 |
+
border-radius: 8px;
|
| 122 |
+
}
|
| 123 |
+
"""
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
with gr.Blocks(theme=gr.themes.Soft(), css=custom_css, title="MediPredict AI") as demo:
|
| 127 |
+
gr.Markdown(
|
| 128 |
+
"""
|
| 129 |
+
# 🏥 MediPredict AI
|
| 130 |
+
A disease prediction system that estimates diabetes risk using a trained machine learning classifier.
|
| 131 |
+
Enter patient health values below to get a risk category and probability score.
|
| 132 |
+
"""
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
with gr.Row():
|
| 136 |
+
with gr.Column():
|
| 137 |
+
pregnancies = gr.Slider(
|
| 138 |
+
minimum=0,
|
| 139 |
+
maximum=20,
|
| 140 |
+
value=1,
|
| 141 |
+
step=1,
|
| 142 |
+
label="Pregnancies",
|
| 143 |
+
)
|
| 144 |
+
glucose = gr.Number(value=120, label="Glucose")
|
| 145 |
+
blood_pressure = gr.Number(value=70, label="Blood Pressure")
|
| 146 |
+
skin_thickness = gr.Number(value=20, label="Skin Thickness")
|
| 147 |
+
|
| 148 |
+
with gr.Column():
|
| 149 |
+
insulin = gr.Number(value=80, label="Insulin")
|
| 150 |
+
bmi = gr.Number(value=25.0, label="BMI")
|
| 151 |
+
age = gr.Number(value=30, label="Age")
|
| 152 |
+
|
| 153 |
+
predict_button = gr.Button("Predict Diabetes Risk", variant="primary")
|
| 154 |
+
|
| 155 |
+
with gr.Row():
|
| 156 |
+
prediction_output = gr.Textbox(label="Prediction", interactive=False)
|
| 157 |
+
probability_output = gr.Textbox(label="Probability Score", interactive=False)
|
| 158 |
+
|
| 159 |
+
predict_button.click(
|
| 160 |
+
fn=predict_diabetes,
|
| 161 |
+
inputs=[
|
| 162 |
+
pregnancies,
|
| 163 |
+
glucose,
|
| 164 |
+
blood_pressure,
|
| 165 |
+
skin_thickness,
|
| 166 |
+
insulin,
|
| 167 |
+
bmi,
|
| 168 |
+
age,
|
| 169 |
+
],
|
| 170 |
+
outputs=[prediction_output, probability_output],
|
| 171 |
+
)
|
| 172 |
+
|
| 173 |
+
gr.Markdown(
|
| 174 |
+
"""
|
| 175 |
+
**Note:** This app is for educational purposes only and is not a medical diagnosis tool.
|
| 176 |
+
Please consult a qualified healthcare professional for medical advice.
|
| 177 |
+
"""
|
| 178 |
+
)
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
if __name__ == "__main__":
|
| 182 |
+
demo.launch()
|
data/.gitkeep
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
model/.gitkeep
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
pandas
|
| 2 |
+
numpy
|
| 3 |
+
scikit-learn
|
| 4 |
+
xgboost
|
| 5 |
+
gradio
|
| 6 |
+
kaggle
|
train.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import pickle
|
| 3 |
+
import zipfile
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
import numpy as np
|
| 7 |
+
import pandas as pd
|
| 8 |
+
from kaggle.api.kaggle_api_extended import KaggleApi
|
| 9 |
+
from sklearn.ensemble import RandomForestClassifier
|
| 10 |
+
from sklearn.linear_model import LogisticRegression
|
| 11 |
+
from sklearn.metrics import (
|
| 12 |
+
accuracy_score,
|
| 13 |
+
f1_score,
|
| 14 |
+
precision_score,
|
| 15 |
+
recall_score,
|
| 16 |
+
roc_auc_score,
|
| 17 |
+
)
|
| 18 |
+
from sklearn.model_selection import train_test_split
|
| 19 |
+
from sklearn.preprocessing import StandardScaler
|
| 20 |
+
from xgboost import XGBClassifier
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
BASE_DIR = Path(__file__).resolve().parent
|
| 24 |
+
DATA_DIR = BASE_DIR / "data"
|
| 25 |
+
MODEL_DIR = BASE_DIR / "model"
|
| 26 |
+
DATASET_SLUG = "uciml/pima-indians-diabetes-database"
|
| 27 |
+
DATA_FILE = DATA_DIR / "diabetes.csv"
|
| 28 |
+
MODEL_FILE = MODEL_DIR / "model.pkl"
|
| 29 |
+
SCALER_FILE = MODEL_DIR / "scaler.pkl"
|
| 30 |
+
|
| 31 |
+
FEATURE_COLUMNS = [
|
| 32 |
+
"Pregnancies",
|
| 33 |
+
"Glucose",
|
| 34 |
+
"BloodPressure",
|
| 35 |
+
"SkinThickness",
|
| 36 |
+
"Insulin",
|
| 37 |
+
"BMI",
|
| 38 |
+
"DiabetesPedigreeFunction",
|
| 39 |
+
"Age",
|
| 40 |
+
]
|
| 41 |
+
|
| 42 |
+
ZERO_AS_MISSING_COLUMNS = [
|
| 43 |
+
"Glucose",
|
| 44 |
+
"BloodPressure",
|
| 45 |
+
"SkinThickness",
|
| 46 |
+
"Insulin",
|
| 47 |
+
"BMI",
|
| 48 |
+
]
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def download_dataset() -> None:
|
| 52 |
+
"""Download the Pima Indians Diabetes dataset using Kaggle API."""
|
| 53 |
+
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
| 54 |
+
|
| 55 |
+
if DATA_FILE.exists():
|
| 56 |
+
print(f"Dataset already exists: {DATA_FILE}")
|
| 57 |
+
return
|
| 58 |
+
|
| 59 |
+
print("Downloading dataset from Kaggle...")
|
| 60 |
+
api = KaggleApi()
|
| 61 |
+
api.authenticate()
|
| 62 |
+
api.dataset_download_files(DATASET_SLUG, path=str(DATA_DIR), unzip=False)
|
| 63 |
+
|
| 64 |
+
zip_path = DATA_DIR / "pima-indians-diabetes-database.zip"
|
| 65 |
+
if not zip_path.exists():
|
| 66 |
+
raise FileNotFoundError(
|
| 67 |
+
"Kaggle download finished, but the expected zip file was not found."
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
with zipfile.ZipFile(zip_path, "r") as zip_ref:
|
| 71 |
+
zip_ref.extractall(DATA_DIR)
|
| 72 |
+
|
| 73 |
+
zip_path.unlink(missing_ok=True)
|
| 74 |
+
|
| 75 |
+
if not DATA_FILE.exists():
|
| 76 |
+
raise FileNotFoundError("diabetes.csv was not found after extracting the dataset.")
|
| 77 |
+
|
| 78 |
+
print(f"Dataset saved to: {DATA_FILE}")
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def load_and_preprocess_data() -> tuple[np.ndarray, np.ndarray, StandardScaler]:
|
| 82 |
+
"""Load data, clean missing medical values, split features, and scale."""
|
| 83 |
+
df = pd.read_csv(DATA_FILE)
|
| 84 |
+
|
| 85 |
+
missing_columns = [col for col in FEATURE_COLUMNS + ["Outcome"] if col not in df.columns]
|
| 86 |
+
if missing_columns:
|
| 87 |
+
raise ValueError(f"Dataset is missing required columns: {missing_columns}")
|
| 88 |
+
|
| 89 |
+
df[ZERO_AS_MISSING_COLUMNS] = df[ZERO_AS_MISSING_COLUMNS].replace(0, np.nan)
|
| 90 |
+
df[ZERO_AS_MISSING_COLUMNS] = df[ZERO_AS_MISSING_COLUMNS].fillna(
|
| 91 |
+
df[ZERO_AS_MISSING_COLUMNS].mean()
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
x = df[FEATURE_COLUMNS]
|
| 95 |
+
y = df["Outcome"]
|
| 96 |
+
|
| 97 |
+
scaler = StandardScaler()
|
| 98 |
+
x_scaled = scaler.fit_transform(x)
|
| 99 |
+
|
| 100 |
+
return x_scaled, y.to_numpy(), scaler
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def evaluate_model(name: str, model, x_test: np.ndarray, y_test: np.ndarray) -> dict:
|
| 104 |
+
"""Calculate common classification metrics for a trained model."""
|
| 105 |
+
predictions = model.predict(x_test)
|
| 106 |
+
|
| 107 |
+
if hasattr(model, "predict_proba"):
|
| 108 |
+
probabilities = model.predict_proba(x_test)[:, 1]
|
| 109 |
+
else:
|
| 110 |
+
probabilities = predictions
|
| 111 |
+
|
| 112 |
+
metrics = {
|
| 113 |
+
"model": name,
|
| 114 |
+
"accuracy": accuracy_score(y_test, predictions),
|
| 115 |
+
"precision": precision_score(y_test, predictions, zero_division=0),
|
| 116 |
+
"recall": recall_score(y_test, predictions, zero_division=0),
|
| 117 |
+
"f1_score": f1_score(y_test, predictions, zero_division=0),
|
| 118 |
+
"roc_auc": roc_auc_score(y_test, probabilities),
|
| 119 |
+
}
|
| 120 |
+
return metrics
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def train_models() -> None:
|
| 124 |
+
"""Train candidate models and save the best model by ROC-AUC."""
|
| 125 |
+
MODEL_DIR.mkdir(parents=True, exist_ok=True)
|
| 126 |
+
download_dataset()
|
| 127 |
+
|
| 128 |
+
x, y, scaler = load_and_preprocess_data()
|
| 129 |
+
x_train, x_test, y_train, y_test = train_test_split(
|
| 130 |
+
x,
|
| 131 |
+
y,
|
| 132 |
+
test_size=0.2,
|
| 133 |
+
random_state=42,
|
| 134 |
+
stratify=y,
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
models = {
|
| 138 |
+
"Logistic Regression": LogisticRegression(max_iter=1000, random_state=42),
|
| 139 |
+
"Random Forest": RandomForestClassifier(
|
| 140 |
+
n_estimators=200,
|
| 141 |
+
random_state=42,
|
| 142 |
+
class_weight="balanced",
|
| 143 |
+
),
|
| 144 |
+
"XGBoost": XGBClassifier(
|
| 145 |
+
n_estimators=200,
|
| 146 |
+
learning_rate=0.05,
|
| 147 |
+
max_depth=3,
|
| 148 |
+
subsample=0.9,
|
| 149 |
+
colsample_bytree=0.9,
|
| 150 |
+
eval_metric="logloss",
|
| 151 |
+
random_state=42,
|
| 152 |
+
),
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
results = []
|
| 156 |
+
trained_models = {}
|
| 157 |
+
|
| 158 |
+
for name, model in models.items():
|
| 159 |
+
print(f"\nTraining {name}...")
|
| 160 |
+
model.fit(x_train, y_train)
|
| 161 |
+
trained_models[name] = model
|
| 162 |
+
metrics = evaluate_model(name, model, x_test, y_test)
|
| 163 |
+
results.append(metrics)
|
| 164 |
+
|
| 165 |
+
results_df = pd.DataFrame(results).sort_values(by="roc_auc", ascending=False)
|
| 166 |
+
print("\nModel Evaluation Results")
|
| 167 |
+
print(results_df.to_string(index=False))
|
| 168 |
+
|
| 169 |
+
best_model_name = results_df.iloc[0]["model"]
|
| 170 |
+
best_model = trained_models[best_model_name]
|
| 171 |
+
|
| 172 |
+
with MODEL_FILE.open("wb") as model_file:
|
| 173 |
+
pickle.dump(best_model, model_file)
|
| 174 |
+
|
| 175 |
+
with SCALER_FILE.open("wb") as scaler_file:
|
| 176 |
+
pickle.dump(scaler, scaler_file)
|
| 177 |
+
|
| 178 |
+
print(f"\nBest model: {best_model_name}")
|
| 179 |
+
print(f"Model saved to: {MODEL_FILE}")
|
| 180 |
+
print(f"Scaler saved to: {SCALER_FILE}")
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
if __name__ == "__main__":
|
| 184 |
+
os.environ.setdefault("KAGGLE_CONFIG_DIR", str(Path.home() / ".kaggle"))
|
| 185 |
+
train_models()
|