CourseAI / ML Engineer: The Complete Course · Course guide · part 4 of 93
Part 4 · Course guide

The Project: Kestrel

13 min read·

Every lesson in this course is set at one company, on one project. Kestrel is made up. The company, its people, its depots, every parcel, message and number in the project are fictional and simulated. The real cases that open each lesson are separate, cited and clearly marked as real.

The company

Kestrel is a UK parcel-delivery company. Its hub is in Leicester, and it runs nine depots from Dartford to Inverness, 1,150 vans, and about 210,000 parcels a day (September 2026). Businesses hand parcels to Kestrel with a promise: next day, two days (standard) or three days (economy). Kestrel delivers Monday to Saturday.

The problem

About one parcel in twelve misses its promised day. When that happens with no warning, the customer calls, writes or complains publicly, and Kestrel pays for it in contact-centre time and lost business. If Kestrel knew in the morning which parcels were at risk, it could warn those customers at 06:00 and send its few spare vans to the routes that need them most.

A data scientist built a prototype in a notebook last spring. It scored well. Nobody could rerun it, it was trained on a column that is only known after delivery, and it has never seen a parcel from Doncaster, the depot that opened in August. Adaeze, the head of data and ML, wants the real thing: a model that is right, that runs every day without anyone touching it, and that tells someone when it stops being right.

You are the ML engineer Kestrel hired in October 2026 to build it.

The model

ItemValue
Namekestrel-late-risk
Predictsthe probability that a parcel misses its promised day (late = 1)
Unitone parcel
When it scoresat collection (online, one parcel at a time) and at 06:00 on the promised day (batch, every parcel due that day)
Who uses itthe 06:00 customer warnings, and the depot planners who place spare vans
Inputsthe 18 columns known at collection (FEATURES, guide 06 block 4: parcel_id plus 17 columns that describe the parcel); never attempts, delivered_date, late or label_ready_date
When the truth arrivesthe day after delivery, when scans are reconciled overnight; later for parcels that were late

What this course uses the project for

This course teaches the AI / ML engineer's job: training models and keeping them correct in production. Kestrel is the instrument. Each module adds one piece and its proof to one repository, so by the capstone the model lives in production, keeps working without anyone watching it, and you can answer the roadmap's five questions from your own measurements.

It is not Northdesk (AI System Design), BrightCart (AI Engineering), Harbourline Freight (Forward Deployed Engineer), the returns desk (Agentic AI Engineer), the Quillpath support desk (Large Language Models), Hollin Answers (RAG), the notes assistant (MCP), the TinyFormer lab (Transformers), Booknook (Python) or the Tidewell Cycles inbox (NLP). Kestrel is a different company, with a different cast, data and problem.

The people

NameRoleIn the projectThe question they ask
Adaeze OkaforHead of data and ML (your manager)Signs off decision records, the promotion gates and the model card. The voice of "the question Adaeze will ask" in every lessonCan you reproduce last month's model exactly?
Morag SinclairOperations directorOwns the business problem: warnings and spare vans. Set the illustrative business costsWhat will this save us, in pounds, and how sure are you?
Tomasz WierzbickiData engineering leadOwns the depot scan feeds, the lake and the warehouse system. Rolled out scanner software v4 on 7 SeptemberWhat happens if the feature pipeline silently changes a column?
Rhys LlewellynPlatform and on-call leadOwns serving, the latency SLO, deployments, rollbacks and the GPU questionWhat is p99, and where does the time go?
Hannah LindqvistCustomer experience and compliance leadOwns the warning wording, customer messages, fairness to island customers, UK GDPR and EU AI Act questions. Her questions are answered from named, verified sources and marked "not legal advice"Is it fair to the customers it serves worst?
Imran QureshiFinance business partnerCost of one prediction, one training run and the GPU bill; self-hosting against an APIWhat does one prediction cost, and one training run?
YouML engineerBuild the model, the pipeline around it and the evidence that it works

Do not add named Kestrel staff. Customers appear only as message senders in the shared data, never by name.

The depots

