Free tutorials & notes in Hindi & English · Clean code examples 🧰 Free PDF & Image Tools →
🏆 Major Project – Churn Prediction  ·  Day 56

Day 56: Project 1: Business Problem & Data

⏱ 3 hours📶 Major project · Part 1 of 5🎯 100% self-study
Day 56 / 60

✅ Today you will learn

  • Understand why churn costs a telecom company crores
  • Turn a business goal into an ML problem
  • Audit the data and catch hidden traps
  • Measure the baseline: churn rate and revenue at risk
  • Write the project charter and success metrics

🏢 In the real world

TeleOne, a mid-size Indian telecom and broadband provider, is losing almost 3 of every 10 customers a year. Getting a new customer costs about five times more than keeping one. The Head of Retention asks you: “Every month, give my team a list of customers who are about to leave and tell us why – so we call the right people with the right offer.” Over the next five days you will deliver exactly that, end to end.

The 5-day project plan

  1. 1Day 56 – business problem, data audit, baseline
  2. 2Day 57 – EDA, statistical tests, insights
  3. 3Day 58 – features, pipelines, model comparison
  4. 4Day 59 – evaluation, threshold, ₹ impact, explanations
  5. 5Day 60 – deployment, risk list, report and presentation
The same workflow real data teams follow

Step 1: understand the business

📉
What is churn?A customer cancels or does not renew. Here: the churn column = left during the observation period.
💰
Why it mattersEach lost customer takes their monthly bill with them – and acquiring a replacement costs ~5× more.
🎯
The actionRetention team calls high-risk customers and offers a discount, upgrade or yearly plan.
📏
SuccessMore customers saved per rupee of offer budget than the current random calling.
The business in four cards

Step 2: frame it as an ML problem

QuestionAnswer for this project
Target (y)churn = Yes / No → binary classification
UnitOne row = one customer
Prediction momentStart of each month, using data known on that day
OutputProbability of churn + top reasons per customer
Who uses itRetention call-centre team (≈ 600 calls/month capacity)
Main metricROC-AUC + recall in the top 20% riskiest customers
Business metricNet ₹ saved = revenue retained − offer cost
⚠️ Leakage check before anything else

Ask for each column: “Is this known before the customer decides to leave?” Columns like “cancellation_reason” or “final_bill_date” would be leakage. In our data all columns describe the customer before churn – but always ask.

Step 3: audit the data

Python
import pandas as pd
raw = pd.read_csv("telecom_churn.csv")
print(raw.shape)
print(raw.dtypes.astype(str).to_dict())
print("Missing values:\n", raw.isna().sum()[raw.isna().sum() > 0])
print("internet_service values:", raw["internet_service"].value_counts(dropna=False).to_dict())
(3000, 13) {'customer_id': 'object', 'gender': 'object', 'senior_citizen': 'int64', 'tenure_months': 'int64', 'contract': 'object', 'internet_service': 'object', 'tech_support': 'object', 'payment_method': 'object', 'monthly_charges': 'float64', 'total_charges': 'float64', 'complaints': 'int64', 'data_usage_gb': 'float64', 'churn': 'object'} Missing values: internet_service 625 total_charges 25 dtype: int64 internet_service values: {'Fiber': 1327, 'DSL': 1048, nan: 625}
🕵️ The hidden trap

625 customers seem to have a missing internet service – but they are not missing! Their value is the text “None” (no internet plan), and pandas converts “None” to NaN by default. Dropping or filling them blindly would corrupt 21% of the data.

Python
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

df = load_churn()
print(df["internet_service"].value_counts().to_dict())
print("Missing after load:", int(df.isna().sum().sum()))
print("Duplicates:", df["customer_id"].duplicated().sum())
check = (df["tech_support"] == "No internet") == (df["internet_service"] == "None")
print("tech_support consistent with internet_service:", bool(check.all()))
{'Fiber': 1327, 'DSL': 1048, 'None': 625} Missing after load: 0 Duplicates: 0 tech_support consistent with internet_service: True
Python
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

df = load_churn()
rules = {"tenure 1–72 months": df["tenure_months"].between(1, 72).all(),
         "monthly charges > 0": (df["monthly_charges"] > 0).all(),
         "total ≈ monthly × tenure": (abs(df["total_charges"] - df["monthly_charges"] * df["tenure_months"]) / df["total_charges"] < 0.06).mean() > 0.95,
         "complaints ≥ 0": (df["complaints"] >= 0).all(),
         "churn only Yes/No": set(df["churn"]) == {"Yes", "No"}}
