Skip to content

The Machine Learning Roadmap

What you’ll learn

  • the whole workflow as one runnable 38-line script, not a diagram of boxes
  • where the lines actually go — 19 of 38 run before a model is chosen
  • the two skill tracks you have to build in parallel, and why
  • a measured case where the most accurate model is also 634× smaller
  • exactly which phase of this module teaches each piece

The whole thing, once

Before the roadmap, here is the destination. This is a complete, runnable supervised-learning pipeline — load, split, clean, encode, fit, tune, evaluate, ship. Every later phase of this module is a deep dive into one of these blocks.

reference_pipeline.py
import joblib
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.ensemble import HistGradientBoostingRegressor
from sklearn.impute import SimpleImputer
from sklearn.metrics import mean_absolute_error
from sklearn.model_selection import GridSearchCV, train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
 
 
def run(csv_path, target, out_path="model.joblib"):
    # --- look at the data ---------------------------------------------
    df = pd.read_csv(csv_path)
    print(df.shape)
    print(df.dtypes)
    print(df.isna().mean().sort_values(ascending=False).head())
    print(df[target].describe())
 
    # --- split, before touching anything ------------------------------
    y = df.pop(target)
    X_train, X_test, y_train, y_test = train_test_split(
        X := df, y, test_size=0.2, random_state=42)
 
    # --- decide how each column is cleaned ----------------------------
    num_cols = X.select_dtypes("number").columns.tolist()
    cat_cols = X.select_dtypes("object").columns.tolist()
    num_impute = SimpleImputer(strategy="median")
    cat_impute = SimpleImputer(strategy="most_frequent")
 
    # --- encode and scale, inside a pipeline --------------------------
    numeric = Pipeline([("impute", num_impute),
                        ("scale", StandardScaler())])
    categorical = Pipeline([
        ("impute", cat_impute),
        ("onehot", OneHotEncoder(handle_unknown="ignore"))])
    prep = ColumnTransformer([("num", numeric, num_cols),
                              ("cat", categorical, cat_cols)])
 
    # --- choose the model ---------------------------------------------
    model = HistGradientBoostingRegressor(random_state=42)
    pipe = Pipeline([("prep", prep), ("model", model)])
 
    # --- cross-validate and tune --------------------------------------
    grid = {"model__max_depth": [None, 4, 8],
            "model__learning_rate": [0.05, 0.1, 0.2]}
    search = GridSearchCV(pipe, grid, cv=5,
                          scoring="neg_mean_absolute_error",
                          n_jobs=-1)
    search.fit(X_train, y_train)
    print(search.best_params_, -search.best_score_)
 
    # --- evaluate against a baseline ----------------------------------
    best = search.best_estimator_
    preds = best.predict(X_test)
    mae = mean_absolute_error(y_test, preds)
    baseline = mean_absolute_error(y_test,
                                   [y_train.median()] * len(y_test))
    print(f"test MAE {mae:.3f} vs baseline {baseline:.3f}")
 
    # --- persist and reload -------------------------------------------
    joblib.dump(best, out_path)
    reloaded = joblib.load(out_path)
    assert reloaded.predict(X_test[:5]).shape == (5,)
    return best, mae