IdDepotNation (bank holidays)RoutesShare of parcelsNotes
LEILeicester hubEngland and Wales3016%, falling to 13% as Doncaster ramps up
BHMBirminghamEngland and Wales2614%
MANManchesterEngland and Wales2614%
LDSLeedsEngland and Wales2212%, falling to 7%gives routes to Doncaster
BRSBristolEngland and Wales2010%the lowest late rate
DARDartfordEngland and Wales3218%the busiest, London east
GLAGlasgowScotland1810%gales 15 to 17 September
INVInvernessScotland106%three routes cross to the islands by ferry
DONDoncasterEngland and Wales160%, rising to 8% from 10 Augustopened 10 August 2026; many new drivers

Shares are of the parcels in the sample (guide 06, block 3). The late rates by depot are measured in guide 06, block 10.

Fixed numbers (simulated)

These are the facts every lesson uses. They do not change; lessons that need another number add it, say "added in this lesson", and label it.

FactValue
Parcels a day (whole company)210,000; the shared data is a 1% sample
Predictions a dayabout 420,000 (two scores per parcel)
Late rate in the sample0.0866 of labelled parcels (measured in guide 06)
Service level objective (Rhys)online scoring p99 at most 50 ms at the service; the 06:00 batch finished within 20 minutes
Illustrative business costs (Morag, GBP)late with no warning £3.80; late and warned at 06:00 £1.10; warned but on time £0.40; one spare van for a day £165. These are round numbers for reasoning, not industry figures, and lessons label them illustrative every time
Promotion gates (Adaeze)on the latest two labelled weeks (from Module 10 on, 7 to 20 September: the locked test set was looked at once in Module 6 and is then retired; from that point those weeks are ordinary labelled production weeks that gate later models). The gate weeks stop one day before the Module 6 test set, which runs to 21 September: 21 September is the Monday that starts the last week, whose late labels are still arriving, so the gates use the two whole weeks whose labels have settled. A challenger is promoted only if: PR-AUC is at least 0.005 above the champion's; the Brier score is no worse; no depot's PR-AUC is more than 0.02 below the champion's; p99 is at most 50 ms in the benchmark; the data contract passes
Retraining budget (Adaeze and Imran)at most 4 automatic retrains a month, each at most 2 CPU-hours; anything more needs Adaeze
Rollout guard (Module 10 lesson 2, added there, illustrative)during a canary: error rate at most 0.1%, p99 at most 50 ms, and the challenger's warn rate within 0.10 of the champion's in the same window
Retraining triggers (Module 10 lesson 3, added there, illustrative)drift trigger at PSI 0.25 on an input; performance trigger when PR-AUC falls under 0.36 on the last fortnight's labelled parcels; a 3-day cooldown between automatic retrains
Registrythe MLflow registered model kestrel-late-risk, aliases champion and challenger

Kestrel's money is in pounds sterling. Cloud and model providers publish prices in US dollars; lessons quote those in dollars and convert only with a dated exchange rate and its source.

Dates

  • The shared data covers parcels shipped Monday 6 July to Sunday 27 September 2026 (12 weeks). The snapshot was taken on Monday 28 September 2026 (DATA_AS_OF). Code counts days to DATA_AS_OF, never to date.today(), so lessons give the same answers whenever you run them.
  • A parcel's label is ready the day after it is delivered. Labels not ready by DATA_AS_OF are missing (late is None); the first ship date with missing labels is 22 September.
  • Dates in prose are written day first ("7 September 2026"); dates in data are ISO (2026-09-07).

Key events in the data

WhenWhat happenedWhere the course uses it
Monday 3 August 2026Scotland's summer bank holiday (Glasgow, Inverness)Module 3 (features), Module 15 (a rule beats the model)
Monday 10 August 2026Doncaster opens and takes routes from Leeds and Leicester over three weeks; its late rate rises as its volume growsModule 6 (error analysis), Module 11 (covariate and label shift), Module 14 (new drivers as a bias source)
Monday 31 August 2026England and Wales summer bank holidayModule 3, Module 15
Monday 7 September 2026Scanner software v4 at every depot: weight_kg now carries grams. Nobody told the ML teamModule 5 (the data contract), Module 11 (drift, the postmortem), Module 16 (the fourth question)
15 to 17 September 2026High winds at Glasgow and Inverness; island ferries stop (ferries are never a column)Module 11 (concept drift), Module 14 (subgroups)
Monday 28 September 2026The snapshot; outcomes after it are not known yetModule 11 (performance with late labels)

