HeartSense / app.py
MrAR's picture
Update app.py
00b3681 verified
Raw
History Blame Contribute Delete
11.9 kB
import numpy as np
import pandas as pd
import gradio as gr
import pickle
import plotly.graph_objects as go
from plotly.subplots import make_subplots
# Load model weights and metadata
with open('model_weights.pkl', 'rb') as f:
model_weights = pickle.load(f)
with open('scaler.pkl', 'rb') as f:
scaler = pickle.load(f)
with open('feature_names.pkl', 'rb') as f:
feature_names = pickle.load(f)
with open('continuous_cols.pkl', 'rb') as f:
continuous_cols = pickle.load(f)
with open('n_models.pkl', 'rb') as f:
n_models = pickle.load(f)
# Load individual models
models = {}
model_names = [
"logistic_regression",
"decision_tree",
"gradient_boosting",
"knn",
"svm",
"random_forest",
"xgboost"
]
model_display_names = {
"logistic_regression": "Logistic Regression",
"decision_tree": "Decision Tree",
"gradient_boosting": "Gradient Boosting",
"knn": "KNN",
"svm": "SVM",
"random_forest": "Random Forest",
"xgboost": "XGBoost"
}
for model_name in model_names:
with open(f'model_{model_name}.pkl', 'rb') as f:
models[model_display_names[model_name]] = pickle.load(f)
print(f"Loaded {len(models)} models successfully!")
def create_gauge_chart(probability):
"""Create a gauge chart for risk probability"""
# Determine color based on risk level
if probability < 30:
color = "#10b981" # Green
risk_level = "Low Risk"
elif probability < 50:
color = "#f59e0b" # Orange
risk_level = "Moderate Risk"
elif probability < 70:
color = "#ef4444" # Red
risk_level = "High Risk"
else:
color = "#dc2626" # Dark Red
risk_level = "Very High Risk"
fig = go.Figure(go.Indicator(
mode = "gauge+number+delta",
value = probability,
domain = {'x': [0, 1], 'y': [0, 1]},
title = {'text': f"<b>{risk_level}</b>", 'font': {'size': 24, 'color': color}},
number = {'suffix': "%", 'font': {'size': 48, 'color': color}},
gauge = {
'axis': {'range': [None, 100], 'tickwidth': 2, 'tickcolor': "gray"},
'bar': {'color': color, 'thickness': 0.75},
'bgcolor': "white",
'borderwidth': 2,
'bordercolor': "gray",
'steps': [
{'range': [0, 30], 'color': '#d1fae5'},
{'range': [30, 50], 'color': '#fef3c7'},
{'range': [50, 70], 'color': '#fee2e2'},
{'range': [70, 100], 'color': '#fecaca'}
],
'threshold': {
'line': {'color': "black", 'width': 4},
'thickness': 0.75,
'value': 50
}
}
))
fig.update_layout(
height=300,
margin=dict(l=20, r=20, t=80, b=20),
paper_bgcolor="white",
font={'family': "Arial"}
)
return fig
def create_summary_text(probability, weighted_sum, threshold, prediction_result, model_predictions, model_weights):
"""Create formatted HTML summary"""
if weighted_sum > threshold:
status_color = "#dc2626"
status_icon = "⚠️"
recommendation = """
<div style='background-color: #fee2e2; padding: 15px; border-radius: 8px; border-left: 4px solid #dc2626;'>
<h3 style='color: #991b1b; margin-top: 0;'>⚠️ Action Recommended</h3>
<ul style='color: #7f1d1d; margin-bottom: 0;'>
<li style='color: #000000;'>Consult a healthcare professional for thorough evaluation</li>
<li style='color: #000000;'>Consider scheduling a cardiac check-up</li>
<li style='color: #000000;'>Monitor your symptoms closely</li>
<li style='color: #000000;'>Maintain a heart-healthy lifestyle</li>
</ul>
</div>
"""
else:
status_color = "#10b981"
status_icon = "✓"
recommendation = """
<div style='background-color: #d1fae5; padding: 15px; border-radius: 8px; border-left: 4px solid #10b981;'>
<h3 style='color: #065f46; margin-top: 0;'>✓ Lower Risk Detected</h3>
<ul style='color: #064e3b; margin-bottom: 0;'>
<li style='color: #000000;'>Continue maintaining a healthy lifestyle</li>
<li style='color: #000000;'>Regular exercise and balanced diet recommended</li>
<li style='color: #000000;'>Periodic health check-ups are still important</li>
<li style='color: #000000;'>Stay aware of any changes in symptoms</li>
</ul>
</div>
"""
# Create model predictions breakdown
predictions_breakdown = ""
disease_count = sum(1 for p in model_predictions.values() if p == 1)
safe_count = len(model_predictions) - disease_count
html = f"""
<div style='font-family: Arial, sans-serif; padding: 20px;'>
{recommendation}
<div style='background-color: #f3f4f6; padding: 15px; border-radius: 8px; margin-top: 20px;'>
<h4 style='color: #374151; margin-top: 0;'>How This Works</h4>
<p style='color: #6b7280; font-size: 14px; line-height: 1.6; margin-bottom: 0;'>
This prediction uses an ensemble of 7 different machine learning models. Each model's prediction is weighted based
on its performance metrics. The final decision is made when the weighted sum exceeds the threshold.
</p>
</div>
<div style='background-color: #fffbeb; padding: 12px; border-radius: 8px; margin-top: 15px; border-left: 4px solid #f59e0b;'>
<p style='color: #92400e; font-size: 13px; margin: 0;'>
Medical Disclaimer: This tool is for educational purposes only and should not replace
professional medical advice, diagnosis, or treatment. Always consult with qualified healthcare providers
for medical decisions.
</p>
</div>
</div>
"""
return html
def predict_heart_disease(age, resting_bp, cholesterol, max_hr, oldpeak, fasting_bs, sex, chest_pain_type, resting_ecg, exercise_angina, st_slope):
# Encode categorical inputs using LabelEncoder compatible mappings
# These must match what LabelEncoder produced during training
sex_mapping = {"Male": 1, "Female": 0}
sex = sex_mapping.get(sex, 0)
# Convert fasting_bs from string to integer
fasting_bs = int(fasting_bs)
# For string categorical values, LabelEncoder sorts them alphabetically:
# ChestPainType: ["ASY", "ATA", "NAP", "TA"] -> 0, 1, 2, 3
chest_pain_type_mapping = {"ASY": 0, "ATA": 1, "NAP": 2, "TA": 3}
chest_pain_type = chest_pain_type_mapping.get(chest_pain_type, 0)
# RestingECG: ["LVH", "Normal", "ST"] -> 0, 1, 2
resting_ecg_mapping = {"LVH": 0, "Normal": 1, "ST": 2}
resting_ecg = resting_ecg_mapping.get(resting_ecg, 1)
# ExerciseAngina: ["N", "Y"] -> 0, 1
exercise_angina_mapping = {"No": 0, "Yes": 1}
exercise_angina = exercise_angina_mapping.get(exercise_angina, 0)
# ST_Slope: ["Down", "Flat", "Up"] -> 0, 1, 2
st_slope_mapping = {"Down": 0, "Flat": 1, "Up": 2}
st_slope = st_slope_mapping.get(st_slope, 2)
# Create input dataframe matching exact training column order
input_df = pd.DataFrame({
"Age": [age],
"Sex": [sex],
"ChestPainType": [chest_pain_type],
"RestingBP": [resting_bp],
"Cholesterol": [cholesterol],
"FastingBS": [fasting_bs],
"RestingECG": [resting_ecg],
"MaxHR": [max_hr],
"ExerciseAngina": [exercise_angina],
"Oldpeak": [oldpeak],
"ST_Slope": [st_slope]
})
# Ensure columns match feature_names order exactly
input_df = input_df[feature_names]
# Scale continuous features
input_df[continuous_cols] = scaler.transform(input_df[continuous_cols])
# Get predictions from all models
model_predictions = {}
weighted_sum = 0
total_weights = sum(model_weights.values())
for name, model in models.items():
prediction = model.predict(input_df)[0]
weight = model_weights[name]
model_predictions[name] = prediction
weighted_sum += prediction * weight
# Calculate probability and threshold
threshold = n_models / 2
# Probability is the weighted sum normalized by total possible weighted sum
max_possible_sum = sum(model_weights.values())
probability = (weighted_sum / max_possible_sum) * 100
# Determine final prediction
if weighted_sum > threshold:
result = "High Risk Detected"
else:
result = "Low Risk - You Are Safe"
# Create visualizations
gauge_chart = create_gauge_chart(probability)
summary_html = create_summary_text(probability, weighted_sum, threshold, result, model_predictions, model_weights)
return summary_html, gauge_chart
# Create Gradio interface with custom CSS
css = """
.gradio-container {
font-family: 'Arial', sans-serif;
}
.output-html {
border: none !important;
}
"""
with gr.Blocks(css=css, theme=gr.themes.Soft()) as iface:
gr.Markdown(
"""
# HeartSense - Heart Disease Prediction System
### Advanced ML Ensemble for Cardiovascular Risk Assessment
Enter patient details below to get a comprehensive risk analysis using 7 different machine learning models.
"""
)
with gr.Column():
with gr.Row(scale=1):
with gr.Column(scale=1):
gr.Markdown("### Patient Information")
age = gr.Number(label="Age", value=50)
sex = gr.Radio(["Female", "Male"], label="Sex", value="Male")
fasting_bs = gr.Radio(["0", "1"], label="Fasting Blood Sugar", value="0")
with gr.Column(scale=1):
gr.Markdown("### Cardiac Measurements")
resting_bp = gr.Number(label="Resting BP (mm Hg)", value=120)
cholesterol = gr.Number(label="Cholesterol (mg/dL)", value=200)
max_hr = gr.Number(label="Max Heart Rate", value=150)
oldpeak = gr.Number(label="Oldpeak", value=0)
with gr.Column(scale=1):
gr.Markdown("### Clinical Indicators")
chest_pain_type = gr.Radio(["ASY", "ATA", "NAP", "TA"], label="Chest Pain Type", value="ASY")
resting_ecg = gr.Radio(["Normal", "ST", "LVH"], label="Resting ECG", value="Normal")
exercise_angina = gr.Radio(["No", "Yes"], label="Exercise Angina", value="No")
st_slope = gr.Radio(["Up", "Flat", "Down"], label="ST Slope", value="Flat")
predict_btn = gr.Button("Analyze Risk", variant="primary", size="lg")
with gr.Column(scale=2):
gauge_output = gr.Plot(label="Risk Gauge")
summary_output = gr.HTML(label="Summary")
gr.Markdown(
"""
### Example Cases
Try these example inputs to see how the system works:
"""
)
gr.Examples(
examples=[
[55, 130, 250, 140, 1.0, "0", "Male", "ASY", "Normal", "No", "Flat"],
[45, 110, 180, 160, 0, "0", "Female", "NAP", "Normal", "No", "Up"],
[60, 140, 280, 130, 2.0, "1", "Male", "ASY", "ST", "Yes", "Down"],
],
inputs=[age, resting_bp, cholesterol, max_hr, oldpeak, fasting_bs, sex, chest_pain_type, resting_ecg, exercise_angina, st_slope],
)
predict_btn.click(
fn=predict_heart_disease,
inputs=[age, resting_bp, cholesterol, max_hr, oldpeak, fasting_bs, sex, chest_pain_type, resting_ecg, exercise_angina, st_slope],
outputs=[summary_output, gauge_output]
)
if __name__ == "__main__":
iface.launch()