reference_pipeline.py
import joblib
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.ensemble import HistGradientBoostingRegressor
from sklearn.impute import SimpleImputer
from sklearn.metrics import mean_absolute_error
from sklearn.model_selection import GridSearchCV, train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
 
 
def run(csv_path, target, out_path="model.joblib"):
    # --- look at the data ---------------------------------------------
    df = pd.read_csv(csv_path)
    print(df.shape)
    print(df.dtypes)
    print(df.isna().mean().sort_values(ascending=False).head())
    print(df[target].describe())
 
    # --- split, before touching anything ------------------------------
    y = df.pop(target)
    X_train, X_test, y_train, y_test = train_test_split(
        X := df, y, test_size=0.2, random_state=42)
 
    # --- decide how each column is cleaned ----------------------------
    num_cols = X.select_dtypes("number").columns.tolist()
    cat_cols = X.select_dtypes("object").columns.tolist()
    num_impute = SimpleImputer(strategy="median")
    cat_impute = SimpleImputer(strategy="most_frequent")
 
    # --- encode and scale, inside a pipeline --------------------------
    numeric = Pipeline([("impute", num_impute),
                        ("scale", StandardScaler())])
    categorical = Pipeline([
        ("impute", cat_impute),
        ("onehot", OneHotEncoder(handle_unknown="ignore"))])
    prep = ColumnTransformer([("num", numeric, num_cols),
                              ("cat", categorical, cat_cols)])
 
    # --- choose the model ---------------------------------------------
    model = HistGradientBoostingRegressor(random_state=42)
    pipe = Pipeline([("prep", prep), ("model", model)])
 
    # --- cross-validate and tune --------------------------------------
    grid = {"model__max_depth": [None, 4, 8],
            "model__learning_rate": [0.05, 0.1, 0.2]}
    search = GridSearchCV(pipe, grid, cv=5,
                          scoring="neg_mean_absolute_error",
                          n_jobs=-1)
    search.fit(X_train, y_train)
    print(search.best_params_, -search.best_score_)
 
    # --- evaluate against a baseline ----------------------------------
    best = search.best_estimator_
    preds = best.predict(X_test)
    mae = mean_absolute_error(y_test, preds)
    baseline = mean_absolute_error(y_test,
                                   [y_train.median()] * len(y_test))
    print(f"test MAE {mae:.3f} vs baseline {baseline:.3f}")
 
    # --- persist and reload -------------------------------------------
    joblib.dump(best, out_path)
    reloaded = joblib.load(out_path)
    assert reloaded.predict(X_test[:5]).shape == (5,)
    return best, mae

If most of that is unfamiliar, good — that is the syllabus. Come back and reread it after Phase 7 and it will look obvious.

Where the work actually is

You will hear that data work is 80% of machine learning. That number is folklore; nobody can cite its source. So instead of repeating it, here is a count of the script above — 38 code lines, excluding imports, blanks and comments, tallied by what each line does.

StageLinesShare
Load + inspect the data513.2%
Split before touching anything37.9%
Impute missing values410.5%
Encode + scale into a pipeline718.4%
Choose the model25.3%
Cross-validate + tune718.4%
Evaluate against a baseline615.8%
Persist + reload410.5%
figureHalf the pipeline runs before a model is chosenmatplotlib
A horizontal bar chart of eight pipeline stages by line count, with the four blue data stages summing to nineteen lines and the choose-the-model stage the shortest at two.A horizontal bar chart of eight pipeline stages by line count, with the four blue data stages summing to nineteen lines and the choose-the-model stage the shortest at two.
Real counts from the script above, not a survey. 19 of 38 lines — exactly 50% — are data work that happens before any model exists. Actually choosing the algorithm is 2 lines, 5.3%, the smallest stage in the file.

Not 80%. Exactly 50%, on this pipeline. And the part everyone thinks of as “machine learning” — picking the algorithm — is two lines.

Two caveats, because line counts are a crude proxy. They ignore time: the four data lines took far longer to get right than the two model lines. And they ignore the work upstream of the file entirely — collecting the data, agreeing on the label definition, and negotiating what the metric should be. Counting only the script understates data work, which makes 50% a floor, not a ceiling.

The learning path

diagram Diagram mermaid
PhaseWhat you buildThe one idea to take away
1 — FoundationVocabulary and judgementThe features set the ceiling
2 — DataCleaning, encoding, pipelinesSplit before you look; leakage is silent
3 — RegressionPredicting numbersFitting is minimising a loss
4 — ClassificationPredicting categoriesAccuracy is the wrong metric more often than not
5 — EnsemblesCombining modelsAveraging works only if members disagree
6 — UnsupervisedStructure without labelsThe distance metric decides the answer
7 — TuningCross-validation and searchOne held-out set is not enough
8 — DeploymentServing and monitoringA model that stops being retrained starts decaying

Two tracks, in parallel

There is a modelling skill set and an engineering skill set, and shipping requires both. It is tempting to treat the second as somebody else’s job; it is not.

Here is the reason, measured. Three models on the breast-cancer dataset, scored two ways: 5-fold accuracy, and the size of the pickled artefact you would have to deploy.

Model5-fold accuracyPickled size
Logistic regression0.98072.0 KB
15-nearest-neighbours0.961498.1 KB
Random forest, 500 trees0.96311,268.3 KB
figureNearly identical accuracy; wildly different things to shipmatplotlib
Two bar charts side by side. The left shows nearly identical accuracies near 0.96 to 0.98; the right shows pickled model sizes on a log scale from 2 KB to 1268 KB.Two bar charts side by side. The left shows nearly identical accuracies near 0.96 to 0.98; the right shows pickled model sizes on a log scale from 2 KB to 1268 KB.
Logistic regression is both the most accurate model here (0.9807) and 634 times smaller than the random forest (2.0 KB against 1,268.3 KB). The 15-NN model has to carry the entire training set with it, which is why it lands in between.