Weather and the gales are simulated. The two bank holidays are real dates, read from gov.uk/bank-holidays.json on 24 October 2026.

The project weeks

WeekMondayModuleWhat happens
12 November 20261The maths behind the prototype, rebuilt by hand; logistic regression from scratch
29 November2The notebook becomes a tested package
316 November3The leak found; model v1, a calibrated LightGBM, against honest baselines
423 November4A network tried against the trees; the message classifier
530 November5The data path: pipeline, lake, data contract, features without leakage
67 December6Tracked experiments; model v2 and an honest comparison
714 December7What training would cost at scale, and why Kestrel does not need a GPU for this model
821 December8ONNX, benchmarks, cost per thousand predictions
928 December9The scoring service: batch at 06:00, online at collection
104 January 202710Registry, gates, shadow, canary, rollback, retraining
1111 January11September replayed: the grams found, alerts tuned, a postmortem
1218 January12A small foundation model adapted for customer messages
1325 January13What depth in tabular and risk modelling means
141 February14Fairness for island customers, explanations, the model card
158 February15Framing, judgement and the portfolio
1615 February16Capstone review with Adaeze, Morag and Rhys: the five questions

Gemini 3.8 Flash's price doubles on 1 January 2027, in week 9: course-guide/02 says so, and Module 12 lesson 2 uses it as the example of a dated price.

Fixed names

So that modules fit together, these names do not change:

  • The repository is kestrel/; the package inside it is kestrel/ (imported as import kestrel); tests live in tests/; pipelines in pipelines/; the service in service/; evaluation of the message models in evals/; documents in docs/; reports generated by code in reports/; model files in artifacts/. Paths below are relative to the repository root.
  • kestrel/data.py holds blocks 1 to 8 of the shared data, copied exactly.
  • FEATURES (in kestrel/features.py, Module 3) is the list of the 18 columns known at collection; FORBIDDEN = ["attempts", "delivered_date", "late", "label_ready_date"] never reach training.
  • Every model, whatever its library, is wrapped to predict_proba(df) -> numpy array of late probabilities, so baselines, trees, networks and the ONNX runtime are interchangeable in the gates and the service.
  • The time-based split: train on ship dates before Monday 24 August 2026, validate on 24 August to 6 September, and keep 7 to 21 September as the locked test set (Module 6 lesson 2). Labels after 21 September are incomplete. The test set starts on the day weight_kg turns into grams: Modules 3 and 4 train and test on dates before 7 September only, and from Module 5 on the data contract converts September's grams back to kilograms before anything trains or scores (kestrel/contracts.py). Monitoring lessons replay the raw feed, grams included, on purpose.
  • The service's request carries the 18 columns of FEATURES (parcel_id included); the response carries late_probability, model_version and fallback (true when the baseline answered).
  • Every run record holds the Git commit, the data fingerprint from guide 06 block 8, the parameters, the metrics and the model file's hash.
  • Reports in reports/ are generated by code and never edited by hand.

One repository: which module adds which file

In Colab each lesson writes the files it needs with %%writefile (one small file per component, never more than about 40 lines) and imports them; on a laptop the same files form the repository. Tests run with !python -m pytest -q in Colab and pytest -q locally. Every lab from Module 2 on starts by checking that the earlier modules' files are present and writing back any that are missing, so you can start at any module. This table is the plan; it will be regenerated from the published lessons' %%writefile cells when the course is built.

