Project: Customer Churn Prediction
The Project
Nigeria has well over 200 million active mobile subscriptions spread across MTN, Airtel, Globacom and 9mobile, and many subscribers carry more than one SIM. Switching networks is as easy as buying a new SIM card, so churn (customers who stop using a service) is a constant battle. Winning a new subscriber usually costs far more than keeping an existing one, which is why telecom operators around the world, from MTN to Vodafone to Verizon, invest heavily in churn models.
Your brief: you are a data scientist at a telecom operator. The retention team can afford to send a bonus data bundle to 1,000 subscribers per week. Build a model that ranks subscribers by churn risk so the bundles go to the people most likely to leave.
Open in Google Colab: Telecom Churn Notebook
Open a new notebook and work through every step. Step 1 generates a realistic 10,000-subscriber dataset. By the end you will have an engineered feature set, three compared models, risk segments and a business-ready target list.
Step 1: Generate the Dataset
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_theme(style="whitegrid")
rng = np.random.default_rng(99)
n = 10000
subs = pd.DataFrame({
"subscriber_id": np.arange(1, n + 1),
"state": rng.choice(["Lagos", "Abuja", "Rivers", "Kano", "Oyo", "Anambra"], n, p=[.32, .16, .12, .16, .12, .12]),
"plan": rng.choice(["Prepaid", "Postpaid"], n, p=[.9, .1]),
"tenure_months": rng.integers(1, 72, n),
"recharge_m1": np.round(rng.gamma(2, 1500, n), -1), # naira recharged in each of the last 3 months
"data_gb_m1": np.round(rng.gamma(2, 2.5, n), 1),
"calls_min_m1": rng.poisson(180, n),
"dropped_calls": rng.poisson(3, n),
"complaints_90d": rng.poisson(0.4, n),
"days_since_recharge": rng.integers(0, 45, n),
"has_second_sim": rng.integers(0, 2, n),
})
decline = rng.uniform(0.4, 1.2, n)
subs["recharge_m2"] = np.round(subs["recharge_m1"] / decline * rng.uniform(0.9, 1.1, n), -1)
subs["recharge_m3"] = np.round(subs["recharge_m2"] / decline * rng.uniform(0.9, 1.1, n), -1)
logit = (-3.4 - 0.03 * subs["tenure_months"] + 0.5 * subs["complaints_90d"] + 0.08 * subs["dropped_calls"]
+ 0.06 * subs["days_since_recharge"] + 0.9 * subs["has_second_sim"] + 1.8 * (1 - decline)
- 0.8 * (subs["plan"] == "Postpaid"))
subs["churned"] = (rng.random(n) < 1 / (1 + np.exp(-logit))).astype(int)
print("Churn rate:", round(subs["churned"].mean() * 100, 1), "%")
Step 2: Quick EDA
subs.groupby("plan")["churned"].mean()
subs.groupby("has_second_sim")["churned"].mean()
sns.boxplot(data=subs, x="churned", y="days_since_recharge")
plt.show()
Always look at the churn rate across your main segments before modelling. It tells you which features are likely to matter and gives you sense-checks for later.
Step 3: Feature Engineering
Raw columns rarely capture behaviour well. The best churn features describe change and engagement. This is where domain knowledge makes the biggest difference.
df = subs.copy()
# Trend: is spending falling? A ratio below 1 means recharges are declining
df["recharge_trend"] = df["recharge_m1"] / (df["recharge_m3"] + 1)
df["avg_recharge_3m"] = df[["recharge_m1", "recharge_m2", "recharge_m3"]].mean(axis=1)
# Intensity: data use per naira of recharge
df["gb_per_1k_naira"] = df["data_gb_m1"] / (df["recharge_m1"] / 1000 + 0.1)
# Experience quality
df["has_complained"] = (df["complaints_90d"] > 0).astype(int)
# Lifecycle stage
df["is_new"] = (df["tenure_months"] <= 6).astype(int)
# Encode categoricals
df = pd.get_dummies(df, columns=["state", "plan"], drop_first=True)
features = [c for c in df.columns if c not in ["subscriber_id", "churned"]]
X, y = df[features], df["churned"]
Notice the +1 and +0.1 in the ratios: they prevent division by zero. Also note what is not included: nothing that is only known after a subscriber has churned. That is how you avoid leakage.
Step 4: Split and Train Three Models
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier, HistGradientBoostingClassifier
from sklearn.metrics import roc_auc_score, average_precision_score
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, stratify=y, random_state=1)
models = {
"Logistic regression": make_pipeline(StandardScaler(), LogisticRegression(max_iter=2000, class_weight="balanced")),
"Random forest": RandomForestClassifier(n_estimators=300, min_samples_leaf=20, class_weight="balanced", n_jobs=-1, random_state=1),
"Gradient boosting": HistGradientBoostingClassifier(max_iter=300, learning_rate=0.05, random_state=1),
}
results = {}
for name, m in models.items():
m.fit(X_train, y_train)
p = m.predict_proba(X_test)[:, 1]
results[name] = {"ROC AUC": roc_auc_score(y_test, p), "Avg precision": average_precision_score(y_test, p)}
pd.DataFrame(results).T.round(3)
class_weight="balanced" tells the model to pay more attention to the rarer churn class.
Step 5: Evaluate the Way the Business Will Use It
The retention team will contact the top 1,000 per week, so the question is not "what is the accuracy?" but "how many real churners are in our top-ranked list?" This is called precision at k (or lift).
best = models["Gradient boosting"]
test = X_test.copy()
test["churned"] = y_test.values
test["risk"] = best.predict_proba(X_test)[:, 1]
test = test.sort_values("risk", ascending=False)
k = 250 # the test set is 25% of subscribers, so 250 here is equivalent to 1,000 across the whole base
top = test.head(k)
print("Churn rate overall: ", round(test["churned"].mean() * 100, 1), "%")
print("Churn rate in top list: ", round(top["churned"].mean() * 100, 1), "%")
print("Lift:", round(top["churned"].mean() / test["churned"].mean(), 1), "x")
A lift of 2.5x means targeting with the model finds two and a half times as many churners as sending bundles at random. That is a number a Chief Marketing Officer understands.
Step 6: Explain the Model
from sklearn.inspection import permutation_importance
imp = permutation_importance(best, X_test, y_test, n_repeats=5, random_state=1, scoring="roc_auc")
pd.Series(imp.importances_mean, index=features).sort_values().tail(10).plot(kind="barh", title="Top churn drivers")
plt.show()
Permutation importance shuffles one feature at a time and measures how much performance drops. You should see the recharge trend, second SIM, days since recharge, complaints and tenure near the top, which are all things the business can act on.
Step 7: Turn Scores Into Actions
test["segment"] = pd.cut(test["risk"], bins=[0, 0.2, 0.5, 1], labels=["Low", "Medium", "High"])
test.groupby("segment", observed=True)["churned"].agg(["count", "mean"])
| Segment | Suggested action |
|---|---|
| High risk + has complained | Call from customer care, and fix the network issue first |
| High risk + falling recharge | Personalised data bundle bonus |
| Medium risk | Automated SMS offer |
| Low risk | No action; do not waste budget |
Deliverable
- A notebook with markdown commentary under each step
- A one-slide summary: churn rate, lift in the top 1,000, top five drivers and recommended actions
- A proposal for an A/B test: send bundles to half of the high-risk group and compare churn with the other half. That is how you prove the campaign, not just the model, works.
Try it yourself
Key Takeaways
- Churn models rank customers by risk so that a limited retention budget reaches the people most likely to leave.
- Feature engineering, such as trends, ratios, engagement and lifecycle stage, often matters more than the choice of algorithm.
- Compare a logistic baseline with tree ensembles using ROC AUC and average precision, and use class_weight to handle imbalance.
- Evaluate the way the business will act: precision and lift in the top-k list translate directly into money saved.
- Explain the model with permutation importance, map risk segments to specific actions, and prove impact with an A/B test.
Quick Quiz
1.Why is recharge_trend (recent recharge divided by recharge three months ago) a strong churn feature?
2.The retention team can only contact 1,000 subscribers a week. Which evaluation is most useful to them?
3.After deploying the model, how do you prove the retention bundles actually reduce churn?
Ready to go further?
CareerEx gives you structured 12-week training, live classes every Saturday and Sunday, real tutor feedback, and a certificate. Join the next cohort.
Join CareerEx