A model is only as good as the data you feed it
Every machine-learning project starts the same way: turn the messy real world into a rectangle of numbers - rows are examples, columns are features, and one column is the answer you want to predict. This session builds that rectangle for Lumen Skincare, and the reason we simulate it (with a fixed seed) rather than download it is honesty: you will know exactly what signal is in the data, so when a model finds it - or fails to - you understand why. That is a far better way to learn than a black-box CSV.
converted and order_value - that you reuse all course long, plus fluency in the scikit-learn estimator API: model.fit(X, y) to learn, model.predict(X) to apply. You will run a first classifier and a first regressor today - not to judge them yet (that starts in b2), but to feel how little code it takes.
Features, targets, and the fit/predict API 6 min live
Two ideas carry the whole course. First: data is a matrix X of features plus a vector y of answers. Second: every scikit-learn model, however different inside, is used the same way - fit to learn from training data, predict to apply to new data. Learn this once and every model in b2-b10 is familiar.
LiveThe estimator API - the four calls you will use all course3 min▶
Every scikit-learn model is an "estimator" and shares the same handful of methods. Learn them once:
model.fit(X, y)- learn from labelled training data. This is the training step.model.predict(X)- apply the learned model to new rows, returning a prediction each.model.predict_proba(X)- for classifiers, the probability of each class (b3 uses this).model.score(X, y)- a quick default metric (accuracy for classifiers, R² for regressors). We replace this with honest metrics in b7-b8.
Self-studyInstall the toolkit3 min▶
Everything in this course runs on three libraries. In a fresh environment:
python -m venv .venv && source .venv/bin/activate # or conda pip install scikit-learn pandas numpy matplotlib pyarrow
scikit-learn is the models, pandas/numpy the data, matplotlib the plots, and pyarrow lets us save the dataset to parquet so later sessions reload it instantly. If you took the statistics course, this is the same stack plus scikit-learn.
Build Lumen once, then run your first model 22 min · everyone builds
We simulate 40,000 Lumen checkout sessions with a fixed seed so your numbers match mine - and match every later builder session. The signal is deliberately realistic: prior spend and returning-customer status drive conversion, prior spend and product category drive order value, and session_hour is pure noise we planted to prove later that more columns is not more signal.
Build the dataset (seed 42). Run this once; it saves lumen_sessions.parquet, which b2 through b10 reload.
import numpy as np, pandas as pd
rng = np.random.default_rng(42)
N = 40_000 # checkout sessions (~10 days at ~4,000/day)
returning = rng.random(N) < 0.35
channel = rng.choice(
["email", "paid_search", "social", "organic", "referral",
"display", "affiliate", "direct", "sms"], N,
p=[.16, .18, .15, .14, .07, .09, .05, .11, .05])
device = rng.choice(["mobile", "desktop", "tablet"], N, p=[.58, .34, .08])
product_category = rng.choice(
["cleanser", "serum", "moisturizer", "set"], N, p=[.34, .28, .26, .12])
pages_viewed = rng.poisson(4, N) + 1
session_hour = rng.integers(0, 24, N) # deliberately near-useless (noise)
prior_30d_spend = np.clip(rng.gamma(2.2, 34.0, N), 0, 400).round(2) # right-skewed, mean ~$75
# conversion: a ~3.2% event driven by returning + prior spend + engagement + channel quality
chan_lift = {"email": .8, "direct": .5, "organic": .3, "referral": .2, "sms": .1,
"paid_search": -.1, "social": -.2, "affiliate": -.3, "display": -.5}
cl = np.array([chan_lift[c] for c in channel])
z = -5.13 + 0.95 * returning + 0.006 * prior_30d_spend + 0.11 * pages_viewed + cl
converted = (rng.random(N) < 1 / (1 + np.exp(-z))).astype(int)
# order value: right-skewed money, correlated r ~ 0.6 with prior spend, driven by category
cat_base = {"cleanser": 28, "serum": 42, "moisturizer": 36, "set": 76}
cb = np.array([cat_base[c] for c in product_category])
order_value = np.round(
0.45 * prior_30d_spend + cb + (rng.gamma(1.5, 22, N) - 1.5 * 22), 2).clip(5, 600)
df = pd.DataFrame({
"channel": channel, "device": device,
"new_vs_returning": np.where(returning, "returning", "new"),
"product_category": product_category, "pages_viewed": pages_viewed,
"session_hour": session_hour, "prior_30d_spend": prior_30d_spend,
"converted": converted, "order_value": order_value,
})
df.to_parquet("lumen_sessions.parquet")
print(df.shape)
print(df.head())
Sanity-check the signal. Confirm your numbers match the course canon before modelling anything.
print("conversion rate : %.2f%%" % (100 * df.converted.mean())) # ~ 3.2%
print("AOV mean / median: $%.0f / $%.0f" % (
df.order_value.mean(), df.order_value.median())) # ~ $73 / $67 (right-skewed)
print("corr(prior_spend, order_value): %.2f" % (
df[["prior_30d_spend", "order_value"]].corr().iloc[0, 1])) # ~ 0.60
# who converts more - the signal we expect a model to find
print(df.groupby("new_vs_returning").converted.mean().round(3)) # returning ~ 2x new
That ~3.2% conversion rate is the same control rate the Lumen A/B tests use in the experimentation course, and the ~0.60 correlation is the exact number the statistics course fit in its regression session. Three courses, one brand, numbers that line up - so a learner can carry intuition straight across. When you see 3.2% again in an A/B test, you will already know the shape of the data underneath it.
Run your first classifier. Predict converted from two numeric features - just to feel the API. We are not judging it yet (that is b2-b3), only learning fit then predict.
from sklearn.linear_model import LogisticRegression X = df[["prior_30d_spend", "pages_viewed"]] # two features, to start y = df["converted"] clf = LogisticRegression() clf.fit(X, y) # <- learn print(clf.predict(X[:5])) # <- apply: 0/1 for the first 5 rows print(clf.predict_proba(X[:5]).round(3)) # probability of each class
Two lines to train a model. Notice most predictions are 0 - with a 3.2% base rate, "probably will not convert" is usually right. That is the seed of the accuracy paradox we confront in b3.
And your first regressor. Same API, different model, different target - predict order_value for the sessions that converted.
from sklearn.linear_model import LinearRegression
buyers = df[df.converted == 1]
Xr = buyers[["prior_30d_spend"]]
yr = buyers["order_value"]
reg = LinearRegression()
reg.fit(Xr, yr) # same fit/predict, different model
print("predicted $ for a $200 prior-spend buyer:",
reg.predict([[200]]).round(2))
print("learned slope:", reg.coef_.round(3), "intercept:", round(reg.intercept_, 1))
fit/predict pattern. That consistency is scikit-learn's superpower and why the rest of the course can focus on judgement, not syntax.
This week ◐ 40 min total
- Reload, don't rebuild. Confirm
pd.read_parquet("lumen_sessions.parquet")gives you back the identical frame. Every later session starts this way. - Explore the features. Use
df.groupby("channel").converted.mean()anddf.groupby("product_category").order_value.mean(). Which channel converts best? Which category has the biggest orders? (You are hand-finding the signal a model will learn.) - Add a third feature to the first classifier (e.g. a one-hot of
new_vs_returningviapd.get_dummies). Does the code still run with the samefit/predict? (We do this properly with pipelines in b9.) - Optional: plot
order_valueas a histogram and confirm the right skew (mean > median) - the same shape the statistics course taught.
Three questions before you go 🎯 ◐ 90 seconds
1 · In scikit-learn, what do X and y hold?
By convention X is the features (one row per example, one column per feature) and y is the target you want to predict. Train/test is a separate split we make in b2.
2 · Why do we build the Lumen dataset with a fixed seed (default_rng(42))?
A fixed seed makes the random generation reproducible. It does not change accuracy or get required by sklearn - it just means everyone (and every later session) works on the identical dataset.
3 · Your first classifier predicts 0 (no conversion) for almost every row. Why is that not surprising - or yet a problem?
With a 3.2% base rate, predicting "no" is right ~97% of the time - so raw accuracy looks great while the model catches no buyers. That is the accuracy paradox, and it is exactly why b3 and b7 bring better metrics.
What this session covers
This builder session covers the setup and estimator-basics content of the sources below - the ~80% you need before modelling - on the Lumen dataset. The train/test split and honest evaluation start in b2.
Builder Session 1 cheat sheet · pin this
X = feature matrix (rows = examples, cols = features), y = target vector. Classification target is a category; regression target is a number.model.fit(X, y) learns, model.predict(X) applies, predict_proba gives class probabilities, score is a quick default metric.np.random.default_rng(42). Fixed seed = your numbers match the course all the way to b10.session_hour carries no signal on purpose - we use it in b9 to prove more columns is not more information.