learn-intro-ml-with-phoebe / Builder session 1 of 10
Learn Intro to Machine Learning with Phoebe · Builder session 1 of 10

Setup, and the dataset you will model all course long

Before you can train a model you need a project, a toolkit, and data. This session installs scikit-learn, builds the Lumen Skincare dataset once with a fixed seed (so every later session reconciles to the same numbers), and introduces the one API that every scikit-learn model shares - fit then predict. By the end you will have run your very first estimator, on the two questions this whole course answers: who converts, and how big the order will be. Everyone starts here; b2 through b10 reload the exact dataset you build today. This is your ▶ start button for the builder track.

🟢 Start here Builders · DA / DE / DS Python · scikit-learn / pandas / numpy 45 min
0-3 · Setup 3-16 · The data & the API 16-40 · Build it in Python 40-45 · Wrap
Part 0

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.

Turn the messy real world into a rectangle of numbers rows = examples columns = features 1 column = the answer X (FEATURES) + y (TARGET) rows = examples, cols = features THE MESSY REAL WORLD no structure to model yet = ready to fit(X, y) Every ML project starts here: rows are examples, columns are features, one column is the answer.
🔍 Click to zoom - the rectangle of numbers is the whole project, before any model
Live - built in session Self-study - run after ★ The one big idea The case: Lumen Skincare
★ What you walk out with today A reproducible Lumen dataset (seed 42) with two prediction targets - 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.
Part 1 · the shape of the work

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.

One table: rows = sessions, columns = features + two targets FEATURES (X) prior_30d_spend · channel · device new_vs_returning · product_category pages_viewed · session_hour (noise) TARGETS (y) converted (0/1) · order_value ($) Classifier predict converted (yes/no) Regressor predict order_value ($) "will buy" score for each new visitor "$ this order" for each likely buyer Same feature columns, two different answer columns - so one dataset teaches both supervised shapes: classification and regression.
🔍 Click to zoom - one Lumen table, two prediction tasks
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.
Why this matters Because the API is identical, swapping a LogisticRegression for a DecisionTree or a RandomForest is a one-line change. You will spend your energy on the data and the evaluation, not on relearning each model - which is exactly where the real work is.
Self-studyInstall the toolkit3 min▶

Everything in this course runs on three libraries. In a fresh environment:

setup (terminal)
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.

★ everyone builds · the running dataset

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.

The planted channel signal - lift added to the conversion score z adds to conversion subtracts from conversion email +0.8 direct +0.5 organic +0.3 referral +0.2 sms +0.1 paid_search -0.1 social -0.2 affiliate -0.3 display -0.5 The exact channel signal planted in build_lumen.py - the lift each channel adds to the conversion score z.
🔍 Click to zoom - email is built to convert best, display worst

Build the dataset (seed 42). Run this once; it saves lumen_sessions.parquet, which b2 through b10 reload.

build_lumen.py
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.

explore.py
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
Real world

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.

first_model.py
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.

first_regressor.py
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))
Notice what did NOT change A classifier and a regressor - completely different maths - used with the identical fit/predict pattern. That consistency is scikit-learn's superpower and why the rest of the course can focus on judgement, not syntax.
Before Session 2

This week ◐ 40 min total

Check yourself

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.

Source material

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.

✓Features/targets (X, y); the fit / predict / predict_proba / score APIPart 1 + demo
✓Building & exploring a reproducible dataset; a first classifier and regressordemo
◐scikit-learn "Getting Started" - fitting and predicting: estimator basicsthe API spine
◐Andrew Ng / DeepLearning.AI ML Specialization - C1 W1 (supervised setup)concept spine
◐The train/test split, leakage, and how to judge these modelsBuilder Sessions 2-8 - not covered here by design

Builder Session 1 cheat sheet · pin this

X and yX = feature matrix (rows = examples, cols = features), y = target vector. Classification target is a category; regression target is a number.
The APImodel.fit(X, y) learns, model.predict(X) applies, predict_proba gives class probabilities, score is a quick default metric.
Same API, any modelLogisticRegression, DecisionTree, RandomForest - all fit/predict. Swapping models is a one-line change.
Reproducibilitynp.random.default_rng(42). Fixed seed = your numbers match the course all the way to b10.
The Lumen numbers~3.2% convert, AOV ~$73 (right-skewed, mean > median), corr(prior_spend, order_value) ~0.60. They match the statistics and experimentation courses.
The planted noisesession_hour carries no signal on purpose - we use it in b9 to prove more columns is not more information.