Your First Machine Learning Model: Predict Churn with scikit-learn
Hands-on lab · IDE in your browser

Your First Machine Learning Model: Predict Churn with scikit-learn

Build a churn model end to end with scikit-learn on a realistic subscription dataset: hold back a test set, beat a no-model baseline, build a Pipeline with one-hot encoding and scaling, read AUC, precision and recall, catch two leaking columns behind a too-good-to-be-true score, compare logistic regression with gradient boosting by cross-validation, and turn the model into a call list the retention team can use.

Time
50 min
Checked steps
5
Level
Beginner
Setup
None
Read step 1

Hands-on labs require Pro · $29.99/mo · cancel anytime

Lab cockpit50 min · 5 stepsSession running
4 / 5 steps passingWho should the team call? · step 5 of 5
churn.py▶ Run✓ Check
# ---------- Step 5: who should the retention team call? ----------def call_list(model, new_df, n=100):    """The n current customers most likely to churn: a DataFrame with customer_id and churn_probability    (rounded to 3), highest first."""  
TerminalOutput

The job

Book Box posts subscribers a hand-picked book every month, and about one in five cancels each month. The retention team can phone 100 people a week with an offer, and wants to know which 100. You have 4,000 past subscribers and whether each one cancelled. You build the model that picks the list, and you find out on the way why a model that scores 0.99 is usually a model that cheats.

5 steps, each checked when you finish it

A check runs your work at the end of every step. Hints and the full solution are there if you get stuck.

  1. 1

    Split first, then a baseline

    Book Box posts subscribers a hand-picked book every month, and about one in five cancels each month.

    You writesplit()baseline()
  2. 2

    A first model

    Models need numbers.

    You writemake_model()evaluate()
  3. 3

    Too good to be true

    AUC 0.99 on churn is not a great model; it is a mistake.

    You writetop_features()
  4. 4

    A fair comparison

    Is there a better model than a straight line?

    You writecompare()
  5. 5

    Who should the team call?

    A model earns its keep when it changes what someone does on Monday.

    You writecall_list()

Step 1 as it appears in the lab

The lab’s own text. The hint and the solution stay inside the lab.

Step 1: Split first, then a baseline

Book Box posts subscribers a hand-picked book every month, and about one in five cancels each month. The retention team can phone 100 people a week and offer a free month. They want to know which 100. You will build that list with scikit-learn, the standard Python library for classical machine learning, from customers.csv: 4,000 past subscribers and whether each one cancelled (churned). data_dictionary.md says what every column means.

A model is only useful on customers it has not seen, so the first thing you do is hide some of them. The test set (20% here) stays untouched until you measure the finished model. Everything else, the training set, is for learning.

The second thing is a baseline: the score you get with no model at all. Without one, "78% accurate" sounds good.

Do this

1. Write split(df). X is every column except customer_id and the label; y is the label:

X = df.drop(columns=["customer_id", TARGET])
y = df[TARGET]
return train_test_split(X, y, test_size=0.2, stratify=y, random_state=42)

stratify=y keeps the churn rate the same in both parts; random_state=42 makes the split the same every time you run it.

2. Write baseline(y_train, y_test). Take the most common label in training, int(y_train.mode()[0]), and return its accuracy on the test set, the churn rate, and its recall: the share of real churners it finds.

3. Answer the question below, then Run and compare.

churn.py, the file you edit77 lines
"""Book Box churn: split, baseline, a first model, a leak, a fair comparison, and a call list."""
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, precision_score, recall_score, roc_auc_score
from sklearn.model_selection import StratifiedKFold, cross_val_score, train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler

TARGET = "churned"


# ---------- Step 1: split first, then a baseline ----------
def load(path="customers.csv"):
    """The CSV as a DataFrame. keep_default_na=False keeps a blank cancel_reason as "" (not NaN)."""
    return pd.read_csv(path, keep_default_na=False)