for rule, ok in rules.items():
    print(("✅" if ok else "❌"), rule)
✅ tenure 1–72 months ✅ monthly charges > 0 ✅ total ≈ monthly × tenure ✅ complaints ≥ 0 ✅ churn only Yes/No

Data dictionary

ColumnTypeMeaning
customer_ididUnique customer code
gender, senior_citizencategory / 0-1Demographics
tenure_monthsnumberMonths with TeleOne (1–72)
contractcategoryMonthly / One year / Two year
internet_servicecategoryFiber / DSL / None
tech_supportcategoryYes / No / No internet
payment_methodcategoryUPI AutoPay / Credit Card / Bank Transfer / Cash
monthly_charges, total_charges₹Current bill and lifetime spend
complaintscountComplaints in the last 6 months
data_usage_gbnumberAverage monthly data
churntargetYes = left

Step 4: measure the baseline

Python
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

df = load_churn()
churners = df[df["churned"] == 1]
rate = df["churned"].mean()
monthly_lost = churners["monthly_charges"].sum()
print(f"Customers: {len(df):,} | churned: {len(churners):,} | churn rate: {rate:.1%}")
print(f"Monthly revenue walking out: ₹{monthly_lost:,.0f}  → per year ≈ ₹{monthly_lost * 12 / 1e5:.1f} lakh")
print(f"Average bill – churners ₹{churners['monthly_charges'].mean():.0f} vs stayers ₹{df.loc[df['churned'] == 0, 'monthly_charges'].mean():.0f}")
Customers: 3,000 | churned: 874 | churn rate: 29.1% Monthly revenue walking out: ₹663,644 → per year ≈ ₹79.6 lakh Average bill – churners ₹759 vs stayers ₹636
Python
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

import matplotlib.pyplot as plt
df = load_churn()
fig, ax = plt.subplots(1, 2, figsize=(11, 3.8))
c = df["churn"].value_counts()
ax[0].pie(c, labels=[f"{k} ({v:,})" for k, v in c.items()], colors=["#94a3b8", "#dc2626"], autopct="%1.1f%%", startangle=90, wedgeprops=dict(width=0.45))
ax[0].set_title("29% of customers churned")
rev = df.groupby("churn")["monthly_charges"].sum() / 1e5
ax[1].bar(rev.index, rev.values, color=["#94a3b8", "#dc2626"]); ax[1].set_ylabel("₹ lakh per month")
ax[1].set_title("Monthly revenue: kept vs walked out")
for i, v in enumerate(rev.values): ax[1].text(i, v + 0.2, f"₹{v:.1f} L", ha="center")
plt.tight_layout(); plt.show()
Donut of churned versus retained customers and bar of monthly revenue lost
One slide that explains why the project matters.

Step 5: business assumptions (agreed with the retention team)

Python
assumptions = {
    "retention offer": "₹100 off per month for 6 months",
    "offer cost per accepted customer (₹)": 600,
    "call cost per customer contacted (₹)": 40,
    "share of true churners saved by a call + offer": 0.30,
    "share of non-churners who still take the offer": 0.50,
    "value of a saved customer (₹)": "monthly bill × 12 (one more year)",
    "call capacity per month": 600,
}
for k, v in assumptions.items():
    print(f"{k:48s} {v}")
saved_value = 759 * 12            # average churner bill ≈ ₹759 (Step 4)
print(f"\nOne saved churner ≈ ₹{saved_value:,} of revenue; cost of trying = ₹40 call (+ ₹600 if they accept)")
retention offer ₹100 off per month for 6 months offer cost per accepted customer (₹) 600 call cost per customer contacted (₹) 40 share of true churners saved by a call + offer 0.3 share of non-churners who still take the offer 0.5 value of a saved customer (₹) monthly bill × 12 (one more year) call capacity per month 600 One saved churner ≈ ₹9,108 of revenue; cost of trying = ₹40 call (+ ₹600 if they accept)
🧠 Why write assumptions down?

