Customer Churn Predictor: A Full ml1 Model Comparison
Data Science & ML Projects (Intermediate)
Chapter 2 · Customer Churn Predictor: A Full ml1 Model Comparison
dsproj1-7's own warn-box was explicit: plain accuracy is the simplified metric, and ml1-6 covers precision, recall, F1, and the confusion matrix in full. That chapter also deliberately used KNN specifically to avoid preempting ml1-5's logistic regression or ml1-7's decision trees. This chapter is the direct payoff of both deferrals.
What We're Building
A churn predictor for a telecom-style customer dataset — real subscriber data (tenure, monthly charges, contract type, services subscribed) used to predict whether a customer will cancel. Two real models, compared honestly: logistic regression (ml1-5) and a random forest (ml1-7), evaluated with the full metric picture ml1-6 covers, validated with real cross-validation (ml1-8).
Step 1: Real Feature Engineering
import pandas as pd df = pd.read_csv("telco_churn.csv") df["avg_monthly_spend"] = df["TotalCharges"] / df["tenure"].replace(0, 1) df["is_long_term"] = (df["tenure"] > 24).astype(int) categorical_cols = ["Contract", "InternetService", "TechSupport"] df = pd.get_dummies(df, columns=categorical_cols, drop_first=True) y = (df["Churn"] == "Yes").astype(int) X = df.drop(columns=["customerID", "Churn", "TotalCharges"])
Genuinely new territory beyond dsproj1-7's own four ready-made numeric measurements: avg_monthly_spend and is_long_term are derived features, engineered because raw tenure and TotalCharges alone don't directly capture them. pd.get_dummies() one-hot encodes categorical columns like Contract into numeric 0/1 columns a model can actually use — real tabular data is rarely all-numeric the way Iris was.
Step 2: Split, and Why One Split Isn't Enough
from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y )
stratify=y keeps the churned/not-churned ratio consistent between train and test sets — worth doing explicitly here, since churn datasets are rarely 50/50, exactly the situation Step 5 comes back to directly.
Step 3: Model 1 — Logistic Regression
from sklearn.linear_model import LogisticRegression logreg = LogisticRegression(max_iter=1000) logreg.fit(X_train, y_train)
ml1-5's own model, applied here for real — a coefficient per feature, each one directly readable as "this feature pushes the prediction toward churn or away from it."
Step 4: Model 2 — Random Forest
from sklearn.ensemble import RandomForestClassifier forest = RandomForestClassifier(n_estimators=100, random_state=42) forest.fit(X_train, y_train)
ml1-7's own ensemble model, fit on the identical training data — an intentional apples-to-apples comparison against logistic regression.
Step 5: The Full Evaluation Picture
from sklearn.metrics import classification_report, confusion_matrix for name, model in [("Logistic Regression", logreg), ("Random Forest", forest)]: preds = model.predict(X_test) print(f"\n--- {name} ---") print(classification_report(y_test, preds, target_names=["Stayed", "Churned"])) print(confusion_matrix(y_test, preds))
ml1-6 warned about. recall on the "Churned" class specifically — how many actual churners the model actually caught — matters far more here than overall accuracy, since missing a real churner is the exact business cost this whole project exists to reduce.
Step 6: Cross-Validation — A More Honest Estimate
from sklearn.model_selection import cross_val_score for name, model in [("Logistic Regression", logreg), ("Random Forest", forest)]: scores = cross_val_score(model, X, y, cv=5, scoring="f1") print(f"{name}: F1 = {scores.mean():.3f} (+/- {scores.std():.3f})")
Per ml1-8, a single train/test split can be a lucky or unlucky draw — cv=5 fits and evaluates each model five separate times on five different splits, reporting the average and spread rather than trusting one number. The +/- spread itself is worth reading: a model whose F1 barely moves across folds is more trustworthy than one that swings wildly from one split to the next.
Step 7: Which Features Actually Drive Churn
importances = pd.Series(forest.feature_importances_, index=X.columns).sort_values(ascending=False) print(importances.head(10))
A genuine business payoff beyond "the model works": feature_importances_ reveals which factors the random forest actually leaned on most — real, actionable information for whoever would act on these predictions, not just a black-box score.
pd.get_dummies()
One-hot encodes categorical columns a model can't use as raw text.
stratify=y
Keeps class balance consistent across train/test splits on imbalanced data.
The accuracy paradox, concretely
High accuracy alone can hide a model that misses most real churners.
cross_val_score()
A more honest performance estimate than trusting a single split.
Try these on your own:
- Try
class_weight="balanced"on both models and see whether recall on the churned class improves. - Add a threshold-tuning step — instead of the default 0.5 cutoff, find the probability threshold that maximizes recall while keeping precision above some minimum.
- Engineer one more derived feature of your own from the raw columns, and check whether it appears near the top of Step 7's own importance ranking.
- Add a third model — a gradient-boosted tree (
GradientBoostingClassifier) — to the Step 5/6 comparison.
What's Next
Chapter 3: Movie Recommender — returning to dsproj1-5's own ratings dataset, but with a genuinely different question: not what can be learned from the data, but what should be recommended from it.
Chapter 2 Quick Reference
- Delivers directly on dsproj1-7's own deferred promise — full ml1 depth: real feature engineering, model comparison, cross-validation, precision/recall/F1
- Logistic regression (ml1-5) vs. random forest (ml1-7), fit on identical data for an honest comparison
- The accuracy paradox (ml1-6) caught concretely in real, imbalanced churn data — recall on the minority class matters more than overall accuracy
- cross_val_score() (ml1-8) gives a more honest performance estimate than one train/test split alone
- Neither model is simply "better" — the right choice trades off interpretability against raw predictive performance