← All Posts
Deep Learning · Data Preparation and Preprocessing · Feature Engineering

Datetime Features

A timestamp can provide several different kinds of features. Its local hour may describe a daily routine; its weekday may describe a weekly pattern; its distance from an earlier event may describe recency. These are different questions, so one large “seconds since 1970” number rarely expresses all of them well.

This chapter follows one timestamp into calendar fields, then explains a clock-shaped encoding for repeating patterns. The final section keeps elapsed time and feature availability separate from calendar position.

Choose whose calendar the timestamp belongs to

A timestamp with a UTC offset identifies an instant. The local date and hour depend on the time zone in which that instant is viewed. For a store-demand model, the store's local calendar may be relevant; for a system-wide maintenance schedule, UTC may be the intended calendar.

Consider the instant 2026-09-14T00:30:00Z, where Z means UTC:

01UTCMonday, 14 September
00:30 · weekday
02America/New_YorkSunday, 13 September
20:30 · weekend
03Asia/KolkataMonday, 14 September
06:00 · weekday
One instant, three local calendar readings. The weekend label here means Saturday or Sunday. Define the business calendar explicitly; local weekends and holidays are not universal.

Convert to the chosen time zone before extracting the date, hour or weekday. A bare string such as “2026-09-14 00:30” lacks the information needed to identify its instant. Establish the source's time-zone rule rather than silently assuming UTC. The parsing chapter covers validating the source representation.

Choose a feature for the pattern you expect

QuestionPossible featureMeaning to preserve
When in the day?Local hour, including fractional minutesDaily cycle; midnight joins the two ends
Which day type?Weekday, weekend or a known holiday flagCalendar categories and the relevant locale
When in the year?Month or a defined annual phaseSeasonality; months have unequal lengths
How long since an event?Elapsed hours since the latest known eventA duration measured at prediction time
Is the system changing over years?Time since a fixed reference dateA trend, rather than a repeating cycle

Keep these distinctions when encoding. Weekday numbers do not imply that Sunday is seven times Monday. A raw year may help represent a trend, but you should evaluate its behavior on later dates. Day-of-year needs a documented convention for leap years if it is used as an annual phase.

Why put hours on a circle?

On a numeric line, hours 23 and 0 differ by 23. On a clock, they are one hour apart. A circular representation preserves that neighborhood across midnight: each hour gets a position around the same circle.

We store the position as two coordinates. For local hour $h$, first turn the fraction of the day $h/24$ into an angle by multiplying by $2\pi$, one complete turn in radians. Then compute:

$$\begin{aligned} \mathrm{hour\_sin}&=\sin(2\pi h/24),\\ \mathrm{hour\_cos}&=\cos(2\pi h/24). \end{aligned}$$

In the diagram, sine supplies the horizontal coordinate and cosine supplies the vertical coordinate. Midnight is at the top. The model receives both numbers as features; it does not receive the drawing.

Hours on a circleMidnight is at the top, 6 on the right, 12 at the bottom and 18 on the left. The selected hour moves around the circle.0 · midnight612 · noon1823:00
hour_sin−0.2588
hour_cos0.9659
23:00 is 1 clock hour from midnight. Its point sits next to midnight on the circle.
23:00, midnight and 01:00 stay neighbors. The orange ring marks midnight; the green point marks the selected hour. The circle describes clock position, not the number of elapsed hours on a particular date.

Keep both coordinates. Sine alone gives zero at midnight and noon; cosine distinguishes them as +1 and −1. The pair identifies a position within the cycle, but deliberately discards which day it was.

The same construction can use period 7 for a weekday index 0–6 or period 12 for a month index 0–11. Choose the origin and period consistently. Month position treats months as equal steps; a day-based annual phase needs a different definition if unequal month lengths matter.

A smooth clock is useful, but it does not fit every daily pattern

A linear model using only one sine/cosine pair can form one smooth daily wave, with a shifted peak. That may be too restrictive for a morning rush and a separate evening rush. The representation removes the midnight jump; it does not automatically provide enough flexibility.

One-hot hours let the model assign a separate level to each hour, at the cost of losing smoothness between neighbors. Additional sine/cosine pairs at higher frequencies or periodic splines can represent more detailed smooth patterns. Compare these choices in validation rather than assuming the two-column encoding is always best. The scikit-learn reference below works through this tradeoff on hourly demand.

Recency is a duration, not a clock position

“Hours since the last purchase” should compare the prediction instant with the latest purchase already known then. Calculate both instants on a common UTC timeline and subtract. A purchase at 23:00 yesterday can be recent at 01:00 today even though their hour numbers have a large difference.