On Day 59 we turn model output into rupees. Agreeing on the cost of a call, the offer and the save-rate before modelling keeps the evaluation honest and lets the business change a number later without redoing the work.

Step 6: the project charter

Markdown – PROJECT_CHARTER.md
# TeleOne Churn Prediction – Project Charter
**Goal:** Reduce customer churn by targeting retention calls at customers most likely to leave.
**Users:** Retention team (600 calls / month).
**Deliverables:** (1) monthly risk list with top reasons, (2) Streamlit app for agents,
(3) model card and one-page report.
**Success metrics:** ROC-AUC ≥ 0.80 on unseen customers; ≥ 2× more churners reached
per call than random calling; positive net ₹ impact under agreed assumptions.
**Data:** 3,000 customers, 13 columns, snapshot at month start. No leakage columns.
**Risks:** small dataset; offer acceptance rate is an assumption → propose an A/B pilot.
**Timeline:** Day 56 problem & data · 57 EDA · 58 models · 59 evaluation · 60 deployment.

🏅 Job skills unlocked today

✔ Translate a business goal into an ML problem✔ Audit data and catch silent parsing issues✔ Quantify the baseline and revenue at risk✔ Write a project charter with success metrics

Practice tasks

🏋️ Practice – do it yourself

Task 1: What would the retention team get from calling 600 random customers? (Expected number of churners reached.)
Show solution
Python
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

df = load_churn()
print(f"Random 600 calls reach ≈ {600 * df['churned'].mean():.0f} churners (the number our model must beat)")
Random 600 calls reach ≈ 175 churners (the number our model must beat)
Task 2: Which segment has the most monthly revenue at risk: Fiber, DSL or None?
Show solution
Python
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

df = load_churn()
print(df[df["churned"] == 1].groupby("internet_service")["monthly_charges"].sum().sort_values(ascending=False))
internet_service Fiber 487009.0 DSL 150615.0 None 26020.0 Name: monthly_charges, dtype: float64
Task 3: Write two more leakage examples for a churn dataset in a real telecom company.

Quick quiz

🧠 Quick Quiz – test yourself

1. Why did 625 rows look like missing internet service?
Use keep_default_na=False.
2. The prediction moment matters because…
Prevents leakage.
3. Random calling 600 customers reaches about…
600 × 29%.
4. A good business metric for this project is…
Revenue retained minus offer cost.
5. Assumptions are agreed before modelling to…
No moving goalposts.

Interview corner

💼 Interview Corner

How would you frame a churn prediction problem?

Define churn and the observation window, the prediction moment and horizon, the unit (customer), allowed features (known before churn), the action (retention offers with limited capacity), and success metrics both statistical (AUC, recall@capacity) and business (net savings).

What data-quality checks do you run first?

Shape and types, missing values (and how they are encoded!), duplicates on the key, valid ranges, cross-column consistency (total ≈ monthly × tenure), target values and class balance.

Summary

  • Business first: churn costs revenue; the action is targeted retention calls.
  • ML framing: binary target, prediction moment, no leakage, capacity-aware metrics.
  • Audit found a silent “None” → NaN trap – fixed in load_churn().
  • Baseline: 29% churn; random calls reach ~175 churners out of 600.
➡️ Next

Day 57: EDA – find who churns and why, test the patterns statistically and write the first insights.

Frequently Asked Questions

Is this real TeleOne data?
TeleOne is a fictional company and the data is realistic simulated data built for this course, so you can explore and share your project freely.
Where can I find real churn datasets?
The public IBM Telco Customer Churn dataset on Kaggle has a similar structure – try repeating this project on it after Day 60.
← Data Science (60 Days)
🔗

Share this topic with a friend

यह topic किसी दोस्त को भेजें

Found it useful? Send it to a classmate learning the same thing.

अच्छा लगा? जो दोस्त यही सीख रहा है, उसे भेज दीजिए।

Next topicDay 57: Project 2: EDA & Insights Your progress: 56 / 60 (93%) · Lesson 57 / 60 Continue →

How was this lesson?

Your feedback helps us make the content better.

Write a review or suggestion (optional)

After approval, your name and city may appear publicly with your review. Please do not write your email or phone number.

▶ CodingEasily on YouTube

Learn with video lessons in easy Hindi

Featured video: RDBMS Part 3 – Class XI Commerce IT

▶ Subscribe – it’s free