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:
00:30 · weekday
20:30 · weekend
06:00 · weekday
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
| Question | Possible feature | Meaning to preserve |
|---|---|---|
| When in the day? | Local hour, including fractional minutes | Daily cycle; midnight joins the two ends |
| Which day type? | Weekday, weekend or a known holiday flag | Calendar categories and the relevant locale |
| When in the year? | Month or a defined annual phase | Seasonality; months have unequal lengths |
| How long since an event? | Elapsed hours since the latest known event | A duration measured at prediction time |
| Is the system changing over years? | Time since a fixed reference date | A 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:
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.
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.
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
- Scikit-learn: time-related feature engineering — comparisons of raw, one-hot, trigonometric and periodic-spline time features.
- Python: zoneinfo — IANA time-zone conversion, ambiguous clock times and the fold attribute.
- Pandas: time-zone handling — the distinction between localizing a clock reading and converting an identified instant.
- Scikit-learn: TimeSeriesSplit — ordered evaluation when training must precede validation.