Spaces:
Paused
Paused
| 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__) | |
| def load_models(): | |
| return joblib.load(os.path.join(BASE, "churn_models.pkl")) | |
| 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.") | |