Use the historical prediction time when constructing training features. Calling the current wall clock while rebuilding an old dataset changes the feature every time the script runs. Also distinguish “no prior purchase” from zero elapsed time: a missing-history indicator or other explicit policy may be needed.

Optional: when the same local clock time happens twice

In New York on 2 November 2025, 01:30 occurred with offset −04:00 and again with offset −05:00. The corresponding UTC instants were 05:30 and 06:30, one elapsed hour apart. Both have the same hour-of-day features. Retaining the offset or resolving an ambiguous local time with the time-zone database is essential before measuring duration. The Python example subtracts UTC instants explicitly.

A timestamp field can still reveal the future

A feature must be available when the prediction is made. A completed trip's arrival time can give a very informative hour, weekday or duration—but those fields are unavailable when predicting at departure.

0110:00 · trip startsDeparture instant is observed.
0210:05 · prediction madeElapsed time since departure is 5 minutes.
0310:20 · trip finishesActual arrival and total duration become known later.
Build the row at the prediction cutoff. At 10:05, the eventual 20-minute duration cannot be an input. The same restriction applies even if you extract only its hour or weekday.

A known future calendar date is different from a future observation. Tomorrow's weekday or a published holiday schedule can be available when forecasting tomorrow's demand. Actual weather tomorrow or the final delivery timestamp generally is not. Record the information's availability, not just the date it refers to.

For forecasting, validate on later periods using time-series cross-validation. Keep historical aggregates inside their time boundaries as explained in aggregation features. A random row split can hide problems that only appear when predicting a new season or later operating conditions.

A small implementation with explicit time semantics

The code requires an offset-bearing timestamp, converts it to the selected business time zone, and includes fractional hours. Saturday/Sunday is an illustrative weekend definition. It also provides a separate function for elapsed age and rejects an event after the prediction time.

Python: calendar fields, cyclic coordinates and elapsed hours
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
from math import sin, cos, tau

def parse_instant(text):
    instant = datetime.fromisoformat(text)
    if instant.tzinfo is None or instant.utcoffset() is None:
        raise ValueError("timestamp must include Z or a UTC offset")
    return instant.astimezone(timezone.utc)

def calendar_features(text, business_zone):
    local = parse_instant(text).astimezone(ZoneInfo(business_zone))
    hour = (local.hour + local.minute/60 + local.second/3600
            + local.microsecond/3_600_000_000)
    return {
        'local_date': local.date().isoformat(),
        'hour': hour,
        'weekday': local.weekday(),  # Monday=0, Sunday=6
        'weekend': int(local.weekday() >= 5),
        'month': local.month,
        'hour_sin': sin(tau*hour/24),
        'hour_cos': cos(tau*hour/24),
    }

def age_hours(event_text, prediction_text):
    elapsed = parse_instant(prediction_text) - parse_instant(event_text)
    if elapsed.total_seconds() < 0:
        raise ValueError("event is after the prediction time")
    return elapsed.total_seconds() / 3600

instant = '2026-09-14T00:30:00Z'
for zone in ('UTC', 'America/New_York', 'Asia/Kolkata'):
    f = calendar_features(instant, zone)
    print(zone, f['local_date'], f['hour'], f['weekday'], f['weekend'])

print(age_hours('2025-11-02T01:30:00-04:00',
                '2025-11-02T01:30:00-05:00'))  # 1.0 elapsed hour

ZoneInfo uses an IANA time-zone database. Preserve the zone and relevant data version with your preprocessing configuration so the same calendar rules can be reproduced. In pandas, the corresponding conversion is tz_convert on timestamps that already have time-zone information.

Encode an already-resolved local hour in C++

Time-zone conversion belongs in a date/time library that knows the calendar rules. Once the local hour is resolved, the cyclic transformation is just two trigonometric functions. This helper accepts fractional hours in [0, 24); midnight is represented as 0.

C++17: convert local hour to its two cyclic features
#include <array>
#include <cmath>
#include <iomanip>
#include <iostream>
#include <stdexcept>

// local_hour is already resolved in the chosen time zone, in [0, 24).
std::array<double, 2> hour_cycle(double local_hour) {
    if (!std::isfinite(local_hour) || local_hour < 0 || local_hour >= 24)
        throw std::invalid_argument("local hour must be in [0,24)");
    const double two_pi = 2 * std::acos(-1.0);
    const double angle = two_pi * local_hour / 24;
    return {std::sin(angle), std::cos(angle)};
}

int main() {
    std::cout << std::fixed << std::setprecision(4);
    for (double hour : {23.0, 0.0, 1.0}) {
        auto pair = hour_cycle(hour);
        std::cout << hour << ": " << pair[0] << ' ' << pair[1] << '\n';
    }
}

References and further reading