def split(df):
    """(X_train, X_test, y_train, y_test): 20% held back for the test, stratified, random_state=42.
    X is every column except customer_id and the label."""
    # TODO (Step 1): X = df without the "customer_id" and TARGET columns; y = df[TARGET].
    # Return train_test_split(X, y, test_size=0.2, stratify=y, random_state=42)
    raise NotImplementedError("Step 1: write split()")


def baseline(y_train, y_test):
    """What you get with no model: always predict the most common class seen in training."""
    # TODO (Step 1): majority = the most common value in y_train: int(y_train.mode()[0])
    # Return {"churn_rate": y_train.mean() rounded to 3,
    #         "accuracy": share of y_test equal to majority, rounded to 3,
    #         "recall": 0.0 if majority is 0 else 1.0}   (always saying "stays" finds none of the churners)
    raise NotImplementedError("Step 1: write baseline()")


# ---------- Step 2: a first model ----------
CATEGORICAL = ["plan", "signup_channel", "region", "gift", "cancel_reason"]
NUMERIC = ["price_gbp", "tenure_months", "boxes_skipped_90d", "avg_rating", "support_tickets_90d",
           "last_login_days", "retention_call"]
LEAKS = []


def columns():
    """The categorical and numeric columns the model may use: everything except the leaks."""
    return [c for c in CATEGORICAL if c not in LEAKS], [c for c in NUMERIC if c not in LEAKS]


def make_model(classifier=None):
    """A Pipeline: one-hot encode the categorical columns, scale the numeric ones, then classify."""
    cat, num = columns()
    raise NotImplementedError("make_model() arrives in Step 2")


def evaluate(model, X_test, y_test):
    """{"accuracy", "precision", "recall", "auc"} on the test set, each rounded to 3 places."""
    raise NotImplementedError("evaluate() arrives in Step 2")


# ---------- Step 3: too good to be true ----------
def top_features(model, n=8):
    """The n features with the largest logistic-regression weights, as [(name, weight)], biggest |weight| first."""
    raise NotImplementedError("top_features() arrives in Step 3")


# ---------- Step 4: a fair comparison ----------
def compare(X_train, y_train):
    """Mean 5-fold cross-validated AUC on the TRAINING data for two models:
    {"logistic": float, "boosting": float}, each rounded to 3 places."""
    raise NotImplementedError("compare() arrives in Step 4")


# ---------- Step 5: who should the retention team call? ----------
def call_list(model, new_df, n=100):
    """The n current customers most likely to churn: a DataFrame with customer_id and churn_probability
    (rounded to 3), highest first."""
    raise NotImplementedError("call_list() arrives in Step 5")
Provided for you:customers.csvdata_dictionary.mdtry_it.py

Frequently asked questions

Do I need to know machine learning already?

No. You need basic Python. Each step explains the one idea it uses, right before you use it, and gives the scikit-learn lines to write.

What is data leakage?

A column that is only known after the outcome, such as a cancellation reason in a churn model. The model reads the answer from it, scores brilliantly in testing and fails in real use. In the lab the leaks lift AUC to 0.99; removing them brings it to an honest 0.84.

Why use cross-validation?

Choosing between models by their test score tunes your choice to the test set. Cross-validation scores each model on five different slices of the training data instead, so the test set stays an honest final check.

Does the lab need a GPU?

No. scikit-learn trains these models in a second or two on a CPU.

A first machine learning model, from split to decision

Most first machine learning tutorials stop at an accuracy number. Real projects go wrong before that number: in a split that leaks, a baseline nobody computed, or a column that is only filled in after the outcome. In this lab you predict subscription churn with scikit-learn, the standard Python library for classical machine learning. You hold back a test set, compute a baseline, build a Pipeline that encodes and scales the data, read accuracy, precision, recall and AUC, find the leaking columns behind an impossible score, compare two models fairly with cross-validation, and produce the list of customers to call.