Time Series Basics: A Practical Introduction for Developers

What Makes Time Series Data Different

Most data you work with as a developer is cross-sectional — rows are independent observations, shuffling them doesn’t break anything, and the order doesn’t carry meaning. Time series data flips this entirely. A time series is a sequence of data points recorded at successive time intervals, and the ordering is the entire point. The value at time t is related to the value at time t-1, t-2, and so on. Shuffle a time series and you destroy its signal.

Stock prices, daily active users, server CPU utilisation, sensor readings, website traffic — these are all time series. The defining property is temporal dependence: past values influence future values, and the data carries a natural sequential structure that standard machine learning models treat as a nuisance rather than a feature.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

# A simple time series: daily active users over 200 days
dates = pd.date_range("2026-01-01", periods=200, freq="D")
users = 1000 + np.arange(200) * 3 + 200 * np.sin(2 * np.pi * np.arange(200) / 7) + np.random.normal(0, 50, 200)

ts = pd.Series(users, index=dates)
print(ts.head())

The Four Components of Every Time Series

Every time series can be decomposed into four latent components. Understanding these is the foundation of everything else.

Trend

The long-term direction of the series — is it generally going up, down, or flat? Trend ignores short-term fluctuations and captures the underlying progression. E-commerce revenue trends upward year over year; the number of active users on a mature product plateaus.

Seasonality

Predictable patterns that repeat at fixed, known intervals. Daily website traffic peaks in the afternoon, retail sales spike every December, server load cycles with the work week. Seasonality is tied to the calendar — time of day, day of week, month of year — and its period is always known.

Cyclic Patterns

Cycles look like seasonality but aren’t fixed. They occur over irregular periods driven by economic or business conditions — boom and bust cycles, product adoption lifecycles. Unlike seasonality, you can’t predict a cycle’s period just by looking at the clock.

Residual (Noise)

Whatever is left after removing trend, seasonality, and cycles. White noise, irregular fluctuations, measurement error. If your decomposition is good, the residuals should look like random scatter with no discernible pattern.

from statsmodels.tsa.seasonal import seasonal_decompose

# Decompose the series into trend, seasonal, and residual
result = seasonal_decompose(ts, model="additive", period=7)

fig, axes = plt.subplots(4, 1, figsize=(10, 8))
ts.plot(ax=axes[0], title="Original")
result.trend.plot(ax=axes[1], title="Trend")
result.seasonal.plot(ax=axes[2], title="Seasonal")
result.resid.plot(ax=axes[3], title="Residual")
plt.tight_layout()

Stationarity: The Foundational Concept

Stationarity is arguably the most important concept in time series analysis. A stationary series has constant statistical properties over time — specifically, its mean, variance, and autocorrelation do not change regardless of which time window you examine.

Most classical forecasting models (ARIMA, exponential smoothing) assume the input series is stationary. If you feed them non-stationary data, they produce unreliable estimates and nonsensical confidence intervals.

Why Non-Stationarity Happens

  • Trend: A rising mean over time violates constant-mean stationarity.
  • Seasonality: Regular fluctuations mean the average value depends on the season, not just the underlying process.
  • Variance changes: Financial data often shows periods of high volatility followed by calm — heteroskedasticity.
  • Structural breaks: A sudden policy change, product launch, or market crash shifts the data-generating process.

Testing for Stationarity

The Augmented Dickey-Fuller (ADF) test is the standard statistical test. The null hypothesis is that the series has a unit root — i.e., it is non-stationary. A p-value below 0.05 rejects the null, giving you confidence the series is stationary.

from statsmodels.tsa.stattools import adfuller

def check_stationarity(series):
    result = adfuller(series.dropna())
    print(f"ADF Statistic: {result[0]:.4f}")
    print(f"p-value: {result[4]:.4f}")
    print("=> Stationary" if result[1] < 0.05 else "=> Non-Stationary")
    return result[1] < 0.05

check_stationarity(ts)

Making a Series Stationary

Three transformations are the standard toolkit for inducing stationarity:

Differencing subtracts each value from its predecessor. First-order differencing y'_t = y_t - y_{t-1} removes linear trends; second-order handles quadratic trends. Seasonal differencing subtracts the value from one season ago.

Log transformation stabilises variance. If the series exhibits exponential growth (e.g., viral user adoption), taking log(y_t) compresses the scale and often makes the variance constant.

Detrending fits a curve (linear regression, LOESS) and subtracts it, leaving the residuals which are trend-stationary.

# First-order differencing
ts_diff = ts.diff().dropna()
check_stationarity(ts_diff)

Autocorrelation and Partial Autocorrelation