ModuleLessons addThe lab adds
1kestrel/maths/linalg.py, kestrel/maths/optimise.py, kestrel/maths/stats.pykestrel/__init__.py, docs/adr/TEMPLATE.md (the ADR template below), kestrel/data.py, kestrel/baseline_logit.py, tests/test_data.py, requirements.txt, .gitignore, README.md
2pyproject.toml, kestrel/schema.py, kestrel/featurise.py, kestrel/profiling.py, kestrel/io.py, kestrel/parallel.py, tests/conftest.py, tests/test_featurise.py, Dockerfile (written, not built), .github/workflows/ci.yml (written, not run)kestrel/cli.py, tests/test_cli.py, Makefile
3kestrel/models/linear.py, kestrel/models/trees.py, kestrel/models/others.py, kestrel/explore/cluster.py, kestrel/explore/anomaly.py, kestrel/forecast.py, kestrel/features.py, kestrel/split.py, kestrel/leakage.py, kestrel/calibrate.py, kestrel/metrics.py, kestrel/baselines.pykestrel/train_v1.py, tests/test_leakage.py, reports/model-v1.md (generated), docs/adr/ADR-001-model-family.md
4kestrel/nn/tiny_autograd.py, kestrel/nn/mlp.py, kestrel/nn/train_loop.py, kestrel/nn/diagnose.py, kestrel/nn/text_model.py, kestrel/nn/vision.py, kestrel/nn/dataset.py, kestrel/nn/lit_module.py, kestrel/seed.pykestrel/nn/tabular_net.py, tests/test_nn.py, artifacts/intent_classifier.pt (generated), docs/adr/ADR-002-trees-or-network.md
5pipelines/ingest_flow.py, kestrel/etl.py, kestrel/backfill.py, kestrel/lake.py, kestrel/contracts.py, kestrel/lineage.py, kestrel/annotation.py, kestrel/pii.py, dvc.yaml, kestrel/feature_build.py, kestrel/skew.py, feature_repo/features.pypipelines/daily.py, tests/test_contracts.py, docs/data-contract.md, docs/adr/ADR-003-data-path.md
6kestrel/track.py, kestrel/tune.py, kestrel/compare.py, kestrel/holdout.py, kestrel/error_analysis.py, docs/experiments/log.mdkestrel/train_v2.py, reports/experiments.md (generated)
7kestrel/scale/vram.py, kestrel/scale/gpu_cost.py, scripts/ddp_train.py, kestrel/scale/checkpoint.py, kestrel/scale/profile_loop.py, kestrel/scale/loader_bench.pykestrel/scale/report.py, reports/training-throughput.md (generated), docs/adr/ADR-004-training-hardware.md
8kestrel/optimise/quantise.py, kestrel/optimise/distil.py, kestrel/optimise/prune.py, kestrel/optimise/export_onnx.py, kestrel/optimise/batcher.py, kestrel/bench.py, kestrel/cost_per_k.pykestrel/optimise/onnx_model.py, artifacts/model.onnx (generated), reports/benchmark.md (generated), docs/adr/ADR-005-inference-runtime.md
9service/app.py, service/schemas.py, kestrel/batch_score.py, service/admission.py, service/fallback.py, service/versions.py, kestrel/preprocess.pyservice/main.py, service/Dockerfile (written, not built), tests/test_service.py, docker-compose.yml (written, not run), docs/slo.md, docs/adr/ADR-006-serving-shape.md
10pipelines/train_flow.py, kestrel/registry.py, kestrel/gates.py, kestrel/reproduce.py, kestrel/rollout.py, kestrel/flags.py, .github/workflows/model-ci.yml (written, not run), docs/runbook-rollback.md, kestrel/triggers.py, kestrel/retrain_budget.py, tests/test_pipeline.py, data/slice.parquet (generated)pipelines/continuous_training.py, .github/workflows/tests.yml (written, not run; supersedes ci.yml), docs/adr/ADR-007-rollout-policy.md
11kestrel/monitor/drift.py, kestrel/monitor/performance.py, kestrel/monitor/segments.py, kestrel/monitor/ood.py, kestrel/monitor/alerts.py, kestrel/monitor/report.py, docs/postmortem-template.mdpipelines/monitor_flow.py, docs/dashboard.md, docs/postmortem-2026-09-07.md, docs/adr/ADR-008-alert-thresholds.md
12kestrel/fm/finetune_set.py, kestrel/fm/lora_intent.py, kestrel/fm/forgetting.py, kestrel/fm/memory_maths.py, kestrel/fm/economics.py, kestrel/fm/load_test.py, evals/messages_set.py, evals/messages_v1.jsonl (generated), evals/judge.py, artifacts/intent_lora/ (generated)evals/messages_eval.py, evals/regression.py, kestrel/llm.py (the setup cell and ask()), docs/adr/ADR-009-self-host-or-api.md
13kestrel/tracks/rank_vans.py, kestrel/tracks/search_messages.py, kestrel/tracks/cost_curve.py, docs/depth-tabular.mddocs/specialisation.md
14kestrel/responsible/fairness.py, kestrel/responsible/explain.py, kestrel/responsible/counterfactual.py, kestrel/responsible/audit.py, kestrel/responsible/privacy.py, kestrel/responsible/robustness.pydocs/model-card.md, reports/fairness.md (generated), docs/adr/ADR-010-deploy-decision.md
15docs/framing.md, kestrel/value.py, docs/writeup.mdREADME.md (portfolio version), docs/interview/kestrel-design.md
16pipelines/make_all.py, service/live.py (serves v1 and v2 on the columns they read), docs/five-answers.md, reports/capstone.md (generated), make all target that rebuilds everything from a commit

