← Back to the full chapter
Lesson

What to learn

Turn messy records into a reproducible analysis, with a clear account of what you know.

8 min read50 hours practice planWeeks 1–5Updated
Read source ↗

Worked lesson: the event happened, but you did not know it

Suppose you predict at the start of 3 January, using the previous seven days. These invented transactions separate when an event happened from when the service received it:

AmountEvent timeAvailable atAvailable for this prediction?
101 January1 JanuaryYes
202 January4 JanuaryNo: it arrived later
304 January4 JanuaryNo: the event is in the future

The historical total is 10. Filtering on event time alone gives 30 and leaks a late arrival. Using today's complete customer total gives 60 and also includes a future event. Values correct today can be wrong inputs for a past decision.

from datetime import datetime, timedelta, timezone

def day(number):
    return datetime(2026, 1, number, tzinfo=timezone.utc)

events = [(10, day(1), day(1)), (20, day(2), day(4)), (30, day(4), day(4))]
as_of = day(3)
total = sum(amount for amount, event_at, available_at in events
            if as_of - timedelta(days=7) <= event_at < as_of
            and available_at <= as_of)
assert total == 10

Try it yourself. Move the decision to 5 January. Which records now qualify, and what is the total? Then add an event exactly at the prediction time. Decide whether it belongs before running.

Hint and explained answer

Both clocks must pass. At 5 January all three original records qualify, so the total is 60. An event at exactly as_of falls outside the half-open event window [as_of - lookback, as_of). That boundary stops the current decision's own activity from becoming its historical input. The availability rule here does include records arriving exactly at as_of. Test and document whether your real extraction order can actually use them.

Use this tiny fixture to test a larger SQL or pandas implementation. The reference lab builds the same habit on a reproducible synthetic project.

Core concepts

Thinking in arrays. You are fluent in loops. Machine learning is written in shapes and batches, and the second dialect takes a little while. What you need is small but you need it cold: shape and dtype, indexing and slicing, views against copies, reshape and transpose, axis semantics, vectorising a loop into an array expression, and above all broadcasting.

Broadcasting deserves the extra time. It is the set of rules by which NumPy stretches a smaller array to match a larger one's shape. A large share of the debugging you will do over the next six months is shape errors, silent broadcasts and transposes in the wrong place.

import numpy as np

predictions = np.arange(100.0).reshape(100, 1)  # one column, shape (100, 1)
targets = np.arange(100.0)                      # shape (100,)
errors = predictions - targets

Try it yourself. Predict errors.shape before running it. Every prediction equals its target, so the mean squared error should be zero. What does (errors ** 2).mean() return, and why?

Hint and explained answer

Align the shapes from the right. (100,) is treated as (1, 100), and each size-1 dimension stretches to match. So errors has shape (100, 100): every prediction minus every target. The mean is 1666.5, the average squared difference over all 10,000 pairs, and nothing raises an error. Compare matching rows with predictions[:, 0] - targets, then assert errors.shape == (100,) before reducing. That one line catches the same bug in a metric, a loss or a pandas-to-NumPy conversion.

Alongside this, watch 3Blue1Brown's Essence of Linear Algebra. Three hours of video, and it gives you one idea that pays off immediately: a matrix is a transformation of space, not a grid of numbers to row-reduce. After that, "a layer is a linear map followed by a nonlinearity" stops being jargon. It helps to pair each chapter with code, though. It is quite possible to finish the series feeling enlightened and still be unable to compute a matrix product.

Thinking in tables. pandas, and the judgement that goes with it. The library is the easy half.

Alongside your notebook, record every cleaning decision you made, what you rejected, and how the choice would change a model downstream. Here is an example decision log, with invented percentages:

## delivery_time — 8.2% missing
Pattern: missingness correlates with carrier == "regional" (34% missing) against 0.4%
for national. This is not missing completely at random: it depends on observed carrier.
This pattern alone cannot establish MAR versus MNAR; missing delivery times are unobserved.
Rejected: mean imputation. It would smear the national distribution across regional rows
          and hide the fact that we simply do not observe this segment.
Rejected: dropping the rows. It removes 8% of the data, and non-randomly.
Chose:    keep the nulls and a delivery_time_observed flag for descriptive coverage checks.
          Report observed timing by carrier, alongside missingness and sample counts.
Impact:   observed rows alone cannot establish timing for all regional-carrier deliveries.

That paragraph is the thing worth showing people. Anyone can call dropna(). The judgement is the scarce part, and it maps directly onto "data literacy" in a hiring rubric.

