A timestamp is not a feature 7 min live
Hand a model a raw datetime and it receives an epoch number: a monotonic counter that climbs steadily from the first row of the table to the last. What that counter mostly encodes is when the dataset was collected. It is a row index wearing a calendar costume. Everything you actually care about - the day of the week, the season, how long this customer has been around - is inside it, and none of it is available until you take it out.
LiveThe three lanes, and which one gets skipped5 min▶
Almost every team gets the first lane. Most get the second. The third is where the signal usually hides.
- Calendar parts answer where in the cycle this event sits: year, month, day of week, hour, quarter. Cheap to compute, immediately interpretable, and the raw material for the cyclical encoding in Part 2.
- Boolean flags answer what kind of moment this was: weekend, public holiday, business hours, month end, promo window. A flag is a human judgement compressed into one bit, which is exactly what Rule #20 means by "human-understandable ways."
- Relative quantities answer how far from now: tenure (prediction time minus signup), recency (prediction time minus last event), time to event. These are the ones teams forget, and they are usually the strongest, because behaviour depends far more on "signed up nine days ago" than on "signed up in November."
The raw-epoch trap. If you leave signup_date as a number and your training rows run January to June while production runs in July, every live row carries an epoch value larger than anything the model ever saw. A tree splits on a threshold it can no longer distinguish; a linear model extrapolates a trend it was never entitled to. The column that looked harmless was quietly encoding "how late in the data collection this row arrived."
Self-studyWhich parts are worth building on Lumen3 min read▶
The workbench keeps this deliberately small - two features, one from each of the last two lanes - so you can see the lift attributable to each idea rather than to a pile of columns.
| Feature | Lane | What it lets the model notice |
|---|---|---|
tenure | Relative | How long this account has existed at the moment of the session |
weekend | Flag | Whether the session happened on a different kind of day |
session_hour | Calendar | Nothing useful - this is the noise column intro-ML exposed, retired properly in b8 |
Two features, +0.021 held-out AUC. Note what that is not: it is not a new algorithm, a bigger dataset, or a tuning run. It is one column that was already in the table, read properly.
Midnight is next to 11pm 8 min live
Here is the problem with calendar parts as plain integers. Hour 23 and hour 0 are one minute apart in reality and 23 units apart in your feature matrix - the maximum possible distance. Month 12 and month 1 have the same defect, as do day 7 and day 1. You have handed the model a ruler that says December and January are the two furthest-apart months of the year.
LiveThe sine-cosine pair, in six lines4 min▶
Map the cycle onto a circle and read off both coordinates. Any periodic part works the same way - just change the period.
import numpy as np
hour = df["signup_date"].dt.hour
df["hour_sin"] = np.sin(2 * np.pi * hour / 24)
df["hour_cos"] = np.cos(2 * np.pi * hour / 24)
month = df["signup_date"].dt.month
df["month_sin"] = np.sin(2 * np.pi * month / 12)
df["month_cos"] = np.cos(2 * np.pi * month / 12)
Why two columns and not one. Sine is symmetric about its peak, so hour 6 and hour 18 both give a sine value that cannot be told apart by sign alone, and hours either side of noon collide the same way. One column collapses two distinct moments into one number. The cosine breaks the tie: together the pair gives every hour a unique position on the circle, and hours that are close in time end up close in feature space.
LiveTwo alternatives from the sklearn docs4 min▶
The sine-cosine pair is not the only honest option, and sklearn ships both of the others.
from sklearn.preprocessing import SplineTransformer, KBinsDiscretizer
# smooth, periodic basis - the ends of the cycle join up
SplineTransformer(degree=3, n_knots=13, extrapolation="periodic")
# coarse time-of-day buckets - the model learns a weight per bucket
KBinsDiscretizer(n_bins=4, strategy="uniform", encode="onehot-dense")
- Splines with periodic extrapolation give a smooth basis that wraps, and the docs prefer splines over polynomials generally because they avoid "oscillatory behaviour at the boundaries... known as Runge's phenomenon" and extrapolate better. Worth knowing: "The SplineTransformer treats each feature separately, i.e. it won't give you interaction terms."
- Binning is the blunt, readable option: night, morning, afternoon, evening. Google's guidance on when to bin fits time of day well - bin when "the overall linear relationship between the feature and the label is weak or nonexistent" or when "feature values are clustered", and remember "the model learns separate weights for each bin." A degree-0 spline is exactly a
KBinsDiscretizer(encode='onehot-dense')withn_bins = n_knots - 1.
Relative to when, exactly? 6 min live
The relative lane is the strongest of the three and the easiest to poison. Every relative feature is a subtraction, and the whole question is what you subtract from. Compute tenure as "today minus signup" while you build the training set and you have just written today's knowledge into a row from eight months ago. The row now knows something it could not have known.
LiveWhat the broken version looks like in code4 min▶
The wrong version is shorter, which is part of why it keeps happening.
# WRONG - "now" is the moment you ran the cell, not the moment
# the prediction would have been made
df["tenure_days"] = (pd.Timestamp.now() - df["signup_date"]).dt.days
# RIGHT - every row carries the timestamp of its own decision point
ref = df["prediction_ts"]
df["tenure_days"] = (ref - df["signup_date"]).dt.days
df["days_since_last_order"] = (ref - df["last_order_ts"]).dt.days
The damage is subtle. Both versions produce plausible numbers, both train fine, and the broken one often scores better on the held-out set, because "now minus signup" is partly encoding how old the row is, and row age correlates with the outcome distribution in most datasets. That is the sklearn definition arriving on schedule: "information that would not be available at prediction time is used when building the model."
Self-studyWhere this goes next3 min read▶
- b5, Leakage: the session. Temporal leakage is one of the four families, and this is its cleanest example. b5 also gives you the three rules that prevent all four, and sklearn's worked result where a leaked step scored 0.76 accuracy on pure noise against an honest 0.50.
- b6, aggregations. The same rule with a bigger blast radius: a group aggregate computed over the whole table leaks the future into every row of the group.
- Leader session a4, feature governance. Doing this correctly at scale has a name - point-in-time correctness - and it is one of the stated reasons feature stores exist. Feast's own docs put it in the value list: "Avoid data leakage by generating point-in-time correct feature sets."
- Not this course. Lag and rolling-window features for forecasting are a related but separate craft, and they live in learn-timeseries-forecasting-with-phoebe.
Turn on the datetime lever ★ 12 min · everyone runs it
The workbench opens where b3 left you: numerics and encoding on, held-out AUC 0.785. Now add the two features you just designed and watch a column you already owned pay for itself.
Confirm the current rung. Numerics and encoding are on; press Score on held-out and read 0.785. Nothing about time is in the model yet, even though signup_date has been sitting in the table since b1.
Switch on Datetime and press Score on held-out again: 0.806. That is +0.021 from a column that was already there. No new data was collected, no new algorithm was chosen.
Look at what the lever actually added: tenure and weekend, both derived from signup_date. One relative quantity, one boolean flag. Two features, and the third-largest jump on the whole ladder.
Compare the size of the win to the effort. Encoding (b3) bought 0.089 and required real thought about cardinality. Datetime bought 0.021 for roughly twenty minutes of decomposition work, which is a very good hourly rate.
Try it yourself - this week ◐ 20-30 min total
- Take one datetime column from a table you own and write out all three lanes for it: every calendar part, every flag worth having, every relative quantity. You will usually find between eight and fifteen candidate features in a column you were treating as one.
- For each relative feature you listed, name the reference timestamp explicitly. If you cannot point to a column that holds each row's prediction moment, that is this week's real finding.
- Build the sine-cosine pair for hour of day, then plot
hour_sinagainsthour_cos. Seeing the twenty-four points land on a circle is the moment the trick stops being abstract. - Run the same model with hour as an integer and as a sin/cos pair, once with logistic regression and once with a tree. Note how much less the encoding matters to the tree, and why.
Official sources covered
This session teaches the working core of the periodic-encoding material in sklearn's preprocessing guide and Google's binning guidance, applied to one column of the running Lumen case. Certificates and graded exercises stay with the official providers.
Three questions before you go 🎯 ◐ 90 seconds
1 · Why does cyclical encoding need two columns rather than one?
The pair places each moment at a unique point on a circle. With one coordinate you know the height but not which side of the circle you are on, so distinct hours collide. With both, hours that are close in time are close in feature space, including 23 and 0.
2 · What does a raw timestamp, handed to a model as a number, mostly encode?
Epoch values climb steadily from the first row to the last, so the column tracks position in the collection window rather than anything meaningful about the event. Worse, production rows carry values above everything in training. The calendar parts, flags, and relative quantities are all in there, but only if you extract them.
3 · What is the leakage rule specific to relative features such as tenure and recency?
"Now minus signup" writes today's knowledge into an eight-month-old row, which is exactly information "that would not be available at prediction time." The tell is that the column changes value every time you rebuild. Doing this correctly at scale is called point-in-time correctness (leader session a4), and the wider family is b5.