Decision records

Decisions are written as architecture decision records (ADRs) in docs/adr/, numbered in the order above (ADR-001 in Module 3 to ADR-010 in Module 14). Every ADR uses this template:

text
# ADR-NNN: <decision in a few words>

Status: proposed | accepted | superseded by ADR-MMM
Date: <ISO date>
Deciders: <names from the cast>

## Context
<the problem, with the measured numbers that force a decision>

## Options considered
<two or three, each with quality (PR-AUC, Brier), latency, cost and what it locks in>

## Decision
<the choice, in one or two sentences>

## Consequences
<what gets easier, what gets harder, what we now have to operate or monitor>

## Revisit when
<the measurable condition that would change the decision, for example "Doncaster's PR-AUC falls 0.02 below the fleet's">

The artifacts you end with

ArtifactFileStartedFinished
The model, versioned in the registrykestrel-late-risk (MLflow), artifacts/model.onnxModule 3 lab (v1)Module 10 lab (gated, promoted)
Decision recordsdocs/adr/Module 3 labModule 14 lab
Data contractdocs/data-contract.md, kestrel/contracts.pyModule 5
Experiment log, with negative resultsdocs/experiments/log.md, reports/experiments.mdModule 6every later module that trains
Benchmark and cost per thousand predictionsreports/benchmark.mdModule 8regenerated by the capstone
SLO and rollback runbookdocs/slo.md, docs/runbook-rollback.mdModules 9, 10rehearsed in the capstone
Monitoring dashboard, alert rules and a postmortemdocs/dashboard.md, docs/postmortem-2026-09-07.mdModule 11
Model card and fairness reportdocs/model-card.md, reports/fairness.mdModule 14
The five answersdocs/five-answers.mdModule 16

The finished system

  • A pipeline from raw depot scans to a registered model, run by one command and versioned (data with DVC, code with Git, models in the registry)
  • A data contract that fails loudly on the 7 September grams
  • Model v1 and v2 with tracked experiments, honest baselines and a significance test
  • An ONNX model in a FastAPI service with a 06:00 batch path and an online path, a fallback to the baseline, and a benchmark against the 50 ms p99 SLO
  • Promotion gates, shadow and canary rollout simulated, and a rehearsed rollback
  • Drift, null-rate, prediction and segment monitoring that works before labels arrive, with alerts tuned on September and a postmortem
  • An automated retraining trigger inside a cost ceiling
  • A small foundation model adapted for customer messages and evaluated against a hosted one
  • A model card, a fairness report and an explanation a customer could read

The five questions

The capstone answers the roadmap's five questions from measured numbers:

  1. What does one prediction cost, and one training run? (Imran; Modules 7 and 8)
  2. What is p99, and where does it go? (Rhys; Modules 8 and 9)
  3. How would you know the model degraded, and how fast? (Adaeze; Module 11)
  4. What happens if the feature pipeline silently changes a column? (Tomasz; Modules 5 and 11, the 7 September grams)
  5. Can you reproduce last month's model exactly? (Adaeze; Modules 6 and 10)

The one idea

Training is the bounded part; keeping a model correct in production is the job. Every component in this course has to earn its place with a number: a leak it removes, a drift it catches, a millisecond it saves, a pound it saves, or a rollback it makes possible.