Spaces:
Runtime error
Runtime error
Upload 2 files
Browse files- pages/4_💃_Model_interpretation.py +123 -0
- pages/5_💲_Magenment_deck.py +87 -0
pages/4_💃_Model_interpretation.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import shap
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
import numpy as np
|
| 5 |
+
import joblib
|
| 6 |
+
import pandas as pd
|
| 7 |
+
from sklearn.metrics import roc_curve, roc_auc_score, precision_recall_curve, auc
|
| 8 |
+
|
| 9 |
+
st.set_page_config(page_title="Model Analysis Dashboard", layout="wide")
|
| 10 |
+
st.title("Model Analysis Dashboard")
|
| 11 |
+
|
| 12 |
+
st.markdown("""
|
| 13 |
+
This dashboard allows you to interactively explore the performance and interpretability of the trained logistic regression model.
|
| 14 |
+
- **Logistic Regression Coefficients:** See which features most strongly influence the model's predictions.
|
| 15 |
+
- **SHAP Analysis:** Understand both global and local feature importance using SHAP values.
|
| 16 |
+
- **ROC/PR Curves:** Evaluate the model's discrimination and precision-recall tradeoff.
|
| 17 |
+
|
| 18 |
+
Use the sidebar to select which plots to display and to adjust the number of features or the local sample for SHAP explanations.
|
| 19 |
+
""")
|
| 20 |
+
|
| 21 |
+
# --- Load model and test data ---
|
| 22 |
+
@st.cache_data
|
| 23 |
+
def load_model_and_data():
|
| 24 |
+
model = joblib.load("model_1mvp.pkl")
|
| 25 |
+
df_test = pd.read_csv("test_data.csv")
|
| 26 |
+
return model, df_test
|
| 27 |
+
|
| 28 |
+
model, df_test = load_model_and_data()
|
| 29 |
+
|
| 30 |
+
target = "y"
|
| 31 |
+
X_test = df_test.drop(columns=[target])
|
| 32 |
+
y_test = df_test[target]
|
| 33 |
+
|
| 34 |
+
preprocessor = model.named_steps["preprocessor"]
|
| 35 |
+
feature_names = preprocessor.get_feature_names_out()
|
| 36 |
+
X_test_transformed = preprocessor.transform(X_test)
|
| 37 |
+
|
| 38 |
+
# --- SHAP Explainer (precompute for efficiency) ---
|
| 39 |
+
explainer = shap.LinearExplainer(model.named_steps["classifier"], X_test_transformed, feature_names=feature_names)
|
| 40 |
+
shap_values = explainer.shap_values(X_test_transformed)
|
| 41 |
+
expected_value = explainer.expected_value
|
| 42 |
+
|
| 43 |
+
# --- Sidebar: Plot selection and controls ---
|
| 44 |
+
with st.sidebar.form("plot_selector"):
|
| 45 |
+
st.markdown("## Select plots to display")
|
| 46 |
+
show_coeff = st.checkbox("Logistic Regression Coefficients", value=True)
|
| 47 |
+
show_shap_global = st.checkbox("SHAP Global (summary plot)", value=True)
|
| 48 |
+
show_shap_local = st.checkbox("SHAP Local (waterfall plot)", value=False)
|
| 49 |
+
show_roc = st.checkbox("ROC/PR Curves", value=True)
|
| 50 |
+
top_n = st.slider("Number of top features for LogReg coeffecients", 5, 30, 15)
|
| 51 |
+
local_idx = st.number_input("Local SHAP sample index", min_value=0, max_value=len(X_test)-1, value=0)
|
| 52 |
+
submitted = st.form_submit_button("Update plots")
|
| 53 |
+
|
| 54 |
+
# --- Logistic Regression Coefficient Plot ---
|
| 55 |
+
if show_coeff and submitted:
|
| 56 |
+
st.header("Logistic Regression Coefficients")
|
| 57 |
+
logreg_model = model.named_steps["classifier"]
|
| 58 |
+
coefficients = logreg_model.coef_[0]
|
| 59 |
+
importance = pd.DataFrame({
|
| 60 |
+
"feature": feature_names,
|
| 61 |
+
"coefficient": coefficients
|
| 62 |
+
}).sort_values(by="coefficient", key=abs, ascending=False)
|
| 63 |
+
fig, ax = plt.subplots(figsize=(8, 6))
|
| 64 |
+
importance.head(top_n).set_index("feature")["coefficient"].plot(kind="barh", ax=ax, color="#4C72B0")
|
| 65 |
+
ax.set_title("Logistic Regression Feature Importance (Coefficients)")
|
| 66 |
+
ax.set_xlabel("Coefficient Value")
|
| 67 |
+
ax.set_ylabel("Feature")
|
| 68 |
+
st.pyplot(fig)
|
| 69 |
+
st.dataframe(importance.head(top_n).style.format({"coefficient": "{:.3f}"}))
|
| 70 |
+
|
| 71 |
+
# --- SHAP Analysis ---
|
| 72 |
+
if (show_shap_global or show_shap_local) and submitted:
|
| 73 |
+
st.header("SHAP Analysis")
|
| 74 |
+
if show_shap_global:
|
| 75 |
+
st.subheader("Global Feature Importance (SHAP Summary Plot)")
|
| 76 |
+
fig, ax = plt.subplots(figsize=(10, 6))
|
| 77 |
+
shap.summary_plot(shap_values, X_test_transformed, feature_names=feature_names, show=False)
|
| 78 |
+
st.pyplot(fig)
|
| 79 |
+
if show_shap_local:
|
| 80 |
+
st.subheader("Local Explanation (SHAP Waterfall Plot)")
|
| 81 |
+
fig2, ax2 = plt.subplots(figsize=(10, 6))
|
| 82 |
+
shap.plots.waterfall(
|
| 83 |
+
shap.Explanation(
|
| 84 |
+
values=shap_values[local_idx],
|
| 85 |
+
base_values=expected_value,
|
| 86 |
+
data=X_test_transformed[local_idx],
|
| 87 |
+
feature_names=feature_names
|
| 88 |
+
),
|
| 89 |
+
max_display=15,
|
| 90 |
+
show=False
|
| 91 |
+
)
|
| 92 |
+
st.pyplot(fig2)
|
| 93 |
+
|
| 94 |
+
# --- ROC and PR Curves ---
|
| 95 |
+
if show_roc and submitted:
|
| 96 |
+
st.header("Model Performance Metrics (ROC / PR Curves)")
|
| 97 |
+
y_pred_proba = model.predict_proba(X_test)[:, 1]
|
| 98 |
+
roc_auc = roc_auc_score(y_test, y_pred_proba)
|
| 99 |
+
fpr, tpr, _ = roc_curve(y_test, y_pred_proba)
|
| 100 |
+
precision, recall, _ = precision_recall_curve(y_test, y_pred_proba)
|
| 101 |
+
pr_auc = auc(recall, precision)
|
| 102 |
+
|
| 103 |
+
col1, col2 = st.columns(2)
|
| 104 |
+
with col1:
|
| 105 |
+
st.metric("ROC AUC", f"{roc_auc:.3f}")
|
| 106 |
+
with col2:
|
| 107 |
+
st.metric("PR AUC", f"{pr_auc:.3f}")
|
| 108 |
+
|
| 109 |
+
fig1, ax1 = plt.subplots(figsize=(5, 5))
|
| 110 |
+
ax1.plot(fpr, tpr, color="darkorange", lw=2, label=f"ROC curve (AUC = {roc_auc:.3f})")
|
| 111 |
+
ax1.plot([0, 1], [0, 1], color="navy", lw=2, linestyle="--", label="Random Guess")
|
| 112 |
+
ax1.set_xlabel("False Positive Rate")
|
| 113 |
+
ax1.set_ylabel("True Positive Rate")
|
| 114 |
+
ax1.set_title("ROC Curve")
|
| 115 |
+
ax1.legend()
|
| 116 |
+
st.pyplot(fig1)
|
| 117 |
+
|
| 118 |
+
fig2, ax2 = plt.subplots(figsize=(5, 5))
|
| 119 |
+
ax2.plot(recall, precision, color="#C44E52")
|
| 120 |
+
ax2.set_xlabel("Recall")
|
| 121 |
+
ax2.set_ylabel("Precision")
|
| 122 |
+
ax2.set_title("Precision-Recall Curve")
|
| 123 |
+
st.pyplot(fig2)
|
pages/5_💲_Magenment_deck.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import numpy as np
|
| 4 |
+
import joblib
|
| 5 |
+
|
| 6 |
+
st.set_page_config(page_title="💲 Management Deck: Savings Analysis", layout="wide")
|
| 7 |
+
st.title("💲 Management Deck: Expected Savings of Model Decisions")
|
| 8 |
+
|
| 9 |
+
st.markdown("""
|
| 10 |
+
This page estimates the **expected savings** from using the model to decide which customers to call in a marketing campaign.
|
| 11 |
+
|
| 12 |
+
- **Savings (True Negative):** The amount saved for each customer the model correctly identifies as not worth calling (i.e., calls avoided).
|
| 13 |
+
- **Cost of missing a true subscriber (False Negative):** The loss incurred when the model fails to identify a customer who would have subscribed.
|
| 14 |
+
|
| 15 |
+
You can adjust these business parameters in the sidebar. The analysis shows the expected savings at the current threshold and how savings change as you vary the decision threshold. The optimal threshold is highlighted in the plot.
|
| 16 |
+
""")
|
| 17 |
+
|
| 18 |
+
# --- Load model and test data ---
|
| 19 |
+
@st.cache_data
|
| 20 |
+
def load_model_and_data():
|
| 21 |
+
model = joblib.load("model_1mvp.pkl")
|
| 22 |
+
df_test = pd.read_csv("test_data.csv")
|
| 23 |
+
return model, df_test
|
| 24 |
+
|
| 25 |
+
model, df_test = load_model_and_data()
|
| 26 |
+
target = "y"
|
| 27 |
+
X_test = df_test.drop(columns=[target])
|
| 28 |
+
y_test = df_test[target]
|
| 29 |
+
|
| 30 |
+
# --- Sidebar: Cost selection ---
|
| 31 |
+
with st.sidebar.form("cost_selector"):
|
| 32 |
+
st.markdown("## Set Business Parameters")
|
| 33 |
+
cost_per_call = st.number_input("Savings (True negative)", min_value=0.0, value=29.7, step=1.0)
|
| 34 |
+
cost_per_missed_subscriber = st.number_input("Cost of missing a true subscriber (FN)", min_value=0.0, value=300.0, step=1.0)
|
| 35 |
+
submitted = st.form_submit_button("Update Savings Analysis")
|
| 36 |
+
|
| 37 |
+
if submitted:
|
| 38 |
+
st.subheader("Expected Savings Based on Model Predictions")
|
| 39 |
+
|
| 40 |
+
# Predict probabilities
|
| 41 |
+
y_pred_proba = model.predict_proba(X_test)[:, 1]
|
| 42 |
+
threshold = 0.5
|
| 43 |
+
|
| 44 |
+
# Prepare a DataFrame for calculations
|
| 45 |
+
df_result = X_test.copy()
|
| 46 |
+
df_result["y"] = y_test.values
|
| 47 |
+
df_result["y_pred_proba"] = y_pred_proba
|
| 48 |
+
df_result["campaign"] = df_test["campaign"].values # Make sure campaign is present
|
| 49 |
+
|
| 50 |
+
# Customers NOT called (predicted negative): y_pred_proba < threshold
|
| 51 |
+
df_temp = df_result[df_result["y_pred_proba"] <= threshold].copy()
|
| 52 |
+
total_campaigns = df_temp["campaign"].sum()
|
| 53 |
+
missed_subscribers = (df_temp["y"] == 1).sum()
|
| 54 |
+
|
| 55 |
+
expected_savings = total_campaigns * cost_per_call - missed_subscribers * cost_per_missed_subscriber
|
| 56 |
+
|
| 57 |
+
st.markdown(f"""
|
| 58 |
+
**Threshold:** {threshold:.2f}
|
| 59 |
+
- **Calls avoided (sum of campaigns):** {total_campaigns} × {cost_per_call} = {total_campaigns * cost_per_call:.2f}
|
| 60 |
+
- **Missed subscribers:** {missed_subscribers} × {cost_per_missed_subscriber} = {missed_subscribers * cost_per_missed_subscriber:.2f}
|
| 61 |
+
---
|
| 62 |
+
## **Expected Savings: {expected_savings:,.2f}**
|
| 63 |
+
""")
|
| 64 |
+
|
| 65 |
+
# Show savings as a function of threshold
|
| 66 |
+
st.subheader("Expected Savings vs. Threshold")
|
| 67 |
+
thresholds = np.linspace(0, 1, 120)
|
| 68 |
+
savings = []
|
| 69 |
+
for t in thresholds:
|
| 70 |
+
df_temp_t = df_result[df_result["y_pred_proba"] <= t].copy()
|
| 71 |
+
total_campaigns_t = df_temp_t["campaign"].sum()
|
| 72 |
+
missed_subscribers_t = (df_temp_t["y"] == 1).sum()
|
| 73 |
+
savings.append(total_campaigns_t * cost_per_call - missed_subscribers_t * cost_per_missed_subscriber)
|
| 74 |
+
import matplotlib.pyplot as plt
|
| 75 |
+
fig, ax = plt.subplots(figsize=(8, 4))
|
| 76 |
+
ax.plot(thresholds, savings, label="Expected Savings")
|
| 77 |
+
ax.axvline(threshold, color="red", linestyle="--", label=f"Current threshold = {threshold:.2f}")
|
| 78 |
+
# Find and plot optimal threshold
|
| 79 |
+
t_star = float(thresholds[np.argmax(savings)])
|
| 80 |
+
ax.axvline(t_star, color="r", linestyle="--", label=f"Optimal Threshold = {t_star:.2f}")
|
| 81 |
+
ax.set_xlabel("Threshold")
|
| 82 |
+
ax.set_ylabel("Expected Savings")
|
| 83 |
+
ax.set_title("Expected Savings vs. Classification Threshold")
|
| 84 |
+
ax.legend()
|
| 85 |
+
st.pyplot(fig)
|
| 86 |
+
|
| 87 |
+
st.caption("You can adjust the business parameters in the sidebar to see their impact on expected savings and optimal threshold.")
|