|
|
import streamlit as st |
|
|
|
|
|
|
|
|
st.set_page_config( |
|
|
page_title="Risk Level Prediction App - Tugas Akhir MBD", |
|
|
page_icon="🩺", |
|
|
layout="wide", |
|
|
initial_sidebar_state="collapsed" |
|
|
) |
|
|
|
|
|
import pandas as pd |
|
|
import numpy as np |
|
|
import joblib |
|
|
import plotly.express as px |
|
|
import plotly.graph_objects as go |
|
|
from datetime import datetime |
|
|
import json |
|
|
|
|
|
|
|
|
@st.cache_resource |
|
|
def load_model(): |
|
|
"""Load the trained model components""" |
|
|
try: |
|
|
components = joblib.load('./src/model.joblib') |
|
|
return components |
|
|
except FileNotFoundError: |
|
|
st.error("Model file 'RiskLevel_prediction_components.joblib' not found!") |
|
|
st.stop() |
|
|
except Exception as e: |
|
|
st.error(f"Error loading model: {str(e)}") |
|
|
st.stop() |
|
|
|
|
|
def predict_RiskLevel(data, model_components): |
|
|
""" |
|
|
Predict maternal health risk category using the trained decision tree model. |
|
|
|
|
|
Parameters: |
|
|
----------- |
|
|
data : dict |
|
|
Dictionary with input features. Must include: |
|
|
- Age: float or int |
|
|
- SystolicBP: float |
|
|
- DiastolicBP: float |
|
|
- BS: float (Blood Sugar) |
|
|
- BodyTemp: float |
|
|
- HeartRate: float |
|
|
|
|
|
Returns: |
|
|
-------- |
|
|
dict |
|
|
Dictionary containing: |
|
|
- prediction: int (class index) |
|
|
- prediction_label: str (Low Risk / Mid Risk / High Risk) |
|
|
- probability: float |
|
|
""" |
|
|
|
|
|
components = joblib.load('./src/model.joblib') |
|
|
|
|
|
|
|
|
model = components['model'] |
|
|
encoding_maps = components['encoding_maps'] |
|
|
feature_names = components['feature_names'] |
|
|
|
|
|
|
|
|
if isinstance(data, dict): |
|
|
df = pd.DataFrame([data]) |
|
|
else: |
|
|
df = data.copy() |
|
|
|
|
|
|
|
|
for column in df.columns: |
|
|
if column in encoding_maps and column != 'RiskLevel': |
|
|
df[column] = df[column].map(encoding_maps[column]) |
|
|
|
|
|
|
|
|
df_for_pred = df[feature_names].copy() |
|
|
|
|
|
|
|
|
prediction = model.predict(df_for_pred)[0] |
|
|
probabilities = model.predict_proba(df_for_pred)[0] |
|
|
|
|
|
|
|
|
prediction = int(prediction.item() if hasattr(prediction, 'item') else prediction) |
|
|
|
|
|
|
|
|
RiskLevel_map_inverse = {v: k for k, v in encoding_maps['RiskLevel'].items()} |
|
|
prediction_label = RiskLevel_map_inverse[prediction] |
|
|
|
|
|
return { |
|
|
'prediction': int(prediction), |
|
|
'prediction_label': prediction_label, |
|
|
'probability': float(probabilities[prediction]), |
|
|
'probabilities': probabilities.tolist() |
|
|
} |
|
|
|
|
|
def validate_inputs(data): |
|
|
errors = [] |
|
|
|
|
|
|
|
|
|
|
|
if not (10 <= data['Age'] <= 50): |
|
|
errors.append("Age should be between 10 and 50") |
|
|
|
|
|
|
|
|
if not (70 <= data['SystolicBP'] <= 140): |
|
|
errors.append("Systolic BP should be between 70 and 140") |
|
|
|
|
|
|
|
|
if not (50 <= data['DiastolicBP'] <= 100): |
|
|
errors.append("Diastolic BP should be between 50 and 100") |
|
|
|
|
|
|
|
|
if not (2 <= data['BS'] <= 18): |
|
|
errors.append("Blood Sugar should be between 2 and 18") |
|
|
|
|
|
|
|
|
if not (96 <= data['BodyTemp'] <= 102): |
|
|
errors.append("Body Temperature should be between 96 and 102 °F") |
|
|
|
|
|
|
|
|
if not (66 <= data['HeartRate'] <= 101): |
|
|
errors.append("Heart Rate should be between 66 and 101 bpm") |
|
|
|
|
|
return errors |
|
|
|
|
|
def export_prediction(data, result): |
|
|
"""Export prediction result to JSON""" |
|
|
export_data = { |
|
|
'timestamp': datetime.now().isoformat(), |
|
|
'input_data': data, |
|
|
'prediction': { |
|
|
'class': result['prediction_label'], |
|
|
'confidence': result['probability'], |
|
|
'raw_prediction': result['prediction'] |
|
|
} |
|
|
} |
|
|
return json.dumps(export_data, indent=2) |
|
|
|
|
|
def reset_session_state(): |
|
|
"""Reset all input values to default""" |
|
|
keys_to_reset = [ |
|
|
'Age', 'SystolicBP', 'DiastolicBP', 'BS', 'BodyTemp', 'HeartRate', 'RiskLevel' |
|
|
] |
|
|
for key in keys_to_reset: |
|
|
if key in st.session_state: |
|
|
del st.session_state[key] |
|
|
|
|
|
|
|
|
model_components = load_model() |
|
|
|
|
|
|
|
|
st.title("🩺 Maternal Health Risk App - Tugas Akhir MBD") |
|
|
st.markdown("Predict the **Risk Level** for maternal health using basic physiological data.") |
|
|
|
|
|
|
|
|
col1, col2 = st.columns([2, 1]) |
|
|
|
|
|
with col1: |
|
|
st.subheader("📝 Input Features") |
|
|
|
|
|
|
|
|
with st.form("prediction_form"): |
|
|
|
|
|
st.markdown("**Medical Measurements**") |
|
|
col_demo1, col_demo2 = st.columns(2) |
|
|
|
|
|
with col_demo1: |
|
|
Age = st.number_input("Age", min_value=10, max_value=50, value=25, key="Age") |
|
|
SystolicBP = st.number_input("Systolic Blood Pressure (mmHg)", min_value=70, max_value=140, value=120, key="SystolicBP") |
|
|
DiastolicBP = st.number_input("Diastolic Blood Pressure (mmHg)", min_value=50, max_value=100, value=80, key="DiastolicBP") |
|
|
|
|
|
with col_demo2: |
|
|
BS = st.number_input("Blood Sugar Level", min_value=2, max_value=18, value=5, key="BS") |
|
|
BodyTemp = st.number_input("Body Temperature (°F)", min_value=96, max_value=102, value=98, key="BodyTemp") |
|
|
HeartRate = st.number_input("Heart Rate (bpm)", min_value=66, max_value=101, value=85, key="HeartRate") |
|
|
|
|
|
st.divider() |
|
|
|
|
|
|
|
|
col_btn1, col_btn2, col_btn3 = st.columns(3) |
|
|
with col_btn1: |
|
|
predict_button = st.form_submit_button("🔮 Predict", type="primary") |
|
|
with col_btn2: |
|
|
reset_button = st.form_submit_button("🔄 Reset") |
|
|
with col_btn3: |
|
|
export_button = st.form_submit_button("📤 Export Last Result") |
|
|
|
|
|
|
|
|
if reset_button: |
|
|
reset_session_state() |
|
|
st.rerun() |
|
|
|
|
|
|
|
|
if predict_button: |
|
|
|
|
|
input_data = { |
|
|
'Age': Age, |
|
|
'SystolicBP': SystolicBP, |
|
|
'DiastolicBP': DiastolicBP, |
|
|
'BS': BS, |
|
|
'BodyTemp': BodyTemp, |
|
|
'HeartRate': HeartRate |
|
|
} |
|
|
|
|
|
|
|
|
validation_errors = validate_inputs(input_data) |
|
|
|
|
|
if validation_errors: |
|
|
with col2: |
|
|
st.error("❌ Validation Errors:") |
|
|
for error in validation_errors: |
|
|
st.error(f"• {error}") |
|
|
else: |
|
|
|
|
|
try: |
|
|
result = predict_RiskLevel(input_data, model_components) |
|
|
|
|
|
|
|
|
st.session_state['last_prediction'] = { |
|
|
'input_data': input_data, |
|
|
'result': result |
|
|
} |
|
|
|
|
|
with col2: |
|
|
st.subheader("🎯 Prediction Results") |
|
|
|
|
|
|
|
|
prediction_color = { |
|
|
'low risk': 'green', |
|
|
'mid risk': 'orange', |
|
|
'high risk': 'red' |
|
|
}.get(result['prediction_label'], 'blue') |
|
|
|
|
|
st.markdown(f"**Predicted Risk:** :{prediction_color}[{result['prediction_label']}]") |
|
|
|
|
|
|
|
|
confidence = result['probability'] * 100 |
|
|
|
|
|
fig_gauge = go.Figure(go.Indicator( |
|
|
mode = "gauge+number+delta", |
|
|
value = confidence, |
|
|
domain = {'x': [0, 1], 'y': [0, 1]}, |
|
|
title = {'text': "Confidence Level (%)"}, |
|
|
gauge = { |
|
|
'axis': {'range': [None, 100]}, |
|
|
'bar': {'color': prediction_color}, |
|
|
'steps': [ |
|
|
{'range': [0, 50], 'color': "lightgray"}, |
|
|
{'range': [50, 80], 'color': "yellow"}, |
|
|
{'range': [80, 100], 'color': "lightgreen"} |
|
|
], |
|
|
'threshold': { |
|
|
'line': {'color': "red", 'width': 4}, |
|
|
'thickness': 0.75, |
|
|
'value': 90 |
|
|
} |
|
|
} |
|
|
)) |
|
|
fig_gauge.update_layout(height=300, margin=dict(l=20, r=20, t=40, b=20)) |
|
|
st.plotly_chart(fig_gauge, use_container_width=True) |
|
|
|
|
|
|
|
|
prob_df = pd.DataFrame({ |
|
|
'Class': ['low risk', 'mid risk', 'high risk'], |
|
|
'Probability': result['probabilities'] |
|
|
}) |
|
|
|
|
|
fig_bar = px.bar( |
|
|
prob_df, |
|
|
x='Class', |
|
|
y='Probability', |
|
|
title='Risk Class Distribution', |
|
|
color='Probability', |
|
|
color_continuous_scale=['orange', 'green'] |
|
|
) |
|
|
fig_bar.update_layout(height=300, margin=dict(l=20, r=20, t=40, b=20)) |
|
|
st.plotly_chart(fig_bar, use_container_width=True) |
|
|
|
|
|
except Exception as e: |
|
|
with col2: |
|
|
st.error(f"❌ Prediction Error: {str(e)}") |
|
|
|
|
|
|
|
|
st.subheader("📊 Feature Importance") |
|
|
|
|
|
if 'model' in model_components: |
|
|
try: |
|
|
feature_names = model_components['feature_names'] |
|
|
feature_importance = model_components['model'].feature_importances_ |
|
|
|
|
|
importance_df = pd.DataFrame({ |
|
|
'Feature': feature_names, |
|
|
'Importance': feature_importance |
|
|
}).sort_values('Importance', ascending=True) |
|
|
|
|
|
fig_importance = px.bar( |
|
|
importance_df, |
|
|
x='Importance', |
|
|
y='Feature', |
|
|
orientation='h', |
|
|
title='Feature Importance in Decision Tree Model', |
|
|
color='Importance', |
|
|
color_continuous_scale='viridis' |
|
|
) |
|
|
fig_importance.update_layout(height=400, margin=dict(l=20, r=20, t=40, b=20)) |
|
|
st.plotly_chart(fig_importance, use_container_width=True) |
|
|
|
|
|
except Exception as e: |
|
|
st.error(f"Error displaying feature importance: {str(e)}") |
|
|
|
|
|
|
|
|
if export_button: |
|
|
if 'last_prediction' in st.session_state: |
|
|
export_data = export_prediction( |
|
|
st.session_state['last_prediction']['input_data'], |
|
|
st.session_state['last_prediction']['result'] |
|
|
) |
|
|
|
|
|
st.download_button( |
|
|
label="📥 Download Prediction Results", |
|
|
data=export_data, |
|
|
file_name=f"RiskLevel_prediction_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json", |
|
|
mime="application/json" |
|
|
) |
|
|
else: |
|
|
st.warning("⚠️ No prediction results to export. Please make a prediction first.") |
|
|
|
|
|
|
|
|
st.markdown("---") |
|
|
st.markdown("*Built with Streamlit • Dr. Eng. Farrikh Alzami, M.Kom*") |