Your first model
Eight lines, 79.6% accurate — and why that number should worry you
make_pipelineSimpleImputerStandardScalerLogisticRegressionDummyClassifierbaselinesWatch it happen
Play it through, or step back and forth yourself.
from sklearn.model_selection import train_test_splitthe splitfrom sklearn.pipeline import make_pipelineso preprocessing travels with the modelfrom sklearn.impute import SimpleImputerprep_min is missing 17% of the timefrom sklearn.preprocessing import StandardScalerput the columns on one scalefrom sklearn.linear_model import LogisticRegressionthe modelX_train, X_test, y_train, y_test = train_test_split( X[NUMERIC], y, test_size=0.25, stratify=y, random_state=0)model = make_pipeline(SimpleImputer(strategy="median"), StandardScaler(), LogisticRegression(max_iter=1000))model.fit(X_train, y_train)model.score(X_test, y_test)Five imports, and each one is a decision you'll meet again: how to split, how to fill gaps, how to scale, what model, and how to hold them together.
The idea
Everything so far, assembled. This is the shape of every supervised project you will ever write.
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
X_train, X_test, y_train, y_test = train_test_split(
X[NUMERIC], y, test_size=0.25, stratify=y, random_state=0)
model = make_pipeline(SimpleImputer(strategy="median"),
StandardScaler(),
LogisticRegression(max_iter=1000))
model.fit(X_train, y_train)
model.score(X_test, y_test) # 0.7956Why each line is there
The split comes first. The moment you have X and y, before you look at a distribution or fill a gap. There should be no window in which you could accidentally use the test rows.
The imputer because prep_min is missing in about 17% of rows, and scikit-learn estimators refuse to fit on NaN. strategy="median" rather than mean because prep times are right-skewed — the same argument as the matplotlib distributions lesson. Lesson 12 shows why this particular choice is quietly lossy here.
The scaler because distance_km runs 0.4–6.5 and hour runs 7–22. Worth being precise about what that buys: on this data it does not change the accuracy at all — 0.7956 with or without. What it changes is the coefficients, from [0.891, 0.099, 0.018, -0.025, 0.367] to [1.018, 0.173, 0.076, -0.125, 0.995], and only the second set is comparable across columns. Scaling matters enormously for some models and hardly at all for others; lesson 10 sorts out which is which.
The pipeline because it chains all three into one estimator. fit on the pipeline fits each step in turn, and every step's parameters are learned from the training rows only. Do this by hand and sooner or later you fit the scaler on all the data and leak. Lesson 13 is about that.
Only numeric columns for now — area, rain and rider are strings, and a model needs numbers. Lesson 11 brings them in, and they help.
79.6% accurate on 225 deliveries the model has never seen. A working model in eight lines. Now the uncomfortable part.
Compared to what?
An accuracy is meaningless in isolation. It has to be read against what a model that learned nothing would score:
from sklearn.dummy import DummyClassifier
dummy = DummyClassifier(strategy="most_frequent").fit(X_train, y_train)
dummy.score(X_test, y_test) # 0.778A model that ignores every feature and always says "on time" scores 77.8% — because 77.8% of deliveries are on time. All eight lines of scikit-learn bought you 1.8 points over a constant that learned nothing.
That is not a bug in your code. It's what accuracy does when one class is common: it mostly measures how common that class is. Fit a DummyClassifier before anything else — it's one line, and it calibrates every number that follows.
(The regression equivalent is DummyRegressor, which predicts the mean; the R² of the mean is 0 by definition, which is what makes R² easier to read than accuracy.)
The number that actually matters
Look at what the model does on the class you care about. The test set has 50 genuinely late deliveries. The model finds 17 of them.
from sklearn.metrics import confusion_matrix
confusion_matrix(y_test, model.predict(X_test))
# [[162 13] 162 on-time, correctly called on-time · 13 false alarms
# [ 33 17]] 33 late deliveries MISSED · 17 caughtIt misses two thirds of the only thing the stall wanted to know — and "79.6% accurate" said nothing about that. The 162 in the top-left is doing all the work; it is most of the number, and it is the easy case.
Nothing here is broken. The data is fine, the code is fine, the model is a reasonable model. The number was the wrong question. Which is exactly where the next module starts.
The shape to remember
- Split.
- Build a pipeline: preprocessing, then a model.
- Fit on train.
- Fit a dummy too.
- Score on test — and on the metric that matches the decision, not the default one.
Every project in this track is that list. What changes is how carefully you do each step, and the next twenty-six lessons are the "carefully".
See it run
The lesson's code, ready to run and to fiddle with.
Putting the kettle on…
Starting up…
Worked example
not gradedAlready written and ready to go — press Run to see what it does, then change a number, a column name, anything, and run it again.
tryswapping LogisticRegression for RandomForestClassifier and watching accuracy barely move.
Your turn
3 exercises. Write the code yourself, then press Check — a nudge and the answer are there if you want them.
Build the pipeline — median imputer, scaler, logistic regression with max_iter=1000 — fit it on the training split, and return its test accuracy rounded to 4 places.
Fit a DummyClassifier(strategy="most_frequent") on the same split and return its test accuracy, rounded to 4 places. This is the number every other score has to beat.
The number that matters. Of the late deliveries in the test set, how many does the model catch? Return [caught, missed] as ints, from the confusion matrix.
