valofils's picture
Fix: pre-trained models + .gitignore
9cea399
Raw
History Blame Contribute Delete
10.8 kB
import streamlit as st
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.ticker as mtick
import seaborn as sns
import joblib
import os
from sklearn.metrics import precision_recall_curve
st.set_page_config(page_title="Customer Churn Predictor", page_icon="πŸ“‰", layout="wide")
COLORS = {"Logistic Regression": "#888780", "Random Forest": "#5DCAA5", "XGBoost": "#D85A30"}
BASE = os.path.dirname(__file__)
@st.cache_resource
def load_models():
return joblib.load(os.path.join(BASE, "churn_models.pkl"))
@st.cache_data
def load_test_data():
return joblib.load(os.path.join(BASE, "churn_test_data.pkl"))
# ── Load instantly ──
results = load_models()
data = load_test_data()
X_test = data["X_test"]
y_test = data["y_test"]
best_name = data["best_name"]
df = data["df"]
y = data["y"]
X = data["X"]
# ── Sidebar ──
st.sidebar.title("πŸ“‰ Navigation")
page = st.sidebar.radio("Go to", [
"🏠 Overview", "πŸ“Š Model Comparison", "πŸ’° Business Impact", "πŸ”¬ Predict"
])
# ── PAGE 1 – Overview ──
if page == "🏠 Overview":
st.title("πŸ“‰ Customer Churn Prediction")
st.markdown(
"End-to-end churn prediction on the **IBM Telco dataset** using "
"Logistic Regression, Random Forest, and XGBoost β€” with business impact analysis."
)
c1, c2, c3, c4 = st.columns(4)
c1.metric("Customers", len(df))
c2.metric("Churn Rate", f"{y.mean():.1%}")
c3.metric("Features", len(X.columns))
c4.metric("Best AUC", f"{results[best_name]['roc_auc']:.3f} ({best_name})")
col1, col2 = st.columns(2)
with col1:
st.subheader("Churn Distribution")
fig, ax = plt.subplots(figsize=(5, 3))
counts = df["Churn"].value_counts()
ax.bar(counts.index, counts.values, color=["#5DCAA5", "#D85A30"])
ax.set_ylabel("Customers")
st.pyplot(fig); plt.close()
with col2:
st.subheader("Monthly Charges by Churn")
fig, ax = plt.subplots(figsize=(5, 3))
sns.boxplot(data=df, x="Churn", y="MonthlyCharges",
palette={"No": "#5DCAA5", "Yes": "#D85A30"}, ax=ax)
st.pyplot(fig); plt.close()
st.subheader("Churn Rate by Contract Type")
contract_churn = df.groupby("Contract")["Churn"].apply(lambda x: (x == "Yes").mean())
fig, ax = plt.subplots(figsize=(6, 3))
contract_churn.sort_values().plot(kind="barh", ax=ax, color="#D85A30")
ax.xaxis.set_major_formatter(mtick.PercentFormatter(xmax=1))
st.pyplot(fig); plt.close()
# ── PAGE 2 – Model Comparison ──
elif page == "πŸ“Š Model Comparison":
st.title("πŸ“Š Model Comparison")
summary = pd.DataFrame([
{"Model": n, "ROC-AUC": r["roc_auc"]}
for n, r in results.items()
]).sort_values("ROC-AUC", ascending=False).reset_index(drop=True)
st.dataframe(summary.style.format({"ROC-AUC": "{:.4f}"}), use_container_width=True)
col1, col2 = st.columns(2)
with col1:
st.subheader("ROC Curves")
fig, ax = plt.subplots(figsize=(6, 5))
for name, res in results.items():
ax.plot(res["fpr"], res["tpr"],
label=f"{name} (AUC={res['roc_auc']})",
color=COLORS[name], linewidth=2)
ax.plot([0, 1], [0, 1], "k--")
ax.set_xlabel("False Positive Rate"); ax.set_ylabel("True Positive Rate")
ax.legend(fontsize=8)
st.pyplot(fig); plt.close()
with col2:
st.subheader("Confusion Matrix")
model_sel = st.selectbox("Select model", list(results.keys()))
fig, ax = plt.subplots(figsize=(5, 4))
sns.heatmap(results[model_sel]["cm"], annot=True, fmt="d", cmap="Blues",
xticklabels=["No Churn", "Churn"],
yticklabels=["No Churn", "Churn"], ax=ax)
ax.set_xlabel("Predicted"); ax.set_ylabel("Actual")
st.pyplot(fig); plt.close()
st.subheader(f"Threshold Analysis β€” {best_name}")
y_prob_best = results[best_name]["y_prob"]
precisions, recalls, thresholds = precision_recall_curve(y_test, y_prob_best)
f1s = 2 * precisions * recalls / (precisions + recalls + 1e-8)
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(thresholds, precisions[:-1], label="Precision", color="#5DCAA5", linewidth=2)
ax.plot(thresholds, recalls[:-1], label="Recall", color="#D85A30", linewidth=2)
ax.plot(thresholds, f1s[:-1], label="F1", color="#7F77DD", linewidth=2)
best_t = thresholds[np.argmax(f1s[:-1])]
ax.axvline(x=best_t, color="gray", linestyle="--", label=f"Best threshold={best_t:.2f}")
ax.set_xlabel("Threshold"); ax.legend(fontsize=9)
st.pyplot(fig); plt.close()
# ── PAGE 3 – Business Impact ──
elif page == "πŸ’° Business Impact":
st.title("πŸ’° Business Impact Analysis")
col1, col2 = st.columns(2)
cost_fn = col1.number_input("Cost per missed churner ($)", value=250, step=10)
cost_fp = col2.number_input("Cost per false retention offer ($)", value=15, step=5)
y_prob_best = results[best_name]["y_prob"]
baseline_cost = int(y_test.sum()) * cost_fn
def compute_savings(threshold):
y_pred = (y_prob_best >= threshold).astype(int)
fp = int(((y_pred == 1) & (y_test == 0)).sum())
fn = int(((y_pred == 0) & (y_test == 1)).sum())
return {"threshold": threshold, "savings": baseline_cost - fp * cost_fp - fn * cost_fn}
sweep_df = pd.DataFrame([compute_savings(t) for t in np.linspace(0.05, 0.95, 100)])
best_idx = sweep_df["savings"].idxmax()
best_t = sweep_df.loc[best_idx, "threshold"]
best_savings = sweep_df.loc[best_idx, "savings"]
c1, c2, c3 = st.columns(3)
c1.metric("Baseline cost (no model)", f"${baseline_cost:,}")
c2.metric("Max savings with model", f"${int(best_savings):,}")
c3.metric("Optimal threshold", f"{best_t:.2f}")
fig, ax = plt.subplots(figsize=(9, 4))
ax.plot(sweep_df["threshold"], sweep_df["savings"], color="#1D9E75", linewidth=2.5)
ax.axvline(x=best_t, color="#D85A30", linestyle="--", label=f"Optimal = {best_t:.2f}")
ax.axhline(y=0, color="gray", linewidth=1)
ax.fill_between(sweep_df["threshold"], sweep_df["savings"], 0,
where=sweep_df["savings"] > 0, alpha=0.15, color="#1D9E75")
ax.set_xlabel("Decision Threshold"); ax.set_ylabel("Net Savings ($)")
ax.legend(); ax.yaxis.set_major_formatter(mtick.FuncFormatter(lambda x, _: f"${x:,.0f}"))
st.pyplot(fig); plt.close()
st.subheader("Risk Segmentation")
res_df = X_test.copy().reset_index(drop=True)
res_df["churn_probability"] = y_prob_best
res_df["actual_churn"] = y_test.values
res_df["risk_tier"] = pd.cut(res_df["churn_probability"],
bins=[0, 0.35, 0.60, 1.0],
labels=["🟒 Low", "🟑 Medium", "πŸ”΄ High"])
tier_summary = res_df.groupby("risk_tier", observed=True).agg(
Customers=("churn_probability", "count"),
Avg_Churn_Prob=("churn_probability", "mean"),
Actual_Churn_Rate=("actual_churn", "mean"),
Avg_Monthly_Charges=("MonthlyCharges", "mean"),
).round(3)
st.dataframe(tier_summary, use_container_width=True)
# ── PAGE 4 – Predict ──
elif page == "πŸ”¬ Predict":
st.title("πŸ”¬ Live Churn Prediction")
model_sel = st.selectbox("Model", list(results.keys()),
index=list(results.keys()).index(best_name))
col1, col2, col3 = st.columns(3)
with col1:
st.subheader("Account Info")
tenure = st.slider("Tenure (months)", 0, 72, 12)
contract = st.selectbox("Contract", ["Month-to-month", "One year", "Two year"])
paperless = st.selectbox("Paperless Billing", ["Yes", "No"])
payment = st.selectbox("Payment Method", [
"Electronic check", "Mailed check",
"Bank transfer (automatic)", "Credit card (automatic)"])
with col2:
st.subheader("Services")
phone_service = st.selectbox("Phone Service", ["Yes", "No"])
multiple_lines = st.selectbox("Multiple Lines", ["Yes", "No", "No phone service"])
internet = st.selectbox("Internet Service", ["DSL", "Fiber optic", "No"])
online_security = st.selectbox("Online Security", ["Yes", "No", "No internet service"])
online_backup = st.selectbox("Online Backup", ["Yes", "No", "No internet service"])
device_prot = st.selectbox("Device Protection", ["Yes", "No", "No internet service"])
tech_support = st.selectbox("Tech Support", ["Yes", "No", "No internet service"])
streaming_tv = st.selectbox("Streaming TV", ["Yes", "No", "No internet service"])
streaming_movies = st.selectbox("Streaming Movies", ["Yes", "No", "No internet service"])
with col3:
st.subheader("Demographics & Charges")
gender = st.selectbox("Gender", ["Male", "Female"])
partner = st.selectbox("Partner", ["Yes", "No"])
dependents = st.selectbox("Dependents", ["Yes", "No"])
monthly_charges = st.slider("Monthly Charges ($)", 18.0, 120.0, 65.0)
total_charges = st.number_input("Total Charges ($)", value=float(tenure * monthly_charges))
if st.button("πŸ” Predict Churn", type="primary"):
input_df = pd.DataFrame([{
"tenure": tenure, "MonthlyCharges": monthly_charges,
"TotalCharges": total_charges,
"charges_per_tenure": monthly_charges / (tenure + 1),
"gender": gender, "Partner": partner, "Dependents": dependents,
"PhoneService": phone_service, "MultipleLines": multiple_lines,
"InternetService": internet, "OnlineSecurity": online_security,
"OnlineBackup": online_backup, "DeviceProtection": device_prot,
"TechSupport": tech_support, "StreamingTV": streaming_tv,
"StreamingMovies": streaming_movies, "Contract": contract,
"PaperlessBilling": paperless, "PaymentMethod": payment,
}])
prob = results[model_sel]["pipe"].predict_proba(input_df)[0][1]
tier = "πŸ”΄ High Risk" if prob >= 0.60 else "🟑 Medium Risk" if prob >= 0.35 else "🟒 Low Risk"
st.markdown("---")
c1, c2, c3 = st.columns(3)
c1.metric("Churn Probability", f"{prob:.1%}")
c2.metric("Risk Tier", tier)
c3.metric("Model", model_sel)
fig, ax = plt.subplots(figsize=(5, 2.5))
ax.barh(["No Churn", "Churn"], [(1-prob)*100, prob*100], color=["#5DCAA5", "#D85A30"])
ax.set_xlabel("Probability (%)"); ax.set_xlim(0, 100)
st.pyplot(fig); plt.close()
st.caption("⚠️ For business decision support only.")