The idea
Every supervised ML problem is set up the same way:
- X, the features: what you know about each example, one row per example, one column per feature. (Also called inputs, predictors, variables.)
- y, the label or target: the answer for each example.
The model learns a function from X to y. Then comes the crucial question: does it work on examples it has never seen? To answer honestly, you hide some examples during training and test on them afterwards.
X and y
This track's main example is customer churn: predicting which customers are about to stop buying, so the shop can offer them something before they go. Each row of churn.csv is one customer. A few columns describe their behaviour, and one column, churned, records whether they actually left. In machine learning terms, the descriptive columns are the features (X) and the answer you want to predict is the label (y):
import pandas as pd
churn = pd.read_csv("churn.csv")
features = ["tenure_months", "orders_90d", "avg_order_value", "days_since_last_order", "support_tickets", "app_user"]
X = churn[features]
y = churn["churned"]
print(X.shape, y.shape)
print(X.head(3))
(400, 6) (400,)
tenure_months orders_90d avg_order_value days_since_last_order support_tickets app_user
0 12 7 163000 47 1 0
1 33 4 472000 73 1 0
2 5 0 124000 14 0 0
Xis 2-D: 400 rows × 6 features. scikit-learn always expects X to be 2-D (rememberreshape(-1, 1)from the NumPy track).yis 1-D: one label per row, in the same order.customer_idis not a feature: it's just a name. A model could "learn" that customer 1003 churned, which tells you nothing about new customers.
Categorical features
Models do maths, so text categories like segment ("Consumer", "Business") must become numbers. The standard way is one-hot encoding: one 0/1 column per category.
encoded = pd.get_dummies(churn[["segment"]], dtype=int)
print(encoded.head(3))
segment_Business segment_Consumer
0 0 1
1 1 0
2 1 0
Why not just Consumer = 0, Business = 1? With two categories that's fine. With three (Jakarta = 0, Bandung = 1, Surabaya = 2), numbers would imply an order and a distance ("Surabaya is twice Bandung"), which is nonsense. One-hot avoids inventing relationships that don't exist. The preprocessing module covers this with scikit-learn tools.
Why a test set?
Imagine a teacher who gives students the exact exam questions to practise. Every student scores 100%, and the score says nothing about whether they learned. A model scored on the data it trained on has the same problem: it might have memorised the examples.
So you split the data:
- training set (typically 70–80%): the model learns from it.
- test set (the rest): locked away, used once at the end to estimate performance on new data.
Here's a split by hand with NumPy, so you see there's no magic:
import numpy as np
rng = np.random.default_rng(42)
order = rng.permutation(len(churn))
cut = int(len(churn) * 0.75)
train_idx, test_idx = order[:cut], order[cut:]
X_train, X_test = X.iloc[train_idx], X.iloc[test_idx]
y_train, y_test = y.iloc[train_idx], y.iloc[test_idx]
print(len(X_train), len(X_test))
print(round(y_train.mean(), 3), round(y_test.mean(), 3))
300 100
0.327 0.36
Shuffling first matters: if the file were sorted (all churners at the end), an unshuffled split would put them all in the test set.
train_test_split
Shuffling and cutting the data by hand works, but it's easy to get wrong, and you'll do it in almost every project. scikit-learn does the whole thing in one call:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.25, random_state=42, stratify=y
)
print(X_train.shape, X_test.shape)
print(round(y_train.mean(), 3), round(y_test.mean(), 3))
(300, 6) (100, 6)
0.337 0.33
test_size=0.25: a quarter goes to the test set.random_state=42: a seed, so the split is the same every run (the NumPy random module's lesson).stratify=y: keeps the churn rate the same in both parts. Important when one class is rarer.
The order of the four results is always X_train, X_test, y_train, y_test.
Leakage: when the answer sneaks into the features
Data leakage happens when a feature contains information you wouldn't have at prediction time, often a disguised version of the answer. The model looks brilliant in testing and fails in real use.
Examples at a shop:
- Predicting churn with a feature "number of retention vouchers sent". Vouchers were sent because customers were already leaving: the feature is a consequence of the label.
- Predicting next month's spend using "total spend this year", where "this year" includes next month.
- Computing the average order value over all data (train and test) and using it as a feature: test information leaked into training.
For every feature, ask: "at the moment I'd make this prediction, would I already know this value?" If not, it leaks. And keep the test set untouched until the very end.
A suspiciously perfect score (99% where experts expected 75%) is the classic symptom.
Why this matters for clean code
A clear X/y setup, with an explicit list of feature names, documents exactly what the model sees. Keep it in one place (FEATURES = [...]) so training and prediction use the same columns in the same order. Seeds make splits reproducible, and an untouched test set makes the final score honest, which is what stakeholders will make decisions on.
Recap
- X = features (2-D, one row per example), y = label (1-D, same order).
- IDs aren't features; text categories become numbers via one-hot encoding.
- Split into training and test sets; shuffle first; the test set is used once, at the end.
train_test_split(X, y, test_size=0.25, random_state=42, stratify=y)returnsX_train, X_test, y_train, y_test.- Leakage: features that wouldn't be known at prediction time. Too-good scores are a warning sign.