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.
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")customers.csvdata_dictionary.mdtry_it.py