The author-hosted missing-data chapter explains why dependence on an observed field can fit MAR (missing at random), and why MNAR (missing not at random) needs sensitivity analysis.

Separate missing features from missing targets. If delivery time is the Chapter 3 target, its eventual value and observed flag cannot be predictors at dispatch. Avoid filling an unfinished target with zero or a mean. A package still being tracked has a right-censored duration: you know how long it has gone undelivered, not when it will arrive. Evaluating only completed deliveries can leave out the slowest ones. The scikit-survival introduction explains event indicators and censoring. A survival model is optional depth, not an extra prerequisite. For a first project, a fixed-horizon target such as “delivered within seven days” is often easier to define. After seven days, a tracked non-delivery is negative. A lost tracking record is still unknown. State which population and follow-up your evaluation covers.

For a feature available before prediction, compare an imputer and missing indicator fitted inside the training pipeline with a model that handles that feature's missing values. Either way, keep the raw missingness information in your analysis.

SQL. Grain, joins and aggregation carry across tools, though syntax and NULL behaviour vary by engine. Get comfortable with CTEs, window functions (ROW_NUMBER, RANK, LAG, running totals), join cardinality, and point-in-time correctness.

That last one matters more than it sounds. Most serious data-leakage bugs in machine learning are, underneath, a query that accidentally used the future.

If an assistant writes SQL, check it against a schema you understand and a tiny table with duplicates, NULLs and boundary dates. Plausible SQL can give the wrong denominator with no error.

A schema is more than column names. Define dtypes, units, allowed categories and what a missing value means. Keep leading zeros in identifiers. Choose a timezone policy. Store money with a documented currency and rounding rule. Tell apart a zero, an unknown value and a value that does not apply. Recheck pandas string and nullable dtype behaviour after upgrades. In pandas 3.0, default string storage uses PyArrow when installed and falls back otherwise, as the string migration guide explains.

Write the expected join cardinality before joining. One customer with three orders and four support events produces twelve rows if both event tables are joined directly. Summing the joined orders inflates revenue fourfold. Aggregate each event table to the intended grain first. In pandas, use validate="many_to_one" for a unique customer lookup and indicator=True to check unmatched rows. Its merge documentation also warns that NULL keys on both sides match each other, unlike usual SQL joins. Test this explicitly.

Write a data contract before training. Name the unit of a row, its unique key, the prediction time, the feature lookback, the outcome window, and when that outcome becomes complete. Keep both event_time (when something happened) and available_at (when your service could first use it). A transaction from Monday that arrived on Friday was unavailable to Tuesday's model. Historical features must pass both clocks, and later corrections need version history. Feast's point-in-time joins show historical retrieval. Check how your own query and tool handle availability time.

Some public exports hold only a current snapshot, with no availability history. Record that limit, and avoid inventing timestamps or claiming a verified backtest. Test your join logic with small synthetic late-arrival cases, and choose a Chapter 3 split your data can support.

For a customer snapshot at time t, build features from [t - lookback, t), using records available by t. Build labels from the outcome window that follows. An unresolved outcome is unknown, not a negative. Record label_available_at and outcome_observed separately from the planned maturity date, because a window can pass while its source ledger is still incomplete. At a simulated training cutoff, use only labels that the observation window, reporting delay and actual availability allow. Report missing outcomes by cohort and exclusion reason. Elapsed time alone does not prove follow-up is complete.

Test unique keys, join row counts, timezone conversion, missingness, late arrivals and duplicate events. Store the extraction cutoff and source version with the data. Record permission to use the data, keep customer identifiers out of public examples, and publish only data whose licence and permissions allow it. When the original data is private, a reproducible synthetic example is enough.

Reproduction needs a data manifest. Record the source URL or export/query version, licence, retrieval date, extraction cutoff, raw-file checksum, schema version, code commit and environment. Where permitted, keep raw inputs unchanged. Document any sampling, seeds and exclusions, and use the same population denominator in your text and charts. Run the cleaning and analysis in a fresh process with the locked environment, not from the notebook's execution history. The questions in Datasheets for Datasets help explain who is represented and which future uses the dataset cannot support.

If raw data is not in the repository, include its exact download or export steps and expected checksum. If the documented input is missing or differs, the analysis should fail clearly, not quietly analyse a newer export.

Something unclear or out of date? Suggest a correction ↗

Find your next idea

Loading local search…

Search is built from this edition. No account needed.