The most accurate model is also 634× smaller than the largest. That is not a general law — often you do trade accuracy for size — but it is a common enough situation that ignoring Track B costs you nothing in accuracy and a great deal in operations.

Track A — modellingTrack B — engineering
Which algorithm suits this dataHow the model is packaged and versioned
What the metric should beLatency and memory at serving time
Cross-validation designReproducibility: seeds, data versions, environments
Feature engineeringMonitoring inputs and outputs in production
Diagnosing bias and varianceRetraining triggers and rollback
Interpreting errorsCost — training and inference

Instance-based or model-based

One distinction is worth meeting now, because it explains the table above and recurs throughout the module.

A model-based learner compresses the training data into parameters and then discards it. Logistic regression stores a handful of coefficients: 2.0 KB.

An instance-based learner keeps the examples and compares new inputs against them at prediction time. 15-NN has to ship the entire training set: 98.1 KB, and prediction cost grows with the data.

two_kinds.py
import pickle
 
from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score, train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
 
X, y = load_breast_cancer(return_X_y=True)
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3,
                                          random_state=0, stratify=y)
 
for name, m in [
    ("model-based    (logistic)",
     make_pipeline(StandardScaler(), LogisticRegression(max_iter=5000))),
    ("instance-based (15-NN)   ",
     make_pipeline(StandardScaler(), KNeighborsClassifier(15))),
]:
    acc = cross_val_score(m, X, y, cv=5).mean()
    m.fit(X_tr, y_tr)
    kb = len(pickle.dumps(m)) / 1024
    print(f"{name}  accuracy {acc:.4f}   pickled {kb:6.1f} KB")
 
# model-based    (logistic)  accuracy 0.9807   pickled    2.0 KB
# instance-based (15-NN)     accuracy 0.9614   pickled   98.1 KB
two_kinds.py
import pickle
 
from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score, train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
 
X, y = load_breast_cancer(return_X_y=True)
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3,
                                          random_state=0, stratify=y)
 
for name, m in [
    ("model-based    (logistic)",
     make_pipeline(StandardScaler(), LogisticRegression(max_iter=5000))),
    ("instance-based (15-NN)   ",
     make_pipeline(StandardScaler(), KNeighborsClassifier(15))),
]:
    acc = cross_val_score(m, X, y, cv=5).mean()
    m.fit(X_tr, y_tr)
    kb = len(pickle.dumps(m)) / 1024
    print(f"{name}  accuracy {acc:.4f}   pickled {kb:6.1f} KB")
 
# model-based    (logistic)  accuracy 0.9807   pickled    2.0 KB
# instance-based (15-NN)     accuracy 0.9614   pickled   98.1 KB

Where time actually goes

An honest ordering of effort on a typical project, from most to least:

  1. Understanding the problem. What is the label, exactly? What decision does the prediction feed? What is the cost of a false positive against a false negative? Getting this wrong makes every later step worthless.
  2. Getting and understanding the data. Where it lives, what the columns mean, which are unreliable, which leak the answer.
  3. Cleaning and feature engineering. Half the reference script, and much more than half the wall-clock time.
  4. Evaluation design. Which split, which metric, which baseline. Cheap to write, expensive to get wrong.
  5. Model selection and tuning. Two lines to choose, seven to search. Genuinely important, and nowhere near the largest slice.
  6. Deployment and monitoring. Small in code, large in consequence.

Notice that “try a fancier algorithm” is item 5. Beginners start there because it is the fun part. The measurement on the lifecycle page shows why that ordering is wrong: swapping the algorithm moved accuracy 0.1142, and flipping 15% of the labels cost exactly the same 0.1142.

See it move

The roadmap above reads as a straight line because lists are straight. Real projects walk it as a loop, and the loop does not visit each stage equally often. The sketch walks a token through the stages with the backward jumps a real project takes — “the metric is wrong” sends you back to framing, “the model is starved” sends you back to data — and tallies the visits as it goes.

sketch The loop you actually walk p5.js
A token moves through the seven roadmap stages, sometimes jumping backwards the way real projects do. The bars count how many times each stage has been visited: data work accumulates visits fastest, and model selection is nowhere near the top. Click to restart the count.

