दिन 60: Project 5: App Deploy और Final Report
✅ आज आप सीखेंगे
- सारे data पर final model फिर train करना
- कारणों और actions के साथ monthly call list बनाना
- Streamlit app और JSON API देना
- Final report और model card लिखना
- 10 slides और 2 मिनट में project present करना
🏢 असली दुनिया में
Budget मंज़ूर! अगले महीने से retention team आपके model से चुने 600 customers को call करेगी। आज आप इसे ship करेंगे: model file, हर महीने की 1 तारीख़ को चलने वाली scoring script, agents के लिए app, CRM के लिए API, और management को सब समझाने वाली report।
हम क्या ship कर रहे हैं
Step 1: final model और model card
import numpy as np
import pandas as pd
def load_churn(path="telecom_churn.csv"):
# "None" means no internet plan – stop pandas from turning it into NaN
df = pd.read_csv(path, keep_default_na=False, na_values=[""])
df["total_charges"] = df["total_charges"].fillna(df["monthly_charges"] * df["tenure_months"])
df["churned"] = (df["churn"] == "Yes").astype(int)
return df
def add_features(df):
df = df.copy()
df["tenure_band"] = pd.cut(df["tenure_months"], [0, 6, 12, 24, 48, 72], labels=["0-6", "7-12", "13-24", "25-48", "49-72"]).astype(str)
df["is_new"] = (df["tenure_months"] <= 6).astype(int)
df["has_complaint"] = (df["complaints"] > 0).astype(int)
df["charge_per_gb"] = (df["monthly_charges"] / df["data_usage_gb"].clip(lower=1)).round(2)
df["avg_monthly_paid"] = (df["total_charges"] / df["tenure_months"]).round(1)
df["monthly_no_support"] = ((df["contract"] == "Monthly") & (df["tech_support"] == "No")).astype(int)
return df
# original columns – the champion model uses these
NUM = ["senior_citizen", "tenure_months", "monthly_charges", "total_charges", "complaints", "data_usage_gb"]
CAT = ["gender", "contract", "internet_service", "tech_support", "payment_method"]
# original + engineered columns (tested on Day 58)
NUM_ALL = NUM + ["is_new", "has_complaint", "charge_per_gb", "avg_monthly_paid", "monthly_no_support"]
CAT_ALL = CAT + ["tenure_band"]
import json
import joblib
import sklearn
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score, StratifiedKFold
df = load_churn()
X, y = df[NUM + CAT], df["churned"]
model = Pipeline([("prep", ColumnTransformer([("num", StandardScaler(), NUM), ("cat", OneHotEncoder(handle_unknown="ignore"), CAT)])),
("clf", LogisticRegression(C=0.1, max_iter=5000))])
cv_auc = cross_val_score(model, X, y, cv=StratifiedKFold(5, shuffle=True, random_state=0), scoring="roc_auc")
model.fit(X, y) # final model uses ALL 3,000 customers
joblib.dump(model, "churn_model_v1.joblib")
card = {"name": "TeleOne churn model", "version": "1.0.0", "algorithm": "logistic regression (C=0.1)",
"trained_on": f"{len(X)} customers", "features": NUM + CAT,
"cv_roc_auc": f"{cv_auc.mean():.3f} ± {cv_auc.std():.3f}", "locked_test_roc_auc_day59": 0.832,
"intended_use": "rank customers for retention calls; not for pricing or credit decisions",
"monitoring": "monthly PSI on inputs, quarterly AUC on new outcomes, retrain if AUC < 0.78",
"sklearn_version": sklearn.__version__}
json.dump(card, open("model_card.json", "w", encoding="utf-8"), indent=2, ensure_ascii=False)
print(json.dumps(card, indent=2, ensure_ascii=False))बंद test set ने कल अपना काम कर दिया: उसने बताया कि method कितना अच्छा है। Production के लिए हम चाहते हैं कि model हर customer से सीखे, इसलिए वही pipeline और settings सभी 3,000 rows पर फिर fit करते हैं – कोई नई tuning नहीं।
Step 2: monthly risk list
import joblib
import numpy as np
import pandas as pd
def load_churn(path="telecom_churn.csv"):
df = pd.read_csv(path, keep_default_na=False, na_values=[""])
df["total_charges"] = df["total_charges"].fillna(df["monthly_charges"] * df["tenure_months"])
return df
model = joblib.load("churn_model_v1.joblib")
pre, clf = model[0], model[-1]
names = pre.get_feature_names_out()
ACTIONS = {"contract_Monthly": "Offer yearly plan at 15% off", "tech_support_No": "Add 3 months free tech support",
"complaints": "Senior agent callback on open complaints", "tenure_months": "Welcome-back / onboarding help",
"monthly_charges": "Review plan – suggest a better-value pack", "internet_service_Fiber": "Fiber quality check visit"}
def label(j, Z, mean):
key = names[j].split("__", 1)[1]
if names[j].startswith("num__"):
if key == "senior_citizen":
return key, "senior citizen"
return key, ("high " if Z[j] > mean[j] else "short " if key == "tenure_months" else "low ") + key.replace("_", " ")
return key, key.replace("_", " ", 1).replace("_", " ")
def explain(Z, mean):
contrib = (Z - mean) * clf.coef_[0]
top = [label(j, Z, mean) for j in np.argsort(contrib)[::-1][:3] if contrib[j] > 0]
action = next((ACTIONS[k] for k, _ in top if k in ACTIONS), "Standard loyalty offer")
return ", ".join(t for _, t in top), action
def monthly_risk_list(customers, capacity=600):
X = customers[model.feature_names_in_]
p = model.predict_proba(X)[:, 1]
Z = pre.transform(X); Z = Z.toarray() if hasattr(Z, "toarray") else Z
mean = np.asarray(Z.mean(axis=0)).ravel()
out = customers[["customer_id", "contract", "tenure_months", "monthly_charges"]].copy()
out["churn_risk"] = p.round(3)
out["risk_band"] = pd.cut(p, [0, 0.3, 0.6, 1], labels=["Low", "Medium", "High"])
expl = [explain(Z[i], mean) for i in range(len(out))]
out["top_reasons"] = [e[0] for e in expl]; out["suggested_action"] = [e[1] for e in expl]
out["value_at_risk_₹"] = (out["churn_risk"] * out["monthly_charges"] * 12).round(0)
return out.sort_values("value_at_risk_₹", ascending=False).head(capacity)
active = load_churn().query("churn == 'No'") # this month: score customers who are still with us
risk = monthly_risk_list(active, capacity=600)
risk.to_csv("risk_list_this_month.csv", index=False)
print(f"Scored {len(active):,} active customers → call list of {len(risk)} saved to risk_list_this_month.csv")
print(risk.head(6).to_string(index=False))
print("\nRisk bands in the call list:", risk["risk_band"].value_counts().to_dict())
hm = risk[risk["risk_band"].isin(["High", "Medium"])]
print(f"High + Medium: {len(hm)} customers | avg risk {hm['churn_risk'].mean():.0%} | value at risk ₹{hm['value_at_risk_₹'].sum() / 1e5:.1f} lakh/yr")हम सिर्फ़ risk से नहीं, churn risk × सालाना bill से sort करते हैं। 60% risk वाला ₹1,200/महीना customer 70% वाले ₹199 customer से ज़्यादा मायने रखता है। यह एक line model को revenue बचाने के business लक्ष्य से जोड़ती है।
# crontab -e 0 6 1 * * cd /opt/teleone-churn && /usr/bin/python3 score_customers.py >> logs/score.log 2>&1
Step 3: retention app
# app_retention.py – run: streamlit run app_retention.py
import joblib
import pandas as pd
import streamlit as st
st.set_page_config(page_title="TeleOne Retention", page_icon="📞", layout="wide")
model = joblib.load("churn_model_v1.joblib")
st.title("📞 TeleOne – Retention Assistant")
tab1, tab2 = st.tabs(["This month’s call list", "Check one customer"])
with tab1:
risk = pd.read_csv("risk_list_this_month.csv")
band = st.multiselect("Risk band", ["High", "Medium", "Low"], default=["High", "Medium"])
view = risk[risk["risk_band"].isin(band)]
c1, c2, c3 = st.columns(3)
c1.metric("Customers to call", len(view))
c2.metric("Avg risk", f"{view['churn_risk'].mean():.0%}")
c3.metric("Value at risk", f"₹{view['value_at_risk_₹'].sum() / 1e5:.1f} lakh / yr")
st.dataframe(view, use_container_width=True, hide_index=True)
st.download_button("Download CSV for the dialler", view.to_csv(index=False), "call_list.csv")
with tab2:
a, b = st.columns(2)
row = {"senior_citizen": int(a.checkbox("Senior citizen")),
"tenure_months": a.slider("Tenure (months)", 1, 72, 6),
"monthly_charges": a.number_input("Monthly bill (₹)", 149, 1500, 850),
"complaints": a.slider("Complaints (last 6 months)", 0, 8, 2),
"data_usage_gb": a.number_input("Data usage (GB/month)", 0.0, 300.0, 40.0),
"gender": b.selectbox("Gender", ["Female", "Male"]),
"contract": b.selectbox("Contract", ["Monthly", "One year", "Two year"]),
"internet_service": b.selectbox("Internet", ["Fiber", "DSL", "None"]),
"tech_support": b.selectbox("Tech support", ["No", "Yes", "No internet"]),
"payment_method": b.selectbox("Payment", ["UPI AutoPay", "Credit Card", "Bank Transfer", "Cash"])}
row["total_charges"] = row["monthly_charges"] * row["tenure_months"]
p = model.predict_proba(pd.DataFrame([row]))[0, 1]
st.metric("Churn risk", f"{p:.0%}")
st.progress(float(p))
if p >= 0.6:
st.error("High risk – call today with a retention offer")
elif p >= 0.3:
st.warning("Medium risk – include in this month’s campaign")
else:
st.success("Low risk – no action needed")<div style="font-family:system-ui,sans-serif;padding:12px;background:#fff"> <h3 style="margin:0 0 8px">📞 TeleOne – Retention Assistant</h3> <div style="display:flex;gap:6px;margin-bottom:10px"><span style="padding:5px 10px;border-bottom:2px solid #ff4b4b;font-weight:600">This month’s call list</span><span style="padding:5px 10px;color:#6b7280">Check one customer</span></div> <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:10px"> <div style="background:#f8fafc;border-radius:8px;padding:8px"><small>Customers to call</small><div style="font-size:22px;font-weight:700">463</div></div> <div style="background:#f8fafc;border-radius:8px;padding:8px"><small>Avg risk</small><div style="font-size:22px;font-weight:700">49%</div></div> <div style="background:#f8fafc;border-radius:8px;padding:8px"><small>Value at risk</small><div style="font-size:22px;font-weight:700">₹22.5 lakh / yr</div></div></div> <table style="width:100%;border-collapse:collapse;font-size:12px"><tr style="background:#f1f5f9"><th align="left">customer</th><th>risk</th><th align="left">top reasons</th><th align="left">suggested action</th></tr> <tr><td>CUST12993</td><td align="center"><b style="color:#dc2626">85%</b></td><td>high monthly charges, contract Monthly, short tenure</td><td>Review plan – better-value pack</td></tr> <tr><td>CUST10745</td><td align="center"><b style="color:#dc2626">81%</b></td><td>high complaints, high monthly charges, contract Monthly</td><td>Senior agent callback on complaints</td></tr> <tr><td>CUST10268</td><td align="center"><b style="color:#dc2626">80%</b></td><td>contract Monthly, high monthly charges, high complaints</td><td>Offer yearly plan at 15% off</td></tr></table> <button style="margin-top:10px;background:#ff4b4b;color:#fff;border:0;padding:7px 12px;border-radius:8px">Download CSV for the dialler</button></div>
Step 4: CRM API
import joblib, pandas as pd
from flask import Flask, request, jsonify
app = Flask(__name__)
model = joblib.load("churn_model_v1.joblib")
FIELDS = list(model.feature_names_in_)
@app.post("/score")
def score():
rows = request.get_json(silent=True)
rows = rows if isinstance(rows, list) else [rows]
bad = [i for i, r in enumerate(rows) if not isinstance(r, dict) or set(FIELDS) - set(r)]
if bad:
return jsonify(error=f"rows {bad} are missing fields", required=FIELDS), 400
p = model.predict_proba(pd.DataFrame(rows)[FIELDS])[:, 1]
return jsonify([{"churn_risk": round(float(x), 3), "call": bool(x >= 0.3)} for x in p])
client = app.test_client()
cust = {"senior_citizen": 0, "tenure_months": 3, "monthly_charges": 950, "total_charges": 2850, "complaints": 2,
"data_usage_gb": 35.0, "gender": "Female", "contract": "Monthly", "internet_service": "Fiber",
"tech_support": "No", "payment_method": "Cash"}
loyal = dict(cust, tenure_months=60, contract="Two year", tech_support="Yes", complaints=0, total_charges=57000)
r = client.post("/score", json=[cust, loyal]); print(r.status_code, r.get_json())
r = client.post("/score", json={"tenure_months": 5}); print(r.status_code, r.get_json()["error"])Step 5: GitHub repository
teleone-churn/ ├── README.md ← business summary first (template below) ├── PROJECT_CHARTER.md ← Day 56 ├── requirements.txt ├── data/sample_customers.csv ├── notebooks/ │ ├── 01_problem_and_audit.ipynb (Day 56) │ ├── 02_eda_insights.ipynb (Day 57) │ ├── 03_features_models.ipynb (Day 58) │ └── 04_evaluation_impact.ipynb (Day 59) ├── src/ churn.py (load_churn, add_features, make_model) ├── train_final.py score_customers.py api.py app_retention.py ├── models/ churn_model_v1.joblib model_card.json └── reports/ figures/ final_report.md slides.pdf
Step 6: final report
# TeleOne Churn Prediction – Final Report ## 1. Problem 29% of customers churned, taking ≈ ₹6.6 lakh of monthly revenue (≈ ₹80 lakh a year). The retention team can call ~600 customers a month and needs to know whom to call and why. ## 2. What drives churn (EDA, Day 57) - Monthly contracts churn ~44% vs ~9% for two-year contracts (largest effect). - First 6 months, multiple complaints, no tech support and Fiber plans raise risk. - Gender has no effect; payment method differences were not significant. ## 3. Model (Days 58–59) - Logistic regression on the 11 original columns (engineered features added nothing). - 5-fold CV ROC-AUC ≈ 0.82; locked test ROC-AUC 0.83 – no sign of leakage or overfitting. - Well calibrated; similar performance for women and men. ## 4. Business impact (agreed assumptions) - Calling the top 20% reaches 2.3× more churners than random calling. - Expected net ≈ ₹12 lakh per monthly campaign vs ≈ ₹3 lakh for random calls. ## 5. Recommendations 1. Use the monthly risk list (sorted by value at risk) for retention calls. 2. Bundle free tech support for monthly Fiber customers; push yearly-plan upgrades. 3. Run a 2-month A/B pilot (call vs hold-out) to measure the real save-rate. 4. Monitor input drift monthly; retrain when AUC on new outcomes < 0.78. ## 6. Limitations Simulated data, single snapshot, save-rate and offer acceptance are assumptions.
Step 7: present करें
| Slide | सामग्री |
|---|---|
| 1 | Title + एक line नतीजा: “उन्हीं calls से 2.3× ज़्यादा churners तक पहुँचें” |
| 2 | ₹ में समस्या: 29% churn, साल में ₹80 lakh |
| 3 | कौन churn करता है: contract × tenure heatmap |
| 4 | Top 5 drivers (lift chart) |
| 5 | तरीका: data → model → risk list (एक diagram) |
| 6 | Model quality: test AUC, gains chart |
| 7 | Business impact: model vs random calling |
| 8 | Demo: retention app |
| 9 | सुझाव + A/B pilot plan |
| 10 | सीमाएँ, monitoring, अगले कदम |
“TeleOne was losing 29% of its customers – about ₹80 lakh a year – and the retention team could only call 600 people a month. I framed it as a classification problem, audited the data (and caught a silent NaN bug), and found in EDA that monthly contracts, the first six months, complaints and missing tech support drive churn. I compared five models with cross-validation; plain logistic regression won with AUC 0.83 on a locked test set, and my engineered features did not help, so I kept it simple. Using agreed costs, calling the top 20% reaches 2.3× more churners than random calls – about ₹12 lakh net per campaign. I shipped a monthly risk list with reasons and suggested offers, a Streamlit app for agents and a Flask API, and proposed an A/B pilot to measure the real save-rate.”
आपने कर दिखाया 🎓
इस project को public dataset (Kaggle पर IBM Telco churn) या अपने domain पर दोहराएँ, README और app के साथ GitHub पर publish करें, हर हफ़्ते LinkedIn पर एक insight post करें, और Day 54 की योजना से apply करना शुरू करें। बनाते रहें – हर महीने एक छोटा project आपकी skills तेज़ रखता है।
🏅 आज के Job Skills
Practice tasks
🏋️ Practice – ख़ुद करें
Solution देखें
import joblib, pandas as pd
m = joblib.load("churn_model_v1.joblib")
df = pd.read_csv("telecom_churn.csv", keep_default_na=False, na_values=[""]).query("churn == 'No'").copy()
df["total_charges"] = df["total_charges"].fillna(df["monthly_charges"] * df["tenure_months"])
band = lambda p: pd.cut(p, [0, 0.3, 0.6, 1], labels=["Low", "Medium", "High"])
now = band(m.predict_proba(df[m.feature_names_in_])[:, 1])
df["tenure_months"] = (df["tenure_months"] + 1).clip(upper=72); df["total_charges"] += df["monthly_charges"]
later = band(m.predict_proba(df[m.feature_names_in_])[:, 1])
print((now != later).sum(), "customers change band;", pd.crosstab(now, later))Quick quiz
🧠 Quick Quiz – ख़ुद को जाँचें
Interview corner
💼 Interview Corner
अपना end-to-end project बताइए।
ऊपर वाला 2-मिनट pitch: rupees में समस्या, data audit, मुख्य EDA drivers, ईमानदार model तुलना (सरल model जीता), test AUC 0.83, business impact 2.3× ज़्यादा churners, risk list + app + API दिए, और अगला कदम A/B pilot।
ज़्यादा समय या data होता तो क्या अलग करते?
Time-to-churn model करने के लिए कई monthly snapshots लेते, experiments से offer uplift मापते (uplift modelling), usage और support-ticket text features जोड़ते, ज़्यादा data पर boosting से फिर तुलना करते, और monitoring alerts के साथ retraining automate करते।
सारांश
- Final model सारे data पर फिर train, model card के साथ version किया।
- Value at risk से ranked monthly risk list, कारणों और actions के साथ।
- Agents के लिए Streamlit app, CRM के लिए Flask API।
- Report + 10-slide deck + 2-मिनट pitch = job-ready portfolio project।
- 60-दिन का Data Science course पूरा करने पर बधाई! 🎉
Course पूरा! आगे: हमारे HTML, CSS और JavaScript day-wise courses से websites बनाएँ।
