Skip to main content

Module 5 — Cyclical features, dates and durations

A date is the richest and most poorly exploited variable in real datasets. As is, it is unusable; well decomposed, it carries seasonality, weekly habits, calendar effects. This module shows how to open it up, and why one special case — cycles — calls for an unexpected treatment.

Decomposing a date: one column becomes ten features

A raw timestamp teaches the model nothing. You must extract the components that carry a regularity:

df["year"]       = df["date"].dt.year
df["month"] = df["date"].dt.month
df["dayofweek"] = df["date"].dt.dayofweek # 0 = Monday
df["hour"] = df["date"].dt.hour
df["is_weekend"] = df["dayofweek"].isin([5, 6]).astype(int)
df["month_start"] = (df["date"].dt.day <= 5).astype(int)

Each of these features answers a different business question: the year captures an underlying trend, the month a seasonality, the day of week consumption habits, the hour daily rhythms. Binary indicators (weekend, start of month) materialize threshold effects a linear model would not find on its own.

Two features are worth adding when the domain allows: public holidays and school holiday periods, decisive on sales, traffic or attendance data. These are external data, but their contribution almost always exceeds the cost of fetching them.

The problem with cycles: December and January are neighbors

Here is the trap specific to temporal features. Encode the month from 1 to 12. To the model, the gap between January (1) and December (12) is 11, whereas in reality those two months are adjacent. Same problem between 11 p.m. and midnight, or between Sunday and Monday.

The consequence is concrete: a regularity crossing the boundary of the cycle — a rise in activity from December to January — becomes invisible, since the model sees two very distant values.

The one-hot encoding of module 4 solves the issue by removing all order, but it loses continuity: February is then no longer closer to January than to July. For twelve months that is acceptable; for the minutes of a day it is untenable.

Cyclical encoding with sine and cosine

The elegant solution places the cycle on a circle. You convert the value into an angle, then take its sine and cosine:

xsin=sin(2πvP),xcos=cos(2πvP)x_{\sin} = \sin\left(\frac{2\pi v}{P}\right), \qquad x_{\cos} = \cos\left(\frac{2\pi v}{P}\right)

where vv is the value and PP the period of the cycle (12 for months, 24 for hours, 7 for days).

import numpy as np
df["month_sin"] = np.sin(2 * np.pi * df["month"] / 12)
df["month_cos"] = np.cos(2 * np.pi * df["month"] / 12)

Two columns replace the variable, and the desired property is achieved: December and January end up geometrically adjacent on the circle, as does any pair of consecutive values. Why two functions rather than one? Because sine alone is ambiguous — it takes the same value at two distinct positions on the circle. The sine-cosine pair identifies the position unambiguously.

This encoding mainly serves distance-sensitive and linear models. Trees, which split by thresholds, cope without it but benefit less: they can isolate "month = 12" through a series of splits.

Durations, recency and gaps: often the most predictive

The most useful features are not dates themselves but gaps between dates. They answer directly business questions:

  • tenure: number of days since signup;
  • recency: number of days since the last purchase — the R of the RFM framework from the previous course;
  • duration: time elapsed between two steps of a process;
  • time to event: days remaining before a contract expires.
df["tenure_days"] = (df["reference_date"] - df["signup_date"]).dt.days

One crucial precaution about the reference date. For a model destined for production, tenure must be computed relative to the moment of prediction, not to a fixed date in the training set. Otherwise the feature shifts over time and the model silently degrades in production.

The most dangerous date is the one that comes after

Using a date later than the predicted event creates massive leakage. Predicting churn with the feature "days since last contact with customer service", when that contact happens during the cancellation, yields a perfect and unusable model. For every temporal feature, ask: is this date known at the moment I must predict? That is the subject of module 8.

Summary

  • A raw date teaches nothing: decompose it into year, month, day of week, hour, plus threshold indicators and, if possible, holidays and school vacations.
  • Numbering a cycle makes December and January artificially distant, which hides regularities crossing the boundary.
  • Cyclical encoding with sine and cosine places the cycle on a circle and restores adjacency; the pair removes the ambiguity of a single function.
  • Gaps between dates (tenure, recency, duration) are often the most predictive features, to be computed relative to the moment of prediction.

Next module: text features, with bag of words, n-grams and TF-IDF weighting.