Project Brief — Customer Churn Prediction¶
A telecom company loses roughly 15–25% of its subscriber base every year to churn. Acquiring a new customer costs five to seven times more than retaining an existing one. The business case for a churn model is simple: if you can flag the right customers two weeks before they leave, the retention team can intervene.
Your job is to build that flag.
Business Objective¶
Identify customers who are likely to churn in the next billing cycle so the retention team can offer them a targeted incentive before they cancel.
Success metric: F1 score on the churned class (label = 1). We care about both precision and recall — false negatives cost retention spend on the wrong customers, false positives mean real churners slip through.
Minimum bar: F1 (churn class) > 0.60 on the held-out test set.
Info
F1 is the harmonic mean of precision and recall. It is the right metric here because the churn class is a minority class and we care about both types of error. Accuracy would be misleading — a model that predicts "no churn" for every customer would be ~75% accurate but useless.
Project Phases¶
| Phase | What you produce |
|---|---|
| EDA and Cleaning | Clean dataset, written findings, no surprises |
| Feature Engineering | Encoded, scaled, leak-free feature matrix |
| Modeling | Baseline + three models, CV comparison |
| Evaluation | Test set metrics, feature importance, threshold analysis |
| Report | Written results section, business recommendation |
Dataset¶
Run the cell below once at the top of your notebook. Every subsequent file in this project references the resulting df object.
import numpy as np
import pandas as pd
rng = np.random.default_rng(42)
n = 1000
# --- Raw features ---
tenure_months = rng.integers(1, 72, size=n)
monthly_charges = rng.uniform(20, 120, size=n).round(2)
num_products = rng.integers(1, 5, size=n)
support_calls = rng.integers(0, 10, size=n)
has_tech_support = rng.choice([0, 1], size=n, p=[0.4, 0.6])
contract_type = rng.choice(
["Month-to-Month", "One Year", "Two Year"],
size=n,
p=[0.55, 0.25, 0.20],
)
payment_method = rng.choice(
["Electronic Check", "Mailed Check", "Bank Transfer", "Credit Card"],
size=n,
p=[0.35, 0.25, 0.20, 0.20],
)
# --- Churn label (correlated with support calls, short tenure, M2M contract) ---
churn_score = (
0.03 * support_calls
- 0.015 * tenure_months
+ 0.004 * monthly_charges
+ 0.30 * (contract_type == "Month-to-Month").astype(float)
- 0.20 * has_tech_support
+ rng.normal(0, 0.15, size=n)
)
churn_prob = 1 / (1 + np.exp(-churn_score)) # sigmoid → probability
churn = (churn_prob > 0.50).astype(int)
# --- Introduce realistic messiness ---
# ~4% missing values in monthly_charges
missing_idx = rng.choice(n, size=int(0.04 * n), replace=False)
monthly_charges_with_na = monthly_charges.astype(float).copy()
monthly_charges_with_na[missing_idx] = np.nan
# ~2% duplicate rows
dup_idx = rng.choice(n, size=int(0.02 * n), replace=False)
df = pd.DataFrame({
"customer_id": [f"CUST_{i:04d}" for i in range(n)],
"tenure_months": tenure_months,
"monthly_charges": monthly_charges_with_na,
"num_products": num_products,
"support_calls": support_calls,
"has_tech_support":has_tech_support,
"contract_type": contract_type,
"payment_method": payment_method,
"churn": churn,
})
# Inject duplicates
df = pd.concat([df, df.iloc[dup_idx]], ignore_index=True)
df.shape
# Output: (1020, 9)
Success
After running this cell you should have a DataFrame with 1,020 rows and 9 columns. The churn rate should be approximately 27–32%. Verify with df["churn"].mean().round(3).
Data Dictionary¶
| Column | Type | Description |
|---|---|---|
customer_id |
string | Unique customer identifier — drop before modeling |
tenure_months |
int | Months the customer has been with the company |
monthly_charges |
float | Monthly bill amount in USD (4% missing) |
num_products |
int | Number of active product subscriptions (1–4) |
support_calls |
int | Support calls made in the last 3 months (0–9) |
has_tech_support |
int | 1 if customer has tech support add-on |
contract_type |
string | Billing contract: Month-to-Month / One Year / Two Year |
payment_method |
string | How the customer pays |
churn |
int | Target — 1 if churned, 0 if retained |
Warning
customer_id is an identifier, not a feature. If you accidentally include it in your feature matrix, tree models will overfit to it and your test score will look artificially high. Drop it before building any feature pipeline.
Project Folder Structure¶
Organise your work like this before you write a single line of model code:
churn-prediction/
├── notebook.ipynb ← single notebook, cells in order
├── README.md ← project summary (filled in at the end)
└── requirements.txt ← pandas, scikit-learn, matplotlib, seaborn
For a team project, separate the notebook into scripts (src/). For this solo exercise, one well-organised notebook is fine.
Tip
Name your notebook sections with markdown headers that match the project phases. Reviewers and interviewers will skim your notebook top-to-bottom — make it easy for them to jump to any phase without reading everything.