Let it run for a few hundred steps. That transition table is a Markov chain, so its long-run visit shares can be computed exactly rather than guessed — power-iterate it and you get:

StageLong-run share of visits
frame4.6%
get data14.8%
clean28.4%
evaluate23.2%
model16.3%
tune8.1%
deploy4.6%

Framing plus data work is 47.8%; modelling plus tuning is 24.4%. That is not a rigged result — the only editorial choices in the table are the backward edges, and backward edges land on data and evaluation because that is where the causes of a bad model live. Note how close 47.8% lands to the independently measured 50% of the reference script’s lines that run before a model is chosen. Two different ways of counting, the same answer: half the work happens before the algorithm exists.

Pitfalls

Starting at step 5. Choosing an algorithm before you understand the label is the most common beginner mistake, and it is expensive because everything downstream inherits the confusion.

Treating deployment as someone else’s problem. The random forest above is 634× larger than the logistic model for lower accuracy. That decision is made during modelling, not after it.

Quoting the 80% figure. It is folklore. The measurement here is 50% of the code, and the honest statement is “data work dominates, and no, I do not have a citation for a precise number.”

Learning the phases strictly in order. Phases 3 and 4 are independent of each other; so are 5 and 6. The dependencies that matter are 1 → 2 → everything, and 7 after you have models to tune.

Skipping the baseline because the model works. If you never computed it, you do not know whether the model works.

Recap

  • The whole workflow is one 38-line script; every later phase expands one of its blocks.
  • 19 of 38 lines — 50% — run before a model is chosen. Choosing the algorithm is 2 lines.
  • The 80% folklore figure has no source; count your own pipeline instead.
  • Two skill tracks run in parallel. On breast cancer the best model (0.9807) was also 634× smaller than the largest (2.0 KB against 1,268.3 KB).
  • Model-based learners store parameters; instance-based learners store the data.
  • Effort order: understand the problem → get the data → clean it → design the evaluation → choose the model → ship it.
quizCheck yourself
  1. In the 38-line reference pipeline, how many lines are spent choosing the model?

    Show answer

    A — 2 — Two lines: instantiate the estimator and wrap it in a Pipeline. Cross-validation and tuning take 7 more, and the data stages take 19 — half the file — before any model exists.

  2. Logistic regression scored 0.9807 and pickled to 2.0 KB; the 500-tree forest scored 0.9631 and pickled to 1,268.3 KB. What is the lesson?

    Show answer

    B — Accuracy and deployment cost are separate axes, and the winner on one is not automatically the loser on the other — Here the simplest model won on both axes, which is common on small tabular data. The general point is that you have to measure both — a model chosen purely on offline accuracy can be an operational problem.

  3. Someone tells you 'data cleaning is 80% of machine learning'. What is the accurate response?

    Show answer

    B — It is folklore with no traceable source — count your own pipeline; this one measured 50% of the code — The figure is repeated constantly and cited nowhere. The measurement on this page is 19 of 38 lines, and even that understates it because line counts ignore time and everything upstream of the script.

  4. Which pair of phases can be learned in either order?

    Show answer

    B — Phase 3 (regression) and Phase 4 (classification) — Regression and classification both depend on Phase 2 but not on each other. Phase 1 → 2 → everything is the real dependency, and Phase 7 needs models in hand before tuning them means anything.

  5. Why does a 15-nearest-neighbours model pickle to 98.1 KB while logistic regression pickles to 2.0 KB?

    Show answer

    B — KNN is instance-based: it has to carry the training data to make predictions, while logistic regression stores only coefficients — Model-based learners compress the data into parameters and discard it. Instance-based learners defer all the work to prediction time, so the training set is part of the artefact — and prediction cost grows with it too.

🧪 Try It Yourself

Exercise 1 – Count your own pipeline

Exercise 2 – Model-based: two numbers

Exercise 3 – Instance-based: all the rows

Exercise 4 – Accuracy against artefact size

Exercise 5 – Baseline before anything

Exercise 6 – Compute where the time actually goes

Next

Artificial Intelligence vs Machine Learning vs Deep Learning — what actually nests inside what, and a measurement showing the neural network losing to gradient boosting on structured columns.

If this helped you, consider buying me a coffee ☕

Buy me a coffee

Was this page helpful?

Let us know how we did