Autocorrelation measures how a value correlates with its past values — correlation with itself, shifted in time. At lag k, it asks: “Does knowing the value at time t-k help predict the value at time t?”

from statsmodels.graphics.tsaplots import plot_acf, plot_pacf

fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 6))
plot_acf(ts.dropna(), lags=30, ax=ax1)
plot_pacf(ts.dropna(), lags=30, ax=ax2)
plt.tight_layout()

The Autocorrelation Function (ACF) plots autocorrelation at each lag. Significant spikes at lags 1, 2, 3 suggest an autoregressive component. Slow decay across lags is a hallmark of non-stationarity or strong trend.

The Partial Autocorrelation Function (PACF) plots the correlation at lag k after removing the effect of all intermediate lags (1 through k-1). This helps identify the order of the autoregressive term in an ARIMA model.

Resampling and Rolling Windows

Resampling

Changing the frequency of a time series is a common first step. You might have minute-level sensor data but only need hourly averages, or daily sales data you want to aggregate to weekly.

# Downsample daily data to weekly averages
weekly = ts.resample("W").mean()

# Upsample with interpolation
hourly_index = pd.date_range("2026-01-01", periods=24 * 7, freq="H")
hourly = weekly.reindex(hourly_index).interpolate(method="linear")

Rolling Statistics

Rolling (moving) windows smooth out short-term noise to reveal the underlying trend. A 7-day rolling average, for example, replaces each point with the mean of the last 7 days.

# 7-day and 30-day rolling averages
ts.rolling(window=7).mean().plot(label="7-Day MA")
ts.rolling(window=30).mean().plot(label="30-Day MA")
ts.plot(label="Original", alpha=0.5)
plt.legend()

Rolling windows aren’t limited to the mean — you can compute rolling standard deviation (to track volatility), rolling quantiles, or rolling correlation between two series.

Lag Features: Time as a Predictor

One of the simplest yet most effective techniques in time series feature engineering is creating lag features. A lag feature shifts the series by k periods so that past values become predictors of the present.

def create_lag_features(series, max_lag=7):
    df = pd.DataFrame({"original": series})
    for lag in range(1, max_lag + 1):
        df[f"lag_{lag}"] = series.shift(lag)
    return df

lagged = create_lag_features(ts, max_lag=7)
print(lagged.head(10))

This is how you bridge classical time series analysis with machine learning: by framing the forecasting problem as a supervised regression task where the features are lags, rolling statistics, and calendar variables (day of week, month, holiday flags).

Simple Forecasting Baselines

Before reaching for ARIMA or LSTM, always establish simple baselines. They’re fast, interpretable, and often surprisingly effective.

  • Naïve forecast: Predict the last observed value. Works well for random walks (stock prices).
  • Seasonal naïve: Predict the value from one season ago. Works well for strongly seasonal data.
  • Mean forecast: Predict the historical average (constant model).
  • Drift forecast: Extrapolate the trend from the first and last observation.
def naive_forecast(series, steps=7):
    return np.repeat(series.iloc[-1], steps)

def seasonal_naive(series, steps=7, season=7):
    return series.iloc[-season:].values[:steps] if steps <= season else np.tile(series.iloc[-season:], int(np.ceil(steps / season)))[:steps]

print("Naïve:", naive_forecast(ts, 7))
print("Seasonal Naïve (7-day):", seasonal_naive(ts, 7))

Evaluate these baselines with Mean Absolute Error (MAE) or Root Mean Squared Error (RMSE) on a held-out test set. If your sophisticated model can’t beat the naïve baseline, something is wrong.

Practical Workflow

A sound time series workflow follows these steps:

  1. Visualise — plot the series, look for trend, seasonality, outliers, and structural breaks.
  2. Decompose — separate trend, seasonal, and residual components.
  3. Check stationarity — run the ADF test. If non-stationary, apply differencing, log transforms, or detrending.
  4. Split chronologically — never shuffle. Train on early data, validate on later data.
  5. Build a baseline — start with naïve or seasonal naïve. Measure MAE/RMSE.
  6. Engineer features — add lags, rolling statistics, calendar variables.
  7. Model — try exponential smoothing, ARIMA/SARIMA, or feed lag features to a gradient boosting model.
  8. Validate — use time series cross-validation (expanding window or sliding window).

Conclusion

Time series analysis is distinct from standard machine learning because time itself is the organising structure. The fundamental concepts — trend and seasonality, stationarity, autocorrelation, differencing, and lag features — form the toolkit you need before touching any model. Get these right and your forecasts will rest on a solid foundation. Skip them and even the most sophisticated LSTM will produce nonsense.

The next time you encounter a dataset with a timestamp column, resist the urge to treat it as just another feature. Respect the order, decompose the signal, and use the unique structure of time to your advantage.